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 知识体系总览 Crash (程序崩溃) 内存操作违规 运算异常 指令/资源问题 SIGSEGV SIGABRT SIGFPE / SIGILL 段错误 / 空指针 堆溢出 / 栈溢出 除零 / 非法指令 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。