崩溃与调试

自动关联目录:崩溃与调试

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 打开
Androidaddr2line 把 tombstone 里的地址还原成行号
iOS崩溃日志 + dSYM 符号化
Linuxcore 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)