Menu
CoddyTech

段错误是什么?

段错误(segmentation fault,简称 segfault)是程序试图读写不允许访问的内存时发生的崩溃,比如通过空指针访问地址 0。操作系统会用 SIGSEGV 信号终止这个程序。

作者 Kevin Spektor, 联合创始人兼 CTO

更新于 2026年9月24日

一个 C 程序打印出第一行,然后就停了下来,还附带一条没有任何人写过的信息:

#include <stdio.h>

int main(void) {
    int *score = NULL;

    printf("About to read the score\n");
    printf("Score: %d\n", *score);
    return 0;
}
About to read the score
bash: line 1: 64030 Segmentation fault: 11  ./seg

这段输出来自 macOS 上的 bash,其中 64030 是进程 ID,11 是信号编号。在 Linux 上,同样的崩溃通常显示为 Segmentation fault (core dumped)。无论哪种情况,程序都没能打印出分数。score 存放的是地址 0,而 *score 要求处理器读取这个地址上的内存,这是任何程序都不被允许做的事。

段错误是怎样发生的

每个程序都运行在自己的虚拟地址空间里,这是一个巨大的地址范围,由操作系统一块一块地填充。操作系统把程序的代码、全局变量、栈和堆映射到这个范围内,映射的单位叫作页(大多数 x86 Linux 系统上是 4 KB,搭载 Apple 芯片的 Mac 上是 16 KB)。大部分地址都没有被映射,地址 0 永远在其中,这样空指针的 bug 才能被发现。

  1. 程序执行一条读写某个地址的指令。在这里,就是读取 *score,即地址 0。
  2. 处理器的内存管理单元在页表中查找这个地址。这一页没有被映射,或者程序正试图写入一个只读的页。
  3. 处理器停止这条指令,通过一次缺页异常把控制权交给内核。
  4. 内核检查这次访问是否可能合法,比如是否只是需要扩展栈。结果并不合法,于是内核向进程发送 11 号信号 SIGSEGV。
  5. SIGSEGV 的默认动作是终止进程,并在系统允许时保存一份核心转储(core dump)。随后 shell 打印出那条信息,并把退出状态设为 139,也就是 128 加上信号编号。

所以,段错误既不是编译器报告的,也不是语言抛出的异常。它是硬件和内核在保护内存,这使它成为最突然的一种运行时错误。Windows 把同样的事件作为访问冲突处理,异常代码是 0xC0000005。

段错误的常见原因

C 和 C++ 允许程序计算出任意地址并使用它,所以原因归结起来都是用了一个无效的地址。

  • 解引用空指针。 int *p = NULL; *p = 5; 像 malloc 或 fopen 这样失败时返回 NULL 的函数,如果不检查它们的返回值,就会走到这一步。
  • 索引远远超出数组的末尾。 C 语言不检查边界,所以 arr[1000000] 只不过是往后数一百万个元素的一个地址。
  • free 之后继续使用内存。 指针里还存着原来的地址,但那块内存已经不属于你了。
  • 未初始化的指针。 int *p; *p = 5; 会通过 p 碰巧存着的随便什么垃圾值去写内存。
  • 栈溢出。 一个没有终止条件的递归函数会不停地添加栈帧,直到超出栈的末尾。下面的例子在 macOS 上崩溃,显示 Segmentation fault: 11,退出状态为 139。
  • 写入字符串字面量。 char *name = "coddy"; name[0] = 'C'; 试图修改只读内存。在 Linux 上这是段错误。在 macOS 上,同样的程序以 Bus error: 10 停止,这是一个相关的信号。
#include <stdio.h>

int depth(int n) {
    return depth(n + 1) + 1;   /* never stops calling itself */
}

int main(void) {
    printf("%d\n", depth(0));
    return 0;
}

小错误往往根本不会崩溃,这反而更危险。下面这个循环读取了一个三元素数组末尾之后的一个元素:

#include <stdio.h>

int main(void) {
    int scores[3] = {72, 88, 95};
    int total = 0;

    for (int i = 0; i <= 3; i++) {   /* <= reads scores[3] */
        total += scores[i];
    }
    printf("Total: %d\n", total);
    return 0;
}

在 Mac 上用 Clang 编译后,它打印出 Total: 256。正确的总分是 255。scores[3] 是栈上紧随其后的 4 个字节,它们属于这个程序,所以不会触发错误,垃圾值就这样被悄悄加了进去。操作系统只会阻止对完全不属于该程序的内存的访问。

这两种错误的修复方法都是同一个习惯:清楚元素有多少个,在跟随指针之前先检查它。

Best: 95
No scores, nothing to read

“core dumped”是什么意思

核心转储是一个文件,保存了程序崩溃那一刻内存的副本。调试器之后可以打开它,准确地显示程序当时执行到了哪里、各个变量的值是什么:gdb ./app core。在许多 Linux 发行版上,systemd-coredump 会收集这些文件,用 coredumpctl list 可以查看。如果核心转储被关闭了,比如执行过 ulimit -c 0,信息就只有 Segmentation fault,没有括号里的内容。

如何找到崩溃的那一行

程序崩溃的那一行往往不是有 bug 的那一行。一个指针可能在某个函数里变得无效,却在很久之后的另一个函数里才被使用。下面这些工具能把两处都找出来。

调试器。 带上调试信息编译,在调试器中运行程序。程序停下来时,bt(backtrace)会打印出函数调用链,带有文件名和行号。在 Linux 上,调试器通常是 gdb;在 macOS 上是 lldb,bt 的用法一样。

gcc -g app.c -o app
gdb ./app
(gdb) run
(gdb) bt

AddressSanitizer。 用 -fsanitize=address 编译(GCC 和 Clang 都支持),然后正常运行程序。它不会只给出一个光秃秃的段错误,而是打印一份报告,指出错误的种类,比如 heap-use-after-free 或 stack-buffer-overflow,并给出发生访问的那一行和分配这块内存的那一行。上面那个悄无声息地多读一个元素的错误本身从不崩溃,它也能抓出来。

Valgrind。 在 Linux 上,valgrind ./app 可以直接运行未经修改的程序,并报告每一次非法读写,比如 Invalid read of size 4。

其他语言中的段错误

Python、Java 和 JavaScript 在使用每一个索引和引用之前都会先检查,所以同样的错误会变成带有清晰信息的异常:Python 中的 IndexError 或 AttributeError,Java 中的 ArrayIndexOutOfBoundsException 或 NullPointerException,JavaScript 中的 TypeError。这些都可以用异常处理来捕获。段错误却不能这样处理:它是一个信号,C++ 的 catch 块看不到它。

当底层的 C 代码出错时,Python 程序仍然可能发生段错误。下面这一行让 ctypes 去读取地址 0:

import ctypes
ctypes.string_at(0)

用 python3 -X faulthandler 运行时,Python 3.12 打印出 Fatal Python error: Segmentation fault,后面跟着当时正在运行的 Python 代码行。C 扩展或原生库存在内存 bug 时,也会发生同样的崩溃。Rust 采用了不同的做法:在程序构建之前,它的编译器就会拒绝大多数可能访问无效内存的代码。

下一步学什么

C 语言指南中的段错误一文,用最小化的程序逐一讲解了每种原因及其修复方法。想从一开始就避免这些错误,可以阅读指针、空指针以及栈和堆,然后在 C 语言课程中练习。至于那些替你检查内存的语言如何报告错误,请看运行时错误这一页。

常见问题

段错误怎么解决?
先找到出错的那一行:用 -g 编译,在 gdb 或 lldb 中运行程序,崩溃后输入 bt;或者用 -fsanitize=address 编译,得到一份详细的报告。然后修复这一行用到的指针或索引:检查指针是否为 NULL,让数组索引小于数组长度,free 之后不要再使用那块内存,并确保递归能够结束。
段错误是内存泄漏吗?
不是,它们恰好是相反的问题。内存泄漏是程序分配了内存却从不释放,所以程序会继续运行,占用的内存越来越多。段错误是访问了不属于程序的内存,程序会被立即终止。free 用错,比如释放后还在使用指针,可能导致段错误;而忘记 free 则会导致内存泄漏。
为什么叫段错误?
这个名字来自内存分段,这是一种较早的设计:程序的内存被划分成有固定边界的段,访问自己段之外的地址就是一次错误(fault)。现代系统改用分页来管理内存,但这个名字和信号名 SIGSEGV 都保留了下来。在 Windows 上,同样的事件叫作访问冲突(access violation)。
Python 会出现段错误吗?
普通的 Python 代码不会,因为 Python 会检查每一个索引和引用,出问题时抛出异常。但 Python 程序仍可能在 C 代码中发生段错误:C 扩展模块、机器学习框架之类的库,或者 ctypes。运行 python -X faulthandler script.py,可以打印出崩溃时正在执行的 Python 代码行。
退出码 139 是什么意思?
退出码 139 表示进程被 11 号信号杀死,也就是 SIGSEGV,即段错误。shell 把因信号退出的进程报告为 128 加信号编号,而 128 + 11 = 139。在 Docker 和 Kubernetes 中,以 139 退出的容器,说明它的主进程发生了段错误。
Coddy 编程语言插图

和 Coddy 一起学编程

开始