常见Crash类型:程序崩溃、系统崩溃、内存溢出等

做Crash分析这么多年,我最大的体会就是——分类清楚,复现就成功了一半。很多新手上来就对着dump文件瞎猜,抓不住重点。其实说白了,Crash就那么几大类,每类的根因、特征、排查思路都相当成熟。你想想看,把类型定准了,后面的活儿就轻松多了。

程序崩溃(Process Crash)

这是咱们最常碰到的,占比得有个七八成。我个人习惯把它叫做“用户态崩溃”——程序自己把自己搞死了,但操作系统还活得好好的。

典型场景:

  • 段错误(SIGSEGV):空指针解引用、访问已释放内存、栈溢出。我记得有一次线上服务半夜挂了,查了半天发现是某个模块在并发场景下对全局指针做了double free。嗯,这种问题最难复现,后来靠加AddressSanitizer才抓到。
  • 非法指令(SIGILL):CPU执行了不认识的指令。我在项目中遇到过某款ARM芯片上的JIT编译器,优化过头生成了无效指令,直接SIGILL。那叫一个头大。
  • 浮点异常(SIGFPE):除零、溢出。其实现在很多编译器和硬件已经默认不抛了,但嵌入式上还是时不时遇到。
  • Abort(SIGABRT):代码主动调用abort(),通常是断言失败或检测到不可恢复的错误。

关键判断点:程序崩溃后,操作系统会生成core dump。如果发现crash地址在0x0附近,十有八九是空指针;如果地址是一个奇怪的堆地址,多半是use-after-free。

我的小技巧:在开发阶段把ulimit -c unlimited设上,再配合systemd-coredump,保你事后分析不慌。

系统崩溃(System Crash)

系统崩溃比程序崩溃恐怖多了——整个OS都挂了。手机黑屏重启、服务器直接死机,都是这种。

常见类型:

  • Kernel Panic:Linux内核遇到无法恢复的错误。我曾经在调试某个NVMe驱动时,一个错误的内存DMA映射直接导致Panic。你想想看,内核态代码出问题,连log都可能打不出来。
  • Watchdog超时:硬件看门狗没有及时喂狗,系统被自动复位。这种问题往往是因为内核某个线程死循环或中断被关太久。
  • OOM Killer:虽然不算严格意义的系统崩溃,但进程被kill掉后,系统还在跑。不过如果OOM发生在内核关键进程(比如init),那就真挂了。

注意:系统崩溃的日志通常存不到磁盘上,因为文件系统可能已经坏掉了。建议配置pstore或netconsole,把内核日志打到别的设备上。

内存溢出(Out Of Memory)

内存溢出我专门拎出来讲,因为它太隐蔽了。很多时候Crash表象是段错误,根子却是内存不够用。

溢出类型表现常见原因
堆内存溢出分配失败返回NULL,或触发OOM Killer内存泄漏、对象不断创建不释放
栈溢出段错误,导致栈上的局部变量被覆盖递归太深、局部大数组
虚拟地址空间耗尽mmap失败,内存映射文件打不开32位系统下地址空间有限,或者有内存碎片
堆内存碎片明明内存还有,但连续大块分配失败频繁分配释放小对象,长时间运行后出现

我再补充一个冷门的——内存踩踏(Memory Corruption)。它不是溢出,但比溢出还难查。比如你写数组越界了,把旁边别的对象的虚表给踩了,然后莫名其妙的虚函数调用崩溃。这种问题,有时候连asan都抓不到。

一句话总结:程序崩溃是“自己作死”,系统崩溃是“全盘皆输”,内存溢出是“慢性毒药”。

其他常见Crash类型

  • Timeout引发的假崩溃:比如你等一个锁等了30秒,业务层觉得超时就给kill了。其实程序没坏,只是慢。我在项目中遇到过Redis集群脑裂后大量超时,生产环境连续告警,最后发现是网络抖动导致的。
  • 资源耗尽(FD、线程、信号量):系统限制把资源用光了,再申请就会返回错误。如果不处理这个错误,后续操作就会失效,引发连锁Crash。
  • JNI/跨语言调用崩溃:Java层调用C/C++,指针一旦传错,直接SIGSEGV。这种调试起来特别烦,因为你不知道Java堆栈和native堆栈怎么关联。

下面这张图能帮你快速建立Crash分类的全局视角——

Crash常见类型分类框架 Crash类型 程序崩溃 系统崩溃 内存溢出 段错误 (SIGSEGV) 非法指令 (SIGILL) Abort / 断言 Kernel Panic Watchdog超时复位 OOM Killer 堆内存泄漏 栈溢出 地址空间耗尽 可能引起 分析顺序建议: 1. 确认是否完整保留crash日志 / core dump 2. 根据crash地址和信号类型定位大类 3. 通过堆栈回溯确定具体代码位置 4. 结合运行时环境(资源限制、配置)排除干扰项

避坑指南:我最开始做Crash分析时,见到段错误就以为是空指针,结果花了两天才发现是栈溢出导致的——栈区溢出不一定会马上崩,但会悄悄改掉相邻的变量。后来养成习惯:先检查栈帧大小,再看堆内存布局。这样能少走很多弯路。

分类清楚了,下一步就是怎么复现。每种Crash的复现策略差异很大——程序崩溃可以靠单元测试+ASan快速触发,系统崩溃往往需要构造特定的硬件时序,内存溢出则要设计压力场景让泄漏积累到阈值。这些具体的方法,咱们下一部分慢慢展开。