为什么在调用printf之前%eax归零?

时间:2011-06-02 09:26:17

标签: macos gcc assembly x86-64 cpu-registers

我想尝试一下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

3 个答案:

答案 0 :(得分:36)

在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>,则生成的程序集会在调用%eax之前将puts()置零:

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();
}

其中function()有一个空参数列表,结果为:

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

答案 1 :(得分:25)

来自x86_64 System V ABI

Register    Usage
%rax        temporary register; with variable arguments
            passes information about the number of vector
            registers used; 1st return register
...

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

请注意,printf必须仅检查%al,因为允许调用者在%rax的较高字节中留下垃圾。 (xor %eax,%eax仍是%al

的最有效方法

如果上述链接过时,请参阅this Q&A代码维基以获取更多详细信息,或查看最新的ABI链接。

答案 2 :(得分:3)

原因是variadic functions的有效实现。当可变参数函数调用va_start时,编译器通常不清楚是否会为浮点参数调用va_arg。因此,编译器始终必须保存所有可保存参数的 all 向量寄存器,以便即使在此期间寄存器已被破坏的情况下,潜在的将来va_arg调用也可以对其进行访问。这是相当昂贵的,因为在x86-64上有八个这样的寄存器。

因此,调用方将向量寄存器的数量作为优化提示传递给可变参数函数。如果调用中没有向量寄存器,则无需保存任何向量寄存器。例如,glibc中sprintf函数的开始看起来像这样:

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