为什么此函数将RAX作为第一个操作推送到堆栈? 汇编中的C++源代码如下。为什么RAX被推到堆栈中

为什么此函数将RAX作为第一个操作推送到堆栈? 汇编中的C++源代码如下。为什么RAX被推到堆栈中,c++,assembly,x86,x86-64,abi,C++,Assembly,X86,X86 64,Abi,我从ABI中了解到,RAX可以包含调用函数中的任何内容。但我们将其保存在这里,然后将堆栈向后移动8个字节。所以堆栈上的RAX,我认为只与std::\uuuu throw\u bad\u function\u call()操作相关 守则:- #include <functional> void f(std::function<void()> a) { a(); } 我相信原因是显而易见的,但我正在努力找出它 下面是一个没有std::function包装器的ta

我从ABI中了解到,RAX可以包含调用函数中的任何内容。但我们将其保存在这里,然后将堆栈向后移动8个字节。所以堆栈上的RAX,我认为只与
std::\uuuu throw\u bad\u function\u call()
操作相关

守则:-

#include <functional> 

void f(std::function<void()> a) 
{
  a(); 
}
我相信原因是显而易见的,但我正在努力找出它

下面是一个没有
std::function
包装器的tailcall,用于比较:

void g(void(*a)())
{
  a(); 
}
琐碎的:

g(void (*)()):             # @g(void (*)())
        jmp     rdi        # TAILCALL
在调用
指令之前,需要将堆栈对齐到16字节

call
在堆栈上推送一个8字节的返回地址,这会中断对齐,因此编译器需要在下一次
调用之前将堆栈再次对齐到16的倍数

(ABI设计选择在
调用之前而不是之后需要对齐,这有一个次要的优点,即如果在堆栈上传递了任何arg,此选择会使第一个arg 16B对齐。)


推送“不在乎”值效果很好,并且可能比启用子rsp 8更有效。(参见注释)。

原因在于,在执行
je.LBB0_1
分支的情况下,将堆栈重新对齐到16字节边界,以符合。放在堆栈上的值不相关。另一种方法是用
子RSP,8
从RSP中减去8。ABI以以下方式说明路线:

输入参数区域的末端应与16(32,如果为m256)对齐 在堆栈上传递)字节边界。换句话说,值(%rsp+8)总是 当控制转移到功能入口点时,16(32)的倍数。堆栈指针%rsp始终指向最近分配的堆栈帧的末尾

在调用函数
f
之前,堆栈按照调用约定对齐16字节。通过调用
f
传输控制后,返回地址被放置在堆栈上,使堆栈偏离8
push rax
是一种从RSP中减去8并重新校准的简单方法。如果分支被带到
调用std::\uuuu throw\u bad\u function\u call()
堆栈将正确对齐,以便调用正常工作

在比较失败的情况下,一旦执行
add rsp,8
指令,堆栈将与函数条目中的堆栈一样出现。调用函数
f
的返回地址现在将回到堆栈顶部,堆栈将再次错位8。这就是我们想要的,因为正在使用
jmp qword ptr[rdi+24]
制作一个函数,以将控制权转移到函数
a
。这将使JMP无法调用该函数。当函数
a
执行RET时,它将直接返回调用
f
的函数

在更高的优化级别上,我希望编译器应该足够聪明来进行比较,并让它直接进入JMP。然后,标签
.LBB0\u 1
上的内容可以将堆栈与16字节边界对齐,以便
调用std::\uuuuuuuuu throw\ubad\u function\ucall()
正常工作


正如@CodyGray所指出的,如果使用优化级别为
-O2
或更高的GCC(而不是CLANG),那么生成的代码看起来确实更合理。GCC 6.1的输出为:

f(标准::函数):
cmp QWORD PTR[rdi+16],0#MEM[(bool(*)(联合任何数据和,常数联合任何数据和,管理器操作)*)a#2(D)+16B],
je.L7#,
jmp[QWORD PTR[rdi+24]#MEM[(const struct function*)a_2(D)]。_M_调用程序
.L7:
副rsp,8#,
调用标准::uuu抛出_u坏函数_u调用()#

这段代码更符合我的预期。在这种情况下,GCC的优化器似乎可以比CLANG更好地处理此代码生成。

在其他情况下,CLANG通常会在返回前修复堆栈

使用
push
可以提高代码大小的效率(
push
仅为1字节,而
sub-rsp为4字节,8
),在英特尔CPU上也可以使用UOP。(不需要堆栈同步uop,如果您直接访问
rsp
,就会得到它,因为将我们带到当前函数顶部的
调用会使堆栈引擎“脏”)

这个冗长而杂乱无章的答案讨论了使用
push-rax
/
pop-rcx
对齐堆栈的最坏性能风险,以及
rax
rcx
是否是寄存器的好选择。
(很抱歉这么长时间。)

(TL:DR:看起来不错,可能的缺点通常很小,而在普通情况下,优点是值得的。但是,如果
al
ax
是“脏的”,Core2/Nehalem上的部分寄存器暂停可能是个问题。没有其他64位CPU有大问题(因为它们不能重命名部分寄存器,或有效地合并),并且32位代码需要超过1个额外的
推送
,以将堆栈与另一个
调用对齐16,除非它已经保存/恢复了一些保留调用的reg以供自己使用。)


使用
push rax
而不是
sub rsp,8
引入了对
rax
旧值的依赖,因此如果
rax
的值是长延迟依赖链(和/或缓存未命中)的结果,您可能会认为这可能会减慢速度

e、 g.调用方可能对
rax
做了一些与函数args无关的慢动作,比如
var=table[x%y];var2=foo(x)

幸运的是,无序执行将在这里做得很好

推送
不会使
g(void (*)()):             # @g(void (*)())
        jmp     rdi        # TAILCALL
f(std::function<void ()>):
        cmp     QWORD PTR [rdi+16], 0     # MEM[(bool (*<T5fc5>) (union _Any_data &, const union _Any_data &, _Manager_operation) *)a_2(D) + 16B],
        je      .L7 #,
        jmp     [QWORD PTR [rdi+24]]      # MEM[(const struct function *)a_2(D)]._M_invoker
.L7:
        sub     rsp, 8    #,
        call    std::__throw_bad_function_call()        #
# example caller that leaves RAX not-ready for a long time

mov   rdi, rax              ; prepare function arg

div   rbx                   ; very high latency
mov   rax, [table + rdx]    ; rax = table[ value % something ], may miss in cache
mov   [rsp + 24], rax       ; spill the result.

call  foo                   ; foo uses push rax to align the stack