为什么?Python 报错直接给你堆栈。C++ 报错就一个 Segmentation fault。
这个问题困扰了大家好多年,其实现在 C++ 打印调用栈用一个 std::stacktrace::current() 就可以了。

std::stacktrace | |
backward-cpp | |
boost::stacktrace | |
libunwindaddr2line | |
backward-cpp |
优先级:C++23 > backward-cpp > 其他。
#include<iostream>#include<stacktrace>voidlevel_3(){std::cout << std::stacktrace::current() << '\n';}voidlevel_2(){ level_3(); }voidlevel_1(){ level_2(); }intmain(){ level_1(); }这就是 C++ 的 traceback.print_stack()。
输出:
0# level_3() at /path/to/main.cpp:7 1# level_2() at /path/to/main.cpp:10 2# level_1() at /path/to/main.cpp:13 3# main at /path/to/main.cpp:16文件名、行号、函数名全有。无需第三方库,无需手动解析 DWARF,无需 fork 子进程调用 addr2line。
默认格式够用,也支持自行控制:
std::stacktrace st = std::stacktrace::current();for (constauto& entry : st) {std::cout << entry.description() // 函数名 << " at " << entry.source_file() << ":" << entry.source_line() << '\n';}#include<csignal>#include<stacktrace>#include<iostream>voidsignal_handler(int sig){std::cerr << "Caught signal " << sig << '\n';std::cerr << std::stacktrace::current() << '\n';std::_Exit(EXIT_FAILURE);}intmain(){std::signal(SIGSEGV, signal_handler);std::signal(SIGABRT, signal_handler);// 业务逻辑}程序崩溃时,信号处理器拦截,在死亡前留下完整堆栈。std::cout 在信号处理器中不安全,安全版本见第五章。
很多工程绑定在 GCC 9/10 上,升级编译器意味着漫长的回归测试。backward-cpp 是这些工程里最务实的选择。
纯 Header-only,Linux 需要 libdw(libdw-dev),macOS 用系统自带的 libunwind,Windows 用系统自带的 DbgHelp。一条 #include "backward.hpp" 就能跑。
#include"backward.hpp"backward::SignalTrace st; // RAII,构造时自动注册信号处理器intmain(){// 程序正常结束时,SignalTrace 不做任何事// 任何位置崩溃,自动打印堆栈后退出}注册 SIGSEGV、SIGABRT、SIGFPE、SIGILL,程序任何位置崩都能拿到堆栈。
不需要全局捕获,只想在特定位置打印:
#include"backward.hpp"voidcrash_func(){ backward::StackTrace st; st.load_here(32); // 捕获当前栈的前 32 帧 backward::Printer p; p.print(st);}大型工程中,堆栈往往被 STL 内部帧淹没。backward-cpp 支持自定义过滤:
backward::StackTrace st;st.load_here(64);backward::Printer p;p.snippet = true; // 显示源码片段p.color = true; // 彩色输出p.address = false; // 生产环境隐藏地址p.context = 2; // 每帧显示 2 行源码p.print(st, [](const backward::Frame& f) {conststd::string& name = f.name;return name.find("std::") == 0 || name.find("__gnu_cxx") == 0;});Windows:没有 execinfo.h 和 addr2line。
Windows 上的调试工具是 WinDbg 或 cdb.exe:
cdb -z my_program.exe -c "ln 0x00007ff6`a1b3c000; q"调试信息格式为 PDB,功能等价于 Linux 的 DWARF。微软符号服务器:SRV*...*https://msdl.microsoft.com/download/symbols。
macOS:依赖 libunwind,符号解析用 atos:
atos -o MyApp.app.dSYM -l 0x100000000 0x401234macOS 的 DWARF 调试信息通常在 .dSYM 文件夹中,发布时不要 strip 掉。
这里有个细节很多人不知道:信号处理器运行在中断上下文,不是所有标准库函数都能在里面调用。
std::cout 涉及缓冲区和内部锁,printf 依赖锁,snprintf 内部可能调用 malloc。
POSIX 明确标注为 async-signal-safe 的 I/O 操作只有一个:write() 系统调用。
所以堆栈打印要这么写:
#include<csignal>#include<stacktrace>#include<unistd.h>#include<cstdlib>voidsig_handler(int){std::stacktrace st = std::stacktrace::current();for (auto it = st.begin(); it != st.end(); ++it) {constauto& e = *it;char frame[512];int pos = 0; frame[pos++] = '#';if (std::distance(st.begin(), it) < 10) frame[pos++] = '0' + std::distance(st.begin(), it); frame[pos++] = ' ';// description(取前 60 字节)conststd::string& desc = e.description();for (size_t i = 0; i < desc.size() && i < 60 && pos < 480; ++i) frame[pos++] = desc[i]; frame[pos++] = ' '; frame[pos++] = '@';// source_fileconststd::string& file = e.source_file();for (size_t i = 0; i < file.size() && pos < 460; ++i) frame[pos++] = file[i]; frame[pos++] = ':';// source_line(纯整数转字符串)int line = static_cast<int>(e.source_line());if (line > 0) {char num[12];int nd = 0, tmp = line;if (tmp == 0) num[nd++] = '0';else { while (tmp > 0) { num[nd++] = '0' + tmp % 10; tmp /= 10; } }for (int j = nd - 1; j >= 0 && pos < 500; --j) frame[pos++] = num[j]; } frame[pos++] = '\n'; write(STDERR_FILENO, frame, pos); } _Exit(EXIT_FAILURE);}还有一种情况:内存分配器已经损坏时,std::stacktrace::current() 本身可能再次触发 SIGSEGV。用 sigsetjmp 保护一下:
#include<csignal>#include<csetjmp>#include<stacktrace>#include<unistd.h>#include<cstdlib>staticthread_local sigjmp_buf jump_buffer;staticthread_localvolatilesig_atomic_t in_handler = 0;voidsafe_handler(int sig){if (in_handler) _Exit(128 + sig); // 防止递归 in_handler = 1; write(STDERR_FILENO, "Fatal signal\n", 14);if (sig == SIGSEGV) write(STDERR_FILENO, "SIGSEGV\n", 8);elseif (sig == SIGABRT) write(STDERR_FILENO, "SIGABRT\n", 8);if (sigsetjmp(jump_buffer, 1) == 0) {for (constauto& e : std::stacktrace::current()) { write(STDERR_FILENO, " ", 2);conststd::string& d = e.description(); write(STDERR_FILENO, d.data(), std::min(d.size(), size_t(64))); write(STDERR_FILENO, "\n", 1); } } _Exit(128 + sig);}Chromium 和 Google crashpad 的崩溃报告用的就是这套逻辑。
堆栈信息不完整,很多人第一反应是代码问题。实际上大概率是编译配置的问题。
Debug 版:
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -fno-omit-frame-pointer -fno-inline")Release 版:
set(CMAKE_CXX_FLAGS_RELEASE "-g -O2 -fno-omit-frame-pointer")**Release 版也要 -g**。
几个关键参数:
-g:生成 DWARF 调试信息-O0:Debug 版关闭优化,保证行号精确-fno-omit-frame-pointer:保留栈帧指针,确保 unwinding 正常-fno-inline:Debug 版禁止内联,减少栈层级丢失-rdynamic(或 -Wl,--export-dynamic):导出动态符号到符号表,addr2line 才能解析动态库中的符号大部分人只在崩溃时才想起堆栈。catch 块其实是获取上下文最丰富的位置:
try { risky_operation();} catch (conststd::exception& e) {std::cerr << "Exception: " << e.what() << '\n';std::cerr << "Stack at throw site:\n" << std::stacktrace::current() << '\n';throw;}还有一个细节:noexcept 函数抛出异常时,程序直接 std::terminate(),完全跳过栈展开。堆栈拿到的是异常发生点,但传播路径全丢了。
// noexcept 内部可能抛异常boolprocess_data(conststd::vector<int>& data)noexcept{for (size_t i = 0; i < data.size(); ++i) {if (data[i] < 0)throwstd::invalid_argument("negative"); }returntrue;}// noexcept(false) 才保留完整堆栈boolprocess_data(conststd::vector<int>& data)noexcept(false){// ...}noexcept 只留给确定不抛异常的底层函数,业务函数通常应该带 noexcept(false) 或不加。
分离调试信息是行业惯例:
# 生成带完整符号的二进制g++ -g -O0 -o my_program_debug main.cpp# 提取调试信息到独立文件objcopy --only-keep-debug my_program_debug my_program.debug# 生成发行版(去除调试节,保留符号表)strip -g my_program_debug -o my_program_releaseLinux 发行版标准路径:/usr/lib/debug/.build-id/xx/yyyy.debug。addr2line 会自动查找对应文件的调试信息。
# 查看 build-idreadelf -n my_program_release | grep Build.ID# 离线解析addr2line -e my_program_release -f -C 0x401234??:0 | -g,检查是否被 strip | |
-fno-inline | ||
-fno-omit-frame-pointer | ||
_Z4funcv | c++filtaddr2line -C | |
-rdynamic | ||
backtrace() + _exit() |
一个头文件解决所有平台差异,条件编译自动选择最优后端:
#pragma once#include<optional>#include<string>#if __has_include(<stacktrace>)#include<stacktrace>#define STACKTRACE_BACKEND "std::stacktrace (C++23)"#define STACKTRACE_IMPL(...) do { \ auto st = std::stacktrace::current(); \ for (auto it = st.begin(); it != st.end(); ++it) { \ auto& e = *it; \ std::cerr << "#" << std::distance(st.begin(), it) << " " \ << e.description() << " @ " \ << e.source_file() << ":" \ << e.source_line() << "\n"; \ } \} while(0)#elif __has_include("backward.hpp")#include"backward.hpp"#define STACKTRACE_BACKEND "backward-cpp"#define STACKTRACE_IMPL(...) do { \ backward::StackTrace st; \ st.load_here(64); \ backward::Printer p; \ p.print(st); \} while(0)#else#include<execinfo.h>#include<cxxabi.h>#include<dlfcn.h>#define STACKTRACE_BACKEND "libunwind (fallback)"#define STACKTRACE_IMPL(...) do { \ void* frames[64]; \ int n = backtrace(frames, 64); \ for (int i = 1; i < n; ++i) { \ Dl_info info; \if (dladdr(frames[i], &info)) { \ int status; \ char* demangled = abi::__cxa_demangle( \ info.dli_sname, nullptr, nullptr, &status); \ const char* name = (status == 0 && demangled) \ ? demangled : info.dli_sname; \ fprintf(stderr, "#%d %s in %s\n", \ i - 1, name ? name : "?", info.dli_fname); \if (demangled) free(demangled); \ } \ } \} while(0)#endifnamespace stacktrace {inlinevoidprint(){std::cerr << "=== Stack trace (backend: " << STACKTRACE_BACKEND << ") ===\n"; STACKTRACE_IMPL();}} // namespace stacktrace使用:
#include"stacktrace.hpp"intmain(){ stacktrace::print();}一个头文件,一行调用。引入新项目直接扔进去,不用关心底层实现。
现在的实现路径其实很清晰:
能用 C++23 先用 C++23。std::stacktrace::current() 是官方标准,零依赖,一行代码。
不能升编译器就用 backward-cpp。Header-only,彩色输出,信号自动捕获,引入成本极低。
堆栈质量的上限由编译配置决定,不是代码。-g 保留,strip 不要用。
还有一个习惯:在 catch 块里顺手打印堆栈,比崩溃后追原因高效得多。
Python 的调用栈打印之所以舒服,是因为它被做进了语言和标准库。C++ 走了一条更长的路,但现在这条路已经走到了终点。
关键命令
# 编译带调试信息g++ -g -O0 -rdynamic -o my_program main.cpp# 解析地址addr2line -e my_program -f -C 0x401234# 分离调试信息objcopy --only-keep-debug my_program my_program.debugstrip -g my_program -o my_program_stripped# 验证 DWARF 存在readelf -S my_program | grep .debug_info