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对待printf和testproc不同.
小智 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)
Gun*_*iez 28
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和x86标记wiki,如果上述链接过时,请参阅最新的ABI链接.
原因是可变参数函数的有效实现。当可变参数函数调用 时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.