Gcc 为什么对_Noreturn的调用会生成真正的调用而不是跳转?
我看着Gcc 为什么对_Noreturn的调用会生成真正的调用而不是跳转?,gcc,assembly,clang,x86-64,icc,Gcc,Assembly,Clang,X86 64,Icc,我看着 //#define _Noreturn //<= uncomment to see jmp's change to call's _Noreturn void ext(int,char const*); __attribute((noinline)) _Noreturn void intern(int X) { ext(X,"X"); //jmp unless ext is _Noreturn } void do_intern(void) { intern(
//#define _Noreturn //<= uncomment to see jmp's change to call's
_Noreturn void ext(int,char const*);
__attribute((noinline))
_Noreturn void intern(int X)
{
ext(X,"X"); //jmp unless ext is _Noreturn
}
void do_intern(void)
{
intern(0); //jmp unless intern is _Noreturn
}
int do_int_intern(void)
{
intern(0); //call in either case,
//would've expected a jmp if intern is _Noreturn
return 42; //erased if intern is _Noreturn
}
/#define _Noreturn/对于您的示例,在为x86_64编译时,不能用JMP指令代替CALL指令,因为堆栈没有正确对齐。被调用者希望堆栈是16字节对齐加上8作为返回值
使用-Os
编译示例代码时,会在godbolt链接中的所有编译器上生成此程序集:
do_intern:
push rax ; ICC uses RSI here instead
xor edi, edi
call intern
要将调用更改为JMP,必须添加额外的指令以在堆栈上推送假返回值,或者撤消在函数顶部进行的堆栈对齐:
do_intern:
push rax
xor edi, edi
push rax ; or pop rax
jmp intern
现在,如果编译器真的很聪明,它就可以意识到确实不需要在函数开始时对齐堆栈,因此不需要撤消堆栈或推送假返回值:
do_intern:
xor edi, edi
jmp intern
但编译器不够聪明,无法做到这一点。可能是因为它在函数在堆栈上分配变量的一般情况下不起作用,也可能是因为通过提高调用永远不会返回的函数的性能,几乎没有什么好处
在没有_Noreturn的尾部调用情况下,调用指令可能在程序执行过程中被多次调用,因此值得作为一种特殊情况处理。使用_Noreturn,调用指令在程序执行期间只能调用一次(除非intern
最终递归调用do_intern
)。尽管这两种情况很相似,但需要新的代码来识别诺雷图恩特例
请注意,至少对于GCC来说,识别Noreturn特例比我最初想象的要困难得多,因为:
这是我的noreturn修补程序的错误--调用noreturn
函数不再有退出的优势,这意味着代码
打算插入sibcall_尾声模式没有
这是一个quandry:我们需要更精确的“控制范围”CFG
“非无效功能”测试结束。但是如果我们用暴力搜索
sibcalls要插入sibcall_尾声模式,我们将使用
flow2关于要删除死尾声代码(加载)的警告
因为保存的呼叫寄存器已失效,因为我们不返回)
我认为解决这一问题的最好方法是不要创建SIB调用
noreturn函数。[……]
术语“同级调用”指的是“同级”调用,其中尾部调用可以使用跳转指令而不是调用指令
这就是为什么,至少在最初,您期望的优化从未(正确地)在GCC中实现的原因。在大多数情况下,当调用非返回函数(如abort
)时,最好有更准确的回溯,这似乎是一个事后的理由,尽管这可能对GCC中从未实现的优化以及clang和ICC中的优化做出了重大贡献。对于您的示例,为x86_64编译时,JMP指令不能代替CALL指令,因为堆栈不会正确对齐。被调用者希望堆栈是16字节对齐加上8作为返回值
使用-Os
编译示例代码时,会在godbolt链接中的所有编译器上生成此程序集:
do_intern:
push rax ; ICC uses RSI here instead
xor edi, edi
call intern
要将调用更改为JMP,必须添加额外的指令以在堆栈上推送假返回值,或者撤消在函数顶部进行的堆栈对齐:
do_intern:
push rax
xor edi, edi
push rax ; or pop rax
jmp intern
现在,如果编译器真的很聪明,它就可以意识到确实不需要在函数开始时对齐堆栈,因此不需要撤消堆栈或推送假返回值:
do_intern:
xor edi, edi
jmp intern
但编译器不够聪明,无法做到这一点。可能是因为它在函数在堆栈上分配变量的一般情况下不起作用,也可能是因为通过提高调用永远不会返回的函数的性能,几乎没有什么好处
在没有_Noreturn的尾部调用情况下,调用指令可能在程序执行过程中被多次调用,因此值得作为一种特殊情况处理。使用_Noreturn,调用指令在程序执行期间只能调用一次(除非intern
最终递归调用do_intern
)。尽管这两种情况很相似,但需要新的代码来识别诺雷图恩特例
请注意,至少对于GCC来说,识别Noreturn特例比我最初想象的要困难得多,因为:
这是我的noreturn修补程序的错误--调用noreturn
函数不再有退出的优势,这意味着代码
打算插入sibcall_尾声模式没有
这是一个quandry:我们需要更精确的“控制范围”CFG
“非无效功能”测试结束。但是如果我们用暴力搜索
sibcalls要插入sibcall_尾声模式,我们将使用
flow2关于要删除死尾声代码(加载)的警告
因为保存的呼叫寄存器已失效,因为我们不返回)
我认为解决这一问题的最好方法是不要创建SIB调用
noreturn函数。[……]
术语“同级调用”指的是“同级”调用,其中尾部调用可以使用跳转指令而不是调用指令
这就是为什么,至少在最初,您期望的优化从未(正确地)在GCC中实现的原因。在大多数情况下,当调用非返回函数(如abort
)时,最好有更准确的回溯,这似乎是一个事后的理由,尽管这可能对GCC中从未实现的优化做出了重大贡献,以及clang和ICC。也许GCC假设noreturn函数可能希望回溯堆栈以显示它们在哪里