【问题标题】:llvm reports: unsupported inline asm: input with type 'void *' matching output with type 'int'llvm 报告:不支持的内联 asm:输入类型为“void *”,匹配输出类型为“int”
【发布时间】:2016-03-30 13:15:41
【问题描述】:

我有以下内联汇编代码:

int get_year(int a, int *b, char * c)
{
    int ret, t1, t2;

    asm (
        "addl %3, %[a]                  \n\t"
        "movl %[a], %[t1]               \n\t"
        "movl $58, %%edx                \n\t"
        "movb %%dl, 0x04(%1)            \n\t"
        : [t1] "=r" (t1), "=&D" (t2)
        : [a] "r" (a), "rm" (*b), "1" (c)
        : "edx", "memory"
    );

    ret = t1;

    return ret;
}

当我通过 llvm 编译时,错误转储:

error: unsupported inline asm: input with type 'char *' matching output with type 'int'
                : [a] "r" (a), "rm" (*b), "1" (c)
                                               ^

但是,linux内核中的memcpy函数与内联汇编的用法是一样的:

void *memcpy(void *dest, const void *src, size_t n)
{
    int d0, d1, d2;
    asm volatile(
        "rep ; movsl\n\t"
        "movl %4,%%ecx\n\t"
        "rep ; movsb\n\t"
        : "=&c" (d0), "=&D" (d1), "=&S" (d2)
        : "0" (n >> 2), "g" (n & 3), "1" (dest), "2" (src)
        : "memory");

    return dest;
}

这可以正常工作,没有任何编译错误。

【问题讨论】:

  • 想必内核针对的gcc编译器没有那么严格。您可以通过将 t2 更改为 char* 来轻松修复它
  • lldb调试器编译?
  • @MichaelPetch 抱歉,这是笔误。我使用 llvm ....
  • 哈哈,这有点好,我想说你可能会遇到比你给 LOL 的错误更严重的问题 ;-)
  • 您是否尝试将此代码编译为 64 位代码而不是 32 位代码? (在 64 位代码中,int(4 字节)和指针(8 字节)的大小不同,可能会引发该错误。

标签: assembly inline-assembly lldb


【解决方案1】:

首先,如果您想开始学习 asm,GNU C 内联 asm 是使用 asm 的最难方法之一。您不仅要编写正确的 asm,还必须花费大量时间使用深奥的语法来告知编译器确切您的代码对输入和输出操作数的需求,否则您将遇到错误时间。在 ASM 中编写整个函数要容易得多。它们不能内联,但无论如何这是一个学习练习。普通函数 ABI 比 C 和带有约束的内联 ASM 之间的边界要简单得多。请参阅 wiki...


除了编译错误之外,你还有一个错误:你破坏了%[a],即使你告诉 gcc 它是一个仅限输入的操作数。

我认为这仍然是“正在进行的工作”,因为您可以使用更好的代码获得相同的结果。 (例如,使用%edx 作为临时注册是完全没有必要的。)当然,在一般情况下,这被内联到代码中a 可能是编译时常量,或者已知与其他东西相关,你' 会从 C 中获得更好的代码(除非您花费大量时间为各种情况制作 inline-asm 变体。)

int get_year(int a, int *b, char * c)
{
    int ret, t1, t2;

    asm (
        "addl %[bval], %[a] \n\t"
        "movb $58, 4 + %[cval]\n\t"  // c is an "offsetable" memory operand

        : [t1] "=&r" (t1), [cval] "=o" (*c)
        : [a] "0" (a), [bval] "erm" (*b)
        : // no longer clobbers memory, because we use an output memory operand.
    );

    ret = t1;  // silly redundancy here, could have just used a as an input/output operand and returned it, since you apparently want the value
    return ret;
}

现在是compiles and assembles(使用 Godbolt 的“二进制”选项来实际组装)。 4 + (%rdx) 会产生警告,但会组装到 4(%rdx)。如果已经存在偏移量,IDK 如何以不会出错的方式写入偏移量。 (例如,如果操作数是*(c+4),那么生成的asm就是4 + 4(%rdx),省略+是行不通的。)

这仍然使用匹配输出操作数技巧,但我改为使用内存或一般约束以允许编译器时常量最终执行addl $constant, %edi

这允许编译器在内联时尽可能多的灵活性。例如如果调用者运行get_year(10, &arr[10], &some_struct.char_member),它可以使用它想要的任何寻址模式进行加载和存储,而不必在单个寄存器中生成c。因此,例如,内联输出最终可能是 movb $58, 4+16(%rbp, %rbx),而不是强制它使用 4(%reg)

【讨论】:

  • 你能解释更多关于匹配输出操作数的技巧吗?我不是很清楚。顺便说一句,我使用苹果的 llvm 编译器在我的 macbook 中编译了你的代码,我得到了<inline asm>:2:17: note: instantiated into assembly here movb $58, 4 + (%rdx)...
  • 我有几个问题:1) 在[cval] "=o" (*c),为什么它是(*c)(c),我认为c 是一个地址,*c 是一个char 变量。 2)在[bval] "erm" (*b)中,e代表什么?我找不到它 gcc 手册...
  • re:匹配输出操作数技巧。我从您发布的代码中得到它... IDK 如果它比 Ross Ridge 的建议有任何优势,即使用输入/输出操作数 "+r" 来处理您想要在 asm 块内破坏的输入。我只是保留了这个技巧,因为我以前从未见过它,并想看看发生了什么。当输出操作数只是一个一次性的 tmp 变量时,我想不出任何我希望匹配输出操作数方法制作更好代码的地方。也许如果它有不同的大小,它可以更容易地避免使用前缀来获得%eax%rcx
  • @DouglasSu: 1:是的,*c 是单个字符,而不是指针。但是我使用了内存限制,所以 gcc 将用括号替换%[cval] 的有效地址,例如(%rdi)。这样你就告诉 gcc where 字节在内存中被写入。没有它,您需要将“内存”列为已破坏,强制重新加载正在缓存内存值的寄存器。 (即编译器内存屏障)。 2e 是一个适合有符号 32 位整数的编译时常量。它在手册的 x86 机器约束部分。这对于 64 位代码很重要。
  • 怎么样:"movb $58, 4 + 0%[cval]\n\t"。在内存操作数中没有可偏移值的情况下,它只是 4+0 偏移量 = 4。如果已经存在偏移量,那么偏移量只需添加一个前导 0 数字(不会更改偏移量的值present),然后添加 4。
【解决方案2】:

如果我只在生成 64 位代码时使用 clang 编译您的代码,我可以重现该问题。定位 32 位代码时没有错误。正如 Michael Petch 所说,这表明问题在于两个操作数的大小不同。

目前还不清楚最好的解决方法是什么,因为您的 asm 语句没有多大意义。相当于:

int get_year(int a, int *b, char *c) {
    a += *b;
    c[4] = 58;
    return a;
}        

使用汇编语句来完成可以使用上面的 C 代码更清晰、更有效地完成的工作没有任何优势。因此,最好的解决方案是用等效的 C 代码完全替换您的代码。

如果您只是在玩内联汇编,那么等效的内联汇编将是:

int get_year2(int a, int *b, char * c)
{
        asm("addl %[b], %[a]"
            : [a] "+r" (a)
            : [b] "m" (*b)
            : "cc");
        asm("movb $58, %[c4]"
            : [c4] "=rm" (c[4]));
        return a;
}

我使用了两个 asm 语句,因为这两个部分是不相关的。将它们分开提供了更多的优化机会。例如,如果你调用这个函数但不使用返回值,编译器可以消除第一个 asm 语句,因为它的结果没有被使用并且没有副作用。

我没有使用匹配约束,"1" 约束给您带来问题,而是使用“+”约束修饰符将操作数标记为输入和输出。我发现这效果更好。 [b] 操作数的约束实际上应该是 "rm"unfortunately clang doesn't handle rm constraints well

您可能注意到我只使用了两个汇编语句,而您的示例使用了四个。 MOVL 指令不是必需的,如果需要,编译器可以处理将结果移入返回值寄存器。您的最后两个汇编语句可以折叠成一个语句,将常量直接移动到内存中而不会破坏寄存器。说到这一点,你的 asm 语句破坏了 EFLAGS,条件代码,所以 "cc" 应该被列为破坏,但正如 Peter Cordes 指出的那样,x86 目标没有必要,但编译器认为它们无论如何都是。

【讨论】:

  • 有趣的事实也让我感到惊讶:x86 / x86-64 inline asm 隐式破坏了标志。 stackoverflow.com/questions/6659414/…。很少有指令影响标志,以至于它会导致比真正有用的优化机会更多的错误,因此他们决定以这种方式定义它。
  • @PeterCordes 啊,我想知道为什么这么多明显损坏的 asm 语句在实践中起作用。我只是假设在 asm 语句中使用标志的机会很少,所以它没有出现。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-05-26
  • 2021-12-01
  • 2022-01-15
  • 1970-01-01
  • 1970-01-01
  • 2019-04-28
  • 1970-01-01
相关资源
最近更新 更多