为什么GCC使用MOV而不是推入函数调用?

nwsw7zdq  于 2022-11-13  发布在  其他
关注(0)|答案(3)|浏览(219)

我有一个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将参数推到堆栈上的新方法吗?如果是,有没有编译器标志可以用来切换这一点?

yks3o0rb

yks3o0rb1#

GCC manual说,

-mpush-args

推入指令将用于在调用函数时传递传出参数。默认情况下启用。

-mno-push-args

使用PUSH操作来存储传出参数。此方法较短,通常与使用SUB/MOV操作的方法一样快,并且默认情况下处于启用状态。在某些情况下,禁用此方法可能会提高性能,因为它改进了调度并减少了相关性。

-maccumulate-outgoing-args

如果启用,将在函数序言中计算传出参数所需的最大空间量。这在大多数现代CPU上更快,因为当首选堆栈边界不等于2时,依赖关系减少、调度改善和堆栈使用减少。缺点是代码大小明显增加。此开关隐含-mno-push-args。
即使默认启用-mpush-args,它也会被默认启用的-maccumulate-outgoing-args覆盖。编译时显式传递选项-mno-accumulate-outgoing-args可能会将指令更改为push

toe95027

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机器代码提示)。

68bkxrlz

68bkxrlz3#

像GCC这样的编译器是由那些 * 非常 * 仔细地考虑如何使经常使用的代码片段(比如函数调用/返回)尽可能高效的人编写的。当然,他们的解决方案针对的是一般情况,在特殊情况下可能会有更好的选择。

相关问题