48

我正在尝试拿起一点x86。我正在使用 gcc -S -O0 在 64 位 mac 上编译。

C中的代码:

printf("%d", 1);

输出:

movl    $1, %esi
leaq    LC0(%rip), %rdi
movl    $0, %eax        ; WHY?
call    _printf

我不明白为什么在调用 'printf' 之前 %eax 被清除为 0。由于printf将打印的字符数返回到%eax我的最佳猜测,它被归零以准备它,printf但我会假设它printf必须负责准备好它。另外,相比之下,如果我调用自己的函数int testproc(int p1)gcc则不需要准备%eax。所以我想知道为什么gcc对待printftestproc不同。

4

3 回答 3

54

在 x86_64 ABI 中,如果一个函数具有可变参数,那么AL(它是 的一部分EAX)应该保存用于保存该函数参数的向量寄存器的数量。

在您的示例中:

printf("%d", 1);

有一个整数参数,因此不需要向量寄存器,因此AL设置为 0。

另一方面,如果您将示例更改为:

printf("%f", 1.0f);

然后将浮点文字存储在向量寄存器中,并相应AL地设置为1

movsd   LC1(%rip), %xmm0
leaq    LC0(%rip), %rdi
movl    $1, %eax
call    _printf

正如预期的那样:

printf("%f %f", 1.0f, 2.0f);

将导致编译器设置AL为,2因为有两个浮点参数:

movsd   LC0(%rip), %xmm0
movapd  %xmm0, %xmm1
movsd   LC2(%rip), %xmm0
leaq    LC1(%rip), %rdi
movl    $2, %eax
call    _printf

至于你的其他问题:

puts%eax尽管它只需要一个指针,但它也在调用之前归零。为什么是这样?

它不应该。例如:

#include <stdio.h>

void test(void) {
    puts("foo");
}

使用 编译时gcc -c -O0 -S,输出:

pushq   %rbp
movq    %rsp, %rbp
leaq    LC0(%rip), %rdi
call    _puts
leave
ret

并且%eax没有归零。但是,如果您删除,则生成的程序集在调用之前#include <stdio.h>会归零:%eaxputs()

pushq   %rbp
movq    %rsp, %rbp
leaq    LC0(%rip), %rdi
movl    $0, %eax
call    _puts
leave
ret

原因与您的第二个问题有关:

这也发生在对我自己的 void proc() 函数的任何调用之前(即使设置了 -O2 ),但在调用 void proc2(int param) 函数时它不会归零。

如果编译器没有看到函数的声明,那么它不会对其参数做任何假设,并且该函数可以很好地接受可变参数。如果您指定一个空参数列表(您不应该这样做,并且它被 ISO/IEC 标记为过时的 C 功能),这同样适用。由于编译器没有关于函数参数的足够信息,它%eax会在调用函数之前清零,因为函数可能被定义为具有可变参数。

例如:

#include <stdio.h>

void function() {
    puts("foo");
}

void test(void) {
    function();
}

wherefunction()有一个空的参数列表,结果是:

pushq   %rbp
movq    %rsp, %rbp
movl    $0, %eax
call    _function
leave
ret

但是,如果您遵循指定void函数何时不接受参数的推荐做法,例如:

#include <stdio.h>

void function(void) {
    puts("foo");
}

void test(void) {
    function();
}

然后编译器知道它function()不接受参数 - 特别是,它不接受可变参数 - 因此%eax在调用该函数之前不会清除:

pushq   %rbp
movq    %rsp, %rbp
call    _function
leave
ret
于 2011-06-02T09:45:03.570 回答
41

来自x86_64 System V ABI寄存器使用表:

  • %rax       临时登记册;使用可变参数传递有关使用的向量寄存器数量的信息;第一个返回寄存器...

printf是一个具有可变参数的函数,使用的向量寄存器的数量为零。

注意printf必须只检查%al,因为调用者被允许在高字节中留下垃圾%rax。(不过,xor %eax,%eax归零是最有效的方法%al

有关更多详细信息,请参阅此 Q&A标签 wiki,或者如果上述链接已过时,请参阅最新的 ABI 链接。

于 2011-06-02T09:36:33.860 回答
9

原因是可变参数函数的高效实现。当可变参数函数调用va_start时,编译器通常不清楚是否va_arg会为浮点参数调用。因此,编译器总是必须保存所有可以保存参数的向量寄存器,以便将来可能的va_arg调用可以访问它,即使在此期间寄存器已被破坏。这是相当昂贵的,因为在 x86-64 上有八个这样的寄存器。

因此,调用者将向量寄存器的数量作为优化提示传递给可变参数函数。如果调用中不涉及向量寄存器,则不需要保存它们。例如,sprintfglibc 中函数的开头如下所示:

00000000000586e0 <_IO_sprintf@@GLIBC_2.2.5>:
   586e0:       sub    $0xd8,%rsp
   586e7:       mov    %rdx,0x30(%rsp)
   586ec:       mov    %rcx,0x38(%rsp)
   586f1:       mov    %r8,0x40(%rsp)
   586f6:       mov    %r9,0x48(%rsp)
   586fb:       test   %al,%al
   586fd:       je     58736 <_IO_sprintf@@GLIBC_2.2.5+0x56>
   586ff:       movaps %xmm0,0x50(%rsp)
   58704:       movaps %xmm1,0x60(%rsp)
   58709:       movaps %xmm2,0x70(%rsp)
   5870e:       movaps %xmm3,0x80(%rsp)
   58716:       movaps %xmm4,0x90(%rsp)
   5871e:       movaps %xmm5,0xa0(%rsp)
   58726:       movaps %xmm6,0xb0(%rsp)
   5872e:       movaps %xmm7,0xc0(%rsp)
   58736:       mov    %fs:0x28,%rax

在实践中,所有实现%al都只使用作为标志,如果它是零则跳过向量保存指令。为避免保存不必要的寄存器而计算的 goto 似乎并不能提高性能。

此外,如果编译器可以检测到va_arg从不调用浮点参数,它们将完全优化向量寄存器保存操作,因此%al在这种情况下设置是多余的。但是调用者无法知道实现细节,所以它仍然需要设置%al

于 2019-11-03T21:04:31.320 回答