4. kdump 与 crash 工具:内核崩溃排查核心方法

当 Linux 内核发生严重错误(如空指针解引用、内核态死锁、内存越界)时,系统通常会触发 Kernel Panic 或 Oops。此时,仅靠日志往往无法定位根因。kdump 机制能够在崩溃瞬间捕获内存快照(vmcore),配合 crash 工具进行离线分析,是内核驱动开发者必须掌握的终极调试手段。

核心概念:kdump 基于 kexec 实现,在崩溃时启动一个“捕获内核”来保存崩溃内核的内存转储,而非依赖 BIOS 重启。这使得 vmcore 能够完整保留崩溃现场的 CPU 寄存器、堆栈、内存数据。

4.1 kdump 机制原理

kdump 的工作流程分为三个阶段:

  1. 预留内存:系统启动时,通过 crashkernel= 内核参数预留一段物理内存,供捕获内核使用。
  2. 崩溃触发:当生产内核崩溃时,kexec 机制快速加载捕获内核到预留内存中,并跳转执行。
  3. 转储保存:捕获内核启动后,将崩溃内核的内存(vmcore)写入磁盘或网络位置。
kdump 崩溃转储流程 生产内核 正常运行 内核崩溃 Panic / Oops 捕获内核 kexec 启动 vmcore 保存 磁盘 / 网络 预留内存(crashkernel=) 捕获内核运行在预留内存中,不污染崩溃现场
关键参数说明:crashkernel=256M 表示预留 256MB 内存给捕获内核。对于内存较大的系统(≥64GB),建议预留 512MB 或更多。可通过 cat /proc/cmdline 确认当前是否已配置。

4.2 安装与配置 kdump

以下以 CentOS/RHEL 7/8 和 Ubuntu 20.04+ 为例,展示标准配置步骤。

4.2.1 安装必要软件包

发行版 安装命令 关键软件包
RHEL/CentOS 7/8 yum install kexec-tools crash kexec-tools, crash, kernel-debuginfo
Ubuntu 20.04+ apt install kdump-tools crash kdump-tools, crash, linux-image-$(uname -r)-dbgsym
openSUSE zypper install kdump crash kexec-tools, crash, kernel-debuginfo
重要提示:kernel-debuginfo 或 -dbgsym 包包含调试符号(vmlinux),crash 工具必须依赖它才能解析内核符号和数据结构。没有调试符号,vmcore 分析将无法获取函数名和行号。

4.2.2 配置内核启动参数

编辑 GRUB 配置文件,添加 crashkernel= 参数:

# 编辑 /etc/default/grub
GRUB_CMDLINE_LINUX="... crashkernel=256M ..."

# 重新生成 GRUB 配置
# RHEL/CentOS 7:
grub2-mkconfig -o /boot/grub2/grub.cfg
# Ubuntu:
update-grub

# 重启系统使配置生效
reboot

4.2.3 配置 kdump 服务

kdump 的核心配置文件位于 /etc/kdump.conf(RHEL/CentOS)或 /etc/default/kdump-tools(Ubuntu)。主要配置项如下:

配置项 说明 推荐值
path /var/crash vmcore 保存路径 /var/crash
core_collector makedumpfile -l -d 31 压缩级别和过滤级别(-d 31 表示过滤掉大部分用户态页) makedumpfile -l -d 31
default reboot 转储完成后自动重启 reboot
net 192.168.1.100:/nfs 远程 NFS 转储(可选) 按需配置
# 启动并启用 kdump 服务
# RHEL/CentOS:
systemctl enable kdump
systemctl start kdump

# Ubuntu:
systemctl enable kdump-tools
systemctl start kdump-tools

# 验证状态
systemctl status kdump
验证配置:执行 kdumpctl status(RHEL/CentOS)或 kdump-config show(Ubuntu)查看当前状态。如果显示 "Kdump is operational",说明配置成功。

4.3 使用 crash 工具分析 vmcore

当系统发生崩溃并生成 vmcore 后,使用 crash 工具进行离线分析。vmcore 通常位于 /var/crash/<时间戳>/ 目录下。

4.3.1 启动 crash 会话

# 基本语法:crash vmlinux vmcore
# vmlinux 来自 kernel-debuginfo 包,带调试符号
crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/2024-01-15-10:30/vmcore

启动后进入 crash 交互式命令行,提示符变为 crash>。

4.3.2 常用 crash 命令

命令 功能 示例
bt 显示当前或指定任务的堆栈回溯 bt -a(所有 CPU)
log 查看内核日志缓冲区(dmesg) log | grep -i error
ps 列出崩溃时的进程列表 ps -t(按线程分组)
dis 反汇编指定地址或函数 dis panic
rd 读取内存内容 rd ffff888000123456
struct 打印内核结构体内容 struct task_struct ffff888001234567
sym 将地址转换为符号名 sym ffffffffa0001234
files 显示进程打开的文件 files 1234(PID)

4.3.3 典型分析流程

以下是一个驱动崩溃的典型分析步骤:

  1. 查看崩溃消息:log 命令查看最后几条内核日志,定位 Oops/Panic 信息。
  2. 获取崩溃堆栈:bt -a 查看所有 CPU 的堆栈,找到触发崩溃的 CPU 和函数。
  3. 检查崩溃指令:dis <崩溃地址> 反汇编崩溃点附近的代码。
  4. 查看寄存器:bt -r 显示崩溃时的 CPU 寄存器值。
  5. 分析数据结构:使用 struct 和 rd 检查相关内核对象。
crash 工具分析 vmcore 典型流程 1. log 查看崩溃日志 2. bt -a 获取堆栈回溯 3. dis 反汇编崩溃点 4. bt -r 检查寄存器 5. struct/rd 分析数据结构 6. 定位根因 驱动代码缺陷 分析要点 重点关注崩溃指令附近的代码、寄存器中的地址值、以及堆栈中驱动模块的函数调用链 使用 mod 命令查看加载的驱动模块,确认崩溃是否发生在第三方驱动中

4.3.4 实战案例:分析驱动空指针解引用

假设 vmcore 中 log 输出显示:

[ 1234.567890] BUG: unable to handle kernel NULL pointer dereference at 0000000000000010
[ 1234.567891] IP: my_driver_ioctl+0x3a/0x100 [my_driver]
[ 1234.567892] PGD 0 P4D 0
[ 1234.567893] Oops: 0002 [#1] SMP PTI

在 crash 中执行以下命令:

crash> bt -a
# 显示崩溃 CPU 的堆栈,确认调用路径
PID: 1234  TASK: ffff880012345678  CPU: 2   COMMAND: "test_app"
 #0 [ffff880012345000] machine_kexec at ffffffff8103d5e3
 #1 [ffff880012345060] __crash_kexec at ffffffff8110f1b2
 #2 [ffff880012345130] panic at ffffffff8108b3d0
 #3 [ffff8800123451a0] oops_end at ffffffff8102f3b0
 #4 [ffff8800123451c0] no_context at ffffffff8106e0f1
 #5 [ffff880012345210] __bad_area_nosemaphore at ffffffff8106e3b9
 #6 [ffff880012345260] bad_area_nosemaphore at ffffffff8106e4ce
 #7 [ffff880012345270] __do_page_fault at ffffffff8106e9b1
 #8 [ffff8800123452d0] do_page_fault at ffffffff8106eb0f
 #9 [ffff880012345300] page_fault at ffffffff8160d088
    [exception RIP: my_driver_ioctl+0x3a]
    RIP: ffffffffa000103a  RSP: ffff8800123453b0  RFLAGS: 00010246
    RAX: 0000000000000000  RBX: ffff880012345678  RCX: 0000000000000001
    RDX: 0000000000000010  RSI: 0000000000000000  RDI: ffff880012345678
    RBP: ffff8800123453e0   R8: 0000000000000000   R9: 0000000000000000
    R10: 0000000000000000  R11: 0000000000000000  R12: ffff880012345678
    R13: 0000000000000000  R14: 0000000000000000  R15: 0000000000000000
    ORIG_RAX: ffffffffffffffff  CS: 0010  SS: 0018

crash> dis my_driver_ioctl
# 反汇编驱动函数,定位偏移 0x3a 处的指令
0xffffffffa0001000 <my_driver_ioctl>:  push   %rbp
0xffffffffa0001001 <my_driver_ioctl+0x1>:  mov    %rsp,%rbp
0xffffffffa0001004 <my_driver_ioctl+0x4>:  mov    0x10(%rdi),%rax  # 这里!rdi 为 NULL
0xffffffffa0001008 <my_driver_ioctl+0x8>:  test   %rax,%rax
...
根因分析:从反汇编可以看出,my_driver_ioctl+0x4 处尝试从 rdi+0x10 读取数据,而 rdi 寄存器值为 0(NULL)。结合堆栈中 RDI: ffff880012345678 是 task_struct 地址,说明驱动在调用 ioctl 时未正确检查传入的用户态指针,导致空指针解引用。修复方法:在访问用户态指针前添加 if (!ptr) return -EINVAL; 检查。

4.4 常见问题与避坑指南

问题 原因 解决方案
kdump 未生成 vmcore 预留内存不足 / 捕获内核崩溃 增大 crashkernel 值;检查 /var/log/messages 中捕获内核日志
crash 无法解析符号 未安装 kernel-debuginfo 或版本不匹配 安装与当前内核完全匹配的 debuginfo 包
vmcore 文件过大 未使用 makedumpfile 过滤 配置 core_collector 为 makedumpfile -l -d 31
崩溃堆栈不完整 内核编译时未开启帧指针(FRAME_POINTER) 重新编译内核,开启 CONFIG_FRAME_POINTER=y
捕获内核无法启动 预留内存地址冲突 使用 crashkernel=256M@128M 指定起始地址
生产环境注意事项:kdump 会消耗预留内存,对于内存敏感的系统(如嵌入式设备),需权衡预留大小。建议在测试环境先模拟崩溃(echo c > /proc/sysrq-trigger)验证 kdump 配置是否正常,再部署到生产环境。

4.5 小结

kdump 与 crash 工具是内核驱动开发者应对内核崩溃的“最后一道防线”。掌握 kdump 的配置原理、vmcore 的生成机制,以及 crash 工具的核心命令(bt、log、dis、struct),能够帮助你在驱动开发中快速定位空指针、内存越界、死锁等致命问题。建议将 kdump 集成到驱动测试流程中,每次驱动变更后都进行崩溃模拟验证,确保转储机制始终可用。