即使我有问题,GCC也不会在我的内联asm函数调用周围推送寄存器

时间:2019-08-28 08:04:55

标签: c gcc x86 inline-assembly

我有一个修改(ecx)(或任何其他寄存器)的函数(C)

int proc(int n) {
    int ret;
    asm volatile ("movl %1, %%ecx\n\t" // mov (n) to ecx
                  "addl $10, %%ecx\n\t" // add (10) to ecx (n)
                  "movl %%ecx, %0" /* ret = n + 10 */
                  : "=r" (ret) : "r" (n) : "ecx");
    return ret;
}

现在我想在另一个函数中调用此函数,该函数在调用“ proc”函数之前将其值移至“ ecx”中

int main_proc(int n) {
    asm volatile ("movl     $55, %%ecx" ::: "ecx"); /// mov (55) to ecx
    int ret;
    asm volatile ("call     proc" : "=r" (ret) : "r" (n) : "ecx"); // ecx is modified in proc function and the value of ecx is not 55 anymore even with "ecx" clobber

    asm volatile ("addl     %%ecx, %0" : "=r" (ret));

    return ret;
}

在此函数中,将(55)移入“ ecx”寄存器,然后调用“ proc”函数(修改“ ecx”)。在这种情况下,“ proc”功能必须先按下“ ecx”,然后将其弹出,但是这种情况不会发生! 这是(-O3)优化级别的组装源

proc:
        movl %edi, %ecx
        addl $10, %ecx
        movl %ecx, %eax
        ret
main_proc:
        movl     $55, %ecx
        call     proc
        addl     %ecx, %eax
        ret

为什么GCC不会为“ ecx”寄存器使用(push)和(pop)?我也用过“ ecx”的垃圾桶!

1 个答案:

答案 0 :(得分:3)

您使用内联汇编完全错误。您的输入/输出约束需要完全描述每个asm语句的输入/输出。要在asm语句之间获取数据,必须将它们保存在它们之间的C变量中。

此外,call通常在内联汇编中并不安全,特别是在System V ABI的x86-64代码中,它踩到了gcc可能一直在保存东西的红色区域。没有办法宣布这一点。您可以先使用sub $128, %rsp跳过红色区域,也可以像普通人一样从纯C进行调用,以便编译器知道这一点。 (请记住,call会发送一个寄信人地址。)您的内联汇编甚至没有意义;您的proc需要一个arg,但是您在呼叫者中没有采取任何措施来传递一个。

proc中由编译器生成的代码也可能破坏了其他所有调用被阻塞的寄存器,因此您至少需要在这些寄存器上声明Clobbers。或在asm中手写整个功能,这样您就知道要放入Clobber中了什么。

  

为什么GCC不会为“ ecx”寄存器使用(push)和(pop)?我也用过“ ecx”的垃圾桶!

ecx掩体告诉GCC,此asm语句破坏了先前ECX中的 GCC 在两个单独的inline-asm语句中使用ECX隔离器不会声明它们之间的任何数据依赖。

这不等同于声明诸如
之类的register-asm局部变量 用作第一个和最后一个asm语句的register int foo asm("ecx");操作数的"+r" (foo)。 (或更简单地说,您将其与"+c"约束一起使用以使普通变量选择ECX)。

从GCC的角度来看,您的消息来源仅意味着约束+ Clobbers告诉了我们的内容。

int main_proc(int n) {
    asm volatile ("movl     $55, %%ecx" ::: "ecx");
      // ^^ black box that destroys ECX and produces no outputs
    int ret;
    asm volatile ("call     proc" : "=r" (ret) : "r" (n) : "ecx");
      // ^^ black box that can take `n` in any register, and can produce `ret` in any reg.  And destroys ECX.

    asm volatile ("addl     %%ecx, %0" : "=r" (ret));
     // ^^ black box with no inputs that can produce a new value for `ret` in any register

    return ret;
}

我怀疑您希望最后一个asm语句为"+r"(ret)以读取/写入C变量ret,而不是告诉GCC它仅是输出。因为您的asm会将其用作add的输入和输出。

在第二个asm语句中添加诸如# %%0 = %0 %%1 = %1之类的注释以查看选择了"=r""r"约束的寄存器可能会很有趣。在the Godbolt compiler explorer上:

# gcc9.2 -O3 
main_proc:
        movl     $55, %ecx
        call     proc         # %0 = %edi   %1 = %edi
        addl     %ecx, %eax    # "=r" happened to pick EAX,
                      # which happens to still hold the return value from  proc
        ret

在此函数内联到其他内容之后,可能不会发生选择EAX作为添加目的地的事故。或GCC碰巧将一些编译器生成的指令放在asm语句之间。 (asm volatile是编译时重新排序的障碍,但并不是一个笨拙的选择。它肯定完全停止了优化工作。)

请记住,内联汇编模板纯粹是文本替换;要求编译器将操作数填充到注释中与模板字符串中的其他任何地方都没有不同。 (默认情况下,Godbolt会删除注释行,因此有时将它们附加到其他指令或nop上很方便)。

如您所见,这是64位代码({x {1}}按照x86-64 SysV调用约定进入EDI,就像您的代码生成方式一样),因此n不会可编码。 push %ecx是。

当然,如果GCC实际上希望通过push %rcx闭包通过asm语句保留一个值,那么它将只使用"ecx"或任何其他未包含在调用中的寄存器垃圾清单。