为什么 %eax 在调用 printf 之前被清零?
我正在尝试使用一点 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
对待 printf
和 testproc
不同。
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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
原因是可变参数函数的高效实现。当可变参数函数调用
va_start
时,编译器通常不清楚va_arg
是否会为浮点参数调用。因此,编译器始终必须保存可以保存参数的所有向量寄存器,以便将来潜在的va_arg
调用可以访问它,即使寄存器在此期间已被破坏。这是相当昂贵的,因为 x86-64 上有八个这样的寄存器。因此,调用者将向量寄存器的数量作为优化提示传递给可变参数函数。如果调用中没有涉及向量寄存器,则不需要保存任何向量寄存器。例如,glibc 中的 sprintf 函数的开头如下所示:
实际上,所有实现仅使用 %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 ifva_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 futureva_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: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
.在 x86_64 ABI 中,如果函数具有可变参数,则
AL
(它是EAX
的一部分)预计将保存用于保存该函数参数的向量寄存器的数量。在您的示例中:
有一个整数参数,因此不需要向量寄存器,因此
AL
设置为 0。另一方面,如果您将示例更改为:
则浮点文字为存储在向量寄存器中,相应地,
AL
设置为1
:正如预期:
将导致编译器将
AL
设置为2
因为有两个浮点参数:至于你的其他问题:
不应该。例如:
当使用
gcc -c -O0 -S
编译时,输出:并且
%eax
不会被清零。但是,如果删除#include
,则生成的程序集会在调用puts()
之前将%eax
清零:原因与你的第二个问题有关:
如果编译器没有看到函数的声明,那么它不会对其参数做出任何假设,并且该函数很可能接受变量参数。如果您指定空参数列表(您不应该这样做,并且它被 ISO/IEC 标记为过时的 C 功能),则同样适用。由于编译器没有足够的有关函数参数的信息,因此它会在调用函数之前将
%eax
清零,因为函数可能被定义为具有可变参数。例如:
其中
function()
有一个空参数列表,结果是:但是,如果您遵循在函数不接受参数时指定
void
的推荐做法,例如:那么编译器知道
function()
不接受参数 - 特别是,它不接受变量参数 - 因此在调用之前不会清除%eax
该函数:In the x86_64 ABI, if a function has variable arguments then
AL
(which is part ofEAX
) is expected to hold the number of vector registers used to hold arguments to that function.In your example:
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:
then the floating-point literal is stored in a vector register and, correspondingly,
AL
is set to1
:As expected:
will cause the compiler to set
AL
to2
since there are two floating-point arguments:As for your other questions:
It shouldn’t. For instance:
when compiled with
gcc -c -O0 -S
, outputs:and
%eax
is not zeroed out. However, if you remove#include <stdio.h>
then the resulting assembly does zero out%eax
right before callingputs()
:The reason is related to your second question:
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:
where
function()
has an empty parameter list, results in:However, if you follow the recommend practice of specifying
void
when the function accepts no parameters, such as: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:从 x86_64 System V ABI 注册用法表:
printf 是一个带有可变参数的函数,并且使用的向量寄存器的数量为零。
请注意,
printf
必须仅检查%al
,因为允许调用者在%rax
的高字节中留下垃圾。 (不过,xor %eax,%eax
是将%al
归零的最有效方法)请参阅此问答和x86 标签 wiki 了解更多详细信息,或了解最新信息如果上述链接已过时,请更新 ABI 链接。
From the x86_64 System V ABI register usage table:
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 x86 tag wiki for more details, or for up-to-date ABI links if the above link is stale.