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函数可能希望回溯堆栈以显示它们在哪里