崩溃与调试
自动关联目录:崩溃与调试
C++ 的崩溃是"内存已经被写坏了,只是现在才发现"。崩溃现场(那一行代码)常常不是问题发生的地方,这是它比别的语言难查的根本原因。
一句话定位:先确定"坏在哪一类",再选工具。用错工具查一天,用对工具十分钟。
崩溃类型
| 类型 | 典型表现 | 常见原因 |
|---|---|---|
| 空指针解引用 | 访问地址 0x0 附近 | 未判空的指针、异步回调时对象已销毁 |
| 野指针 / UAF | 随机地址、随机时机 | 已释放内存继续用(见 移动语义与 RAII) |
| 栈溢出 | stack overflow | 深递归、栈上大数组 |
| 堆损坏 | malloc/free 断言、随机崩 | 越界写、double free |
| 虚表损坏 | 调用虚函数崩在奇怪地址 | 对象已被覆盖或已析构 |
| 断言失败 | check/ensure/assert | 逻辑前置条件不满足 |
| OOM | 分配失败 | 泄漏或真的不够 |
判断"这是哪一类"最快的方法是看崩溃地址:
0x00000000附近 → 空指针 + 小偏移(访问成员)0xCCCCCCCC/0xCDCDCDCD(MSVC Debug)→ 未初始化或已释放内存0xDDDDDDDD→ 已释放(MSVC Debug 填充)- 随机大地址 → 野指针
拿到栈
| 平台 | 做法 |
|---|---|
| 开发机(IDE) | 直接看调用栈,配好符号就行 |
| Windows 现场 | 生成 dump(minidump),用 WinDbg/VS 打开 |
| Android | addr2line 把 tombstone 里的地址还原成行号 |
| iOS | 崩溃日志 + dSYM 符号化 |
| Linux | core dump + gdb |
# Android 还原(UE 提供 ndk 里的工具链)
aarch64-linux-android-addr2line.exe -f -C -e libMyGame.so 0x0000000000123456没有符号的栈等于没有栈。发布流程必须包含"保留符号表"这一步——把符号剥离但单独存档(见 Security 里关于符号剥离的代价)。
编译器与运行时工具
| 工具 | 查什么 | 代价 |
|---|---|---|
| AddressSanitizer(asan) | 越界、UAF、double free | 内存 2–3 倍,速度慢 2 倍 |
| UndefinedBehaviorSanitizer(ubsan) | 未定义行为(溢出、对齐全) | 较轻 |
| ThreadSanitizer(tsan) | 数据竞争 | 较重 |
| MemorySanitizer(msan) | 未初始化读取 | 需重编所有依赖 |
| Valgrind | 泄漏、非法访问 | 极慢,Linux 用 |
_CrtSetDbgFlag(MSVC) | 泄漏检测 | 轻 |
asan 是性价比最高的一个:越界写和 UAF 这两类最难查的问题,它能直接指到分配和释放的那两行。
-fsanitize=address -fno-omit-frame-pointer -g需要重编,且不能与某些自定义分配器直接配合(UE 需要额外配置)。
UE 侧的手段
| 机制 | 用途 |
|---|---|
check() | 失败即崩(发布版可编译掉) |
verify() | 表达式始终求值,但只在 debug 断言 |
ensure() | 失败不崩,上报一次,可继续跑(适合线上) |
ensureAlways() | 每次都上报 |
UE_LOG | 日志 |
FDebug::DumpStackTraceToLog | 主动打栈 |
| Crash Reporter | 自动收集上传 |
StompAllocator | 立刻捕获 UAF(页保护) |
ensure 与 check 的选择:开发期用 check 尽早暴露;线上用 ensure 避免一崩全崩。
静态检查
| 工具 | 作用 |
|---|---|
编译器警告(-Wall -Wextra / /W4) | 最便宜的 bug 来源 |
| clang-tidy | 现代 C++ 的写法与常见错误 |
| 静态分析(PVS-Studio、MSVC analyze) | 更深的数据流分析 |
| 把警告当错误 | 强烈推荐,否则警告会累积到没人看 |
把警告清零后再开启 -Werror,一次性开会在存量代码上淹死人。
崩溃排查流程
1. 拿到栈(符号化)
2. 判断崩溃类型(地址/异常码)
3. 复现(最小步骤)
4. 缩小范围(二分、加日志、条件屏蔽)
5. 上工具(asan / StompAllocator / 增加断言)
6. 定位根因后,补一条断言或单测防回归第 3 步"复现"是分水岭:能稳定复现的问题基本都能查清;不能复现的,靠的是"崩溃聚合 + 增加诊断信息 + 等下一次"。
防回归
| 手段 | 说明 |
|---|---|
| 崩溃后必须写断言 | 把"这次为什么崩"变成永久的前置检查 |
| 加单测 | 尤其是边界条件 |
| 崩溃聚合看趋势 | 同一栈出现 N 次就提优先级 |
| 保留符号与版本映射 | 否则拿到栈也没用 |
| Code Review 关注生命周期 | 异步回调、跨线程、容器失效 |
常见坑
| 坑 | 说明 |
|---|---|
| 发布时不保留符号 | 拿到栈也无法还原 |
| 只在 Release 下崩 | 通常是未初始化变量(debug 会填 0xCC) |
| 只在 Debug 下崩 | 通常是迭代器/断言/内存布局差异 |
| 崩溃点不是问题点 | 内存早就写坏了,去查谁写坏了它 |
用 volatile 掩盖问题 | 只是让时序变了,没修根因 |
| 日志加太多导致问题消失 | 时序问题,改用记录缓冲而非实时打印 |
不区分 check 与 ensure | 线上一个断言把整个进程带崩 |
| 警告不清理 | 真正的错误淹没在几百条里 |
异步回调直接用裸 this | 对象已销毁(用弱引用,见 GC) |
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/da216d6c.html
更新于:2026年10月10日