为什么此函数将RAX作为第一个操作推送到堆栈? 汇编中的C++源代码如下。为什么RAX被推到堆栈中
我从ABI中了解到,RAX可以包含调用函数中的任何内容。但我们将其保存在这里,然后将堆栈向后移动8个字节。所以堆栈上的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
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
传输控制后,返回地址被放置在堆栈上,使堆栈偏离8push 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