图解 Linux 内核 ORC 解栈(unwinding)原理
ORC(Oops Rewind Capability)是一种内核栈展开机制,相比传统方法,它能以更低性能开销提供更可靠的栈回溯。
一、ORC 的核心数据结构是 ORC entry
一条 ORC entry 描述的是“某一指令地址处的栈形状”。在 objtool/内核头文件里(x86)你会看到类似 orc_entry的关键字段语义(不同版本名字略有变化):
1. sp_reg:用于计算“上一帧栈顶/CFA”的基寄存器(常见:ORC_REG_SP/ORC_REG_BP,还有 _INDIRECT以及个别保存的 reg 作为基)
2. sp_offset:偏移量,上一帧的某个值 = base + sp_offset
3. bp_reg/ bp_offset:可选的 BP 恢复规则
4. type:这一“栈记录”属于哪种返回形态
ORC_TYPE_CALL:普通 call 栈帧(返回地址就在栈上 sp - 8附近)
ORC_TYPE_REGS/ ORC_TYPE_REGS_IRET:表示“这里其实不是正常 call 链,而是异常/中断入口保存了一个 struct pt_regs”,IP 要从 pt_regs->ip取,SP 也从 pt_regs->sp恢复
也就是说:ORC 不依赖“栈上有一串 RBP 链表”,而是把“如何从当前 (IP,SP,BP,regs) 回到 caller”编码成一张查表问题。
二、编译期(objtool):把“栈规则”固化成一张只读表
objtool 以 .o为输入,做指令级栈状态仿真:逐条指令追踪 SP/BP 的变化、push/pop 配对、是否保存 pt_regs。据此为每个指令地址生成一个 orc_entry,描述“在此 IP 处,返回地址与上一帧 SP 如何定位”。所有函数的 ORC 条目被聚合成两个 ELF 段:.orc_unwind_ip(地址索引)和 .orc_unwind(规则数组),并在链接后排序、建立 orc_lookup[]加速索引。复杂推理全部发生在这里,内核运行时只消费结果。
三、运行期(unwinder):查表 + 简单算术完成回溯
当内核 oops/panic 时,unwind_start()用当前 regs初始化 unwind_state(IP/SP/BP)。unwind_next_frame()的核心逻辑是:
1)orc_find(IP-1)查表拿到当前帧的 orc_entry;
2)按 orc->sp_reg取运行时寄存器值(SP/BP),加上查表得到的 sp_offset,算出上一帧栈顶;
3)根据 orc->type取 caller IP:普通调用从栈槽读取,异常帧则从 pt_regs中取。
随后更新 state,循环直至遇到 ORC_REG_UNDEFINED或异常入口。整个过程无状态机、无递归、无内存分配,仅做“寄存器 + 常量偏移 + 内存读取”,在中断/NMI/故障上下文都能安全执行。
本质:编译期把“如何回溯”的规则算清楚,运行期只做查表与加法,用少量内存换取极高的确定性与 panic 安全性。