外观
Stage07|Modern C++ Foundation
Lesson094|GDB、Sanitizer、崩溃和内存错误排查
Tags: #GDB #Sanitizer #Crash #DebuggingDifficulty: ⭐⭐⭐⭐⭐
一、问题场景:崩溃行为什么不是根因
程序在 vector::push_back 崩溃,根因可能是更早的越界写破坏了堆,也可能是另一个线程正在销毁 vector。崩溃现场告诉你“损坏被发现的位置”,不一定是“损坏发生的位置”。
二、本课目标
- 保存可调试符号并读取调用栈。
- 使用断点、条件断点和 watchpoint。
- 用 ASan/UBSan/TSan 定位不同错误。
- 建立最小复现和证据链。
- 处理 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_asanTSan 通常单独构建:
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 捕获非法行为,再通过最小复现和回归测试闭环。
