凌晨三点,服务器告警骤响。一个运行了三周的生产进程突然退出,日志只剩下冷冰冰的"Segmentation fault (core dumped)"。没有堆栈信息,没有用户请求上下文,一切似乎都随着进程的消亡而烟消云散。
但事实并非如此。那个被悄悄写入磁盘的 core 文件,保存了程序崩溃瞬间的完整内存映像。它不会说话,却记录着一切真相。
一个名字的六十年回响
“Core dump” 这个词在 1959 年首次出现在计算机文献中。那时的"core"并非指 CPU 核心,而是指磁芯存储器(magnetic core memory)——一种由细小磁环编织而成的早期存储技术。
每个磁环只能存储一个比特,通过导线穿过其中进行读写。一个 32×32 的磁芯板只能存储 128 字节。当程序出错时,工程师会把整个磁芯存储器的内容"倾倒"(dump)出来,打印在几米长的折叠纸带上,逐位分析。
磁芯存储器早已退出历史舞台,但这个名字被保留了下来。今天的 core dump 不再打印在纸带上,而是以 ELF 格式写入磁盘,体积也从几百字节膨胀到几 GB。但它的本质没有变:一张程序死亡现场的快照。
ELF 格式:用结构化的方式存储混乱
Linux 的 core dump 使用 ELF(Executable and Linkable Format)格式。这并非巧合——ELF 本身就是为描述程序如何加载到内存而设计的,用它来描述程序在内存中的状态顺理成章。
一个 core dump 文件的 ELF 头部会标记 e_type = ET_CORE,表明这是一个核心转储文件。真正的内容存在于两类程序头(Program Header)中:
PT_LOAD 段:描述进程的内存区域。每个段记录了虚拟地址、大小、权限标志,以及实际存储的数据。这些段可能对应:
- 代码段(只读可执行)
- 数据段(读写)
- 堆(动态分配的内存)
- 栈(每个线程一个)
- 内存映射文件
- 共享库
PT_NOTE 段:存储元数据,以"笔记"的形式组织。每条笔记包含名称、类型和数据三部分:
NT_PRSTATUS:寄存器状态,包括崩溃时的指令指针、栈指针NT_PRPSINFO:进程信息,如进程名、参数列表NT_SIGINFO:导致崩溃的信号详情NT_AUXV:辅助向量,包含程序加载时的环境信息NT_FILE:内存映射文件列表,记录哪些内存区域对应哪些文件
这种设计的一个精妙之处在于:对于只读的内存映射文件(如共享库的代码段),core dump 不必重复存储其内容,只需记录文件路径和偏移量。调试器会从原始文件中读取。这大大减小了 core dump 的体积。
信号的判决:谁有资格留下遗言
并非所有程序退出都会产生 core dump。内核只会在进程收到特定信号时才触发转储:
| 信号 | 编号 | 含义 |
|---|---|---|
| SIGSEGV | 11 | 段错误,访问非法内存 |
| SIGABRT | 6 | 程序主动中止(abort) |
| SIGBUS | 7 | 总线错误,对齐问题 |
| SIGFPE | 8 | 浮点异常,如除零 |
| SIGILL | 4 | 非法指令 |
| SIGQUIT | 3 | 终端退出请求 |
其中 SIGSEGV 是最常见的"杀手"。当程序尝试访问未映射的内存地址、写入只读区域、或执行没有执行权限的代码时,CPU 触发页面错误异常,内核将其转换为 SIGSEGV 信号发送给进程。
信号的来源决定了能否生成 core dump。可以通过 ulimit -c 命令查看当前限制:
$ ulimit -c
0 # 默认禁用
$ ulimit -c unlimited # 启用,无大小限制
但即便 ulimit 允许,某些情况下 core dump 仍会被禁止:
- SUID/SGID 程序:出于安全考虑,默认不生成 core dump(可通过
/proc/sys/kernel/suid_dumpable调整) - 已自定义信号处理:如果程序注册了 SIGSEGV 处理函数且未重新触发默认行为
- 容器环境:可能在 namespace 隔离后无法写入
内核的死亡证明签发流程
当信号触发 core dump 时,内核会执行一系列精密操作:
第一步:检查权限。查看进程的 dumpable 属性。对于 SUID 程序,这个属性由 /proc/sys/kernel/suid_dumpable 控制,默认为 0(禁止转储)。
第二步:确定输出方式。读取 /proc/sys/kernel/core_pattern:
$ cat /proc/sys/kernel/core_pattern
|/usr/lib/systemd/systemd-coredump %P %u %g %s %c %h
如果第一个字符是 |,内核会把 core dump 通过管道传给指定程序处理(如 systemd-coredump);否则按路径模板写入文件。模板支持多种展开符:
%p:进程 ID%u:用户 ID%s:信号编号%e:程序名
第三步:遍历内存映射。通过 /proc/PID/maps 获取所有 VMA(Virtual Memory Area),根据 /proc/PID/coredump_filter 决定哪些区域需要转储:
$ cat /proc/self/coredump_filter
00000033
这是一个位掩码,每一位代表一类内存:
- bit 0:匿名私有内存(栈)
- bit 1:匿名共享内存
- bit 2:文件映射私有内存
- bit 3:文件映射共享内存
- bit 4:ELF 头
- bit 5:大页私有内存
- bit 6:大页共享内存
默认值 0x33 表示转储栈、匿名共享内存、ELF 头和大页内存,但不转储文件映射内容——因为后者可以从原始文件恢复。
第四步:写入 ELF 文件。按 PT_NOTE(寄存器等元数据)和 PT_LOAD(内存内容)的顺序组织数据,写入输出。
整个过程对进程是透明的——进程已经死亡,它在内存中的最后一刻被永久定格。
GDB 的现场重建术
拿到 core dump 后,GDB 如何重建崩溃现场?
第一步:加载可执行文件。GDB 需要原始的可执行文件(以及可能的共享库)来获取符号表和调试信息:
$ gdb ./my_program core.12345
为什么需要可执行文件?Core dump 只包含内存内容和寄存器状态,不包含:
- 函数名和变量名(符号表)
- 源代码行号映射(调试信息)
- 类型定义(结构体布局等)
这些信息存储在可执行文件的 .symtab 和 .debug_* 段中。
第二步:重建内存布局。解析 PT_LOAD 段,恢复进程的虚拟地址空间。对于 FileSiz 为 0 的段(只读文件映射),GDB 会从原始文件中读取。
第三步:恢复线程上下文。从 NT_PRSTATUS 笔记中提取每个线程的寄存器值:
rip/eip:指令指针,指向崩溃时的代码位置rsp/esp:栈指针rbp/ebp:帧指针(如果使用)- 其他通用寄存器
第四步:回溯调用栈。这是最复杂的部分。GDB 需要从当前的栈帧开始,逐帧回溯:
对于帧指针方式:编译器在函数开头会保存旧的帧指针(push %rbp; mov %rsp, %rbp),栈中形成链表结构。GDB 只需跟随这个链表即可。
但现代编译器默认省略帧指针(-fomit-frame-pointer)以节省一个寄存器。这时 GDB 必须依赖 DWARF 调试信息。
DWARF 的 .eh_frame 段包含"调用帧信息"(Call Frame Information),以字节码形式描述每个指令地址对应的栈帧布局:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16 # CFA(规范帧地址)= rsp + 16
.cfi_offset %rbp, -16 # rbp 保存在 CFA - 16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp # CFA 现在通过 rbp 计算
GDB 会执行这些指令,模拟栈帧的变化,从而计算出返回地址和上一帧的位置。
调试信息的分离与重聚
生产环境的可执行文件通常会 strip 掉调试信息以减小体积:
$ strip --strip-debug ./my_program
这会导致 core dump 分析变成"裸读"——只能看到机器码地址,没有函数名和行号。
解决方案是调试信息分离:
# 提取调试信息到单独文件
$ objcopy --only-keep-debug ./my_program ./my_program.debug
# 剥离原文件
$ strip --strip-debug ./my_program
# 添加 .gnu_debuglink 段指向调试文件
$ objcopy --add-gnu-debuglink=./my_program.debug ./my_program
GDB 会自动查找并加载对应的调试文件。更现代的方式是使用 build-id:每个编译产物有唯一的 160 位标识符,调试器可以通过 debuginfod 服务按需下载调试信息。
ASLR:随机化的代价
地址空间布局随机化(ASLR)是一项重要的安全机制,它让栈、堆、共享库的加载地址每次运行都不同。但这给 core dump 分析带来了挑战:
问题一:符号地址不匹配。共享库中的函数地址在编译时未知,需要运行时重定位。Core dump 记录的是运行时地址,调试器需要知道加载偏移量才能映射回符号表。
解决方案:NT_FILE 笔记记录了每个共享库的加载地址范围。GDB 结合 .dynamic 段中的 DT_DEBUG 条目(指向 r_debug 结构),可以重建动态链接器的加载信息。
问题二:跨机器调试困难。如果 core dump 生成在 A 机器,在 B 机器上分析,可能遇到:
- 相同的共享库文件不同版本
- 内核版本不同
- 调试信息不匹配
解决方案:使用容器或虚拟机保持环境一致,或者使用 debuginfod 按需获取匹配的调试文件。
体积与信息的权衡
一个使用了大量堆内存的程序,其 core dump 可能达到数 GB。在存储受限的嵌入式设备或容器环境中,这是不可接受的。
策略一:过滤内存类型。通过 coredump_filter 排除堆:
# 只保留栈、共享内存、ELF头
$ echo 0x13 > /proc/self/coredump_filter
策略二:截断栈深度。每个线程的栈可能很大,但只有栈顶部分包含有用信息。可以只保存每个栈的前 8KB 或 16KB。
策略三:设备端回溯。在生成 core dump 之前,直接在设备上进行栈回溯,只输出函数名和行号列表。这需要设备上有足够的符号信息,但输出体积极小。
Memfault 的实践表明,通过这些优化,一个 2.6 MB 的完整 core dump 可以压缩到约 75 KB,体积减少 35 倍,同时仍能获取完整的调用栈。
安全暗流:当诊断工具成为攻击面
2025 年 5 月,Qualys 披露了 CVE-2025-4598,一个影响 systemd-coredump 的 TOCTOU(Time-of-Check-Time-of-Use)漏洞。
攻击原理是利用 PID 重用窗口:当 SUID 程序崩溃时,systemd-coredump 会从 /proc/$PID/auxv 读取进程信息来决定 core dump 的访问权限。如果攻击者能在窗口期内让内核重用该 PID(通过快速创建大量进程),并在 systemd-coredump 读取时用普通进程"抢占"该 PID,systemd-coredump 就会误判进程类型,将本应只有 root 可访问的 SUID 进程 core dump 暴露给普通用户。
这个漏洞揭示了一个深层问题:core dump 系统横跨内核态和用户态,涉及进程生命周期管理,其攻击面比想象中更大。
缓解措施包括:
- 设置
suid_dumpable=0(完全禁止 SUID 程序的 core dump) - 升级到使用内核 6.15 的 pidfd 机制的系统(pidfd 不会因 PID 重用而指向不同进程)
超越快照:时间旅行调试
Core dump 是静态的——它只捕获一个瞬间。但有些 bug 需要理解"如何到达这一刻"才能定位。
Mozilla 开发的 rr 项目提供了一种可能:它通过记录所有非确定性事件(系统调用结果、信号、线程调度顺序),实现对程序执行的确定性重放。
$ rr record ./my_program
# 程序运行并被记录
$ rr replay
# 可以在任何时刻暂停、反向执行、设置反向断点
(gdb) reverse-continue
rr 的开销很低(通常小于 2x),适合在 CI 环境中使用。但它依赖特定的硬件特性(性能计数器)和内核版本,且无法用于已经在生产环境中崩溃的程序。
对于后者,core dump 仍是不可替代的"黑匣子"。
结语
程序崩溃后的 core dump 就像案发现场的法医证据:它不会主动告诉你真相,但只要你懂得如何解读,它会回答所有问题。
从磁芯存储器时代的纸带打印,到今天基于 ELF 的结构化存储;从简单的寄存器转储,到包含完整内存映像的诊断文件;从依赖帧指针的朴素回溯,到利用 DWARF 字节码的精确重建——core dump 技术在六十年间不断演进,但其核心价值从未改变:让死去的进程开口说话。
下次看到"Segmentation fault (core dumped)“时,别急着重启服务。那个默默生成的 core 文件,可能正藏着你需要的答案。
参考资料
- Linux Coredumps (Part 1-3), Memfault Interrupt Blog
- Anatomy of an ELF core file, Gabriel Féron
- What’s Inside a Linux Kernel Core Dump, Oracle Linux Blog
- Stack unwinding, MaskRay
- Analysis of CVE-2025-4598: systemd-coredump, Oracle Linux Blog
- Core dump - Wikipedia
- proc_pid_coredump_filter(5) - Linux manual page
- core(5) - Linux manual page
- rr: lightweight recording & deterministic debugging, rr-project.org
- Hacker Folklore, Matthias Endler
- DWARF Debugging Information Format, dwarfstd.org