1. 调试环境

使用Docker构建Linux Kernel的运行及调试环境

1.1 构建环境

如果没有一个便捷的方式运行与调试代码,那么想要阅读 Linux Kernel 的源码将是十分困难的,本文意在分享如何通过 Docker, 并基于 busybox, qemu-kvm 以及 gdb 等工具构建一个编译、运行、调试 Linux Kernel 的容器环境。

首先,我们需要一个能够编译 Linux Kernel 的 Docker 镜像。新建目录 $HOME/linux/docker:

在该目录下创建文件 build-kernel.sh 并写入如下内容:

#!/bin/bash

cd /workspace/linux-5.12.14
make O=../obj/linux/ -j$(nproc)

该文件用来编译内核,在后续”编译Kernel“一节会使用到。

在该目录下创建文件 start-gdb.sh 并写入如下内容:

#!/bin/bash

echo 'add-auto-load-safe-path /workspace/linux-5.12.14/scripts/gdb/vmlinux-gdb.py' > /root/.gdbinit # 让 gdb 能够顺利加载内核的调试脚本,如果在下一节编译 Linux Kernel 时下载的是另一版本的 Linux Kernel 代码,请修改这里的版本号
cd /workspace/obj/linux/
gdb vmlinux -ex "target remote :1234" # 启动 gdb 远程调试内核

该文件用于调试 Linux Kernel 时进入 GDB 调试器,在最后一节“调试Linux Kernel”一节中会使用到。

另外再创建文件 Dockerfile 并写入如下内容:

FROM debian:10.8-slim

RUN apt-get update
RUN apt install -y apt-transport-https ca-certificates \
    && echo 'deb https://mirrors.tuna.tsinghua.edu.cn/debian/ buster main contrib non-free \n\
    deb https://mirrors.tuna.tsinghua.edu.cn/debian/ buster-updates main contrib non-free \n\
    deb https://mirrors.tuna.tsinghua.edu.cn/debian/ buster-backports main contrib non-free \n\
    deb https://mirrors.tuna.tsinghua.edu.cn/debian-security buster/updates main contrib non-free\n'\
    > /etc/apt/sources.list \
    && apt update && apt-get install -y \
    bc \
    bison \
    build-essential \
    cpio \
    flex \
    libelf-dev \
    libncurses-dev \
    libssl-dev \
    vim-tiny \
    qemu-kvm \
    gdb
ADD ./start-gdb.sh /usr/local/bin
ADD ./build-kernel.sh /usr/local/bin
RUN chmod a+x /usr/local/bin/*.sh
WORKDIR /workspace

通过如下命令构建镜像:

为了简便,我们将运行与调试需要的工具 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.shstart-kernel.sh请参见前文对应章节。

可以看到第一行便是我们添加的日志。 printk 的详细用法可以参考 https://www.kernel.org/doc/html/latest/core-api/printk-basics.html

Last updated

Was this helpful?