21、信号处理调试:SIGSEGV、SIGABRT的捕获
说到信号处理,我脑子里第一个蹦出来的就是当年那个通宵排查的野指针问题。那时候刚入行不久,程序跑着跑着就崩了,连个像样的日志都没留下。后来才明白——信号处理,就是给程序崩溃装个黑匣子。
今天咱们聊聊两个最要命的信号:SIGSEGV(段错误)和 SIGABRT(异常终止)。你想想看,程序崩了不可怕,可怕的是崩了你不知道它为什么崩。捕获信号,就是让程序在临死前留下遗言。
21.1 信号是什么?
信号是操作系统发给进程的一种异步通知。说白了,就是内核告诉你:「嘿,你的程序出事了,赶紧处理一下。」
常见的致命信号有这些:
| 信号 | 默认行为 | 典型触发场景 |
|---|---|---|
| SIGSEGV | 终止进程 + core dump | 访问非法内存地址(野指针、数组越界) |
| SIGABRT | 终止进程 + core dump | 调用 abort()、assert 失败 |
| SIGFPE | 终止进程 + core dump | 除零操作、浮点异常 |
| SIGILL | 终止进程 + core dump | 非法指令、损坏的代码段 |
嗯,这里要注意:不是所有信号都能被捕获。SIGKILL 和 SIGSTOP 就不行,这是内核保留的「核武器」。
21.2 捕获 SIGSEGV 和 SIGABRT
捕获信号用 signal() 或者 sigaction()。我个人习惯用 sigaction(),因为它更可靠,行为也更可控。
先看一个最简单的例子:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void signal_handler(int sig) {
// 注意:信号处理函数中能做的事非常有限
// 最好只做简单记录,然后退出
fprintf(stderr, "捕获到信号 %d\n", sig);
_exit(1);
}
int main() {
struct sigaction sa;
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGABRT, &sa, NULL);
// 故意触发段错误
int *p = NULL;
*p = 42; // 这里会触发 SIGSEGV
return 0;
}
运行后你会看到输出:捕获到信号 11(SIGSEGV 的编号就是 11)。
21.3 获取更多崩溃信息
光知道「收到了 SIGSEGV」还不够。我想知道的是:哪行代码?哪个变量?当时的调用栈是什么?
这里就要用到 sigaction 的 SA_SIGINFO 标志了。它能让信号处理函数拿到更多信息:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <ucontext.h>
#include <execinfo.h>
void signal_handler(int sig, siginfo_t *info, void *context) {
fprintf(stderr, "信号: %d\n", sig);
fprintf(stderr, "错误地址: %p\n", info->si_addr);
// 打印调用栈
void *buffer[100];
int n = backtrace(buffer, 100);
backtrace_symbols_fd(buffer, n, STDERR_FILENO);
_exit(1);
}
int main() {
struct sigaction sa;
sa.sa_sigaction = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO; // 关键标志
sigaction(SIGSEGV, &sa, NULL);
// 触发段错误
int *p = (int*)0x12345678;
*p = 42;
return 0;
}
输出会包含错误地址和调用栈信息。我在项目中遇到过好几次,靠这个调用栈直接定位到了野指针的源头。
-g -rdynamic 选项,调用栈信息会更详细,能显示函数名和行号。
21.4 捕获 SIGABRT 的特殊场景
SIGABRT 通常不是意外触发的,而是程序主动调用了 abort() 或者 assert 失败。但捕获它同样有价值——尤其是你想在断言失败时做点清理工作。
我曾经在一个嵌入式项目里,用 SIGABRT 的捕获来做「最后的日志刷出」。因为有些日志是缓冲的,程序一崩就丢了。捕获 SIGABRT 后,先把缓冲区刷到 Flash,再退出。
void abort_handler(int sig) {
// 刷出日志缓冲区
log_flush();
// 保存崩溃现场到 EEPROM
save_crash_context();
// 然后退出
_exit(1);
}
int main() {
signal(SIGABRT, abort_handler);
// 某个断言失败
assert(0); // 会触发 SIGABRT
return 0;
}
21.5 信号处理的限制与陷阱
信号处理不是万能的。有几个坑我得提醒你:
- 信号处理函数中不能做复杂操作——比如不能调用 malloc、free、printf、互斥锁等。这些函数不是异步信号安全的。
- 不能捕获所有信号——SIGKILL 和 SIGSTOP 是抓不住的。
- 多线程环境要小心——信号默认发给主线程,但 SIGSEGV 会发给触发它的线程。处理不好可能死锁。
- 信号处理函数执行完后,程序行为不确定——除非你调用 _exit() 或 longjmp() 跳出去。
说白了,信号处理函数就是个「临终遗言」的地方,别指望它做太多事。
21.6 实战:一个完整的崩溃捕获框架
下面是我个人常用的一个框架,整合了信号捕获、调用栈打印、日志刷出等功能:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <execinfo.h>
#include <ucontext.h>
#include <unistd.h>
#define BACKTRACE_SIZE 100
void crash_handler(int sig, siginfo_t *info, void *context) {
// 1. 打印信号信息
write(STDERR_FILENO, "=== CRASH DETECTED ===\n", 23);
char buf[64];
int len = snprintf(buf, sizeof(buf), "Signal: %d\n", sig);
write(STDERR_FILENO, buf, len);
len = snprintf(buf, sizeof(buf), "Address: %p\n", info->si_addr);
write(STDERR_FILENO, buf, len);
// 2. 打印调用栈
void *buffer[BACKTRACE_SIZE];
int n = backtrace(buffer, BACKTRACE_SIZE);
backtrace_symbols_fd(buffer, n, STDERR_FILENO);
// 3. 刷出日志(假设有日志系统)
// log_flush();
// 4. 退出
_exit(1);
}
void setup_crash_handler() {
struct sigaction sa;
sa.sa_sigaction = crash_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO | SA_NODEFER;
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGABRT, &sa, NULL);
sigaction(SIGFPE, &sa, NULL);
sigaction(SIGILL, &sa, NULL);
}
int main() {
setup_crash_handler();
// 正常业务逻辑...
// 如果崩溃了,会打印调用栈
return 0;
}
这个框架我用了好几年,在好几个项目里都帮了大忙。尤其是嵌入式环境,没有 GDB 的时候,这个就是救命稻草。
21.7 知识体系总览
下面这张图总结了信号处理调试的核心逻辑:
21.8 避坑指南
最后,分享几个我踩过的坑:
- 不要在信号处理函数里用 longjmp() 跳回去继续执行——程序状态已经损坏了,跳回去只会引发更奇怪的问题。
- 记得在信号处理函数末尾调用 _exit()——而不是 exit()。exit() 会调用 atexit 注册的函数,可能再次触发信号。
- 多线程程序里,信号处理函数要加锁吗?——不建议。信号处理函数中加锁很容易死锁。最好用无锁队列或者直接 write()。
- 我曾经在 SIGSEGV 处理函数里调用了 printf——结果程序直接卡死,连错误信息都没打印出来。后来改用 write() 才搞定。
信号处理调试,说白了就是给程序装个「黑匣子」。平时用不上,但一旦出问题,它就是救命稻草。希望今天的内容能帮你少走一些弯路。
公众号:蓝海资料掘金营,微信deep3321