内核调试是操作系统开发和运维中最具挑战性的领域之一。与调试用户态程序不同,内核崩溃一旦发生,往往意味着整个系统不可用。如何在上千行汇编与 C 代码交织的内核世界中定位问题,如何在一瞬间的 panic 中保留现场,如何在不影响线上业务的前提下实时观测内核行为——这些正是本文要系统讲解的主题。
本文从 kgdb 远程内核调试出发,深入分析 panic 与 oops,讲解 kdump 的 vmcore 收集与 crash 工具的分析能力,覆盖 printk 动态日志、ftrace 函数追踪以及 bpftrace/kprobe 的动态追踪技术,帮助你建立从开发调试到生产诊断的完整内核工具链。
一、kgdb 远程内核调试
1.1 为什么要用 kgdb
GDB 可以调试用户态程序,而 kgdb(Kernel GDB) 将 GDB 的调试能力扩展到内核空间。通过串口或网络连接,开发者可以在另一台主机上远程单步调试目标机器的内核代码,设置断点、查看变量、检查调用栈——就像调试普通用户程序一样。
1.2 编译带调试符号的内核
要使用 kgdb,目标内核必须开启以下选项:
# .config 中启用
CONFIG_DEBUG_KERNEL=y # 基础调试支持
CONFIG_DEBUG_INFO=y # 包含调试符号(-g)
CONFIG_KGDB=y # kgdb 主开关
CONFIG_KGDB_SERIAL_CONSOLE=y # 通过串口的 kgdb
CONFIG_KGDB_KDB=y # KDB(kgdb 的内置前端,不需要 GDB 主机)
编译并安装调试内核后,重启进入该内核。
1.3 串口连接配置
kgdb 最常用的连接方式是串口(serial),因为它不依赖网络栈,即使在网络子系统崩溃时仍然可用。
目标机(被调试机器)启动参数:
# GRUB 启动参数中添加
console=ttyS0,115200 kgdboc=ttyS0,115200 nokaslr
console=ttyS0,115200:将内核日志输出到串口kgdboc=ttyS0,115200:kgdb over console,指定串口为 kgdb 通道nokaslr:关闭内核地址空间布局随机化,固定符号地址便于调试
手动触发 kgdb 中断:
# 在目标机上
echo g > /proc/sysrq-trigger
此时内核进入 kgdb 中断状态,等待 GDB 连接。
主机侧 GDB 连接:
# 主机通过串口连接目标机
gdb ./vmlinux
(gdb) set remotebaud 115200
(gdb) target remote /dev/ttyUSB0
# 连接成功后,可以看到内核当前停在哪里
(gdb) bt # 查看当前调用栈
(gdb) info registers # 查看寄存器
(gdb) list # 查看当前源码
(gdb) continue # 继续执行
1.4 kgdb 常用调试命令
# 在内核函数上设置断点
(gdb) break do_page_fault
(gdb) break __alloc_pages_nodemask
# 条件断点
(gdb) break sys_read if $rsi == 0
# 查看内核全局变量
(gdb) p init_task # 查看 init 进程 task_struct
(gdb) p &jiffies # 查看 jiffies 变量地址
(gdb) p ((struct task_struct *)$rax)->comm # 查看当前进程名
# 查看内核内存
(gdb) x/16x 0xffffffff80000000 # 查看线性映射区内存
二、panic 与 oops 分析
2.1 Oops 与 panic 的区别
- Oops:内核检测到异常情况(如非法内存访问),但认为仍可恢复。内核打印详细的错误信息,杀死触发异常的进程,系统继续运行(可能已处于不稳定状态)
- Panic:内核认为无法安全恢复,调用
panic()停止所有 CPU 并输出最后的信息
2.2 Oops 日志分析
典型的 oops 日志结构如下:
[ 123.456] BUG: unable to handle page fault for address: 0000000000000000
[ 123.456] #PF: supervisor write access in kernel mode
[ 123.456] #PF: error_code(0x0002) - not-present page
[ 123.457] PGD 0 P4D 0
[ 123.457] Oops: 0002 [#1] PREEMPT SMP
[ 123.457] CPU: 2 PID: 1234 Comm: myapp Tainted: G OE 5.15.0-generic
[ 123.457] Hardware name: QEMU Standard PC, BIOS 1.0.0
[ 123.457] RIP: 0010:my_buggy_function+0x15/0x30 [my_module]
[ 123.457] Code: 48 89 c7 ...
[ 123.457] RSP: 0018:ffff88800a123f48 EFLAGS: 00010246
[ 123.457] RAX: 0000000000000000 RBX: ffff88800a123f70 ...
[ 123.457] RIP: my_buggy_function+0x15/0x30 [my_module]
[ 123.457] ? another_function+0x30/0x50
[ 123.457] ? sys_init_module+0x1a0/0x1f0
[ 123.457] entry_SYSCALL_64_after_hwframe+0x44/0xae
关键信息解读:
RIP:异常发生时的指令指针。my_buggy_function+0x15/0x30表示在my_buggy_function函数的 0x15 偏移处,函数总大小为 0x30Tainted:内核状态标志。G表示加载了专有模块,O表示加载了 out-of-tree 模块,E表示加载了未签名的模块Code:行:异常指令处的机器码,可用objdump反汇编确认Call Trace:异常发生时的完整调用栈
2.3 用 addr2line 定位源码
如果是自己编译的内核或模块,可以使用 addr2line 将 RIP 地址定位到源码:
# RIP = my_buggy_function+0x15, 模块地址范围从 /proc/modules 获取
$ sudo cat /proc/modules | grep my_module
my_module 16384 0 - Live 0xffffffffc0123000
# 计算实际地址
# 如果 RIP = 0x15 偏移,模块基址 = 0xffffffffc0123000
# 实际地址 = 0xffffffffc0123015
$ addr2line -e my_module.ko -fip 0x15
my_buggy_function at /home/user/mymod.c:42
三、kdump 与 vmcore 收集
3.1 kdump 的原理
当内核 panic 或 oops 达到不可恢复状态时,系统已经无法正常收集崩溃信息。kdump 通过双内核机制解决此问题:
- 主内核(primary kernel)正常运行
- 预留一块物理内存区域,加载一个精简的捕获内核(capture kernel)
- 主内核崩溃时通过 kexec 快速切换到捕获内核(不经过 BIOS)
- 捕获内核收集主内核的内存镜像到文件,然后正常关机或重启
主内核运行 ──Panics──> kexec 快速启动捕获内核
│
└──> makedumpfile / vmcore 写入磁盘
3.2 kdump 配置
# 安装 kdump 工具
sudo apt-get install kdump-tools linux-crashdump
# GRUB 启动参数,为主内核预留捕获内核内存
crashkernel=512M # 预留 512MB 给捕获内核
# 配置 /etc/kdump.conf
path /var/crash
core_collector makedumpfile -c -d 31 # 压缩,过滤零页/缓存页
3.3 手动测试 kdump
# 模拟 panic
echo c > /proc/sysrq-trigger
# 系统会自动重启,之后在 /var/crash/ 下找到 vmcore 文件
$ ls /var/crash/
20261002-120000/
四、crash 工具分析 vmcore
4.1 crash 概述
crash 是一个用于分析 Linux vmcore 转储文件的强大工具,它结合了 GDB 的调试能力与内核数据结构的解析,可以深入分析 panic 现场。
# 安装 crash
sudo apt-get install crash
# 分析 vmcore
crash /usr/lib/debug/boot/vmlinux-5.15.0-generic \
/var/crash/20261002-120000/vmcore
4.2 crash 常用命令
# 查看 panic 发生的调用栈
crash> bt
PID: 1234 TASK: ffff88800a123000 CPU: 2 COMMAND: "myapp"
#0 [ffff88800a123f48] my_buggy_function at ffffffffa0123015
#1 [ffff88800a123f68] another_function at ffffffffa0123000
#2 [ffff88800a123f88] sys_init_module at ffffffff81234a00
# 查看当前运行的任务列表
crash> ps
PID PPID CPU TASK ST %MEM VSZ RSS COMM
> 0 0 0 ffff888... RU 0.0 0 0 [swapper/0]
> 1234 1000 2 ffff888... RU 0.1 1234 567 myapp
# 查看内核消息缓冲区(dmesg 的 crash 版本)
crash> log
# 查看内存页表映射
crash> vtop 0xffff88800a123000
# 查看特定进程的内核栈
crash> task -R ffff88800a123000
# 查看 slab 分配器状态
crash> kmem -s
# 查看进程的打开文件
crash> files -p 1234
# 查看已加载模块信息
crash> mod
4.3 vmcore 分析实战流程
# 1. 确认 panic 类型和错误码
crash> log | tail -30
crash> sys
# 2. 查看崩溃时的调用栈
crash> bt
# 3. 分析崩溃点的源代码上下文
crash> dis my_buggy_function
# 4. 检查相关数据结构
crash> p *(struct task_struct *)0xffff88800a123000
crash> p init_task.mm.pgd
# 5. 检查是否存在内存损坏(对比 slab 魔数)
crash> kmem -s | grep -i corrupted
五、printk 与 Dynamic Debug
5.1 printk 日志级别
Linux 内核日志有 8 个级别(0-7):
| 级别 | 宏 | 含义 |
|---|---|---|
| 0 | KERN_EMERG | 系统不可用 |
| 1 | KERN_ALERT | 必须立即行动 |
| 2 | KERN_CRIT | 严重情况 |
| 3 | KERN_ERR | 错误 |
| 4 | KERN_WARNING | 警告 |
| 5 | KERN_NOTICE | 正常但重要 |
| 6 | KERN_INFO | 信息 |
| 7 | KERN_DEBUG | 调试信息 |
当前控制台日志级别存储在 /proc/sys/kernel/printk:
$ cat /proc/sys/kernel/printk
4 4 1 7
# 当前级别 默认级别 最小级别 启动默认级别
要查看 KERN_DEBUG 级别的消息:
sudo dmesg -n 7 # 设置控制台日志级别为 7
echo 8 | sudo tee /proc/sys/kernel/printk
5.2 Dynamic Debug(pr_debug 动态开关)
内核中大量的 pr_debug()、dev_dbg() 日志默认不输出。通过 dynamic debug 可以运行时打开特定文件或函数的调试日志:
# 列出所有可动态开关的 debug 点
sudo cat /sys/kernel/debug/dynamic_debug/control | head
# 开启某个文件的所有 debug 日志
sudo echo 'file mm/page_alloc.c +p' > /sys/kernel/debug/dynamic_debug/control
# 开启某个函数
sudo echo 'func __alloc_pages_nodemask +p' > /sys/kernel/debug/dynamic_debug/control
# 带条件:只打印 CPU 0 上的消息
sudo echo 'func do_page_fault cpu==0 +p' > /sys/kernel/debug/dynamic_debug/control
# 关闭
sudo echo 'file mm/page_alloc.c -p' > /sys/kernel/debug/dynamic_debug/control
六、ftrace 函数追踪
6.1 ftrace 概述
ftrace(Function Tracer)是内核内置的追踪框架,使用编译期插入的 mcount 探针,无需额外模块即可追踪函数调用。它几乎零开销(未启用时),是分析内核调用链和时延的首选工具。
6.2 ftrace 文件系统接口
# ftrace 接口位于 debugfs
sudo mount -t debugfs none /sys/kernel/debug
# 追踪器类型
cat /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/current_tracer
# 可用的追踪器
cat /sys/kernel/debug/tracing/available_tracers
function_graph function nop blk mmiotrace
6.3 函数追踪示例
# 1. 切换到 function tracer
echo function > /sys/kernel/debug/tracing/current_tracer
# 2. 设置要追踪的函数
echo do_page_fault > /sys/kernel/debug/tracing/set_ftrace_filter
echo __alloc_pages_nodemask >> /sys/kernel/debug/tracing/set_ftrace_filter
# 3. 启用追踪
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 4. 执行测试操作
# ... 触发 page fault 或内存分配 ...
# 5. 停止并查看结果
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace
# 6. 清空
echo > /sys/kernel/debug/tracing/trace
echo > /sys/kernel/debug/tracing/set_ftrace_filter
6.4 function_graph 追踪器
function_graph 不仅记录函数进出,还记录持续时间:
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo do_page_fault > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... trigger ...
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace
输出示例:
0) | do_page_fault() {
0) 0.234 us | find_vma();
0) 0.456 us | handle_mm_fault();
0) 0.123 us | alloc_pages_vma();
0) 1.234 us | handle_mm_fault();
0) 2.567 us | } /* do_page_fault */
七、bpftrace 与 kprobe 动态追踪
7.1 kprobe 基础
kprobe 是内核动态插桩机制,可以在几乎所有内核函数的任意指令处设置探测点,无需重新编译内核。kprobe 被 eBPF 和 ftrace 等上层工具广泛使用。
7.2 bpftrace 单行脚本
bpftrace 是 Brendan Gregg 主导开发的高级 eBPF 追踪语言,语法类似 awk+C,适合快速诊断:
# 追踪 do_page_fault 调用频率
sudo bpftrace -e 'kprobe:do_page_fault { @[comm] = count(); }'
# 追踪 read 系统调用的文件描述符分布
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_read { @[args->fd] = count(); }'
# 追踪 kmalloc 分配大小分布
sudo bpftrace -e 'kprobe:__kmalloc { @bytes = hist(args->size); }'
# 追踪网络包收发的进程分布
sudo bpftrace -e 'kprobe:ip_rcv { @[comm] = count(); }'
# 追踪 CPU 调度事件,显示调度延迟分布
sudo bpftrace -e 'tracepoint:sched:sched_switch {
$prev = args->prev_comm;
$next = args->next_comm;
printf("%s -> %s\n", $prev, $next);
}'
7.3 bpftrace 多行脚本
# vfsread.bt — 追踪 VFS 读取延迟
sudo bpftrace -e '
kprobe:vfs_read {
@start[tid] = nsecs;
}
kretprobe:vfs_read /@start[tid]/ {
$latency_us = (nsecs - @start[tid]) / 1000;
@latency[comm] = hist($latency_us);
delete(@start[tid]);
}'
7.4 kprobe 直接使用
如果系统未安装 bpftrace,也可以通过 /sys/kernel/debug/tracing/kprobe_events 直接定义 kprobe:
# 注册一个 kprobe
echo 'p:myprobe do_page_fault address=%si' > /sys/kernel/debug/tracing/kprobe_events
# 启用
echo 1 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
# 查看输出
cat /sys/kernel/debug/tracing/trace_pipe
# 清理
echo 0 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
echo '-:myprobe' >> /sys/kernel/debug/tracing/kprobe_events
八、生产环境调试策略选择
| 场景 | 推荐工具 | 侵入性 | 备注 |
|---|---|---|---|
| 内核启动失败 | 串口 console + earlyprintk | 无 | 用串口捕获启动日志 |
| 偶发 panic | kdump + crash | 低 | crashkernel 预留少量内存 |
| 性能问题 | ftrace / bpftrace | 极低 | 按需启用,可随时关闭 |
| 驱动开发 | kgdb | 高 | 需要串口或网络连接 |
| 内存泄漏 | kmemleak / slub_debug | 中 | 需要特殊编译选项 |
| 现场分析 | crash + vmcore | 无 | panic 后离线分析 |
| 动态日志 | dynamic debug | 极低 | 不重启动态开关 |
相关阅读
- https://plumephp.com/os-kernel-module/ —— 内核模块开发入门与调试环境搭建
- https://plumephp.com/os-ebpf-observability/ —— eBPF 可观测性的进阶实践,与 bpftrace 一脉相承
- https://plumephp.com/os-system-call-internals/ —— 系统调用实现细节,理解追踪器 hook 点的必备基础
延伸阅读
- Linux Kernel Documentation:
Documentation/dev-tools/kgdb.rst - Linux Kernel Documentation:
Documentation/trace/ftrace.rst - Brendan Gregg, “BPF Performance Tools” — eBPF 追踪的权威实战指南
- crash 命令参考:
crash -h与crash> help - systemd-coredump 与 apport —— 用户态 core dump 收集机制
- LWN.net 系列文章:“A guide to kernel debugging”
# ============================================================
# 完整可运行示例:bpftrace 单行脚本集与 ftrace 快速追踪
# 需要 root 权限和 bpftrace 安装
# ============================================================
#!/bin/bash
set -e
echo "=== 1. 追踪 do_page_fault 频率 ==="
sudo bpftrace -e 'kprobe:do_page_fault { @[comm] = count(); }' \
-c "sleep 2" || true
echo ""
echo "=== 2. 追踪 kmalloc 分配大小分布 ==="
sudo bpftrace -e 'kprobe:__kmalloc { @bytes = hist(args->size); }' \
-c "sleep 2" || true
echo ""
echo "=== 3. 追踪当前 shell 的系统调用延迟 ==="
sudo bpftrace -e '
tracepoint:raw_syscalls:sys_enter {
@start[tid] = nsecs;
}
tracepoint:raw_syscalls:sys_exit /@start[tid]/ {
$lat = (nsecs - @start[tid]) / 1000;
if ($lat > 100) {
printf("slow syscall: %s tid=%d lat=%d us\n",
comm, tid, $lat);
}
delete(@start[tid]);
}
' -c "sleep 3" || true
echo ""
echo "=== 4. ftrace function_graph: do_page_fault ==="
TRACEDIR=/sys/kernel/debug/tracing
if [ -d "$TRACEDIR" ]; then
echo $$ | sudo tee $TRACEDIR/set_ftrace_pid >/dev/null
echo function_graph | sudo tee $TRACEDIR/current_tracer >/dev/null
echo do_page_fault | sudo tee $TRACEDIR/set_graph_function >/dev/null
echo 1 | sudo tee $TRACEDIR/tracing_on >/dev/null
# 触发一些事件
cat /proc/self/maps > /dev/null
echo 0 | sudo tee $TRACEDIR/tracing_on >/dev/null
echo "ftrace output (last 20 lines):"
sudo tail -20 $TRACEDIR/trace
# 清理
echo nop | sudo tee $TRACEDIR/current_tracer >/dev/null
echo | sudo tee $TRACEDIR/set_graph_function >/dev/null
echo | sudo tee $TRACEDIR/set_ftrace_pid >/dev/null
else
echo "debugfs not mounted. Run: sudo mount -t debugfs none /sys/kernel/debug"
fi
echo ""
echo "=== 5. 查看 dynamic debug 控制点数量 ==="
if [ -f /sys/kernel/debug/dynamic_debug/control ]; then
echo "Available dynamic debug points:"
wc -l /sys/kernel/debug/dynamic_debug/control
fi
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。