深入 Linux 内核崩溃分析:Kdump + Crash 工具全链路实战
Linux 内核崩溃(Kernel Panic/Oops)是生产环境中最严重的事故之一。一次内核崩溃可能意味着数据丢失、服务中断甚至硬件损坏。与用户态程序的 Segmentation Fault 不同,内核崩溃意味着整个系统的"大脑"停止了工作,留给运维人员的只有一堆十六进制寄存器和栈地址——如果你没有事先部署正确的工具链。
本文将从工程实战角度,完整覆盖从 Kdump 部署、vmcore 捕获、Crash 工具深度使用,到最终定位根因的全链路方法论。我们会通过一个真实的 use-after-free 崩溃案例,演示如何从一份 vmcore 中精准定位到问题代码行。
一、Kdump 的双内核架构
Kdump 的核心设计哲学是:用一个干净的小内核来捕获崩溃现场,而不是试图让崩溃的大内核自救。
当生产内核(第一内核)发生 panic 时,它已经处于不可靠状态——调度器可能已经混乱,内存管理器可能已经损坏,I/O 子系统可能挂起。如果让同一个内核来执行崩溃转储,很可能在转储完成前就彻底死锁。
Kdump 的方案是引入"第二内核"(capture kernel):
- 系统启动时,通过
kexec -p预加载一个微型捕获内核到预留内存区域(通常 128MB-512MB) - 当生产内核 panic 时,直接跳转到捕获内核的执行入口
- 捕获内核启动后,通过
/proc/vmcore(ELF 格式)访问生产内核的完整物理内存快照 - 将快照写入本地磁盘、远程服务器或裸设备
这种架构的优势在于捕获内核完全独立运行,不依赖生产内核的任何子系统。转储的可靠性远高于让崩溃内核"自救"。
1.1 保留内存机制
Kdump 需要一块在正常运行时不被使用的内存区域。配置方式是在内核 cmdline 中添加 crashkernel=256M(或 crashkernel=256M,high + crashkernel=64M,low)。
# 查看当前保留的 crashkernel 内存
cat /proc/iomem | grep "Crash kernel"
# 输出类似:
# 000000000a000000 - 0000000019ffffff : Crash kernel
# 查看 kdump 服务状态
systemctl status kdump
对于支持 CMA(Contiguous Memory Allocator)的现代内核(5.10+),可以使用 crashkernel=256M,cma,让保留内存按需分配,平时可被系统复用。
1.2 触发条件
Kdump 在以下情况下触发崩溃转储:
panic()被调用(显式 panic 或 BUG_ON)oops后panic_on_oops=1触发内核主动 panic- NMI watchdog 检测到硬锁死(hard lockup)
- soft lockup 检测器超时
- 手动触发:
echo c > /proc/sysrq-trigger
二、Kdump 生产部署实战
2.1 安装与基础配置
# RHEL/CentOS/Fedora
dnf install kexec-tools crash kernel-debuginfo
# Ubuntu/Debian
apt install kexec-tools crash linux-image-$(uname -r)-dbgsyms
# 启用 kdump 服务
systemctl enable --now kdump
# 验证加载成功
kexec -v
kdumpctl status
2.2 kdump.conf 精细调优
/etc/kdump.conf 是 Kdump 的核心配置文件:
# 路径配置
path /var/crash
# 转储目标(本地)
core_collector makedumpfile -l --message-level 1 -d 31
# 远程转储(NFS)
net nfs-server.example.com:/dump
nfs /var/lib/nfs/rpc_pipefs
# 远程转储(SSH)
ssh [email protected]
sshkey /root/.ssh/id_rsa
# 转储后动作
default shell
reboot -f
makedumpfile 是性能关键:-d 31 排除零页、缓存页、用户态页,通常将 64GB 内存的 vmcore 压缩到 2-5GB,大幅缩短转储时间。
# 不同过滤级别的对比(64GB 物理内存,ext4 文件系统)
# -d 1: 排除零页 → ~18GB
# -d 7: 零页 + 缓存页 → ~8GB
# -d 15: + 用户态页 → ~3GB
# -d 31: + 可回收/不可分配页 → ~1.5GB
# -c -d 31 (LZO压缩): → ~600MB
2.3 验证触发流程
部署完成后,应当测试整个链路:
# 手动触发崩溃(务必在测试环境!)
echo c > /proc/sysrq-trigger
# (系统将重启到捕获内核,执行转储,然后重新启动)
# 检查转储结果
ls -lh /var/crash/*/vmcore
# 应看到 vmcore 文件和对应的 dmesg.txt
三、Crash 工具深度指南
Crash 是 Red Hat 开发的内核调试器,支持解析 vmcore、Live System(通过 /dev/mem 或 /proc/kcore)、以及压缩的 vmcore 文件。
3.1 启动方式
# 分析 vmcore 快照(需要匹配的 debuginfo)
crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/127.0.0.1-2026-09-30-01:31:42/vmcore
# 实时分析运行中的系统
crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /proc/kcore
# 调试转储日志(不含 vmcore,仅有 dmesg)
crash --log dmesg.txt
# 进入后查看模块列表
crash> mod
3.2 核心命令速查
bt(BackTrace)—— 栈回溯之王
# 查看崩溃时的调用栈
crash> bt
# PID CPU TASK COMMAND
# 8767 2 ffff8a4d3c2a0000 "kworker/2:1"
# #0 [ffffb1e44032fbe0] machine_kexec at ffffffffb1c5f8e9
# #1 [ffffb1e44032fc28] __crash_kexec at ffffffffb1c3a120
# #2 [ffffb1e44032fc90] panic at ffffffffb1c2c5e7
# #3 [ffffb1e44032fd00] oops_end at ffffffffb1c2c0a8
# #4 [ffffb1e44032fd20] no_context at ffffffffb1c7a3f1
# #5 [ffffb1e44032fd80] do_page_fault at ffffffffb1c7b8e4
# #6 [ffffb1e44032fdb0] page_fault at ffffffffb1e0112e
# [Exception RIP: my_drv_release+87]
# 查看所有 CPU 的栈
crash> bt -a
# 查看指定进程的栈
crash> bt 8767
# 简化输出(仅函数名)
crash> bt -s
kmem —— 内存分配器分析
# 查看 slab 缓存状态摘要
crash> kmem -s
CACHE NAME OBJSIZE ALLOCATED TOTAL SLABS SSIZE
ffff8a4d39a1d4c0 dentry 192 52418 52650 835 64 k
ffff8a4d39a1d280 inode_cache 584 41023 41230 644 64 k
ffff8a4d3b2a8e00 my_drv_object 256 1337 2048 32 64 k
^^^^^^^^^^^^
自定义驱动对象缓存
# 查看某 slab 缓存的详细分配情况
crash> kmem -S my_drv_object
# 输出每个 slab 上的空闲/已分配对象列表
struct/rb —— 数据结构解析
# 查看 task_struct
crash> struct task_struct.pid,comm,state ffff8a4d3c2a0000
pid = 8767
comm = "kworker/2:1"
state = 2 # TASK_UNINTERRUPTIBLE
# 查看特定结构体的字段
crash> struct file.f_op,file.count ffff8a4d39a1f000
# 输出操作引用计数
# 以 rb_tree 形式展示红黑树
crash> rb -t vma 0xffff888123456000
files 与 mount —— 系统状态快照
# 查看打开的文件描述符
crash> files ffff8a4d3c2a0000
# 系统全局文件状态
crash> mount
# 查看活跃的网络连接
crash> net
crash> net -s # 仅 summary
crash> net -r # 路由表
3.3 内存分析关键技巧
# 查看物理内存布局
crash> kmem -i
PAGES TOTAL PERCENTAGE
TOTAL MEM 16252928 62.0 GB
FREE 4128832 15.7 GB 25%
USED 12124096 46.3 GB 75%
BUFFERS 24576 96.0 MB
CACHED 8192000 31.3 GB
...
# 查看 NUMA 分布
crash> kmem -n
NODE 0: 32 GB (16777216 pages)
NODE 1: 30 GB (15728640 pages)
# 查看page信息
crash> kmem -p # 按页显示
crash> kmem -v # 按 vmap area 显示
# 搜索内核符号
crash> sym ffffffffb1c7a3f1
do_context+134
# 反汇编
crash> dis my_drv_release
0xffffffffc00000b0 <my_drv_release>: push %rbp
0xffffffffc00000b1 <my_drv_release+1>: mov %rsp,%rbp
...
四、实战案例:一次 use-after-free 崩溃分析全过程
4.1 事故背景
某生产服务器在凌晨 2:17 突然内核崩溃。vmcore 已自动捕获,dmesg 显示如下关键信息:
[82451.234] BUG: KASAN: use-after-free in my_drv_release+0x57/0x120 [my_drv]
[82451.235] Read of size 8 at addr ffff8a4d3b2a8e00 by task kworker/2:1
...
[82451.240] Call Trace:
[82451.241] my_drv_release+0x57/0x120 [my_drv]
[82451.241] kfree+0x4a/0x280
[82451.243] process_work+0x89/0x150 [my_drv]
[82451.245] kthread+0x123/0x150
4.2 分析过程
# 1. 加载 vmcore
crash vmlinux vmcore
# 2. 查看崩溃时的调用栈
crash> bt
PID CPU TASK COMMAND
8767 2 ffff8a4d3c2a0000 "kworker/2:1"
#0 machine_kexec+217
#1 __crash_kexec+176
#2 panic+215
#3 oops_end+96
#4 no_context+321
#5 do_page_fault+612
#6 page_fault+78
#7 my_drv_release+87 ← 问题起源点
#8 kfree+74 ← 二次释放
#9 process_work+137
#10 kthread+291
# 3. 查看崩溃时的寄存器状态
crash> bt -f
# rdi 寄存器指向被释放的对象 (0xffff8a4d3b2a8e00)
# rsi 指向释放后写入的新数据
# 4. 反汇编定位具体代码行
crash> dis -l my_drv_release
/mydrv/my_drv.c:187
0x57 mov 0x30(%rdi),%rax # 访问 obj->callback —— 野指针
# 5. 查看 slab 缓存中的对象状态
crash> kmem -S my_drv_object
ffff8a4d3b2a8e00: freed, in freelist ← 已被释放
slab @ ffff8a4d3b100000:
MY_DRV_OBJECT: 36 entries allocated, 28 free
(freed: ffff8a4d3b2a8e00 ffff8a4d3b2a8c00 ffff8a4d3b2a9000)
# 6. 查看释放时的调用栈(通过 slab 调试信息)
crash> kmem -s my_drv_object | grep freed
ffff8a4d39a1f0c0: 3 freed objects detected
ffff8a4d3b100000: 28 allocated, 3 freed (含 ffff8a4d3b2a8e00)
# 7. 查看该对象的完整数据状态
crash> rd -64 ffff8a4d3b2a8e00 8
ffff8a4d3b2a8e00: deadbeef ffff8a4d ← 典型的 poisoned 标记
~~~~~~~~
释放后填充的毒化值
4.3 根因定位
# 查看该对象的引用计数历史
crash> rd -S my_drv_object.refs ffff8a4d3b2a8e00
refs.counter = 0 # 引用计数已归零
crash> dis -l process_work
/mydrv/my_drv.c:125
0x89 call my_drv_put(obj) # 释放引用,触发 kfree
—— 但另一个 CPU 已在读该对象的 callback
# 查看进程上下文,确认竞态条件
crash> foreach bt -c 3 # 查看 CPU 3 上正在运行的任务
PID 512 CPU 3 my_drv_write # 另一个进程正在访问该对象
根因:my_drv 模块中 process_work 在获得对象引用后调用 my_drv_put(obj),但此时 CPU3 上的 my_drv_write 已经在读该对象的 callback 字段,形成了经典的引用计数竞态导致的 use-after-free。
修复方案是使用 refcount_t 的 atomic_fetch_add_unless 进行引用计数检查,或在 my_drv_get 路径中高概率检查对象是否已被标记为 dying。
五、Panic Notifier Chain:在崩溃时刻抢救数据
内核提供了 panic_notifier_list,允许模块在 kernel panic 前执行代码,这对于收集最后一刻的状态信息非常有用。
#include <linux/notifier.h>
#include <linux/kdebug.h>
static int mydrv_panic_handler(struct notifier_block *nb,
unsigned long action, void *data)
{
struct my_drv_stats stats = {0};
/* panic 即将执行,系统即将停止——快速收集关键数据 */
mydrv_collect_crash_info(&stats);
/* 通过 KMSG_DUMP 写入 printk 环形缓冲区 */
printk(KERN_EMERG "MYDRV_CRASH: tx_pending=%u, rx_pending=%u, "
"last_err=0x%x\n",
stats.tx_pending, stats.rx_pending, stats.last_error_code);
return NOTIFY_DONE;
}
static struct notifier_block mydrv_panic_nb = {
.notifier_call = mydrv_panic_handler,
.priority = INT_MAX, /* 确保最后执行,获取最终状态 */
};
static int __init mydrv_init(void)
{
atomic_notifier_chain_register(&panic_notifier_list, &mydrv_panic_nb);
return 0;
}
通过 panic_notifier_chain_register 注册的回调会在 panic 流程早期执行。利用这个机制,你可以在崩溃前将硬件寄存器状态、DMA 描述符环、最后 100 个请求的序列号等关键数据写入 printk——这些数据会被 Kdump 捕获在 dmesg.txt 中,成为后续分析的重要线索。
六、高级生产优化
6.1 makedumpfile 的增量过滤
当系统内存超过 1TB 时,即使 -d 31 过滤后 vmcore 仍然可能达到数百 GB。此时可以结合多种过滤器:
# /etc/kdump.conf 中配置
core_collector makedumpfile -c --message-level 1 -d 31 \
-x /path/to/exclude_pages.yaml
# 排除特定内核模块占用的内存区域
core_collector makedumpfile -c -d 31 --split dump.elf dump.kcd
# 转储到多个并行目标(本地 + 远程)
core_collector makedumpfile -c -d 31 -Z /var/crash/ | \
ssh admin@backup "cat > /backup/vmcore"
6.2 自动化分析流水线
生产环境中,可以写一个自动化分析脚本在 Kdump 完成后立即执行:
#!/bin/bash
# /etc/kdump/post.d/10-analyze.sh
VMCORE_DIR=$1
REPORT="${VMCORE_DIR}/analysis_report.txt"
crash --minimal vmlinux ${VMCORE_DIR}/vmcore <<'CRASH_EOF' > $REPORT 2>&1
bt
bt -a | head -20
log | grep -E "(BUG|Error|Call Trace)"
kmem -i
runq
dev -d
CRASH_EOF
# 推送到监控平台
curl -X POST https://monitor.example.com/api/crash_report \
-F "host=$(hostname)" \
-F "report=@${REPORT}"
6.3 Kdump 的局限性
Kdump 并非万能。以下场景它无能为力:
- 内存部分损坏:如果页表或内存分配器已损坏,捕获内核可能无法正确读取 vmcore
- 硬件故障:NMI 触发的崩溃可能直接跳转失败(NMI 上下文限制)
- 嵌套 panic:崩溃处理过程中再次 panic,通常只能依赖串口日志
- 加密内存:AMD SEV-SNP 等机密计算环境下,加密内存不可读
在这些场景下,串口日志(serial console)、pstore(oops/panic 持久化存储)、以及 BMC/IPMI 的 SEL log 成为最后的救命稻草。
七、最佳实践速查表
| 场景 | 推荐方案 | 备注 |
|---|---|---|
| 标准生产服务器 | kdump + makedumpfile -d 31 | 预留 256MB-512MB |
| 大内存服务器(>1TB) | kdump + 远程转储 | 避免本地磁盘撑爆 |
| 容器/k8s 节点 | 节点级 kdump + CMA crashkernel | crashkernel=128M,cma |
| 边缘/嵌入式 | ramdump + pstore | 通常无磁盘空间 |
| 云主机 | 串口日志 + pstore 持久化 | 无本地存储可用 |
| 开发/测试环境 | kdump + netconsole | 实时调试 |
八、总结
Linux 内核崩溃分析是系统工程师的"终极技能"——它要求你对内核调度器、内存管理、块设备栈和网络栈都有深入理解。Kdump + Crash 的组合是目前业界最成熟、最高效的内核崩溃分析方案,值得在任何生产环境中部署。
核心要点:
- 预防性部署:不要等出事了才配置 Kdump,
crashkernel=256M的开销几乎为零 - debuginfo 链:确保 debuginfo 包的版本与运行内核严格匹配
- 演练:每季度手工触发崩溃一次,验证全链路可用性
- 日志关联:将 vmcore 分析与 dmesg、journal、application log 时间线关联
- 能力建设:团队中至少两名工程师熟练 bt、kmem、struct、dis 命令
记住:panic 不是终点,而是起点。一份完整的 vmcore 包含了崩溃时刻系统状态的全量快照——足够的耐心加上正确的工具,总能找到藏在寄存器深处的真相。

发表评论 取消回复