Skip to content

Stage07|Modern C++ Foundation ​

Lesson094|GDB、Sanitizer、崩溃和内存错误排查 ​

Tags: #GDB #Sanitizer #Crash #DebuggingDifficulty: ⭐⭐⭐⭐⭐


一、问题场景:崩溃行为什么不是根因 ​

程序在 vector::push_back 崩溃,根因可能是更早的越界写破坏了堆,也可能是另一个线程正在销毁 vector。崩溃现场告诉你“损坏被发现的位置”,不一定是“损坏发生的位置”。

二、本课目标 ​

  1. 保存可调试符号并读取调用栈。
  2. 使用断点、条件断点和 watchpoint。
  3. 用 ASan/UBSan/TSan 定位不同错误。
  4. 建立最小复现和证据链。
  5. 处理 Release、Native 和多线程崩溃边界。

三、回到 TaskSystem:故意制造并定位错误 ​

最后两课之前,TaskSystem 应有三类可诊断故障:worker 捕获过期对象造成 use-after-free,任务统计或队列访问造成竞态,构建配置不匹配造成符号/链接问题。调试课不只是记 GDB 命令,而是把“现象→证据→根因→修复→回归”走完整。

四、先保留现场 ​

至少记录:

text
程序版本和 commit
平台、架构、构建配置
崩溃时间和操作步骤
完整线程调用栈
符号文件是否匹配
日志中最后一个业务阶段

Native 崩溃地址必须配合同一构建产物的符号解析。不同版本符号即使函数名相似,行号也不可信。

五、GDB 基本流程 ​

bash
g++ -std=c++17 -O0 -g crash.cpp -o crash
gdb ./crash

常用命令:

text
run                 启动
break file.cpp:42   设置断点
bt                  当前线程栈
thread apply all bt 所有线程栈
frame 2             切换栈帧
print value         查看表达式
info locals         查看局部变量
watch object.state  监视写入
continue            继续

优化构建中变量可能被消除或重排。必要时使用带符号的接近 Release 配置复现,但不要因此忽略优化相关竞态。

六、Sanitizer 分工 ​

text
ASan:越界、use-after-free、部分泄漏
UBSan:有符号溢出、错误转换、对齐等未定义行为
TSan:数据竞态和部分同步错误

GCC/Clang 示例:

bash
clang++ -std=c++17 -O1 -g -fno-omit-frame-pointer \
  -fsanitize=address,undefined crash.cpp -o crash_asan

TSan 通常单独构建:

bash
clang++ -std=c++17 -O1 -g -fsanitize=thread -pthread race.cpp -o race_tsan

不同平台、编译器和库的支持不同,不能假设所有 Sanitizer 可以任意组合。

七、可复现故障:use-after-free ​

cpp
#include <iostream>
#include <vector>

int main() {
    std::vector<int> values{1, 2, 3};
    int* first = &values[0];
    values.reserve(1000); // 可能搬迁存储
    std::cout << *first << '\n';
}

使用 ASan 运行,观察报告中的:错误类型、非法访问地址、释放/重新分配栈和当前访问栈。修复方法不是“去掉 reserve”,而是不跨可能扩容的操作保存元素地址,改用索引或在最终容量前先 reserve。

八、从报告到根因 ​

text
确认第一条 sanitizer 错误
找到第一次非法访问栈
找到对象分配和释放栈
画出 owner 与线程时间线
建立最小复现
修复不变量或生命周期
加入回归测试
在原场景验证

后续几十条错误可能是首次内存破坏的连锁反应。

九、崩溃类型判断 ​

现场优先方向
地址接近 0空指针或小偏移成员访问
固定释放函数崩溃双重释放、allocator 不匹配、堆已损坏
只在压力下出现竞态、生命周期、容量边界
只在 Release 出现未定义行为、竞态、未初始化、时序变化
Web 正常 Native 崩溃Binding、线程、原生所有权、平台 API

十、调试纪律 ​

不要一次改五处“试试”。每个假设写出可观察证据,例如对象析构序号、队列长度、线程 ID、资源 owner。sleep、扩大数组和吞异常可能让故障消失,但没有建立正确性。

十一、练习与答案 ​

1. 为什么调用栈里最上层函数不一定有 Bug? ​

内存可能更早已被破坏;当前函数只是首次访问损坏状态。

2. ASan 能否证明没有内存错误? ​

不能。它只覆盖实际执行路径和可检测类型,仍需要测试覆盖、代码审查与生命周期设计。

3. 多线程卡死最需要什么现场? ​

所有线程堆栈、锁状态和任务队列状态,单看当前线程通常无法还原等待环。

十二、本课总结 ​

可靠排查从匹配符号和完整现场开始,用调试器观察状态,用 Sanitizer 捕获非法行为,再通过最小复现和回归测试闭环。