段错误是什么?
段错误(segmentation fault,简称 segfault)是程序试图读写不允许访问的内存时发生的崩溃,比如通过空指针访问地址 0。操作系统会用 SIGSEGV 信号终止这个程序。
更新于 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 才能被发现。
- 程序执行一条读写某个地址的指令。在这里,就是读取
*score,即地址 0。 - 处理器的内存管理单元在页表中查找这个地址。这一页没有被映射,或者程序正试图写入一个只读的页。
- 处理器停止这条指令,通过一次缺页异常把控制权交给内核。
- 内核检查这次访问是否可能合法,比如是否只是需要扩展栈。结果并不合法,于是内核向进程发送 11 号信号
SIGSEGV。 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 则会导致内存泄漏。为什么叫段错误?
SIGSEGV 都保留了下来。在 Windows 上,同样的事件叫作访问冲突(access violation)。Python 会出现段错误吗?
ctypes。运行 python -X faulthandler script.py,可以打印出崩溃时正在执行的 Python 代码行。退出码 139 是什么意思?
SIGSEGV,即段错误。shell 把因信号退出的进程报告为 128 加信号编号,而 128 + 11 = 139。在 Docker 和 Kubernetes 中,以 139 退出的容器,说明它的主进程发生了段错误。