我想尝试一下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
以不同的方式对待printf
和testproc
。
答案 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)
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
答案 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
。