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;
  • 某些资源限制相关信号,例如 SIGXCPUSIGXFSZ

实际行为还取决于信号处理函数、进程权限、资源限制和内核配置。

例如:

#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 接收内核传来的转储。它通常负责:

  1. 接收内核传来的 Core dump 数据;
  2. 记录进程名、PID、UID、信号、时间等元数据;
  3. 将元数据写入 systemd journal;
  4. 根据配置将 Core dump 保存在磁盘,或者只保留元数据;
  5. 通过 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:单个进程转储处理上限;
  • ExternalSizeMaxMaxUseKeepFree:控制磁盘使用和剩余空间。

配置值过小会导致大进程的 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 是否超过单文件限制;
  • 保留策略是否已清理旧文件;
  • 当前用户是否没有访问权限。

ExecMainCodeExecMainStatus 能帮助区分正常退出、信号退出等情况,但最终信号编号和程序日志仍应交叉验证。

如果进程运行在容器中,必须区分三个层次:

容器内进程的 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 -tttperf、调度跟踪或 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 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。