1. 调试环境
使用Docker构建Linux Kernel的运行及调试环境
1.1 构建环境
如果没有一个便捷的方式运行与调试代码,那么想要阅读 Linux Kernel 的源码将是十分困难的,本文意在分享如何通过 Docker, 并基于 busybox, qemu-kvm 以及 gdb 等工具构建一个编译、运行、调试 Linux Kernel 的容器环境。
首先,我们需要一个能够编译 Linux Kernel 的 Docker 镜像。新建目录 $HOME/linux/docker
:
在该目录下创建文件 build-kernel.sh
并写入如下内容:
该文件用来编译内核,在后续”编译Kernel“一节会使用到。
在该目录下创建文件 start-gdb.sh
并写入如下内容:
该文件用于调试 Linux Kernel 时进入 GDB 调试器,在最后一节“调试Linux Kernel”一节中会使用到。
另外再创建文件 Dockerfile 并写入如下内容:
通过如下命令构建镜像:
为了简便,我们将运行与调试需要的工具 qemu-kvm 与 gdb 也安装到了该镜像中,这样本文涉及到的所有操作都可以使用一个镜像完成。
1.2 编译 Linux Kernel
1.2.1 下载代码
下载最新稳定版的内核代码:
或者访问 https://kernel.org, 通过如下链接下载代码,并将代码解压至目录 $HOME/linux/
1.2.2 编译Kernel
创建编译结果的输出目录:
进入目录 $HOME/linux/
并运行如下命令,进入容器编译内核:
在容器内进入解压后内核源代码目录,并配置 Kernel 的编译选项:
该命令会打开一个基于命令行的配置页面,如下图所示:
我们需要将调试信息编译至内核文件中,导航至 Kernel hacking ---> Compile-time checks and compiler options
并选中如下选项:
保存并退出。此时的配置信息写入到了文件 /workspace/obj/linux/.config
, 也可以直接修改该文件来完成配置。例如此处我们勾选的编译选项对应的配置项为:
CONFIG_DEBUG_INFO=y
CONFIG_GDB_SCRIPTS=y
选中 CONFIG_GDB_SCRIPTS
是为了调试时能够使用内核代码中提供的 gdb 脚本,gdb 支持使用 python 来扩展命令,内核代码中提供的脚本在目录 $HOME/linux/linux-5.12.14/scripts/gdb
中。
完成配置之后,通过如下命令编译内核:
第一次编译耗时较长,编译结果都在目录 /workspace/obj/linux
中。
将上述编译命令保存到文件 build-kernel.sh
中,并且为该文件添加可执行权限:
以后每次修改了代码后,就可以运行该脚本来编译内核了。
1.3 使用 Busybox 构建 initramfs
1.3.1 编译busybox
回到宿主机,下载 busybox 到工作目录并解压:
回到编译内核的容器 linux-builder
中,对 busybox 进行编译配置:
最后一条命令会打开配置目录,选中 Settings ---> Build static binary (no shared libs)
, 如下图所示:
然后通过如下命令编译并安装 busybox:
1.3.2 构造 initramfs
此处我们使用 busybox 构建一个极简的 initramfs, 能引导 Linux 启动并进入一个 shell 环境就足够。在容器中回到目录 /workspace
执行如下命令:
此时我们已经将 busybox 生成的可执行文件全部拷贝到了对应目录,但还缺少一个 init 程序,可以简单写一个 shell 脚本来充当 init, 将如下内容写入文件 /workspace/initramfs/busybox/init
中:
为文件添加可执行权限:
通过如下命令将所有内容打包:
文件 /workspace/obj/initramfs-busybox.cpio.gz
便是最终的 initramfs, 该文件会在启动内核时作为参数传递给 qemu.
1.4 运行 Linux Kernel
重新启动一个容器,在该容器中启动编译好的内核:
进入容器后运行如下命令启动内核:
成功启动并进入 shell:
我们将运行内核的命令整理成一个脚本保存在文件 start-kernel.sh
中:
并且为该文件添加可执行权限:
然后就可以运行该脚本来启动内核了。
1.5 调试 Linux Kernel
1.5.1 GDB
使用上一节的方式启动内核,但需要为 qemu-system-x86_64
添加参数 -s -S
:
-s shorthand for -gdb tcp::1234
-S freeze CPU at startup (use ’c’ to start execution)
同时还需要将参数 -append "console=ttyS0"
修改为 -append nokaslr
, 否则 qemu 在启动过程不会在断点处停止。我们创建一个启动脚本 start-kernel-debugger.sh
, 包含如下内容:
此时该容器不会有任何输出,因为参数 -S
让 qemu 暂时停止执行,需要通过 gdb 发送命令让其继续。接下来我们进入当前容器,并使用脚本 /usr/local/bin/start-gdb.sh
进入GDB:
进入 gdb 命令行之后,连上 qemu 暴露的端口进行远程调试,然后就可以正常调试内核代码了:
由于去掉了 -append "console=ttyS0"
, 所以此时 qemu 的窗口不会输出任何日志,我们可以通过内核代码中附带的命令 lx-dmesg
来查看:
内核附带的命令都有前缀 lx-
, 可以通过如下命令查看所有的命令:
此时,就可以通过GDB来调试内核的所有代码了!
1.5.2 printk
GDB 的使用有一定的学习门槛,更便捷的方式是使用函数printk
打印日志来对代码进行追踪与调试,我们只需要在感兴趣的地方打印出日志,然后使用前面讨论过的方式重新编译内核并运行内核,就可以在启动日志中看到结果。
例如我们找到内核的入口函数,位于init/main.c
文件中的函数start_kernel()
, 在函数开头添加如下代码:
然后使用脚本build-kernel.sh
编译内核,这次的编译会快很多,编译完成之后执行脚本start-kernel.sh
, 便可以在启动日志的最前面看到我们添加的内容:
脚本build-kernel.sh
与start-kernel.sh
请参见前文对应章节。
可以看到第一行便是我们添加的日志。 printk
的详细用法可以参考 https://www.kernel.org/doc/html/latest/core-api/printk-basics.html
Last updated