使用backtrace收集崩溃调用栈时缺失testCrash函数的问题排查
问题分析与解决方案
首先,你遇到的问题非常典型——backtrace()默认只会捕获当前信号处理函数所在线程的调用栈,而非触发崩溃的那个线程的原始调用栈。
为什么看不到testCrash?
当testCrash触发空指针访问时,系统会发送SIGSEGV信号。此时内核会暂停崩溃线程,切换到专门的信号处理线程来执行你的SignalExceptionHandler。这时候调用backtrace(),拿到的是信号处理线程的调用链(也就是从SignalExceptionHandler往上的栈),而不是崩溃线程里testCrash所在的原始栈。日志里的_sigtramp是信号跳转的中间函数,后面的???就是未被捕获到的原始崩溃栈片段。
修复方案:捕获崩溃线程的上下文
要拿到原始崩溃栈,你需要在信号处理函数中获取崩溃线程的上下文信息,通过ucontext_t结构体提取崩溃时的寄存器状态,进而解析出原始调用栈。
1. 修改信号处理函数,加入上下文解析
#include <ucontext.h> void SignalExceptionHandler(int signal, siginfo_t *info, void *context) { NSMutableString *mstr = [[NSMutableString alloc] init]; [mstr appendString:@"Stack:\n"]; // 获取崩溃时的线程上下文 ucontext_t *uc = (ucontext_t *)context; void* callstack[128]; int frames = 0; // 根据架构提取崩溃时的PC(程序计数器)和FP(帧指针) #if defined(__arm64__) uintptr_t pc = uc->uc_mcontext.__ss.__pc; uintptr_t *frame = (uintptr_t *)uc->uc_mcontext.__ss.__fp; #elif defined(__x86_64__) uintptr_t pc = uc->uc_mcontext->__ss.rip; uintptr_t *frame = (uintptr_t *)uc->uc_mcontext->__ss.rbp; #endif // 手动遍历原始栈帧 callstack[frames++] = (void *)pc; while (frame && frames < 128) { callstack[frames++] = (void *)frame[1]; // 取出下一个栈帧的PC frame = (uintptr_t *)frame[0]; // 移动到下一个帧指针 } // 转换为可读符号 char** strs = backtrace_symbols(callstack, frames); for (int i = 0; i < frames; ++i) { [mstr appendFormat:@"%s\n", strs[i]]; } NSLog(@"%@", mstr); free(strs); // 重新触发信号,让系统正常处理崩溃(避免App卡死) signal(signal, SIG_DFL); raise(signal); }
2. 改用sigaction安装信号处理器
原来的signal()函数无法传递上下文信息,需要换成sigaction:
void InstallSignalHandler(void) { struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_sigaction = SignalExceptionHandler; sa.sa_flags = SA_SIGINFO; // 启用上下文传递 sigaction(SIGHUP, &sa, NULL); sigaction(SIGINT, &sa, NULL); sigaction(SIGQUIT, &sa, NULL); sigaction(SIGABRT, &sa, NULL); sigaction(SIGILL, &sa, NULL); sigaction(SIGSEGV, &sa, NULL); sigaction(SIGFPE, &sa, NULL); sigaction(SIGBUS, &sa, NULL); sigaction(SIGPIPE, &sa, NULL); }
额外注意事项
- 符号化问题:如果是Release版本,App的符号表可能被剥离,需要用
atos工具结合dSYM文件手动解析地址到函数名;Debug版本下应该能直接看到testCrash的函数名。 - 架构兼容性:上面的代码适配了arm64和x86_64架构,不同架构的寄存器名称不同,需要对应调整。
- 必须重新触发信号:处理完栈信息后,一定要恢复默认信号处理并重新触发崩溃,否则App会卡在信号处理函数中无法正常退出。
这样修改后,你就能在日志里看到testCrash函数出现在调用栈的顶部了。
内容的提问来源于stack exchange,提问作者wangqi060934




