Linux 基础体系 · 第 78/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。
Linux Core Dump 与崩溃诊断:coredumpctl、gdb、符号和隐私
Core Dump 到底是什么
Core dump(核心转储)是进程异常终止时保存的一份进程状态快照,通常包括:
- 进程的虚拟内存映射及其中一部分内容;
- 所有线程的寄存器状态;
- 当前导致进程终止的信号;
- 内存映射、共享库、文件等元数据;
- 内核认为对调试有用的辅助信息。
它不是“进程运行录像”,也不是某个函数的日志。Core dump 只能回答“进程结束这一刻大致处于什么状态”,不能直接回答:
- 之前几分钟发生了哪些系统调用;
- 哪个线程先修改了某个共享变量;
- 某个网络包是否到达;
- 一个锁等待了多长时间;
- 进程崩溃前的完整请求链路是什么。
因此,Core dump 适合分析崩溃时的内存、调用栈、寄存器和线程状态;系统调用、性能和时间序列问题则通常还需要 strace、日志、perf 或 eBPF 等证据。
在 Linux 中,Core dump 通常由以下路径产生:
进程收到致命信号
│
▼
内核判断是否允许转储
│
├── 不允许:进程终止,不产生可用 core
│
└── 允许:根据 core_pattern 决定文件或处理程序
│
┌───────────┴───────────┐
▼ ▼
直接写 core 文件 交给 systemd-coredump
│
▼
文件系统和/或 systemd journal
│
▼
coredumpctl
│
▼
gdb 分析
这里的每一步都可能失败。即使应用确实崩溃,也不代表一定存在一个可以被 gdb 打开的 Core dump。
哪些信号会触发 Core dump
Linux 信号有不同的默认动作。部分信号的默认动作是“终止进程并生成 Core dump”,常见的包括:
SIGSEGV:非法内存访问;SIGABRT:通常由abort()产生;SIGBUS:总线错误,例如某些错误的内存映射访问;SIGILL:非法指令;SIGFPE:算术异常;SIGQUIT:默认会终止并生成 Core dump;- 某些资源限制相关信号,例如
SIGXCPU、SIGXFSZ。
实际行为还取决于信号处理函数、进程权限、资源限制和内核配置。
例如:
#include <signal.h>
#include <stdlib.h>
int main(void) {
abort();
}
abort() 会向当前进程发送 SIGABRT。如果该信号没有被有效拦截,且系统允许生成 Core dump,程序会终止并进入转储流程。
相反,下面的程序虽然收到了 SIGSEGV,但安装了信号处理函数:
#include <signal.h>
#include <unistd.h>
static void handler(int sig) {
const char message[] = "caught\n";
(void)sig;
(void)write(STDERR_FILENO, message, sizeof(message) - 1);
}
int main(void) {
signal(SIGSEGV, handler);
int *p = 0;
*p = 1;
return 0;
}
信号处理函数可能打印 caught,然后返回。对于同步产生的非法访问,程序通常会再次面对同一条故障指令,最终仍可能终止;但如果处理函数改变了执行状态或调用了 _exit(),最终结果就可能不是 Core dump。不能仅根据“出现了段错误”推断“必然有 core 文件”。
生产程序一般不应试图在普通信号处理函数中完成复杂诊断。信号处理函数中只有极少数函数是异步信号安全的,调用 malloc()、printf()、复杂日志库或锁操作可能导致二次崩溃或死锁。更可靠的做法是让默认致命信号行为保留,依靠 Core dump 和进程外的收集器诊断。
内核如何决定是否生成 Core dump
RLIMIT_CORE:进程的 Core 大小限制
每个进程有一个 RLIMIT_CORE 资源限制,由软限制和硬限制组成。Shell 中可以查看:
ulimit -c
常见输出:
0
表示当前 Shell 启动的进程默认不允许生成传统意义上的 Core 文件。设置为无限制:
ulimit -c unlimited
./crash-demo
这个设置只影响当前 Shell 及其后代进程,不会自动影响已经运行的服务,也不会持久化到所有登录会话。
对于 systemd 服务,应在 unit 中设置:
[Service]
LimitCORE=infinity
修改后执行:
sudo systemctl daemon-reload
sudo systemctl restart example.service
验证服务进程的实际限制:
pid=$(systemctl show -p MainPID --value example.service)
sudo awk '/Max core file size/ {print}' "/proc/$pid/limits"
预期应能看到类似:
Max core file size unlimited unlimited bytes
RLIMIT_CORE 是必要条件之一,但不是充分条件。core_pattern、可转储属性、磁盘空间、systemd-coredump 配置和权限仍可能阻止最终保存。
core_pattern:内核把 core 交给谁
查看当前配置:
cat /proc/sys/kernel/core_pattern
如果输出类似:
core
内核通常会在当前工作目录生成名为 core 的文件。
如果输出以竖线开头,例如:
|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h
表示内核不直接写普通文件,而是启动指定的 Core dump 处理程序,并把转储数据通过标准输入传给它。具体路径和参数在不同发行版、systemd 版本中可能不同,不能硬编码某个发行版的路径。
core_pattern 是全局内核参数,修改它会影响整台机器上的进程:
sudo sysctl -w kernel.core_pattern=/var/crash/core.%e.%p.%t
其中 %e、%p、%t 等是内核支持的替换字段,例如可分别表示程序名、进程 ID 和时间戳。字段集合和行为应以当前系统的 core(5) 手册为准。
直接改变全局配置前必须确认是否已有 systemd-coredump、发行版崩溃收集器或第三方代理。覆盖 core_pattern 可能导致 coredumpctl 看不到新的崩溃,也可能让敏感的 Core 文件散落到应用工作目录。
Dumpable 属性和特权程序
Linux 对 set-user-ID、set-group-ID 程序和改变过凭据的进程有额外保护。进程的 dumpable 属性可以通过:
cat /proc/<PID>/status | grep '^Dumpable:'
查看,但部分内核版本和工具展示细节存在差异。程序也可以使用 prctl(PR_SET_DUMPABLE, ...) 改变该属性。
系统级参数:
cat /proc/sys/fs/suid_dumpable
控制特权程序的转储策略。原因很直接:特权进程的内存可能包含高权限凭据、其他用户数据或密钥。如果不做限制,普通的崩溃文件就可能变成权限泄露渠道。
因此,调试一个 setuid 程序时,不能只执行:
ulimit -c unlimited
还要确认其 dumpable 状态、suid_dumpable 策略、文件权限和收集器配置。生产环境不应为了“方便拿到 core”而盲目放宽全局特权转储策略。
systemd-coredump 与 coredumpctl
现代使用 systemd 的发行版经常通过 systemd-coredump 接收内核传来的转储。它通常负责:
- 接收内核传来的 Core dump 数据;
- 记录进程名、PID、UID、信号、时间等元数据;
- 将元数据写入 systemd journal;
- 根据配置将 Core dump 保存在磁盘,或者只保留元数据;
- 通过
coredumpctl提供查询和导出接口。
这不是 Linux 内核规范要求的唯一实现,而是 systemd 的常见实现。没有 systemd,或者发行版启用了 ABRT、Apport、云厂商代理等其他机制时,命令和存储位置会不同。
查询崩溃记录
列出已记录的崩溃:
coredumpctl list
典型输出可能类似:
TIME PID UID GID SIG COREFILE EXE
Mon 2025-03-10 14:20:01 UTC 4120 1000 1000 11 present /usr/local/bin/crash-demo
字段含义:
SIG=11表示SIGSEGV;COREFILE=present表示收集器认为 Core dump 可用;EXE是发生崩溃的可执行文件路径。
需要注意,list 的记录来自收集器的元数据;present 并不保证你当前用户一定有权限读取,也不保证调试所需的原始二进制和动态库仍在系统中。
按程序过滤:
coredumpctl list /usr/local/bin/crash-demo
按 PID 查看详细元数据:
coredumpctl info 4120
可以看到信号、时间、命令行、用户、内核、可执行文件以及 Core 文件大小等信息。不同 systemd 版本的字段略有差异。
导出 Core dump
将某次崩溃导出到当前目录:
coredumpctl dump 4120 --output=crash.core
验证文件类型:
file crash.core
常见输出:
crash.core: ELF 64-bit LSB core file, x86-64, ...
导出动作可能需要 root 权限,因为 Core 文件通常含有崩溃进程的全部敏感内存:
sudo coredumpctl dump 4120 --output=crash.core
不要把 --output 指向一个会被普通用户读取的共享目录,也不要把导出的文件自动上传到公共制品库。
直接用 coredumpctl debug
在安装了 GDB 且版本支持该操作时,可以执行:
coredumpctl debug 4120
它通常会从记录中找到对应的可执行文件和 Core dump,并启动调试器。这个命令适合交互式初步诊断,但它依赖:
- 收集器仍保留 Core dump;
- 原始可执行文件路径仍可访问;
- 相关共享库仍可找到;
- 当前用户有读取权限;
- GDB 已安装。
在故障归档或跨机器分析时,通常更可控的方式是显式导出:
sudo coredumpctl dump 4120 --output=/secure/incident/crash.core
sudo gdb /path/to/exact-executable /secure/incident/crash.core
这里的 exact-executable 必须是产生该 Core dump 的同一构建产物,而不是“源码相同但重新编译”的文件。
一个可重复的崩溃实验
下面的程序故意构造空指针写入,并通过多层函数调用制造可观察的调用栈:
// crash-demo.c
#include <stdio.h>
static void write_bad_address(int value) {
int *p = NULL;
printf("about to crash, value=%d\n", value);
*p = value;
}
static void level_two(int value) {
write_bad_address(value + 1);
}
static void level_one(void) {
level_two(41);
}
int main(void) {
level_one();
return 0;
}
编译:
gcc -g -O0 -fno-omit-frame-pointer -o crash-demo crash-demo.c
这些选项的作用不同:
-g:生成 DWARF 调试信息,使 GDB 可以把地址映射到源文件、行号、变量和类型;-O0:关闭优化,便于演示,但不代表生产程序必须关闭优化;-fno-omit-frame-pointer:保留帧指针,通常有助于某些栈回溯工具,但它不能替代 DWARF,也不是所有架构和编译器配置下的必要条件。
允许当前 Shell 的子进程生成 core:
ulimit -c unlimited
./crash-demo
预期可能看到:
about to crash, value=42
Segmentation fault (core dumped)
如果系统使用 systemd-coredump,当前目录可能没有名为 core 的文件。此时查询:
coredumpctl list ./crash-demo
导出最近一次对应记录:
coredumpctl dump ./crash-demo --output=crash.core
如果存在多次记录,应使用 PID 或时间条件精确选择,不要误分析另一份崩溃:
coredumpctl list --since "10 minutes ago" ./crash-demo
随后启动 GDB:
gdb ./crash-demo crash.core
在 GDB 中执行:
set pagination off
bt
info threads
thread apply all bt full
frame 0
info registers
x/i $pc
list
info locals
预期的调用栈可能类似:
#0 write_bad_address (value=42) at crash-demo.c:7
#1 level_two (value=41) at crash-demo.c:11
#2 level_one () at crash-demo.c:15
#3 main () at crash-demo.c:19
每条命令提供的证据不同:
bt:当前线程的调用栈;info threads:Core dump 中保存的线程列表;thread apply all bt full:对所有线程打印调用栈和局部变量;frame 0:切换到最内层当前栈帧;info registers:查看故障时的寄存器;x/i $pc:查看程序计数器指向的机器指令;list:显示对应源代码;info locals:显示当前帧中可恢复的局部变量。
在本例中,$pc 通常位于 *p = value 对应的写指令附近,而相关寄存器中可能有值为零的地址。真正的诊断不能只看函数名:应核对故障指令、寄存器、参数、线程状态和映射信息是否相互一致。
GDB、可执行文件和符号
符号不是只有“函数名”
符号可以泛指将机器地址映射到可读信息的数据,至少包括:
- 函数名;
- 全局变量名;
- 源文件和行号;
- 局部变量;
- 类型信息;
- 内联函数信息;
- 异常展开和栈回溯规则。
ELF 可执行文件中,调试信息通常以 DWARF 形式存放在 .debug_* 段。strip 可以删除其中一部分或大部分调试信息,从而减小部署文件,但不改变已经编译出的机器代码。
例如:
cp crash-demo crash-demo.debuggable
strip --strip-debug crash-demo
file crash-demo
此时 GDB 仍可能识别一部分动态符号或导出函数,但无法可靠显示源代码行、局部变量和类型。也就是说:
能显示函数名 ≠ 拥有完整调试符号
能回溯几个地址 ≠ 能解释源码和变量
为什么必须使用完全匹配的二进制
Core dump 中保存的是进程当时的地址和映射信息。GDB 需要用产生该 Core dump 的可执行文件和共享库解释这些地址。
以下文件即使来自同一份源码,也可能不匹配:
- 重新编译但编译器版本不同;
- 编译参数不同;
- 链接顺序不同;
- 只替换了一个依赖库;
- 部署文件被重新打包;
- 同名程序来自另一个构建流水线。
构建标识通常通过 ELF note 中的 Build ID 关联可执行文件、共享库和独立 debuginfo。可以检查:
readelf -n crash-demo | grep -A2 'Build ID'
在发行版中,调试包常常根据 Build ID 安装到调试符号目录。生产环境更稳妥的做法是同时归档:
可执行文件
所有相关共享库
Build ID
独立 debuginfo 或 DWARF 文件
编译器/链接器版本
构建参数和源码版本
不要只保存一个“当前线上二进制”。如果线上程序已经被更新,旧 Core dump 仍然需要旧版本的二进制和库。
分离调试符号
常见做法是把调试信息从发布二进制中分离:
objcopy --only-keep-debug crash-demo crash-demo.debug
strip --strip-debug crash-demo
objcopy --add-gnu-debuglink=crash-demo.debug crash-demo
这里的目标是:
- 线上二进制保留足够的运行元数据和 Build ID;
crash-demo.debug存放在受控的符号服务器或归档系统;- GDB 通过 debug link、Build ID 目录或显式路径找到调试信息。
显式指定符号搜索路径:
set debug-file-directory /srv/debug-symbols
如果共享库来自另一个根文件系统,可以设置:
set sysroot /srv/rootfs
也可以使用:
set solib-search-path /srv/rootfs/lib:/srv/rootfs/usr/lib
具体路径必须与目标程序的架构、发行版和库版本一致。只放入一个同名 libc.so.6 并不能保证可用,因为 ABI、Build ID 和内部布局都可能不同。
优化会改变调试体验
生产程序通常启用 -O2、-O3、LTO 或尾调用优化。此时 GDB 可能显示:
optimized out
或者出现看似不连续的源码行。原因包括:
- 变量被放入寄存器,随后被复用;
- 变量被完全消除;
- 多个源码语句被合并;
- 函数被内联;
- 尾调用重用当前栈帧;
- 栈已经损坏;
- 调试信息只能近似描述机器指令和源码的关系。
关闭优化能提高可读性,但可能改变时序、内存布局和竞态行为。用“专门的低优化版本”替代线上构建进行复现时,必须意识到它可能不再复现原始问题。
用 GDB 解释一个真实 Core dump
启动 GDB:
gdb /srv/artifacts/app-2025-03-10 /secure/cores/app.core
常用检查路径如下。
1. 确认崩溃信号和程序计数器
info program
info registers
x/i $pc
如果程序因 SIGSEGV 终止,$pc 是故障时执行或即将执行的指令地址。要判断是“读错地址”还是“写错地址”,需要结合反汇编和相关寄存器,而不能只看信号名称。
disassemble /m function_name
2. 查看当前线程调用栈
bt
bt full
bt full 会尝试打印参数和局部变量,但可能暴露密码、令牌、请求内容等敏感信息,也可能因为栈损坏产生误导性的变量值。
3. 查看所有线程
info threads
thread apply all bt
多线程程序中,触发崩溃的线程未必是根因所在的线程。例如:
- 线程 A 释放了对象;
- 线程 B 后续访问该对象;
- 最终由线程 B 触发
SIGSEGV。
此时仅查看当前线程的栈会漏掉其他线程的锁等待、生命周期操作和后台任务。
4. 查看内存和映射
info proc mappings
对某个地址进行检查:
x/16gx 0xADDRESS
0xADDRESS 必须替换为实际地址。若地址不在任何可读映射中,可能是空指针、悬空指针、越界指针或已经失效的映射。
Core dump 不一定包含所有原始内存页,因此对某些地址执行 x 可能失败。失败不等于进程当时没有该地址,而可能表示该页没有被转储、已被截断,或者收集器限制了大小。
5. 检查变量时避免把 GDB 结果当成绝对事实
frame 1
print value
print *pointer
如果栈已被破坏、变量被优化、寄存器值被重用,GDB 显示的变量可能是“无法恢复”或近似结果。诊断应将以下证据结合起来:
源代码位置
机器指令
寄存器
栈回溯
线程关系
内存映射
程序日志
构建符号
Core dump 不等于完整内存镜像
Core dump 的内容受多个因素控制。
映射和转储过滤
进程可以通过 /proc/self/coredump_filter 控制不同类型内存映射是否进入 Core dump:
cat /proc/self/coredump_filter
这是一个位掩码,不同位代表匿名映射、文件映射、私有映射、共享映射等类别。位的精确含义应查看当前系统的 core(5) 手册,因为内核版本可能引入差异。
程序还可以通过 madvise() 请求某个内存区域不进入 Core dump:
#include <sys/mman.h>
#include <stddef.h>
void *secret = /* 一段映射区域 */;
size_t length = /* 区域长度 */;
madvise(secret, length, MADV_DONTDUMP);
MADV_DONTDUMP 是减少敏感数据进入 Core dump 的工具,但不是全局保密保证:
- 数据可能已经复制到其他缓冲区;
- 密钥可能出现在寄存器、线程栈或库内部;
- 其他映射可能仍包含相同内容;
- Core dump 收集器和应用框架可能另有行为。
如果某内存区域对故障诊断非常重要,却被设置为 MADV_DONTDUMP,则相应证据会消失。安全和可诊断性必须按数据分类权衡。
Core 大小限制和截断
systemd-coredump 通常会配置单个 Core dump 大小上限、总占用上限和保留空间阈值。常见配置文件包括:
/etc/systemd/coredump.conf
/etc/systemd/coredump.conf.d/*.conf
可查看当前生效配置:
systemd-analyze cat-config systemd/coredump.conf
典型配置项可能包括:
[Coredump]
Storage=external
ProcessSizeMax=2G
ExternalSizeMax=10G
MaxUse=20G
KeepFree=5G
这些选项的可用性和默认值随 systemd 版本、发行版补丁而变化。含义大致是:
Storage=external:将 Core dump 保存为外部文件;Storage=journal:主要写入 journal;Storage=none:不保存 Core dump,只可能保留元数据;ProcessSizeMax:单个进程转储处理上限;ExternalSizeMax、MaxUse、KeepFree:控制磁盘使用和剩余空间。
配置值过小会导致大进程的 Core dump 不完整或根本不保存;配置过大则可能迅速耗尽根分区。磁盘耗尽会影响日志、数据库、更新和服务启动,因此 Core dump 保留策略本身也是生产可用性问题。
修改后应验证,而不是只检查配置文件:
sudo systemctl restart systemd-coredump.socket 2>/dev/null || true
coredumpctl info <PID>
df -h /var
不同发行版可能使用静态服务、socket 或其他收集器,重启命令不能机械套用。
隐私:Core dump 为什么是高敏感数据
进程内存可能包含:
- HTTP 认证头和 Cookie;
- OAuth、JWT、数据库密码和 API Token;
- TLS 会话密钥;
- 用户个人信息;
- 数据库查询结果;
- 尚未落盘的业务数据;
- 其他租户的数据;
- 加密前的明文内容。
因此 Core dump 的敏感性通常接近进程内存本身,而不是普通错误日志。
访问权限
不要使用以下方式处理生产 Core dump:
chmod 644 crash.core
cp crash.core /tmp/
scp crash.core public-host:/shared/
/tmp 可能被多个用户访问,权限不等于加密。更合理的归档目录至少应满足:
sudo install -d -m 0700 -o root -g root /secure/cores
sudo chmod 0600 /secure/cores/crash.core
分析时可以给专门的故障诊断组读取权限,但应明确:
- 谁可以读取;
- 保存多久;
- 是否记录访问审计;
- 是否允许跨境或跨租户传输;
- 上传前是否经过脱敏和审批。
删除和保留
删除 Core dump 后,磁盘上的文件可能仍存在于备份、快照、日志聚合或对象存储中。若涉及密钥或个人数据,应把 Core dump 纳入数据保留和删除策略,而不是只执行:
rm crash.core
systemd-coredump 的历史记录可以通过:
coredumpctl list
检查,过期清理通常受 systemd 配置和定期清理机制控制。清理前应确认是否有事故调查、法律留存或合规要求。
禁用和降级
不需要保存完整 Core dump 的生产节点可以采用更保守的策略:
[Coredump]
Storage=none
或者只对特定服务设置:
[Service]
LimitCORE=0
但这会降低事后诊断能力。一个常见取舍是:
- 开发、测试环境:保存完整 Core dump 和符号;
- 受控预发布环境:保存有限时间;
- 生产环境:按服务重要性、数据敏感度和合规要求决定,必要时只保存元数据或经批准的转储。
不能把“关闭 Core dump”理解为完成了全部隐私保护,因为应用日志、调试接口、崩溃报告和内存采集工具也可能泄露相同数据。
没有 Core dump 时如何定位原因
首先确认崩溃记录是否存在:
coredumpctl list --since today
如果没有记录,按以下顺序核查:
ulimit -c
cat /proc/sys/kernel/core_pattern
df -h
journalctl -u systemd-coredump --since today
对 systemd 服务:
systemctl show example.service -p LimitCORE -p ExecMainCode -p ExecMainStatus
还应检查:
- 服务是否真的由 systemd 启动;
- 崩溃是否发生在容器内部;
- 进程是否是 setuid 或主动设置了不可转储;
- 收集器是否被禁用;
- Core dump 是否超过单文件限制;
- 保留策略是否已清理旧文件;
- 当前用户是否没有访问权限。
ExecMainCode 和 ExecMainStatus 能帮助区分正常退出、信号退出等情况,但最终信号编号和程序日志仍应交叉验证。
如果进程运行在容器中,必须区分三个层次:
容器内进程的 RLIMIT_CORE
宿主机内核的 kernel.core_pattern
容器/宿主机上的 Core 收集器和存储目录
容器中没有 systemd 时,容器内执行 coredumpctl 通常没有意义。即使应用在容器内崩溃,Core dump 也可能由宿主机内核和宿主机收集器处理。容器还需要一个可写、容量受控且权限正确的挂载目录,具体方案取决于运行时和编排平台。
通过 Core dump 诊断内存错误的边界
Core dump 最适合以下类型的问题:
- 空指针解引用;
- 越界访问导致的
SIGSEGV; - 非法指令;
abort()触发的断言失败;- 某线程处于异常栈状态;
- 崩溃时的死锁或线程停顿线索;
- 动态库、地址空间和加载版本不一致。
但它不能单独证明内存错误的根因。以 use-after-free 为例:
线程 A:释放对象
线程 B:继续使用对象
线程 B:访问已复用的地址
线程 B:最终触发 SIGSEGV
Core dump 通常只记录线程 B 崩溃时的状态。对象何时释放、由谁释放,可能已经无法从内存中恢复。此类问题还需要:
- AddressSanitizer;
- ThreadSanitizer;
- Valgrind;
- 业务日志;
- 锁和对象生命周期追踪;
- 可复现测试。
另一个反例是数据竞争。Core dump 看到的变量值只是某个瞬间的结果,不能证明哪一次写入违反了内存模型。若需要分析时间顺序,应使用日志、strace -f -ttt、perf、调度跟踪或 eBPF 等工具获取动态证据。
Core dump 与 strace、性能分析的关系
不同工具观察的是不同层次:
| 工具 | 主要观察对象 | 能回答的问题 |
|---|---|---|
| Core dump + GDB | 进程终止瞬间 | 哪个线程、哪条指令、什么栈和寄存器导致终止 |
strace |
系统调用边界 | 进程是否阻塞、返回了什么错误、等待了哪个内核接口 |
perf |
采样或硬件性能事件 | CPU 时间花在哪里、是否存在热点或调度问题 |
| eBPF | 内核和用户态动态事件 | 延迟、I/O、网络、调度、内存分配等运行过程 |
| 日志和指标 | 应用语义与时间序列 | 请求、错误率、资源趋势和业务影响 |
例如,Core dump 显示线程停在 read() 的包装函数附近,并不能证明该调用是阻塞根因;此时需要查看寄存器、调用栈、系统调用跟踪和文件描述符状态。反过来,strace 发现某线程频繁返回 EAGAIN,也不能解释另一个线程为什么发生内存破坏。
可靠的崩溃调查通常把证据按时间和层次拼接:
部署版本/Build ID
│
├── 应用日志:发生了什么业务操作
├── strace:进入了哪些系统调用、如何返回
├── perf/eBPF:CPU、I/O、调度和延迟
└── Core dump:终止瞬间的线程、栈、寄存器、内存映射
Core dump 是事后状态证据,不是性能分析工具;性能工具也不能替代符号正确的 GDB 分析。
生产环境的诊断流程
一个可审计的流程可以按以下顺序执行。
1. 先保存元数据
coredumpctl info <PID> > incident-<PID>.info
同时记录:
崩溃时间
主机和容器标识
服务版本
可执行文件 Build ID
信号
命令行参数
相关日志时间范围
命令行参数本身也可能包含密码,因此归档前要检查并脱敏。
2. 再以只读方式导出原始 Core
sudo coredumpctl dump <PID> --output=/secure/cores/incident-<PID>.core
sudo chmod 0600 /secure/cores/incident-<PID>.core
sha256sum /secure/cores/incident-<PID>.core
哈希用于确认传输和分析过程中文件未被替换,但哈希不能保护内容机密。
3. 收集完全匹配的符号
至少准备:
崩溃时的可执行文件
崩溃时加载的共享库
独立调试符号
Build ID 对应关系
若无法得到匹配符号,可以先执行:
info files
info sharedlibrary
bt
但要明确标记结论可信度。地址看似落在某个函数中,并不等于源码行和局部变量已经可靠恢复。
4. 进行最小化初步分析
推荐先执行:
set pagination off
info program
info threads
thread apply all bt
info registers
x/i $pc
info proc mappings
只有在确认访问边界和审批要求后,再执行:
thread apply all bt full
因为 bt full 和内存检查可能把敏感内容显示在终端、终端录制、CI 日志或聊天系统中。
5. 复现和修复
Core dump 提供故障位置,但修复还需要回到源码和测试:
- 识别非法访问的对象生命周期;
- 判断是输入触发、竞态、资源耗尽还是 ABI 不一致;
- 增加回归测试;
- 使用 Sanitizer 或专用调试构建验证;
- 重新构建并记录新的 Build ID;
- 在受控环境确认新的 Core dump 可收集、可读取、可分析。
修复后不要用旧 Core dump 验证新二进制是否“能打开”。旧 Core dump 必须使用旧构建产物分析;新二进制只能用于新的复现或新的 Core dump。
常见误解
“开启 ulimit -c unlimited 就一定有 core”
不对。它只修改当前 Shell 的资源限制。systemd 服务、容器、特权程序和已有进程有各自的限制和转储策略。
“Core 文件越大,信息越完整”
不一定。转储内容还受到映射过滤、MADV_DONTDUMP、收集器上限和磁盘空间影响。更大的文件也意味着更高的隐私和存储风险。
“有函数名就说明符号完整”
不对。动态符号、导出符号和调试符号提供的信息层次不同。没有匹配 DWARF 时,局部变量、源码行和类型可能完全不可用。
“GDB 显示的局部变量一定是真的”
不对。优化、内联、寄存器复用和栈破坏都可能让变量显示为 optimized out 或不可信值。应将变量值与指令、寄存器和内存布局交叉验证。
“Core dump 能找到所有崩溃原因”
不对。它记录的是终止时刻,不记录完整的历史。use-after-free、数据竞争、偶发超时和资源耗尽通常需要运行时跟踪、日志或动态分析工具补充。
“把 Core dump 上传到调试平台就结束了”
不对。Core dump 可能包含密码、密钥和用户数据。上传前应确认访问控制、传输加密、保留期限、跨环境权限和删除策略。即使平台只显示调用栈,原始文件也可能被长期保存。
Core dump 的价值在于把“进程为什么死了”从猜测转化为可检查的状态证据;coredumpctl 负责发现和提取,GDB 负责解释,匹配的符号负责把地址还原为源码语义,而权限、保留和脱敏策略决定这份证据是否能够安全地进入生产诊断流程。
系列导航与关联阅读
- 系列入口:Linux 完整学习路线:从内核与文件系统到网络、性能和生产运维
- 上一篇:Linux 时间同步:Clocksource、NTP、chrony、漂移和分布式系统影响
- 下一篇:Linux perf 与火焰图:采样、调用栈、符号、CPU 和 Off-CPU
- 延伸:Linux 系统调用与 strace:调用边界、阻塞、错误和性能诊断
- 延伸:Linux 性能分析方法:CPU、内存、磁盘、网络与 eBPF 证据链
官方资料
本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。

评论
0 条讨论