signal() 失效因不可靠:自动重置为默认行为、无信号屏蔽、不传siginfo_t,且调用非异步安全函数易二次崩溃;应改用sigaction()配sa_siginfo与sa_onstack,并用_exit()终止。

段错误发生时,signal() 为什么经常失效?
因为 signal() 在多数 Linux 系统上是不可靠的:它会将信号处理函数重置为默认行为(比如 SIGSEGV 又变回终止进程),且不保证信号屏蔽、无法传递 siginfo_t。更糟的是,若在信号处理中调用非异步信号安全函数(如 printf、malloc),会导致二次崩溃。
实操建议:
- 永远不要用
signal()处理SIGSEGV、SIGBUS等致命信号 - 若必须兼容老代码,至少用
siginterrupt(SIGSEGV, 0)防止系统调用被中断后不重启 - 真正可靠的起点是
sigaction(),它原子地安装 handler 并控制标志位
sigaction 捕获 SIGSEGV 的最小可靠写法
关键不是“能进 handler”,而是“进得安全、能诊断、不破坏上下文”。以下是最小但生产可用的骨架:
struct sigaction sa;
sa.sa_sigaction = [](int sig, siginfo_t* info, void* uctx) {
ucontext_t* context = static_cast<ucontext_t>(uctx);
// 注意:只调用 async-signal-safe 函数!
write(STDERR_FILENO, "SIGSEGV caught\n", 17);
// 打印出错地址(最常需信息)
char addr_buf[32];
int len = snprintf(addr_buf, sizeof(addr_buf), "addr=%p\n", info->si_addr);
write(STDERR_FILENO, addr_buf, len);
_exit(128 + SIGSEGV); // 不 return,不调用 exit()
};
sa.sa_flags = SA_SIGINFO | SA_ONSTACK; // 必须开 SA_SIGINFO 才能拿到 info
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, nullptr);
</ucontext_t>
注意点:
-
SA_ONSTACK很重要——段错误可能发生在栈溢出时,没独立信号栈就直接 crash - 必须用
sa_sigaction而非sa_handler,否则拿不到si_addr和寄存器上下文 -
_exit()是唯一安全的终止方式;return或exit()可能触发清理逻辑再次访问非法内存
从 siginfo_t 里能挖出什么真实线索?
siginfo_t 是调试段错误的核心数据源,但容易被忽略字段:
-
info->si_code区分原因:SEGV_MAPERR(地址无映射)、SEGV_ACCERR(权限错误,如往只读页写) -
info->si_addr是触发访问的虚拟地址,可结合/proc/self/maps判断是否在堆/栈/库区间 - 配合
ucontext_t中的uc_mcontext.gregs[REG_RIP](x86_64)可定位精确指令地址 - 注意:
si_code == SI_USER表示是kill()发的,不是真段错误——别误判
为什么加了 handler 还是看不到崩溃现场?
捕获成功 ≠ 调试成功。常见断点失效、core dump 缺失、日志乱序,本质是干扰了系统默认行为:
- 默认
SIGSEGV会生成 core 文件,但自定义 handler 后需手动raise(SIGABRT)或调用abort()触发 dump(前提是ulimit -c已设) - GDB 下,若用
handle SIGSEGV stop print,handler 仍会执行;想跳过 handler 直接停在 fault 指令,得用handle SIGSEGV nostop noprint pass - 多线程下,
SIGSEGV默认只发给出错线程,但sigaction是进程级设置,所有线程共享 handler——确保线程安全
真正难的从来不是注册 handler,而是在信号上下文中安全地提取、序列化、落盘足够多的上下文信息,又不引发二次故障。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!









