无效地址函数调用后信号处理器内栈回溯触发SIGSEGV问题咨询
问题背景
开发C++程序的SIGSEGV/SIGABRT信号处理逻辑时,若段错误由调用无效地址的函数触发,信号处理器内无法正常完成栈回溯操作,复现测试代码如下:
#include <boost/stacktrace.hpp> #include <signal.h> void signal_handler(int signum) { if(signum == SIGSEGV) { write(STDERR_FILENO, "before\n", 7); boost::stacktrace::safe_dump_to(STDOUT_FILENO); write(STDERR_FILENO, "after\n", 6); } } int main() { struct sigaction sa={0}; sa.sa_handler = signal_handler; sigaction(SIGSEGV, &sa, nullptr); auto fp = reinterpret_cast<void(*)()>(0x55); fp(); }
使用g++ test.cpp && ./a.out命令编译运行后,程序仅打印before,既不打印after,也没有输出二进制栈数据。通过lldb调试可观察到如下现象:
lldb a.out (lldb) target create "a.out" Current executable set to '/home/russell/teststack/a.out' (x86_64). (lldb) r Process 88677 launched: '/home/russell/teststack/a.out' (x86_64) Process 88677 stopped * thread #1, name = 'a.out', stop reason = signal SIGSEGV: invalid address (fault address: 0x55) frame #0: 0x0000000000000055 error: memory read failed for 0x0 (lldb) bt * thread #1, name = 'a.out', stop reason = signal SIGSEGV: invalid address (fault address: 0x55) * frame #0: 0x0000000000000055 frame #1: 0x0000555555555247 a.out`main + 108 frame #2: 0x00007ffff7829290 libc.so.6`___lldb_unnamed_symbol3123 + 128 frame #3: 0x00007ffff782934a libc.so.6`__libc_start_main + 138 frame #4: 0x00005555555550a5 a.out`_start at start.S:115 (lldb) c Process 88677 resuming before Process 88677 stopped * thread #1, name = 'a.out', stop reason = signal SIGSEGV: invalid address (fault address: 0x55) frame #0: 0x00007ffff7e8d72e libgcc_s.so.1`uw_frame_state_for at md-unwind-support.h:63:6 (lldb) bt * thread #1, name = 'a.out', stop reason = signal SIGSEGV: invalid address (fault address: 0x55) * frame #0: 0x00007ffff7e8d72e libgcc_s.so.1`uw_frame_state_for at md-unwind-support.h:63:6 frame #1: 0x00007ffff7e8f4ab libgcc_s.so.1`_Unwind_Backtrace at unwind.inc:303:14 frame #2: 0x00005555555553de a.out`boost::stacktrace::detail::this_thread_frames::collect(void const**, unsigned long, unsigned long) + 122 frame #3: 0x0000555555555499 a.out`unsigned long boost::stacktrace::detail::this_thread_frames::safe_dump_to_impl<int>(int, unsigned long, unsigned long) + 106 frame #4: 0x00005555555551be a.out`signal_handler(int) + 69 frame #5: 0x00007ffff783e8e0 libc.so.6`___lldb_unnamed_symbol3219 + 1 frame #6: 0x0000000000000055 frame #7: 0x0000555555555247 a.out`main + 108 frame #8: 0x00007ffff7829290 libc.so.6`___lldb_unnamed_symbol3123 + 128 frame #9: 0x00007ffff782934a libc.so.6`__libc_start_main + 138 frame #10: 0x00005555555550a5 a.out`_start at start.S:115
aarch64架构上测试结果基本一致,仅触发的信号为SIGBUS而非SIGSEGV。核心疑问为:当栈顶栈帧为无效地址时,是否有可行方法能够正常获取栈回溯信息?
故障原因
- 调用无效函数指针时,CPU直接跳转到非法地址执行,栈顶返回地址对应的上下文完全无效,不存在合法的DWARF栈展开信息
- boost::stacktrace底层依赖libgcc的
_Unwind_Backtrace接口做栈遍历,逻辑默认从信号触发时保存的栈顶帧(即指向非法地址0x55的帧)开始逐帧解析,第一步读取0x55地址对应的unwind表就会触发二次段错误,回溯流程直接中断,后续合法栈帧完全没有机会被遍历到 - 当前代码注册信号处理函数时未设置
SA_SIGINFO标志,也没有为信号处理器配置独立备用栈,二次段错误触发后无对应处理逻辑,进程直接终止。
可行解决方案
方案1:利用sigaction上下文跳过无效首帧
注册信号处理函数时传入SA_SIGINFO标志,此时信号处理函数可以拿到内核保存的信号触发时的硬件上下文(ucontext_t结构),直接修改上下文中的指令指针寄存器,跳过无效的0x55栈帧后再启动栈回溯:
- x86_64架构下修改
ucontext->uc_mcontext.gregs[REG_RIP]的值,指向触发非法调用的指令的下一条地址(即main函数中fp();之后的位置) - aarch64架构下修改
ucontext->uc_mcontext.pc的值即可
修改后栈展开逻辑从合法的main栈帧开始遍历,不会触发二次段错误。使用该方案需要在信号处理函数中提前判断故障地址是否属于非法可执行地址区间,避免误跳过正常栈帧。
方案2:使用帧指针(FP)回溯替代DWARF回溯
编译时添加-fno-omit-frame-pointer选项,放弃依赖DWARF unwind表的回溯逻辑,直接通过帧指针寄存器(x86_64为RBP、aarch64为X29)遍历栈上保存的帧指针和返回地址:
- 该回溯方式不需要读取代码段的unwind元数据,遇到非法返回地址时可以直接做地址合法性校验,跳过无效地址后继续向上遍历栈
- 可以自行实现极简的FP遍历逻辑,不依赖boost::stacktrace的默认实现,遍历过程中通过解析
/proc/self/maps拿到进程合法可执行内存区间,只要判断返回地址不在合法区间就直接跳过 - 注意该方案要求所有依赖库(包括libc、libstdc++等)编译时均开启帧指针支持,否则回溯会提前截断,目前多数主流发行版的系统库已经默认开启帧指针,可直接使用。
方案3:配置备用栈+嵌套信号容错
- 调用
sigaltstack注册独立的备用信号栈,注册信号处理函数时加上SA_ONSTACK标志,避免信号处理逻辑在损坏的栈上执行 - 注册信号处理函数时添加
SA_NODEFER标志,允许信号处理函数运行时再次接收SIGSEGV/SIGBUS信号 - 单独实现极简的二次故障处理逻辑,当回溯过程触发二次段错误时,直接进入容错分支,从当前栈位置向上扫描内存,匹配符合可执行段地址范围、满足架构对齐要求的可能返回地址,完成剩余栈遍历
该方案兼容性最好,但实现复杂度最高,栈扫描逻辑需要针对不同架构做适配,存在极低概率的地址误报。
内容的提问来源于stack exchange,提问作者Russell Greene
相关产品推荐
相关产品推荐

