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

发布于 2024-11-10 18:33:48 字数 574 浏览 7 评论 0原文

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

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 不同。

I am trying to pick up a little x86. I am compiling on a 64bit mac with gcc -S -O0.

Code in C:

printf("%d", 1);

Output:

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

I do not understand why %eax is cleared to 0 before 'printf' is called. Since printf returns the number of characters printed to %eax my best guess it is zeroed out to prepare it for printf but I would have assumed that printf would have to be responsible for getting it ready. Also, in contrast, if I call my own function int testproc(int p1), gcc sees no need to prepare %eax. So I wonder why gcc treats printf and testproc differently.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

握住我的手 2024-11-17 18:33:49

原因是可变参数函数的高效实现。当可变参数函数调用 va_start 时,编译器通常不清楚 va_arg 是否会为浮点参数调用。因此,编译器始终必须保存可以保存参数的所有向量寄存器,以便将来潜在的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

The reason is the efficient implementation of variadic functions. When a variadic function calls va_start, it is often not clear to the compiler if va_arg will ever be invoked for a floating point argument. Therefore, the compiler always has to save all vector registers which can hold parameters, so that a potential future va_arg call can access it even if the register has been clobbered in the meantime. This is fairly costly because there are eight such registers on x86-64.

Therefore, the caller passes the number of vector registers as an optimization hint to the variadic function. If there are no vector registers involved in the call, none of them need to be saved. For example, the start of the sprintf function in glibc looks like this:

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

In practice, all implementations use %al only as flag, jumping over the vector save instructions if it is zero. A computed goto to avoid saving unnecessary registers does not seem to improve performance.

Furthermore, if compilers can detect that va_arg is never called for a floating point argument, they will optimize away the vector register save operation completely, so setting %al is superfluous in that case. But the caller cannot know that implementation detail, so it will still have to set %al.

白鸥掠海 2024-11-17 18:33:48

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

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

In the x86_64 ABI, if a function has variable arguments then AL (which is part of EAX) is expected to hold the number of vector registers used to hold arguments to that function.

In your example:

printf("%d", 1);

has an integer argument so there’s no need for a vector register, hence AL is set to 0.

On the other hand, if you change your example to:

printf("%f", 1.0f);

then the floating-point literal is stored in a vector register and, correspondingly, AL is set to 1:

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

As expected:

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

will cause the compiler to set AL to 2 since there are two floating-point arguments:

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

As for your other questions:

puts is also zeroing out %eax right before the call though it only takes a single pointer. Why is this?

It shouldn’t. For instance:

#include <stdio.h>

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

when compiled with gcc -c -O0 -S, outputs:

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

and %eax is not zeroed out. However, if you remove #include <stdio.h> then the resulting assembly does zero out %eax right before calling puts():

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

The reason is related to your second question:

This also happens before any call to my own void proc() function (even with -O2 set), but it is not zeroed when calling a void proc2(int param) function.

If the compiler doesn't see the declaration of a function then it makes no assumptions about its parameters, and the function could well accept variable arguments. The same applies if you specify an empty parameter list (which you shouldn’t, and it’s marked as an obsolescent C feature by ISO/IEC). Since the compiler doesn’t have enough information about the function parameters, it zeroes out %eax before calling the function because it might be the case that the function is defined as having variable arguments.

For example:

#include <stdio.h>

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

void test(void) {
    function();
}

where function() has an empty parameter list, results in:

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

However, if you follow the recommend practice of specifying void when the function accepts no parameters, such as:

#include <stdio.h>

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

void test(void) {
    function();
}

then the compiler knows that function() doesn't accept arguments — in particular, it doesn’t accept variable arguments — and hence doesn’t clear %eax before calling that function:

pushq   %rbp
movq    %rsp, %rbp
call    _function
leave
ret
对风讲故事 2024-11-17 18:33:48

x86_64 System V ABI 注册用法表:

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

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

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

请参阅此问答 标签 wiki 了解更多详细信息,或了解最新信息如果上述链接已过时,请更新 ABI 链接。

From the x86_64 System V ABI register usage table:

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

printf is a function with variable arguments, and the number of vector registers used is zero.

Note that printf must check only %al, because the caller is allowed to leave garbage in the higher bytes of %rax. (Still, xor %eax,%eax is the most efficient way to zero %al)

See the this Q&A and the tag wiki for more details, or for up-to-date ABI links if the above link is stale.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文