经近调用进入的函数执行远尾调用是否会破坏返回地址预测?
分析结论
_tail实现一定会导致返回栈缓冲(Return Stack Buffer, RSB)的返回预测错误。
核心原因
先明确RSB的硬件工作逻辑:
- RSB是CPU专门为函数返回设计的专用预测器,只识别
call和ret两类指令,完全不感知栈上手动构造的返回地址,也不会在执行普通跳转类指令时做额外的RSB操作:- 只要执行
call(不管是近调用call还是远调用lcall),CPU就会自动把call指令后面的下一条指令地址压入RSB的硬件栈; - 只要执行
ret(不管是近返回ret还是远返回lret),CPU就会直接从RSB栈顶弹出一个地址作为预测的返回目标,不会提前比对栈上的实际返回地址值。
- 只要执行
再对比两个实现的流程差异:
- 对照项
_non_tail用lcall进入64位的heavensgate代码段:执行lcall时CPU已经把lcall后面的ret指令地址压入了RSB,后续执行lret时弹出的RSB预测值正好和栈上实际返回地址一致,不会触发误预测。 - 目标项
_tail进入64位段用的是ljmp远跳转,不是任何形式的call指令:执行ljmp时CPU不会往RSB压入任何地址。等后续执行lret时,CPU依然会按照固定逻辑从RSB弹栈顶值做预测,这时候弹出的要么是更早之前其他函数调用压入的无关地址,要么是RSB为空时填充的默认哨兵值,哪怕手动在栈上构造的返回地址和正常call/ret流程的目标地址完全一样,RSB的预测值和实际返回目标也不匹配,必然产生误预测。
额外纠正一个认知偏差:描述里提到的“近调用与远返回配对”的前提不成立,_tail的整个执行流程里根本没有执行近调用指令,只是手动在栈上拼了远返回需要的CS段寄存器值+返回地址结构,全程没有触发RSB压栈的动作,这才是预测错误的核心根源。
待分析的原始汇编代码如下:
.globl _non_tail, _tail .text .code32 _non_tail: lcall $0x33, $_non_tail.heavensgate ret .code64 _non_tail.heavensgate: # do stuff. there's 12 bytes on the stack before the first argument lret .code32 _tail: pushl (%esp) movw %cs, 4(%esp) ljmp $0x33, $_tail.heavensgate .code64 _tail.heavensgate: # do stuff. there's 8 bytes on the stack before the first argument lret
内容的提问来源于stack exchange,提问作者Joseph Sible-Reinstate Monica
相关产品推荐
相关产品推荐

