4. GDB 基础与内核调试入门

在 KGDB 双机调试环境中,GDB 是宿主机端控制目标机内核执行的核心工具。本章将回顾 GDB 的常用命令,并重点讲解如何在内核模块加载后管理符号表,以及如何在目标机上正确触发断点。掌握这些基础操作是后续进行中断下半部与工作队列协同调试的前提。

4.1 GDB 常用命令回顾

GDB 提供了丰富的命令用于控制程序执行、检查状态和定位问题。下表列出了在 KGDB 内核调试中最常用的命令及其作用:

命令 缩写 功能描述 内核调试示例
break b 在指定位置设置断点 b do_IRQ
continue c 恢复程序执行,直到遇到下一个断点 c
step s 单步进入函数内部 s
next n 单步执行,不进入函数内部 n
print p 打印变量或表达式的值 p irq_stat
backtrace bt 打印当前线程的调用栈 bt
info registers ir 显示所有寄存器的当前值 info registers
list l 显示源代码上下文 l
调试技巧: 在 KGDB 中,step 和 next 的行为与用户态调试一致,但需要注意内核代码可能包含内联函数或宏展开,导致单步行为与源码不完全对应。建议在关键路径上优先使用 break + continue 组合。

以下是一个典型的 GDB 调试会话片段,展示了在目标机上触发断点后的交互流程:

(gdb) break do_IRQ
Breakpoint 1 at 0xc0100a00: file arch/x86/kernel/irq.c, line 200.
(gdb) continue
Continuing.

Breakpoint 1, do_IRQ (regs=0xc1e03f80) at arch/x86/kernel/irq.c:200
200     {
(gdb) print irq_count
$1 = 1024
(gdb) backtrace
#0  do_IRQ (regs=0xc1e03f80) at arch/x86/kernel/irq.c:200
#1  0xc0100b50 in common_interrupt ()
#2  <signal handler called>
(gdb) step
201         irq_enter();
(gdb) next
202         generic_handle_irq(regs->orig_ax);
(gdb) print regs->orig_ax
$2 = 47
(gdb) continue
Continuing.

4.2 内核模块加载与符号表管理

KGDB 调试的核心挑战之一是如何让宿主机 GDB 识别目标机内核中的符号,特别是动态加载的内核模块。当目标机通过 insmod 加载一个模块时,该模块的代码段和数据段地址是动态分配的,GDB 必须获取这些信息才能正确设置断点和解析符号。

4.2.1 模块加载后的符号同步流程

下图展示了从目标机加载模块到宿主机 GDB 获取符号的完整流程:

内核模块加载与符号同步流程 目标机:执行 insmod my_module.ko 内核分配模块地址空间 模块初始化函数执行 KGDB 捕获模块加载事件 通过串口发送模块符号信息 宿主机 GDB:接收符号通知 GDB 自动加载模块符号表 符号解析成功,可设置断点 GDB 提示:Reading symbols from my_module... 串口/KGDB 协议 关键点:模块符号自动加载依赖 KGDB 的 gdb_serial 和模块通知链机制 若 GDB 未自动加载,可使用 add-symbol-file 手动指定模块地址

4.2.2 手动管理模块符号表

虽然 KGDB 支持自动符号加载,但在某些场景下(如模块加载后 GDB 未及时响应、或需要调试尚未加载的模块),需要手动管理符号表。常用命令如下:

  • add-symbol-file:手动加载模块的符号表,需要指定模块的 .text 段基址。
  • info sharedlibrary:查看当前已加载的所有共享库/模块及其符号状态。
  • remove-symbol-file:卸载不再需要的模块符号表。

手动加载模块符号的典型步骤:

# 在目标机上查看模块加载地址
target# cat /sys/module/my_module/sections/.text
0xe0800000

# 在宿主机 GDB 中手动加载符号
(gdb) add-symbol-file /path/to/my_module.ko 0xe0800000
add symbol table from file "/path/to/my_module.ko" at
    .text_addr = 0xe0800000
Reading symbols from /path/to/my_module.ko...done.
(gdb) break my_module_function
Breakpoint 2 at 0xe0800100: file my_module.c, line 50.
注意事项: 手动加载符号时,必须确保提供的基址与目标机实际加载地址完全一致。地址错误会导致断点设置到错误位置,可能引发内核崩溃。建议优先使用 KGDB 的自动符号加载机制。

4.3 在目标机上触发断点

在 KGDB 调试中,触发断点的方式与用户态调试有显著差异。由于内核是全局共享的地址空间,断点设置后会影响整个系统的执行流。以下是几种常见的断点触发场景:

4.3.1 断点类型与触发机制

断点类型 GDB 命令 触发方式 适用场景
软件断点 break 将目标指令替换为 int3 (0xCC) 大多数内核代码调试
硬件断点 hbreak 使用 CPU 调试寄存器 只读数据区、ROM 代码
条件断点 break ... if cond 每次命中时检查条件 高频调用路径过滤
临时断点 tbreak 命中一次后自动删除 单次检查特定路径

4.3.2 触发断点的典型操作流程

以下是一个完整的操作序列,演示如何在目标机上触发一个内核模块中的断点:

  1. 建立 KGDB 连接:确保宿主机 GDB 已通过串口连接到目标机,并进入等待断点状态。
  2. 设置断点:在宿主机 GDB 中设置目标断点,例如 break my_module_init。
  3. 恢复目标机执行:输入 continue,让目标机继续运行。
  4. 触发断点条件:在目标机上执行触发断点的操作,例如加载模块 insmod my_module.ko。
  5. 断点命中:目标机执行到断点位置时暂停,宿主机 GDB 显示断点信息并等待用户命令。
  6. 调试分析:使用 print、backtrace、step 等命令检查内核状态。
  7. 恢复执行:输入 continue 让目标机继续执行,或使用 finish 退出当前函数。

实际调试会话示例:

# 宿主机 GDB 端
(gdb) target remote /dev/ttyS0
Remote debugging using /dev/ttyS0
kgdb_connected () at kernel/debug/debug_core.c:100
100     {
(gdb) break my_module_init
Breakpoint 1 at 0xe0800100: file my_module.c, line 30.
(gdb) continue
Continuing.

# 目标机端执行 insmod
target# insmod /path/to/my_module.ko

# 宿主机 GDB 端自动暂停
Breakpoint 1, my_module_init () at my_module.c:30
30          printk(KERN_INFO "Module initialized\n");
(gdb) print __func__
$1 = "my_module_init"
(gdb) backtrace
#0  my_module_init () at my_module.c:30
#1  0xc01a2f40 in do_one_initcall ()
#2  0xc01a30a0 in do_init_module ()
#3  0xc01a3200 in sys_init_module ()
(gdb) step
31          my_work = alloc_workqueue("my_wq", 0, 0);
(gdb) continue
Continuing.

4.3.3 断点触发时的系统行为

当目标机内核命中断点时,整个系统会暂停,所有 CPU 都会进入 KGDB 的调试状态。这意味着:

  • 所有进程停止:包括中断处理、定时器、任务调度等全部冻结。
  • 外部 I/O 可能超时:串口、网络等外设因内核暂停而无法响应。
  • 实时性要求高的系统需谨慎:长时间停在断点可能导致硬件看门狗复位。
最佳实践: 在调试中断下半部和工作队列时,建议在 tasklet_action 或 worker_thread 等关键入口设置断点,然后通过触发特定中断或提交工作项来精确命中断点,避免在全局热路径上设置断点导致系统频繁暂停。