assert不能用于生产环境崩溃诊断,因其在NDEBUG定义后完全消失、无调用栈输出;需通过跨平台信号/异常接管(如SetUnhandledExceptionFilter、sigaction)配合异步信号安全的堆栈符号化解析实现统一崩溃捕获。

为什么 assert 不能直接用于生产环境崩溃诊断
assert 在 NDEBUG 宏定义后完全消失,且不输出调用栈;一旦程序在 Release 模式下崩溃,你只能看到“已停止工作”或信号中断,没有任何上下文。真正需要的是:即使关闭断言检查,也能在崩溃时自动抓取堆栈,并支持 Windows(SEH)、Linux(sigaction + backtrace)、macOS(signal + backtrace_symbols)三端统一触发点。
核心思路不是重写断言逻辑,而是接管崩溃信号/异常,并在其中完成符号化解析与打印——这意味着你必须绕过标准库的 abort() 或 std::terminate 默认行为。
- Windows 下需用
SetUnhandledExceptionFilter捕获EXCEPTION_ACCESS_VIOLATION等结构化异常,不能只依赖signal(SIGSEGV) - Linux/macOS 必须用
sigaction(而非signal),避免信号处理函数被重入或中断 - 堆栈地址转符号需加载调试信息:Linux 依赖
libdl和libexecinfo,macOS 需-ldl,Windows 则靠DbgHelp.lib+.pdb文件 - 所有符号解析操作必须在信号处理函数中「异步信号安全」:禁止调用
malloc、std::string、std::cout、printf—— 只能用write()、snprintf、静态缓冲区
如何用纯 C 风格实现跨平台崩溃捕获入口
信号/异常处理函数本身必须是 C 链接、无栈展开、无异常抛出。C++ 成员函数或 lambda 不可直接注册;所有状态需通过全局变量或静态指针传递(注意多线程下可能冲突,但崩溃处理本就单线程)。
示例关键注册逻辑:
#ifdef _WIN32
#include <windows.h>
LONG WINAPI CrashHandler(EXCEPTION_POINTERS* info) {
PrintStackTrace(info->ContextRecord); // 自定义 Win32 堆栈提取
ExitProcess(1);
}
SetUnhandledExceptionFilter(CrashHandler);
#else
#include <signal.h>
void SignalHandler(int sig, siginfo_t* info, void* uctx) {
PrintStackTrace(uctx); // 提取 ucontext_t 中的寄存器状态
_exit(1); // 不用 exit(),避免调用 atexit 注册函数
}
struct sigaction sa = {};
sa.sa_sigaction = SignalHandler;
sa.sa_flags = SA_SIGINFO | SA_RESETHAND;
sigaction(SIGSEGV, &sa, nullptr);
#endif
</signal.h></windows.h>
- Windows 的
EXCEPTION_POINTERS*可直接传给StackWalk64;Linux/macOS 的ucontext_t*需用getcontext()或从sigaction第三个参数提取 - 务必设置
SA_RESETHAND:防止二次崩溃时陷入死循环 - 避免在 handler 中调用任何 C++ 对象析构函数(如全局
std::ofstream),否则可能再次触发崩溃
怎样让堆栈地址变成可读函数名(不依赖 GDB 或外部工具)
核心难点是:不同平台符号解析 API 行为差异大,且默认不包含行号和源文件。你需要分层处理:
- Linux:
backtrace()获取地址数组 →backtrace_symbols()得到带偏移的字符串(如"./a.out(_Z3foov+0x15) [0x40054b]")→ 再用addr2line -e ./a.out 0x40054b才能得到行号;但addr2line是外部命令,不可在信号 handler 中调用。所以实际做法是:Release 构建时用objdump -l -C -S binary > symbols.txt预生成映射,运行时查表(仅限固定二进制) - macOS:
backtrace()+backtrace_symbols()返回格式类似 Linux,但不支持dladdr()解析行号;必须开启-g编译 + 使用atos工具离线处理,或改用libunwind(更底层、更可控) - Windows:
SymInitialize()+StackWalk64()+SymFromAddr()可直接获取函数名,配合 PDB 能拿到完整路径和行号;但需确保SymInitialize在主线程提前调用,且 PDB 路径正确(默认同 EXE 目录)
轻量级妥协方案:只做函数名 + 偏移(不强求行号),用 dladdr()(POSIX)或 SymFromAddr()(Windows)即可覆盖 80% 场景。例如:
Dl_info info;
if (dladdr(addr, &info) && info.dli_sname) {
snprintf(buf, sizeof(buf), "%s+%#zx", info.dli_sname, (char*)addr - (char*)info.dli_saddr);
}
如何把断言失败也导向同一套堆栈打印流程
不要用 #define assert(x) ((x) ? (void)0 : DoCrashReport(#x, __FILE__, __LINE__)) 这种简单宏——它无法捕获表达式副作用,且在 Release 下仍会求值 x(违反预期)。正确方式是:用 __builtin_expect(GCC/Clang)或 __assume(0)(MSVC)提示编译器分支概率,再手动触发崩溃信号。
- Linux/macOS:
raise(SIGTRAP)或__builtin_trap(),比abort()更干净(不走 stdlib 清理逻辑) - Windows:
__debugbreak()或直接RaiseException(EXCEPTION_BREAKPOINT, ...) - 所有平台都应确保该触发点能被前面注册的信号/异常处理器捕获,即与崩溃路径完全一致
- 断言消息(文件、行号、条件)需通过线程局部存储(
thread_local static char msg_buf[256])暂存,再由信号 handler 读取并打印——这是唯一安全跨过信号边界的通信方式
最终效果:无论是空指针解引用、数组越界,还是 MY_ASSERT(ptr != nullptr) 失败,都输出类似:
Assertion failed: ptr != nullptr File: util.cpp, Line: 42 #0 0x000055e2d9f1c3a2 in ProcessData() at util.cpp:42 #1 0x000055e2d9f1c41f in main at main.cpp:17
符号解析质量高度依赖构建配置:Linux 需 -rdynamic -g,macOS 需 -g -fno-omit-frame-pointer,Windows 需 /Zi /DEBUG:FULL 并部署 .pdb。少一个,就只剩十六进制地址。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











