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

sh5*_*h54 32 macos assembly gcc x86-64 cpu-registers

我想拿起一点x86.我正在使用gcc -S -O0在64位mac上进行编译.

C中的代码:

printf("%d", 1);
Run Code Online (Sandbox Code Playgroud)

输出:

movl    $1, %esi
leaq    LC0(%rip), %rdi
movl    $0, %eax        ; WHY?
call    _printf
Run Code Online (Sandbox Code Playgroud)

我不明白为什么在调用'printf'之前%eax被清除为0.由于printf返回打印的字符数量为%eax我最好的猜测,因此将其归零以准备它,printf但我认为printf必须负责准备它.而且,相反,如果我调用自己的功能int testproc(int p1),则gcc认为没有必要准备%eax.所以我想知道为什么gcc对待printftestproc不同.

小智 37

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

在你的例子中:

printf("%d", 1);
Run Code Online (Sandbox Code Playgroud)

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

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

printf("%f", 1.0f);
Run Code Online (Sandbox Code Playgroud)

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

movsd   LC1(%rip), %xmm0
leaq    LC0(%rip), %rdi
movl    $1, %eax
call    _printf
Run Code Online (Sandbox Code Playgroud)

正如所料:

printf("%f %f", 1.0f, 2.0f);
Run Code Online (Sandbox Code Playgroud)

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

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

至于你的其他问题:

puts%eax虽然只需要一个指针,但在调用之前也会将其清零.为什么是这样?

它不应该.例如:

#include <stdio.h>

void test(void) {
    puts("foo");
}
Run Code Online (Sandbox Code Playgroud)

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

pushq   %rbp
movq    %rsp, %rbp
leaq    LC0(%rip), %rdi
call    _puts
leave
ret
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

例如:

#include <stdio.h>

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

void test(void) {
    function();
}
Run Code Online (Sandbox Code Playgroud)

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

pushq   %rbp
movq    %rsp, %rbp
movl    $0, %eax
call    _function
leave
ret
Run Code Online (Sandbox Code Playgroud)

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

#include <stdio.h>

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

void test(void) {
    function();
}
Run Code Online (Sandbox Code Playgroud)

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

pushq   %rbp
movq    %rsp, %rbp
call    _function
leave
ret
Run Code Online (Sandbox Code Playgroud)

  • ABI 的注释:“我们使用向量寄存器来指代 SSE 或 AVX 寄存器。” (2认同)
  • 在 `%rax` 中传递向量计数有什么好处?是否仅出于性能,以避免在“寄存器保存区域”上保存无用的寄存器? (2认同)
  • @CiroSantilli新疆改造中心法轮功六四事件:是的。实际要求是 AL &gt;= FP/向量参数的数量。您不能使用“AL=0”让被调用者仅在堆栈中查找 FP 参数;这是 ABI 违规,如果按照 GCC 通常的方式编译,被调用者将只加载从未溢出 xmm0..7 的内存。 (2认同)

Gun*_*iez 28

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
...
Run Code Online (Sandbox Code Playgroud)

printf 是具有可变参数的函数,并且使用的向量寄存器的数量为零.

注意,printf必须只检查%al,因为允许调用者在更高的字节中留下垃圾%rax.(仍然xor %eax,%eax是最有效的零方式%al)

有关详细信息,请参阅此Q&A标记wiki,如果上述链接过时,请参阅最新的ABI链接.

  • 对于记录,它发生在调用`void proc()之前,因为C中的签名实际上没有说明proc的arity,它也可能是一个可变函数,因此需要将rax归零.`void proc()`与`void proc(void)`不同.请参阅http://stackoverflow.com/questions/693788/c-void-arguments (5认同)
  • `puts` 也在调用之前将 `%eax` 清零,尽管它只需要一个指针。为什么是这样? (2认同)

Flo*_*mer 8

原因是可变参数函数的有效实现。当可变参数函数调用 时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
Run Code Online (Sandbox Code Playgroud)

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

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