4. GDB 基础命令:启动、加载与核心操作
GDB(GNU Debugger)是 Linux 内核调试的基石工具。本章将系统讲解 GDB 的启动方式、如何加载内核符号文件 vmlinux、目标架构设置,以及最常用的断点与执行控制命令。掌握这些基础操作,是后续深入内核调试的前提。
4.1 启动 GDB 的三种方式
根据调试场景不同,GDB 的启动方式分为本地调试、远程调试和附加进程三种。下表对比了它们的适用场景与命令格式:
| 启动方式 | 命令格式 | 典型场景 |
|---|---|---|
| 本地直接启动 | gdb vmlinux |
分析内核崩溃转储文件(vmcore) |
| 远程调试(KGDB) | gdb vmlinux -ex "target remote /dev/ttyS0" |
双机内核调试,目标机通过串口连接 |
| 附加到 QEMU | gdb vmlinux -ex "target remote :1234" |
QEMU/KVM 虚拟机内核调试 |
提示: 推荐使用
gdb-multiarch 替代原生 gdb,以支持跨架构调试(如 x86 主机调试 ARM 内核)。
4.2 加载 vmlinux 与设置架构
vmlinux 是编译内核后生成的未压缩 ELF 文件,包含完整的符号表和调试信息。GDB 通过它才能将地址解析为函数名、行号和变量。
4.2.1 加载 vmlinux
# 方式一:启动时加载
gdb /usr/src/linux/vmlinux
# 方式二:在 GDB 内部加载
(gdb) file /usr/src/linux/vmlinux
# 方式三:使用符号文件(分离调试信息时)
(gdb) symbol-file /path/to/vmlinux.debug
4.2.2 设置目标架构
当调试非本地架构的内核(如 ARM64、RISC-V)时,必须显式设置架构,否则 GDB 无法正确解析指令和寄存器。
# 设置 ARM64 架构
(gdb) set architecture aarch64
# 设置 x86_64 架构(默认)
(gdb) set architecture i386:x86-64
# 查看当前架构
(gdb) show architecture
注意: 架构设置必须在加载
vmlinux 之后、连接远程目标之前完成。错误的架构设置会导致反汇编结果完全不可读。
4.3 核心断点命令:break
break(简写 b)是 GDB 中使用频率最高的命令。内核调试中,断点可以设置在函数入口、行号、条件表达式或地址上。
| 断点类型 | 命令示例 | 说明 |
|---|---|---|
| 函数断点 | b do_fork |
在 do_fork 函数入口暂停 |
| 行号断点 | b kernel/fork.c:123 |
在源文件指定行暂停 |
| 条件断点 | b do_fork if pid == 0 |
满足条件时才触发 |
| 地址断点 | b *0xffffffff81000000 |
在虚拟地址处暂停 |
| 硬件断点 | hbreak do_fork |
使用硬件调试寄存器,适合只读内存区域 |
4.4 执行控制命令:run / continue / next / step
设置断点后,需要通过执行控制命令来驱动调试过程。下表总结了四个核心命令的差异:
| 命令 | 简写 | 行为 | 内核调试典型用法 |
|---|---|---|---|
| run | r |
启动程序(或重新启动) | 在本地调试 vmcore 时无效;远程调试时启动目标内核 |
| continue | c |
继续执行直到下一个断点 | 从当前断点恢复内核执行 |
| next | n |
单步执行,不进入函数内部 | 跳过内核函数调用,只观察当前函数流程 |
| step | s |
单步执行,进入函数内部 | 跟踪进入子函数(如 schedule、do_exit) |
实战要点:
- 内核调试中
run通常不直接使用,因为内核已经在运行;continue才是恢复执行的主要命令。 next和step在汇编级别分别对应nexti(ni)和stepi(si),后者在无源码时更常用。- 在中断上下文或自旋锁持有期间使用
step可能导致系统死锁,需谨慎。
4.5 断点与执行控制流程
下图展示了 GDB 内核调试中从启动到断点触发的完整流程:
4.6 常用辅助命令
除了核心的断点和执行控制命令,以下辅助命令在日常内核调试中同样不可或缺:
list(l):显示当前断点附近的源代码,默认 10 行。info registers(ir):查看所有 CPU 寄存器的当前值。backtrace(bt):打印当前调用栈,定位内核执行路径。print(p):打印变量或表达式的值,如p current->pid。disassemble(disas):反汇编当前函数或指定地址。thread:切换当前调试的 CPU 核心(SMP 系统)。
效率技巧: 在 GDB 中按 Tab 键可自动补全命令和符号名;按 Ctrl+C 可中断正在执行的内核并返回 GDB 提示符(需 KGDB 支持)。
4.7 实战示例:调试 do_fork 流程
以下是一个完整的调试会话,演示从启动到单步跟踪 do_fork 的过程:
# 1. 启动 GDB 并加载内核符号
$ gdb /usr/src/linux/vmlinux
# 2. 设置架构(ARM64 示例)
(gdb) set architecture aarch64
# 3. 连接 QEMU 远程目标
(gdb) target remote :1234
# 4. 在 do_fork 函数入口设置断点
(gdb) b do_fork
# 5. 恢复内核执行,等待断点触发
(gdb) c
# 6. 断点命中后,查看调用栈
(gdb) bt
#0 do_fork (clone_flags=..., stack_start=..., ...) at kernel/fork.c:2020
#1 kernel_thread (fn=..., arg=..., flags=...) at kernel/fork.c:2550
#2 ...
# 7. 单步进入 do_fork 内部
(gdb) s
__do_fork (clone_flags=..., ...) at kernel/fork.c:2100
# 8. 打印当前进程 PID
(gdb) p current->pid
$1 = 1
# 9. 继续执行到下一个断点
(gdb) c
关键总结:
break是内核调试的起点,支持函数名、行号、地址和条件四种形式。continue是恢复内核执行的主要命令,step用于深入函数内部,next用于跳过函数调用。- 加载正确的
vmlinux和设置匹配的架构是 GDB 正确工作的前提。 - 结合
backtrace和print命令,可以在断点处快速获取内核执行上下文。