内核调试:kgdb、kdump、crash 与动态追踪

内核调试是系统稳定性保障的最后一道防线。本文系统讲解 kgdb 远程内核调试的配置与串口连接流程,深入分析 panic 与 oops 日志,覆盖 kdump/vmcore 的收集机制与 crash 工具的强大分析能力,以及 printk 日志级别、dynamic debug、ftrace 函数追踪和 bpftrace/kprobe 动态追踪等生产环境常用诊断技术。

内核调试是操作系统开发和运维中最具挑战性的领域之一。与调试用户态程序不同,内核崩溃一旦发生,往往意味着整个系统不可用。如何在上千行汇编与 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 偏移处,函数总大小为 0x30
  • Tainted:内核状态标志。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 通过双内核机制解决此问题:

  1. 主内核(primary kernel)正常运行
  2. 预留一块物理内存区域,加载一个精简的捕获内核(capture kernel)
  3. 主内核崩溃时通过 kexec 快速切换到捕获内核(不经过 BIOS)
  4. 捕获内核收集主内核的内存镜像到文件,然后正常关机或重启
主内核运行 ──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):

级别宏含义
0KERN_EMERG系统不可用
1KERN_ALERT必须立即行动
2KERN_CRIT严重情况
3KERN_ERR错误
4KERN_WARNING警告
5KERN_NOTICE正常但重要
6KERN_INFO信息
7KERN_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无用串口捕获启动日志
偶发 panickdump + 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 点的必备基础

延伸阅读

  1. Linux Kernel Documentation: Documentation/dev-tools/kgdb.rst
  2. Linux Kernel Documentation: Documentation/trace/ftrace.rst
  3. Brendan Gregg, “BPF Performance Tools” — eBPF 追踪的权威实战指南
  4. crash 命令参考:crash -h 与 crash> help
  5. systemd-coredump 与 apport —— 用户态 core dump 收集机制
  6. 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

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「os」更多文章

  1. cgroups v2 与命名空间底层实现与资源隔离
  2. Linux 设备驱动模型:字符设备、块设备与 sysfs
  3. 系统调用实现:从 glibc 到内核态切换