llvm 报告:不支持的内联 asm:输入类型 'void *' 匹配输出类型 'int'

llvm reports: unsupported inline asm: input with type 'void *' matching output with type 'int'

我有下面的内联汇编代码:

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 , %%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;
}

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

如果我仅在生成 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 , %[c4]"
            : [c4] "=rm" (c[4]));
        return a;
}

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

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

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

首先,如果您想开始学习 asm,GNU C 内联 asm 是 最难 使用 asm 的方法之一。您不仅必须编写正确的 asm,还必须花费大量时间使用深奥的语法来告知编译器确切您的代码需要输入和输出 ope运行ds,否则你会过得很糟糕。在 ASM 中编写整个函数容易得多。它们不能内联,但无论如何这是一种学习练习。普通函数 ABI much 比 C 和带约束的内联 ASM 之间的边界简单。请参阅 维基...


除了那个编译错误,你还有一个错误:你破坏了 %[a],即使你告诉 gcc 它是一个仅输入的操作运行d.

我认为这仍然是 "work in progress",因为您可以使用更好的代码获得相同的结果。 (例如,使用 %edx 作为暂存 reg 是完全没有必要的。)当然,在将其内联到代码中的一般情况下,其中 a 可能是编译时常量,或者已知与别的,你会得到更好的代码,只用 C 来做(除非你花了很多时间为各种情况制作内联 asm 变体。)

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

    asm (
        "addl %[bval], %[a] \n\t"
        "movb , 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 的 "binary" 选项实际上 assemble)。 4 + (%rdx) 产生警告,但 assemble 到 4(%rdx)。 IDK 如何以一种不会出错的方式写入偏移量(如果已经存在偏移量)。 (例如,如果 ope运行d 是 *(c+4),那么生成的 asm 是 4 + 4(%rdx),省略 + 是行不通的。)

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

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