4.1 Linux 内核调试
准备工作
与用户态程序不同,为了进行内核调试,我们需要两台机器,一台调试,另一台被调试。在调试机上需要安装必要的调试器(如GDB),被调试机上运行着被调试的内核。
这里选择用 Ubuntu16.04 来展示,因为该发行版默认已经开启了内核调试支持:
$ cat /boot/config-4.13.0-38-generic | grep GDB
# CONFIG_CFG80211_INTERNAL_REGDB is not set
CONFIG_SERIAL_KGDB_NMI=y
CONFIG_GDB_SCRIPTS=y
CONFIG_HAVE_ARCH_KGDB=y
CONFIG_KGDB=y
CONFIG_KGDB_SERIAL_CONSOLE=y
# CONFIG_KGDB_TESTS is not set
CONFIG_KGDB_LOW_LEVEL_TRAP=y
CONFIG_KGDB_KDB=y获取符号文件
下面我们来准备调试需要的符号文件。看一下该版本的 code name:
然后在下面的目录下新建文件 ddebs.list,其内容如下(注意看情况修改Codename):
http://ddebs.ubuntu.com 是 Ubuntu 的符号服务器。执行下面的命令添加密钥:
然后就可以更新并下载符号文件了:
完成后,符号文件将会放在下面的目录下:
可以看到这是一个静态链接的可执行文件,使用 gdb 即可进行调试,例如这样:
获取源文件
将 /etc/apt/sources.list 里的 deb-src 行都取消掉注释:
然后就可以更新并获取 Linux 内核源文件了:
printk
在用户态程序中,我们常常使用 printf() 来打印信息,方便调试,在内核中也可以这样做。内核(v4.16.3)使用函数 printk() 来输出信息,在 include/linux/kern_levels.h 中定义了 8 个级别:
用法是:
而当前控制台的日志级别如下所示:
这 4 个数值在文件定义及默认值在如下所示:
虽然这些数值控制了当前控制台的日志级别,但使用虚拟文件 /proc/kmsg 或者命令 dmesg 总是可以查看所有的信息。
QEMU + gdb
QEMU 是一款开源的虚拟机软件,可以使用它模拟出一个完整的操作系统(参考章节2.1.1)。这里我们介绍怎样使用 QEMU 和 gdb 进行内核调试,关于 Linux 内核的编译可以参考章节 1.5.9。
接下来我们需要借助 BusyBox 来创建用户空间:
生成默认配置文件并修改 CONFIG_STATIC=y 让它生成的是一个静态链接的 BusyBox,这是因为 qemu 中没有动态链接库:
编译安装后会出现在 _install 目录下:
接下来创建 initramfs 的目录结构:
最后把它们打包:
这样 initramfs 根文件系统就做好了,其中包含了必要的设备驱动和工具,boot loader 会加载 initramfs 到内存,然后内核将其挂载到根目录 /,并运行 init 脚本,挂载真正的磁盘根文件系统。
QEMU 启动!
-s:-gdb tcp::1234的缩写,QEMU 监听在 TCP 端口 1234,等待 gdb 的连接。-S:在启动时冻结 CPU,等待 gdb 输入 c 时继续执行。-kernel:指定内核。-initrd:指定 initramfs。nographic:禁用图形输出并将串行 I/O 重定向到控制台。-append "console=ttyS0:所有内核输出到 ttyS0 串行控制台,并打印到终端。
在另一个终端里使用打开 gdb,然后尝试在函数 cmdline_proc_show() 处下断点:
可以看到,当我们在内核里执行 cat /proc/cmdline 时就被断下来了。
现在我们已经可以对内核代码进行单步调试了。对于内核模块,我们同样可以进行调试,但模块是动态加载的,gdb 不会知道这些模块被加载到哪里,所以需要使用 add-symbol-file 命令来告诉它。
来看一个 helloworld 的例子,源码:
Makefile 如下:
编译模块并将 .ko 文件复制到 initramfs,然后重新打包:
最后重新启动 QEMU 即可:
三个命令分别用于载入、列出和卸载模块。
再回到 gdb 中,add-symbol-file 添加模块的 .text、.data 和 .bss 段的地址,这些地址在类似 /sys/kernel/<module>/sections 位置:
在这个例子中,只有 .text 段:
然后就可以对该模块进行调试了。
kdb
参考资料
Last updated
Was this helpful?