深入 Linux 内核崩溃分析:Kdump + Crash 工具全链路实战

Linux 内核崩溃(Kernel Panic/Oops)是生产环境中最严重的事故之一。一次内核崩溃可能意味着数据丢失、服务中断甚至硬件损坏。与用户态程序的 Segmentation Fault 不同,内核崩溃意味着整个系统的"大脑"停止了工作,留给运维人员的只有一堆十六进制寄存器和栈地址——如果你没有事先部署正确的工具链。

本文将从工程实战角度,完整覆盖从 Kdump 部署、vmcore 捕获、Crash 工具深度使用,到最终定位根因的全链路方法论。我们会通过一个真实的 use-after-free 崩溃案例,演示如何从一份 vmcore 中精准定位到问题代码行。

一、Kdump 的双内核架构

Kdump 的核心设计哲学是:用一个干净的小内核来捕获崩溃现场,而不是试图让崩溃的大内核自救。

当生产内核(第一内核)发生 panic 时,它已经处于不可靠状态——调度器可能已经混乱,内存管理器可能已经损坏,I/O 子系统可能挂起。如果让同一个内核来执行崩溃转储,很可能在转储完成前就彻底死锁。

Kdump 的方案是引入"第二内核"(capture kernel):

  1. 系统启动时,通过 kexec -p 预加载一个微型捕获内核到预留内存区域(通常 128MB-512MB)
  2. 当生产内核 panic 时,直接跳转到捕获内核的执行入口
  3. 捕获内核启动后,通过 /proc/vmcore(ELF 格式)访问生产内核的完整物理内存快照
  4. 将快照写入本地磁盘、远程服务器或裸设备

这种架构的优势在于捕获内核完全独立运行,不依赖生产内核的任何子系统。转储的可靠性远高于让崩溃内核"自救"。

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 的组合是目前业界最成熟、最高效的内核崩溃分析方案,值得在任何生产环境中部署。

核心要点:

  1. 预防性部署:不要等出事了才配置 Kdump,crashkernel=256M 的开销几乎为零
  2. debuginfo 链:确保 debuginfo 包的版本与运行内核严格匹配
  3. 演练:每季度手工触发崩溃一次,验证全链路可用性
  4. 日志关联:将 vmcore 分析与 dmesg、journal、application log 时间线关联
  5. 能力建设:团队中至少两名工程师熟练 bt、kmem、struct、dis 命令

记住:panic 不是终点,而是起点。一份完整的 vmcore 包含了崩溃时刻系统状态的全量快照——足够的耐心加上正确的工具,总能找到藏在寄存器深处的真相。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部