4、KGDB双机调试:KGDB内核配置、串口/网络连接设置、调试会话建立与断点管理

KGDB,说白了就是让内核可以被GDB远程调试。你想想看,内核跑在目标机上,调试器跑在开发机上,两边通过串口或网络连起来——这就是双机调试的核心思路。

我最早接触KGDB是在调试一个USB驱动的时候。那驱动一加载就死机,printk打出来的日志根本不够用。后来配了KGDB,单步跟踪进去,才发现是个DMA缓冲区的对齐问题。嗯,从那以后,KGDB就成了我调试内核的标配工具。

4.1 KGDB内核配置

要启用KGDB,第一步就是重新编译内核。我个人习惯在现有内核配置基础上,打开以下几个关键选项:

Kernel hacking  --->
    [*] KGDB: kernel debugger
    [*]   KGDB: use kgdb over serial console
    [*]   KGDB: internal debug console
    [*]   KGDB: allow debugging with USB debug port
    [*]   KGDB: allow debugging with ethernet

这里有几个坑,我踩过不止一次:

注意:

  • KGDB: use kgdb over serial console 必须打开,否则串口调试用不了
  • 如果要用网络调试,KGDB: allow debugging with ethernet 也要勾上,但需要额外的驱动支持
  • 调试符号必须保留——编译时加 -g 选项,且不要 strip 掉 vmlinux

配置完以后,编译安装新内核:

make -j$(nproc)
make modules_install
make install

重启后,用新内核启动目标机。这时候KGDB其实还没激活,需要你在启动参数里告诉内核:我要进调试模式。

4.2 串口连接设置

串口是最常用的KGDB连接方式。为什么?因为它简单、稳定、不依赖网络协议栈。我在项目里遇到过网络驱动本身就有bug的情况,这时候串口就是唯一的救命稻草。

硬件连接方面,你需要一根交叉串口线(null modem cable),把开发机和目标机的串口连起来。波特率我一般设115200,再高容易丢数据。

目标机的内核启动参数要加上:

kgdboc=ttyS0,115200 kgdbwait

解释一下:

  • kgdboc 指定调试用的串口设备和波特率
  • kgdbwait 让内核在启动时等待GDB连接

开发机这边,用minicom或screen打开串口:

screen /dev/ttyUSB0 115200

你会看到目标机内核启动到某个点就停住了,打印出类似这样的信息:

Waiting for connection from remote gdb...

嗯,这时候目标机就在等你了。

4.3 网络连接设置

串口虽然稳,但速度慢。如果你要传大量数据(比如调试带符号的内核模块),串口能急死人。这时候网络调试就派上用场了。

网络KGDB需要内核支持 KGDB: allow debugging with ethernet,并且网卡驱动要支持 polling 模式。为什么不能用中断?因为内核被KGDB停住时,中断是关闭的,只能用轮询方式收发包。

目标机启动参数改成:

kgdboe=@192.168.1.10/,@192.168.1.20/ kgdbwait

格式是 kgdboe=本地IP/本地MAC,远程IP/远程MAC。MAC地址可以省略,让KGDB自己广播发现。

小技巧:

我建议网络调试时,把开发机和目标机用一根直连网线连起来,不要经过交换机。这样能避免很多网络层面的干扰。曾经有一次,交换机上的广播风暴直接把我的KGDB会话冲断了,排查了半天才找到原因。

4.4 调试会话建立

连接建立的过程,我画了张图帮你理解:

KGDB双机调试会话建立流程 开发机(Host) 目标机(Target) 1. 启动GDB,加载vmlinux 2. 执行 target remote 连接 3. 设置断点,继续执行 4. 断点命中,进入调试 1. 内核启动,kgdbwait等待 2. 收到连接请求,握手 3. 继续内核初始化流程 4. 执行到断点,暂停 串口/网络 调试数据流 图:KGDB双机调试会话建立流程

具体操作步骤是这样的:

目标机启动后停在 Waiting for connection,这时候在开发机上执行:

gdb vmlinux
(gdb) target remote /dev/ttyS0

如果是网络连接:

gdb vmlinux
(gdb) target remote 192.168.1.10:6443

连接成功后,GDB会打印出目标机的内核版本和当前CPU状态。这时候你就可以开始调试了。

关键点:

vmlinux 必须带调试符号,而且要和目标机上运行的内核完全一致。我见过有人拿错了vmlinux,结果GDB显示的地址全是错的,断点根本打不进去。

4.5 断点管理

断点管理是KGDB调试的核心操作。常用的断点命令和普通GDB一样:

命令 说明 示例
break 设置断点 break do_fork
break file:line 按文件行号设断点 break kernel/fork.c:123
hbreak 硬件断点 hbreak *0xc0100120
watch 数据断点 watch my_var
info breakpoints 查看所有断点 info break
delete 删除断点 delete 1
disable/enable 禁用/启用断点 disable 2

这里有个重要的区别:软件断点和硬件断点。软件断点(break)是在指令码里插入断点指令,内核需要能写代码段。硬件断点(hbreak)用的是CPU的调试寄存器,不修改内存。我在调试只读内存区域时,就必须用硬件断点。

实战经验:

我曾经调试一个内核模块的初始化函数,那个函数在 module_init 里注册后就再也没被调用过。我在 init_module 入口设了断点,然后 continue 让内核继续跑。结果断点根本没命中——后来发现是模块加载路径和我想的不一样,函数地址被重定位了。解决办法是用 break module_init 而不是 break init_module,因为前者是内核模块加载器调用的入口。

调试会话的控制命令也很关键:

  • continue(或 c):让内核继续执行
  • step(或 s):单步进入函数
  • next(或 n):单步跳过函数
  • finish:执行到当前函数返回
  • backtrace(或 bt):查看调用栈
  • info registers:查看CPU寄存器

调试过程中,如果目标机死机或者断点打不进去,别慌。先检查连接是否正常,再确认vmlinux版本是否匹配。我遇到过最诡异的一次,是串口线接触不良,导致GDB收发的数据包偶尔丢字节,断点设置命令被截断了。换了一根线就好了。

重要提醒:

KGDB调试时,整个内核是暂停的。这意味着所有进程都停止运行,包括时钟中断、磁盘IO、网络协议栈。如果你在调试文件系统相关的代码,小心别让日志写操作把自己卡死——因为写日志也需要内核服务,而内核正被你停着呢。

嗯,KGDB双机调试的基本配置和操作就这些。串口适合快速上手和网络不可用的情况,网络适合需要传输大量数据的场景。断点管理这块,多练几次就熟了。记住一点:调试内核和调试用户态程序不一样,你停住的是整个系统,所以操作要快,别让硬件看门狗超时复位了目标机。