本文目录导读:

程序崩溃(Crash)的溯源排查是一个系统性工程,核心目标是从“现象”定位到“根因”,这通常需要结合日志、工具、代码分析等多种手段。
以下是分步骤、体系化的排查指南,按紧急程度和技术深度排序。
第一阶段:现场保全与信息收集(黄金30分钟)
在程序崩溃后,立即收集以下信息,否则一旦重启,关键线索可能丢失。
-
获取 Crash Dump(转储文件)
- Windows: 在任务管理器中右键进程 -> “创建转储文件” 或使用
procdump(Sysinternals工具),系统层面可设置Windows Error Reporting (WER)自动保存。 - Linux:
ulimit -c unlimited可开启核心转储,发生崩溃后会在当前目录或/var/crash/下产生core.xxx文件。 - 移动端/Web: 使用平台SDK(如Firebase Crashlytics、Sentry、Bugly)自动收集。
- Windows: 在任务管理器中右键进程 -> “创建转储文件” 或使用
-
查看应用程序日志(App Logs)
- 寻找崩溃前最后几秒的关键日志,特别是
ERROR、FATAL级别日志,以及异常堆栈信息。 - 注意:日志可能因缓冲区未刷新而丢失,排查时需谨慎。
- 寻找崩溃前最后几秒的关键日志,特别是
-
查看系统日志(System Logs)
- Windows: 事件查看器 -> Windows日志 -> 应用程序/系统,查找“错误”级别事件,ID通常为 1000、1001、1005等,会给出错误模块路径、异常代码(如
0xc0000005访问违规)。 - Linux:
journalctl -xe或dmesg | tail -50,注意oom-killer(内存不足)、segfault(段错误)、kernel: Call Trace等信息。
- Windows: 事件查看器 -> Windows日志 -> 应用程序/系统,查找“错误”级别事件,ID通常为 1000、1001、1005等,会给出错误模块路径、异常代码(如
-
记录环境信息
- 操作系统版本、补丁级别。
- 软件版本(自己的程序、依赖库、运行时如 .NET/JDK版本)。
- 硬件配置(CPU、内存、磁盘空间)。
- 负载情况(QPS、并发数、CPU/内存使用率)。
第二阶段:初步定性分析(是什么类型的崩溃?)
根据初步信息,判断崩溃类型,缩小排查范围。
-
崩溃类型判断
- 段错误 (Segmentation Fault): 最常见,原因:访问空指针/野指针、缓冲区溢出、栈溢出、use-after-free(使用已释放内存)。
- 内存不足 (OOM): 程序或系统内存耗尽,被内核杀掉(通过
dmesg | grep oom-killer确认),原因:内存泄露、配置过低。 - 栈溢出 (Stack Overflow): 递归无终止条件、函数调用过深、大量局部变量。
- 非托管异常 (C++/.NET P/Invoke): 原生代码崩溃,托管层无法捕获,原因:内存错误或底层库Bug。
- 托管异常 (.NET/Java): 抛出未捕获的异常(如 NullReferenceException, OutOfMemoryException)。
- 看门狗超时 (Watchdog Timeout): 主线程卡死(如死锁、无限循环),系统或应用框架强制结束。
-
重现模式
- 稳定复现: 每次必崩,好排查,通常是逻辑Bug或数据问题。
- 偶发/压测下复现: 与并发、资源竞争、内存分布、特定时序有关,最难排查,需工具介入。
第三阶段:深度定位与根因分析(用工具说话)
根据崩溃类型,选择对应工具。
针对“段错误”/“访问违规”
-
分析 Crash Dump(最核心手段)
-
Windows (WinDbg):
# 先加载了符号文件(.pdb) .sympath+ C:\MySymbols; SRV*C:\DownstreamStore*http://msdl.microsoft.com/download/symbols .reload /f # 分析异常 !analyze -v # 查看堆栈 kbn 100 # 查看线程 ~* kbn 100 # 查看异常对象 .exr -1 # 查看内存(例如地址为0x00000000) !address 0x00000000
-
Linux (GDB):
# 加载dump (或直接gdb ./your_program) gdb ./your_program core.xxxx # 自动分析 bt full # 查看所有线程 thread apply all bt # 查看寄存器 info registers # 查看内存 x/10gx $rsp # 查看具体地址 p *pointer_name
-
-
内存错误检测工具 (适用于 C/C++):直接运行程序,它会在第一次非法访问时停下来。
- AddressSanitizer (ASan): 编译时加
-fsanitize=address,最快最准,推荐首选。 - Valgrind:
valgrind --tool=memcheck ./your_program,会明显拖慢程序,适合调试阶段。 - Dr. Memory: 类似Valgrind的Windows版本。
- AddressSanitizer (ASan): 编译时加
针对“内存不足 (OOM)”
-
分析内存占用:
- Windows: 使用
Process Explorer(Sysinternals) 查看私有字节、工作集、虚拟大小。 - Linux: 使用
top查看 RES 列,或pmap -x <PID>查看详细映射。 - Java:
jmap -heap <PID>或jcmd <PID> GC.heap_dump生成堆转储。 - .NET:
dotnet-dump collect生成Dump,然后用 WinDbg 或dotnet-dump analyze分析!dumpheap -stat。
- Windows: 使用
-
定位泄漏点:
- C/C++: 使用 Valgrind 的
--leak-check=full选项,或者 LeakSanitizer (LSan),可以精确到每个未释放的malloc/new的调用栈。 - Java/.NET: 分析堆转储(Heap Dump)。
- 找出 GC Root 强引用路径。
- 查看占用内存最大的前10个对象类型(
!dumpheap -stat)。 - 观察是否有大量的
byte[]、char[]、String、Dictionary等。 - 检查静态集合(
static List)是否无限增长。
- C/C++: 使用 Valgrind 的
针对“死锁/线程挂起”
-
获取线程堆栈:
- Windows: 使用 WinDbg 加载Dump后,
!locks查看锁,~* kbn看到所有线程的栈。 - Linux (Java):
kill -3 <PID>输出到标准输出,或jstack <PID>。 - Linux (C++):
gdb的thread apply all bt。 - .NET:
dotnet-dump analyze里的!clrstack。
- Windows: 使用 WinDbg 加载Dump后,
-
分析模式:
- 死锁: 多个线程互相等待对方持有的资源(如A等待B的锁,B等待A的锁),在堆栈中能清晰看到
Wait()/Join()等调用,并且锁的拥有者正是对方线程,WinDbg 的!locks或!clrstack -a可以定位。 - 活锁/饥饿: 线程一直运行但无法推进逻辑,状态可能是
Running,需要分析代码逻辑。
- 死锁: 多个线程互相等待对方持有的资源(如A等待B的锁,B等待A的锁),在堆栈中能清晰看到
第四阶段:根因确认与修复
找到“是哪一行代码导致崩溃”后,需要确认“为什么”。
- 自底向上追溯:从异常堆栈的顶层(
0x00000000或std::bad_alloc)往下看,找到自己代码的调用帧(Frame)。 - 检查变量与状态:在崩溃点,检查函数的参数和局部变量(特别是指针、句柄、上下文对象),是谁传入的?为什么是空/脏数据?
- 检查多线程安全:如果操作了共享资源,是否有锁?锁是否正在被正确获取和释放?是否存在数据竞争 (Data Race)?
- 检查资源生命周期:对象是否在另一个线程被释放了?是否跨出了对象的生命周期?
修复示例:
- Segfault 在
strcpy: -> 检查源字符串是否为空或长度不够,改用strncpy_s或std::string。 - OOM 在
HashMap::put: -> 检查HashMap是否没有按预期清理,导致无限增长。 - 死锁:-> 重排锁的获取顺序,或使用
std::lock同时锁定多个mutex。
一个万能排查流程图
- 有 Crash Dump 吗?
- 有 -> 直接分析。
- 无 -> 复现现场(用原数据/请求),并在崩溃发生时生成Dump。
- 分析 Dump:
- 能看到异常代码(如
0xc0000005) -> 找栈->看寄存器/内存->定位代码行。 - 看不到代码栈(如OOM Kill) -> 分析内存使用(Heap碎片?Leak?)。
- 线程都在某个函数里等待 -> 找锁,找死锁。
- 能看到异常代码(如
- 无法复现?
- 加日志,加监控。
- 用Sanitizer(ASan, LSan, TSan, UBSan)编译并跑压力测试。
- 使用静态分析工具。
- 修复验证:
- 重放导致崩溃的那组数据/请求,确认不再崩溃。
- 压测 48小时以上,确认无内存增长或新崩溃。
核心思想:拿到最原始的证据(Crash Dump + 系统日志),然后用最合适的核心工具(WinDbg/GDB/ASan/Heap Analyzer) 将抽象的错误信号映射到具体的一行代码和一个变量上。