我有一个C程序的例子。
int worship(long john)
{
return 0 * john;
}
int main()
{
return worship(666);
}
该程序集(基本上)如下所示:
worship(long):
pushq %rbp
movq %rsp, %rbp
movq %rdi, -8(%rbp)
movl $0, %eax
popq %rbp
ret
main:
pushq %rbp
movq %rsp, %rbp
movl $666, %edi
call worship(long)
popq %rbp
ret
我在阅读到堆栈粉碎时遇到了这个问题。在汇编代码worship(long):
部分,它说movq %rdi, -8(%rbp)
,我认为它应该使用pushq
,这是GCC将参数推到堆栈上的新方法吗?如果是,有没有编译器标志可以用来切换这一点?
3条答案
按热度按时间yks3o0rb1#
GCC manual说,
推入指令将用于在调用函数时传递传出参数。默认情况下启用。
使用PUSH操作来存储传出参数。此方法较短,通常与使用SUB/MOV操作的方法一样快,并且默认情况下处于启用状态。在某些情况下,禁用此方法可能会提高性能,因为它改进了调度并减少了相关性。
如果启用,将在函数序言中计算传出参数所需的最大空间量。这在大多数现代CPU上更快,因为当首选堆栈边界不等于2时,依赖关系减少、调度改善和堆栈使用减少。缺点是代码大小明显增加。此开关隐含-mno-push-args。
即使默认启用
-mpush-args
,它也会被默认启用的-maccumulate-outgoing-args
覆盖。编译时显式传递选项-mno-accumulate-outgoing-args
可能会将指令更改为push
。toe950272#
x86-64 System V传递寄存器RDI、RSI、RDX、RCX、R8、R9中的前6个整数参数。因此,在
main
中,我们使用mov $666, %edi
(零扩展到完整的RDI)来传递64位参数long john
。push
无法写入寄存器; nothing 1可以阻止GCC使用mov
来设置寄存器,您也不希望这样做。如果您传递了7个或更多参数,GCC通常会使用main
中的push
来传递堆栈上的第7个参数,因为-mno-accumulate-outgoing-args
是现代GCC中的默认值。push
在x86上一直很高效,因为Pentium-M等引入了一个“堆栈引擎”来跟踪堆栈-指针会专门更新。Sunil Bojanapally的答案涵盖了这些选项,这些选项与32位代码更相关,因为32位代码中所有的参数都在堆栈上传递。如果您是通过搜索标题问题找到这里的,请查看该答案或 * Why does gcc use movl instead of push to pass function args? * 该答案是关于实际问题的,即关于被调用方在调试版本中如何处理传入的参数,而不是如何将参数传递给它。
你说的是被调用方内部的代码,它将传入的参数存储到堆栈中。这不是 * 传递 * 参数,它只是a consequence of a debug build-每个C变量都获得一个内存地址,除非用默认的
-O0
反优化级别声明register
。编译器发出指令将传入的寄存器参数存储到堆栈中。在这种情况下,
movq %rdi, -8(%rbp)
被存储到RSP下面的red zone,因为worship()
是叶函数。堆栈空间已经被有效地保留(下至-128(%rsp)
,并且在该点RBP=RSP)。需要说明的是,这不是函数 * 调用*. Spilling incoming args to the stack inside the callee only happens in a debug build的一部分,也不是调用约定的一部分。
如果它需要
sub $16, %rsp
/mov
-store /leave
,例如,如果你用-mno-red-zone
编译,那么是的,用push %rdi
做溢出可能是一种优化。但是existing compilers don't do that optimization用于初始化+创建局部变量。worship
中的push %rdi
会要求编译器使用leave
,而不是只使用pop %rbp
,这会稍微增加开销。并且在push %rbp
将堆栈对齐到RSP%16 == 0之后,它只会将堆栈对齐到RSP%16 == 8;编译器倾向于使堆栈按16对齐,即使它们不再进行函数调用。当然,如果您刚刚启用了优化,
worship
将只是xor %eax,%eax
/ret
,而不会浪费指令将寄存器参数放在任何地方。脚注1:
-Oz
个(支持代码大小,不考虑速度)可能会使用3字节push imm8
/pop rdi
而不是5字节mov edi, imm32
来具体化寄存器中的值(如果该值在-128..+127范围内)。但666不是这样,因此mov
也是将寄存器设置为该值的最小方式,而没有任何预先存在的已知寄存器值接近该值。(代码golf x86-64机器代码提示)。68bkxrlz3#
像GCC这样的编译器是由那些 * 非常 * 仔细地考虑如何使经常使用的代码片段(比如函数调用/返回)尽可能高效的人编写的。当然,他们的解决方案针对的是一般情况,在特殊情况下可能会有更好的选择。