Core Dump:跨平台采集、符号化与证据生命周期
服务在发布后收到 SIGSEGV,进程管理器立即拉起新实例,终端曾短暂显示 (core dumped)。值班同学第二天执行 coredumpctl list,还能看到 PID、时间和可执行文件,COREFILE 却是 missing;另一个人从旧制品库找来同名二进制,GDB 也给出了一条带函数名的栈。事故现场看似齐全,实际上内存快照已经过期,符号身份也没有证明。
Core Dump 是进程某一时刻的寄存器与选定虚拟内存映射,不是执行录像。采集是否发生由崩溃信号、资源限制、dumpable、权限、存储空间、内核模式和用户态处理器共同决定;分析是否可信又取决于 executable、符号、源码、共享库和构建身份。把“生成文件”当终点,会同时漏掉最容易失败的三段:触发之前没有容量和权限,导出之后没有身份校验,事故结束之后没有敏感数据销毁。
先画清采集链,而不是先找文件
Linux 的传统路径是:致命信号触发内核 coredump,内核按 RLIMIT_CORE、core_pattern 和进程状态把内容写成文件。Linux man-pages 的 core(5) 列出了不会生成文件的条件以及管道处理器语义。许多 systemd 主机把 core_pattern 配成以 | 开头的处理器,内核把数据经标准输入交给 systemd-coredump,后者再决定 journal 元数据、外部文件、压缩与保留。此时 shell 的工作目录未必出现 core,而且管道处理器路径不会执行 RLIMIT_CORE 对普通 Core 文件的大小限制。
Windows 常见的是 Windows Error Reporting、任务管理器、调试器或 Sysinternals ProcDump 生成 minidump/full dump;macOS 可由内核生成 core,也可由 LLDB 保存进程现场。三者都包含进程内存或其子集,但格式、触发器、权限与符号身份不同。Linux ELF 通常用 Build ID 与 DWARF,Windows PE/PDB 用 PDB 签名身份,Mach-O/dSYM 用 UUID。跨平台治理可以统一证据清单,不能把一条平台命令复制到另一平台。
选择采集形态时先问故障需要什么:崩溃调用栈可能只需较小 minidump;native heap 损坏、JIT 或锁对象可能需要完整内存;挂死没有致命信号,需要主动快照;数据竞争的历史顺序则超出普通 dump 能力。采集范围越大,复盘能力通常越强,暂停、I/O、空间和泄露风险也越高。
用目标主机的版本和生效配置说话
Linux 先确认内核入口、shell 限制、systemd/elfutils/GDB 能力和真正生效的配置。所有检查都是只读的:
日常分析机至少安装目标发行版提供的 GDB、binutils/elfutils 与 systemd 客户端;coredumpctl 不存在时,先查询发行版仓库中 systemd 或 systemd-coredump 的拆包关系。安装 systemd-coredump 可能接管全局 core_pattern,应先在同版本测试机比较安装前后的生效值,不能把“装一个 CLI”当作无行为变更。Windows 从 Microsoft Sysinternals/Windows SDK 的批准渠道安装 ProcDump、WinDbg 或 CDB,macOS 的 LLDB 通常由受控 Xcode Command Line Tools 提供;三类安装物都要保存来源、签名和版本证据。
uname -a
ulimit -Sc
ulimit -Hc
cat /proc/sys/kernel/core_pattern
cat /proc/sys/kernel/core_uses_pid
cat /proc/sys/kernel/core_pipe_limit
coredumpctl --version
gdb --version
readelf --version
systemd-analyze cat-config systemd/coredump.confulimit -Sc 是当前 shell 的 soft limit,服务进程可能从 systemd unit、容器 runtime 或其他父进程继承完全不同的限制。core_pattern 以普通路径模板开头时,关注路径、工作目录/mount namespace、写权限和文件名碰撞;以 | 开头时,关注处理器是否存在、并发 core_pipe_limit、journal 与外部存储。systemd-analyze cat-config 能合并主配置和 drop-in,比只读 /etc/systemd/coredump.conf 更接近生效事实;字段默认值和支持项仍以目标主机自带 man page 为准,不拿网站的 latest 文档替代本机版本。
Windows 先记录 OS build、工具来源和架构:
Get-ComputerInfo | Select-Object WindowsProductName, WindowsVersion, OsBuildNumber, OsArchitecture
Get-AuthenticodeSignature .\procdump64.exe
.\procdump64.exe -?
Get-Command cdb.exe, windbg.exe -ErrorAction SilentlyContinueProcDump 应来自 Microsoft Sysinternals 的受控安装物,签名状态需要有效;目标是 64 位进程时使用匹配工具和分析器。macOS 则记录系统、core 路径、资源限制和 LLDB:
sw_vers
uname -m
sysctl kern.corefile
launchctl limit core
ulimit -Sc
lldb --versionulimit 只影响当前 shell 及其子进程,launchd 服务要核对自身限制;kern.corefile 给出路径模板,不代表目录存在、可写或策略允许生成。版本确认的目的不是追新,而是让采集器、dump 格式、分析器和符号来自可解释的兼容组合。
在隔离 Linux 目录完成正向崩溃实验
使用无业务数据的小程序制造确定性 SIGSEGV。volatile 阻止编译器简单消除写操作,-g3 -O0 让第一轮符号化更直观:
#include <stdio.h>
static void fail_request(int request_id) {
volatile int *address = NULL;
fprintf(stderr, "request=%d before crash\n", request_id);
*address = request_id;
}
int main(void) {
fail_request(3909);
return 0;
}mkdir -p build evidence
cc -g3 -O0 -fno-omit-frame-pointer crash-demo.c -o build/crash-demo
sha256sum build/crash-demo | tee evidence/crash-demo.sha256
readelf -n build/crash-demo | sed -n '/Build ID/p' | tee evidence/crash-demo.build-id
ulimit -c unlimited
ulimit -Sc
(
set +e
./build/crash-demo
crash_status=$?
printf 'crash exit=%s\n' "$crash_status"
test "$crash_status" -eq 139
)ulimit -c unlimited 可能因 hard limit 或服务管理器约束而失败,必须先看它的退出码和随后打印的 soft limit;不能把命令写进脚本就当成已经生效。在 Linux Bash 中,这个程序预期先向标准错误写出 request=3909 before crash,shell 随后报告 Segmentation fault,包装块打印 crash exit=139 并以 0 结束;139 是 Bash 对信号 11 的 128 + signal 编码,不是程序主动返回的业务码。其他 shell 的提示文字和信号状态编码可能不同,应以目标 shell 文档以及 Core 元数据中的 SIGSEGV 为准。有些 shell 还会显示 (core dumped),但这只说明内核走到了 coredump 路径,不证明完整文件仍可访问。立即记录时间、PID(若由包装脚本启动)、boot ID、executable 绝对路径和 Build ID,再沿本机采集模式查找。
systemd-coredump 主机可先列出候选,再用明确 PID 或 journal match 选中本次现场:
coredumpctl list --since '-5min' /absolute/path/to/build/crash-demo
coredumpctl info <CRASH_PID>
install -d -m 0700 evidence/private
umask 077
coredumpctl dump <CRASH_PID> --output=evidence/private/crash-demo.core
chmod 0600 evidence/private/crash-demo.core
test -s evidence/private/crash-demo.core
gdb -q -nx -batch \
-iex 'set auto-load off' \
-iex 'set debuginfod enabled off' \
./build/crash-demo evidence/private/crash-demo.core \
-ex 'info files' -ex 'bt full'<CRASH_PID> 必须替换为本次列表中的值;自动化还应限制当前 boot ID 与时间窗,避免 PID 复用或同名旧记录。coredumpctl dump 以 0 退出且 test -s 通过,才说明这个导出路径得到了非空文件;两者任一失败都不能继续声称正向采集成立。完整正向结果还应满足:COREFILE 为可访问状态,GDB 报告导致终止的 SIGSEGV,回溯包含 fail_request 和 main,源码行指向空地址写入。GDB 批处理以 0 退出只表示命令没有报错,构建身份仍须独立核对。若系统使用传统文件模式,就从 core_pattern、崩溃进程的工作目录和 mount namespace 定位文件,再执行相同的权限收敛与 GDB 分析。
反向实验要覆盖“没有文件”和“文件不可信”
第一类失败发生在采集前。新 shell 中设置 ulimit -c 0 后再次运行测试程序:
(
ulimit -c 0
./build/crash-demo
)传统文件模式下预期不会产生新的 core。若 core_pattern 指向管道处理器,处理器仍可能收到崩溃并留下元数据或文件,这正说明不能用 ulimit -c 单点推断。即使 limit 非零,目录不可写、文件系统只读或已满、进程不可 dump、set-ID/file capability、安全模块、CONFIG_COREDUMP、MADV_DONTDUMP 和 coredump_filter 仍可能阻断或裁剪现场。
第二类失败是记录存在但内容不可用。coredumpctl list 的 COREFILE 需要逐项解释:present 表示外部文件可访问,journal 表示内容在 journal,truncated 表示不完整,error 表示访问/处理失败,missing 表示元数据还在但外部文件已删除,none 表示未保存。看到 missing 后不能继续承诺可导出;应转向日志、指标、符号化旧摘要或重新复现,并修正保留窗口。
第三类失败是权限。非特权用户通常只能读取自己崩溃进程的相关记录;访问其他服务时可能得到无匹配项或 journal 权限错误。正确修复是通过短时组、受控导出服务或双人审批取得所需现场,不是复制整个 /var/lib/systemd/coredump、放宽目录为全员可读或以 root 运行所有分析工具。导出副本的 owner、mode 和访问日志应单独核对,因为 journal 元数据权限与外部文件权限不是同一生命周期。
符号匹配决定分析结论能否成立
同名文件、相同版本字符串甚至同一 commit 都不保证是产生 Core 的那个二进制;编译器、链接顺序、feature flag、LTO 和依赖变化都可能改变地址。分析包至少包含 executable 摘要、Build ID、符号身份、源码 commit、编译命令/工具链和目标 rootfs 或共享库清单。
在实验中另编一份不同产物,制造错误身份:
cc -g3 -O2 crash-demo.c -o build/crash-demo-wrong
readelf -n build/crash-demo build/crash-demo-wrong | grep -A1 'Build ID'
gdb -q -nx -batch \
-iex 'set auto-load off' \
-iex 'set debuginfod enabled off' \
./build/crash-demo-wrong evidence/private/crash-demo.core \
-ex 'info files' -ex 'bt full'预期两个 Build ID 不同。GDB 可能提示 core 与 executable 不匹配,也可能给出 ??、错误行号、异常帧或一条表面合理的回溯;这条批处理命令也可能以 0 退出,因为身份警告未必属于命令执行错误。身份不一致时必须停止解释,而不是挑选最像业务原因的帧。正确 executable 若使用分离 DWARF,还要让 debug 文件的 Build ID/debuglink 匹配。共享库来自容器或另一发行版时,为 GDB 配置对应 sysroot,不能用分析机当前 glibc 代替目标 glibc。
Windows dump 要匹配 PE 与 PDB 身份,符号服务器应按产品、版本和访问域分层;.reload /f 的成功下载不等于源码就是正确提交。macOS 可用 dwarfdump --uuid 对比 executable、dSYM 与 Core 中镜像 UUID。三个平台共同的停线条件是“符号身份无法证明”,而不是“分析器还能显示几个函数名”。
systemd-coredump 要同时治理元数据与外部文件
systemd-coredump 通常先接收内核管道中的内容,再根据 coredump.conf 决定处理和存储。Storage= 决定 Core 留在 journal、外部文件还是不长期保存,Compress= 决定外部文件是否压缩,ProcessSizeMax= 决定多大的现场还会被处理并生成栈,ExternalSizeMax= 与 JournalSizeMax= 分别约束两种存储路径。它们不是同一个“文件大小上限”:例如 Core 超过 ProcessSizeMax= 后,即便仍按 ExternalSizeMax= 保存,也可能没有用户态生成的回溯。不同 systemd 版本的字段、默认值和语义会变化,部署前应查看本机 man coredump.conf 与合并配置。
一个面向服务的接入流程不是直接把所有限制设为 unlimited,而是先估算峰值 RSS、并发崩溃数、可接受 I/O 与保留时间,再配置专用 drop-in。服务自身的 core limit 也要核对:
systemctl show example.service -p LimitCORE -p User -p Group
systemctl cat example.service
systemd-analyze cat-config systemd/coredump.conf
journalctl SYSLOG_IDENTIFIER=systemd-coredump --since '-1h'配置要分成两层。服务 unit 的 LimitCORE= 决定该服务继承的 RLIMIT_CORE;/etc/systemd/coredump.conf.d/*.conf 是主机级 collector 策略,会影响该主机上的全部崩溃进程,不能伪装成某个服务的私有参数。先在测试节点建立一个有版本号的 drop-in,并把数值替换为容量评估结果:
# /etc/systemd/coredump.conf.d/60-production-budget.conf
[Coredump]
Storage=external
Compress=yes
ProcessSizeMax=2G
ExternalSizeMax=2G# systemctl edit example.service
[Service]
LimitCORE=2G修改后用 systemd-analyze cat-config systemd/coredump.conf 和 systemctl show example.service -p LimitCORE 读取合并结果,再触发无业务数据的测试崩溃。预期是新事件可列出、可导出且大小不越界;反例把测试进程 RSS 做到超过 ProcessSizeMax=,应观察 Core 是否仍被外部保存、是否标记截断以及栈是否缺失,不能只检查文件存在。回退时删除这两个受管 drop-in、执行 systemctl daemon-reload 并再次读取生效配置;coredump.conf 变化由下一次采集读取,不需要为了它重启整台主机。上述 2G 只是演示预算,不是推荐默认值。
Storage=none 只描述长期存储选择,不自动代表进程内存从未被用户态处理;若要连处理也禁用,systemd 文档给出的组合是 Storage=none 与 ProcessSizeMax=0,仍须在目标版本验证生效结果。反过来,把外部文件保留很久也不保证 journal 元数据同样久,最终会出现只有文件没有上下文,或只有元数据却 missing。容量策略应让崩溃事件、外部 Core、符号和发布清单的可用窗口重叠;systemd-tmpfiles、journald 保留和对象存储生命周期必须一起核对。
管道处理器还有并发瓶颈。崩溃风暴超过 core_pipe_limit 或存储吞吐时,采集本身会竞争 CPU、内存与磁盘,甚至延长服务恢复。团队应在测试环境制造受控并发崩溃,观察处理队列、journal 错误、截断状态、磁盘水位和应用重启时间;生产容量则设置最大并发、单文件上限、分区配额和丢弃优先级。比“全部保留”更可靠的目标是:最有诊断价值的首个/代表性现场可用,采集器不会把故障扩大成磁盘耗尽。
Windows 用触发规则选择 minidump 或 full dump
Microsoft ProcDump 文档 定义了异常、挂起和资源阈值等触发器。实验目录先限制 ACL 和剩余空间,然后用 -x 启动测试程序并在未处理异常时生成 dump:
$dumpDir = 'C:\DebugEvidence\CrashDemo'
New-Item -ItemType Directory -Force $dumpDir | Out-Null
icacls $dumpDir /inheritance:r /grant:r "${env:USERNAME}:(OI)(CI)F"
.\procdump64.exe -accepteula -ma -e -x $dumpDir C:\Lab\crash-demo.exe
Get-ChildItem $dumpDir | Select-Object Name,Length,LastWriteTime-e 监控未处理异常,-ma 请求完整 dump,-x 指定 dump 目录并启动目标。预期目标异常退出且目录产生 .dmp;若没有文件,检查触发器是否匹配、目标与工具架构、目录 ACL、EDR/安全策略和空间。对已经运行的进程,procdump64.exe -ma <PID> <FILE.dmp> 是主动快照,会造成暂停和较大 I/O,不应循环无上限采集。
用 WinDbg 或 CDB 打开后,先设置批准的私有符号源并检查模块身份,再执行异常分析与线程栈,例如 cdb -z crash.dmp 后运行 .symfix/.sympath、.reload /f、!analyze -v、~* kb。公共 Microsoft 符号源只解决系统模块,业务 PDB 必须来自对应发布。分析器显示 *** WARNING: Unable to verify checksum、PDB 不匹配或模块时间戳/签名异常时应停止业务归因。
full dump 可能接近进程提交内存并包含凭证;minidump 更小,但所含 heap、handle、thread 或 module 信息取决于 dump 类型。选择依据是故障问题,而不是固定使用 -ma。
WER LocalDumps 适合受管主机的持续策略。全局键会影响所有适用进程,更稳妥的起点是按可执行文件建立子键;DumpType=1 是 mini,2 是 full,DumpCount 达到上限后最旧文件会被替换。下面的配置需要管理员权限,目录 ACL 还必须允许实际服务账号写入:
$app = 'CrashDemo.exe'
$root = "HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\$app"
$dumpDir = 'C:\DebugEvidence\WER-CrashDemo'
New-Item -ItemType Directory -Force $dumpDir | Out-Null
icacls $dumpDir /inheritance:r /grant:r 'SYSTEM:(OI)(CI)F' 'Administrators:(OI)(CI)F'
New-Item -Path $root -Force | Out-Null
New-ItemProperty -Path $root -Name DumpFolder -PropertyType ExpandString -Value $dumpDir -Force
New-ItemProperty -Path $root -Name DumpType -PropertyType DWord -Value 1 -Force
New-ItemProperty -Path $root -Name DumpCount -PropertyType DWord -Value 3 -Force
Get-ItemProperty -Path $root | Select-Object DumpFolder,DumpType,DumpCount正向实验让 CrashDemo.exe 发生一次未处理异常,预期目录新增非零 .dmp,WinDbg 能确认异常和模块身份。反向实验改用没有对应 per-app 键的另一测试文件名,预期这里不产生 dump;若仍产生,说明存在全局 LocalDumps、ProcDump/WER 之外的采集器或继承策略,需先查清来源。清理时执行 Remove-Item -LiteralPath $root -Recurse,确认注册表键消失后再按保留规则删除实验 dump;不能只删目录而留下永久采集策略。应用自定义崩溃上报器可能绕过 LocalDumps,自动调试设置也会影响采集,因此“注册表值存在”不是成功证据。
macOS 同时受资源限制、目录与签名权限约束
在隔离终端中,先核对 kern.corefile 和 core limit,再运行测试 Mach-O。若策略允许,可在当前 shell 临时提高 soft limit;这不会修改其他 launchd 服务:
ulimit -c unlimited
./crash-demo
sysctl kern.corefile
ls -ld /cores 2>/dev/null || true预期是进程因信号退出;Core 是否出现取决于模板目录、权限、空间、dumpable/安全策略和进程签名状态。路径模板可能指向 /cores/core.<pid>,但必须以 sysctl 实际输出为准。不要为了本地实验关闭 SIP、全局改变系统保护或把 /cores 改成全员可写。launchd 托管进程还需查看 service limit 与运行用户,当前 shell 的 ulimit 不会神奇地传给它。
LLDB 离线分析使用匹配 executable 与 Core:
lldb -c /controlled/path/core.<pid> ./crash-demo
(lldb) image list -v
(lldb) thread list
(lldb) thread backtrace allimage list -v 中的 UUID 要与 executable/dSYM 的 dwarfdump --uuid 对齐。主动现场可在已授权 LLDB 会话中使用 process save-core,但这同样会暂停目标并复制内存。缺少 entitlement、权限或签名约束时,应建立可审计的调试构建/采集身份,而不是把系统安全边界当作“命令不好用”去关闭。
把采集接进发布与故障工作流
每个可发布构建应输出一份 machine-readable manifest,至少记录产品版本、commit、构建时间、目标 OS/CPU、编译器/linker、优化与 LTO、二进制摘要、ELF Build ID/PDB 身份/dSYM UUID、符号制品位置和基础镜像或系统库身份。运行平台把实例、镜像 digest 或安装包版本绑定到该 manifest,采集器在事件元数据中保存同一关联键。
崩溃处理器不应把大文件直接塞进告警消息。更可控的链路是:采集器写入本机受限暂存区,计算摘要并记录大小;上传到加密对象存储的隔离前缀;工单只保存事件 ID、身份和授权链接;分析机在短时凭证下下载并校验;结果脱敏后再进入共享复盘。网络隔离环境则预置分析器、sysroot 和符号快照,用离线介质或受控摆渡传输,避免 debuginfod/符号服务器查询暴露 Build ID 与源码路径。
项目的健康检查与重启策略也会影响现场。进程退出后立即重启是恢复目标,但采集器必须在资源预算内完成读取;容器退出和 Pod 删除可能让本地 Core 一起消失。容器场景要明确 Core 由节点、sidecar、runtime 还是宿主 systemd 接管,目标 mount/PID namespace 如何影响路径,并用一次真实测试 Pod 证明文件最终落在哪里。不要仅因为宿主 coredumpctl 可用,就假定所有容器崩溃都被完整采集。
在线远程分析适合现场不能离开受管网络、符号库持续更新且分析者已有短时授权的场景,但每次符号查询、源码读取和调试会话都会扩大网络与权限面;不应把原始 dump 暴露为共享文件服务或把 gdbserver 当下载通道。离线事故室适合高敏数据、跨团队复核和长期重现:先把 dump、匹配 executable、符号、sysroot、manifest 与校验值打成只读证据包,再在断网分析机验证。代价是准备包更大、符号更新不即时,且普通 Core 仍无法继续执行。选择依据是数据驻留、暂停预算、符号可达性和复盘周期,而不是哪种方式命令更少。
容量模型必须覆盖崩溃风暴
简单预算可以从 单次最大转储 × 同时崩溃实例 × 重试/重复采集 × 副本数 × 保留窗口 起步,再加压缩临时空间、上传缓冲、符号和索引。Core 可能是 sparse file,逻辑大小、实际块占用、压缩后大小和对象存储计费要分别测量;不支持 sparse 的复制路径可能把一个看似占用很小的文件展开。
容量门槛至少设置四层:单进程最大处理量、主机暂存配额、事故/服务级对象配额、全局保留上限。达到门槛时,优先保留首个具有正确身份的完整现场,后续同签名崩溃只保留计数或较小摘要;不同 Build ID、不同异常地址或不同发布批次不能草率去重。采集失败也要形成指标,包括 truncated、missing、权限拒绝、上传失败、符号不可用和清理失败,否则“没有 dump”会被误读为“没有崩溃”。
压缩能省空间,却增加 CPU 和恢复时间;加密能保护静态数据,却要求密钥在整个保留期可恢复。删除旧符号可节约容量,但会让仍在保留期内的 Core 失去解释能力。三者的取舍应围绕可复盘窗口设计,而不是各系统独立设置默认保留天数。
转储按生产数据级别保护并可证明销毁
栈、堆、环境变量、命令行、TLS 会话、数据库口令、访问令牌、客户请求和已释放内存残留都可能进入 dump。采集目录使用专用 owner 和最小 mode,上传强制加密和短时凭证,下载与解密要有审计;分析输出默认只共享必要帧和脱敏字段。自动扫描可以发现常见 secret/PII 模式,却不能证明 dump 已安全脱敏,因此原始文件仍按最高可能敏感等级处理。
事故关闭时先确认保留依据与法律冻结,再按证据 ID 删除每个副本:采集主机、systemd 外部存储、Windows/macOS 目录、上传缓冲、对象存储当前版本和历史版本、分析机、工单附件、备份、符号/源码下载缓存。Linux 实验目录可这样收尾:
find evidence -type f -printf '%m %u %g %s %p\n'
sha256sum -c evidence/crash-demo.sha256
rm -f evidence/private/crash-demo.core
find evidence/private -mindepth 1 -print
du -sh evidence build第三条之后,evidence/private 应为空;若 systemd-coredump 仍保留原始现场,应由有权限的运维入口按该事件的保留策略清除,不能让普通项目脚本盲删共享目录。Windows 同时复查 ProcDump 监控进程、计划任务/服务、WER 策略和 dump 目录;macOS 复查 Core 路径和临时 limit 是否只存在于已退出 shell。销毁记录保存的是证据 ID、摘要、位置、操作人和结果,不应再复制原始敏感内容。
成熟的转储体系不会承诺“每次崩溃都有一份无限大的 Core”。它承诺的是:触发规则和容量边界可预测,最有价值的现场能绑定唯一构建,错误符号与权限失败会被明确拒绝,远程与离线分析各有受控入口,到了保留终点又能证明所有副本和临时权限已经退出。
