Crash 基础概念:什么是 Crash,常见类型
说实话,咱们做底层开发的,谁还没跟 Crash 打过照面呢?
Crash,说白了就是程序彻底跑不动了。
进程挂了,核心 dump 一地,日志停在那儿。
我刚开始工作时,遇到 Crash 就慌。后来发现,Crash 其实有固定的“套路”。搞懂它,你就能快速定位。今天咱们就先把地基打好。
什么是 Crash
Crash 是操作系统给程序判的“死刑”。硬件或操作系统检测到程序干了不该干的事——比如访问了不属于自己的内存——就会强制终止它。
这种行为,在 Linux 上叫 SIGSEGV、SIGABRT 等信号。在 Windows 上叫“异常”。
有一次我排查一个线上问题,服务老是无预警退出。查了半天,原来是空指针解引用。嗯,这就是典型的 Crash。
核心原则:Crash 不是随机的。它一定有明确的原因。找到它,就能解决它。
常见 Crash 类型
我遇到的 Crash 类型,基本能归到下面几类。咱们一个一个说。
1. 段错误(Segmentation Fault)
这是最最最常见的 Crash。我估计你遇到十次 Crash,八次都是它。
段错误发生在程序试图访问它没有权限访问的内存时。比如写只读区域、访问已释放的内存、或者越界访问。
为什么会这样?说白了就是你的指针指向了不该去的地方。操作系统发现你越界了,直接给你发 SIGSEGV。
我印象特别深,之前做一个嵌入式项目,堆栈分配得太小,递归一深直接溢出,段错误满天飞。排查了好久才发现问题。
避坑指南:写代码时多检查指针有效性。 release 版最好打开 AddressSanitizer 或者 Valgrind 跑一遍。
2. 空指针解引用(Null Pointer Dereference)
空指针解引用,其实也是段错误的一种。不过它太典型了,值得单独拎出来讲。
当你对一个 NULL 指针做解引用操作(比如 *ptr 或 ptr->field),就会触发 Crash。
为什么会有空指针?函数返回失败没有检查,或者对象指针被释放后没有置空。
我曾经给一个新人 review 代码,他写了这样的代码:
char* p = get_name();
printf("%s", p); // 如果 get_name 返回 NULL,就崩了
嗯,教训就是:每次调用可能返回 NULL 的函数,都得先判空。
警告:不要觉得“这个地方肯定不会返回 NULL”。系统调用、malloc、new、函数返回值,都随时可能给你 NULL。
3. 栈溢出(Stack Overflow)
栈溢出发生在函数调用嵌套太深,或者局部变量占用太多栈空间时。栈空间是有限的——Linux 默认栈大小一般是 8MB,Windows 上 1MB。
你想想看,一个递归函数没加终止条件,或者一个结构体超大数组直接定义在函数内,都会撑破栈。
我记得在调试一个网络协议栈时,解析函数深层递归,到第几千层时直接栈溢出。当时我看到日志里全是“stack smashing detected”——嗯,挺刺激的。
| Crash 类型 | 根本原因 | 常见信号/异常 |
|---|---|---|
| 段错误 | 非法内存访问 | SIGSEGV (11) |
| 空指针解引用 | 对 NULL 指针操作 | SIGSEGV (11) |
| 栈溢出 | 栈空间耗尽 | SIGABRT (6) 或 SIGSEGV |
| 堆溢出 | 堆内存越界写 | SIGABRT 或 SIGSEGV |
| 除零错误 | 整数除零 | SIGFPE (8) |
| 非法指令 | 执行了非法 CPU 指令 | SIGILL (4) |
4. 堆溢出(Heap Overflow)
堆溢出跟栈溢出不同,它发生在动态分配的内存上。你 malloc 了 100 字节,结果写入了 200 字节——后面的 100 字节就覆盖了堆管理器的元数据或者其他对象。
堆溢出特别隐蔽,可能不会立即 Crash,而是过一会儿才崩。我遇到过最离谱的一次,是某个模块的内存越界把另一个完全无关的模块的指针给改写了。查了三天,最后用 AddressSanitizer 才定位出来。
技巧:如果遇到随机、不固定的 Crash,优先怀疑堆溢出。用 ASan 或 Valgrind 一键排查。
5. 除零错误(Division by Zero)
整数除零在 CPU 层面会触发浮点异常(SIGFPE)。别小看它,我见过有人把除数当配置项,配成 0 了也没做校验。
6. 非法指令(Illegal Instruction)
CPU 遇到不认识的指令就会触发 SIGILL。常见原因:编译时指定的 CPU 架构不对,或者代码段被写坏了。
Crash 知识体系总览
为了让你对 Crash 有一个全局认识,我画了一张图。它把 Crash 的触发源、信号类型和常见场景串在一起。你看一眼,后面学起来会更顺。
Crash 发生的底层机制
你可能想问:操作系统怎么知道程序犯了错?
靠硬件和操作系统协作。现代 CPU 有内存管理单元(MMU),程序每访问一个内存地址,MMU 都会查页表。如果地址非法,CPU 就触发一个异常。
操作系统捕获这个异常,判断是哪种问题,然后给进程发信号。进程如果没有注册信号处理函数,默认行为就是终止 + 生成 core dump。
所以我说,Crash 不是玄学,是有完整链路可查的。
- 硬件:MMU 检测到非法访问,触发异常。
- 操作系统:异常处理例程转化为信号(SIGSEGV 等)。
- 进程:默认终止,或者调用自定义信号处理函数。
- 事后:core dump 保存现场,供 gdb 等工具分析。
记住这个链路:非法操作 → 硬件异常 → 操作系统信号 → 进程终止 → core dump。你后面用 gdb 定位问题,就是沿着这条链反向回溯。
什么时候该担心 Crash
不是所有 Crash 都需要立即处理。比如开发测试环境下,频繁重现的 Crash 反而是好事——你能快速定位。最怕的是线上偶发、难以重现的 Crash。这时候就需要堆栈、日志、core dump 三板斧了。
我个人习惯是:在代码里主动用 signal() 注册自定义处理函数,至少把堆栈打印出来。后面章节我们会讲怎么用 backtrace 打印调用栈。
小贴士:产品发布前,一定要在测试环境打开 ASan 跑一遍所有用例。这能发现大部分内存相关 Crash。