Linux 内核模块调试方法

调试内核模块和调试 Linux 程序本身差不多,都可以使用 gdb + qemu 来调试,也可以使用 Linux 内核自带的 kgdb 工具调试。不过因为内核模块是动态加载的,需要 add-symbol-file 来指定模块的加载地址。

使用 kgdb 调试

需要使用到串口进行通信。可以在 qemu 启动系统的时候加入参数 -serial tcp:localhost:4321,server,nowait,将内核的串口设备映射到本机 tcp 连接上,然后编辑 /etc/default/grub 在系统启动参数里加入 kgdboc=ttyS1,115200 nokalsr ,并运行 sudo update-grub 之后重启虚拟机,启动 gdb 使用 target remote localhost:4321 连接到串口调试端口,之后,在虚拟机操作系统里,运行 echo g > /proc/sysrq-trigger 触发断点,将控制权交给 gdb,使用 gdb 设置好断点后按 c 继续运行。后续想要将控制权交给 gdb 都需要使用上面的 echo 语句触发断点。需要注意的是,如果是给内核模块打断点,需要先通过 cat /sys/module/<模块名>/sections/.text 查看模块被加载到哪里了,然后再使用 add-symbol-file /path/to/foo.ko <输出的地址> 加载调试符号。另外,给内核模块打断点需要使用 hbreak 打硬件断点,否则 gdb 会提示无法插入断点。另外退出 gdb 之前记得删除所有断点,不然会导致客户机在下一次触发断点的时候卡死。

使用 qemu gdb server 调试

qemu 本身也支持 gdb 连接到 qemu 程序对客户机进行调试,这种方法只需要在 qemu 启动的时候加入参数 -gdb tcp::1234 就可以了,后面直接启动 gdb 用 target remote localhost:1234 连接到 qemu 调试服务器,这时候按 Ctrl+C 可以直接取得控制权,进行打断点操作。如果要调试内核模块,需要在编译内核的时候以下的配置项都是关闭的(可以直接修改 .config 文件):

CONFIG_DEBUG_RODATA=n
CONFIG_DEBUG_RODATA_TEST=n
CONFIG_DEBUG_SET_MODULE_RONX=n

然后参照上面的方法加载调试符号即可,可以直接使用 break 来设置断点,不需要用硬件断点。

如果需要访问内核模块的全局变量,还需要加载其他段的地址,可以用这个脚本快速获得所有地址的加载命令:

#!/bin/bash
cd /sys/module/$1/sections
echo -n add-symbol-file $2 `/bin/cat .text`
for section in .[a-z]* *; do
    if [ $section != ".text" ]; then
echo " \\"
echo -n " -s" $section `/bin/cat $section`
    fi
done
echo

脚本第一个参数是模块名,第二个参数是模块 .ko 文件地址。