# 1. 调试环境

## 1.1 构建环境 <a href="#org00b72fb" id="org00b72fb"></a>

如果没有一个便捷的方式运行与调试代码，那么想要阅读 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

```

通过如下命令构建镜像：

```
docker build -t linux-builder .
```

为了简便，我们将运行与调试需要的工具 qemu-kvm 与 gdb 也安装到了该镜像中，这样本文涉及到的所有操作都可以使用一个镜像完成。

## 1.2 编译 Linux Kernel <a href="#org0cc53df" id="org0cc53df"></a>

### 1.2.1 下载代码 <a href="#org82ea43a" id="org82ea43a"></a>

下载最新稳定版的内核代码：

```
cd $HOME/linux/
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.12.14.tar.xz
tar -xvJf linux-5.12.14.tar.xz
```

或者访问 [https://kernel.org](https://kernel.org/), 通过如下链接下载代码，并将代码解压至目录 `$HOME/linux/`

### 1.2.2 编译Kernel <a href="#orgc7f0fb9" id="orgc7f0fb9"></a>

创建编译结果的输出目录：

```
mkdir -p $HOME/linux/obj
```

进入目录 `$HOME/linux/` 并运行如下命令，进入容器编译内核：

```
docker run -it --name linux-builder -v $HOME/linux:/workspace linux-builder
```

在容器内进入解压后内核源代码目录，并配置 Kernel 的编译选项：

```
cd /workspace/linux-5.12.14
make O=../obj/linux menuconfig
```

该命令会打开一个基于命令行的配置页面，如下图所示：&#x20;

![](https://640510796-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Me-3_JXLYv-4hrEXyDb%2F-Me-5Nqpzgf8CaMOV64i%2F-Me-6oHfhTkKK8uZZ4vU%2Fkernel_cfg.png?alt=media\&token=c01e6bb5-e82c-4192-a16f-55ab08d5b34c)

我们需要将调试信息编译至内核文件中，导航至 `Kernel hacking ---> Compile-time checks and compiler options` 并选中如下选项：&#x20;

![](https://640510796-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Me-3_JXLYv-4hrEXyDb%2F-Me-5Nqpzgf8CaMOV64i%2F-Me-70_aaSll63LjAd-d%2Flinux_kernel_opts.png?alt=media\&token=4c81c1a5-79f8-4a95-9f7c-c25b419a3dff)

保存并退出。此时的配置信息写入到了文件 `/workspace/obj/linux/.config`, 也可以直接修改该文件来完成配置。例如此处我们勾选的编译选项对应的配置项为：

> &#x20;CONFIG\_DEBUG\_INFO=y
>
> CONFIG\_GDB\_SCRIPTS=y&#x20;

选中 `CONFIG_GDB_SCRIPTS` 是为了调试时能够使用内核代码中提供的 gdb 脚本，gdb 支持使用 python 来扩展命令，内核代码中提供的脚本在目录 `$HOME/linux/linux-5.12.14/scripts/gdb` 中。

完成配置之后，通过如下命令编译内核：

```
docker run -it --name linux-builder --rm -v $HOME/linux:/workspace linux-builder bash build-kernel.sh
```

第一次编译耗时较长，编译结果都在目录 `/workspace/obj/linux` 中。

将上述编译命令保存到文件 `build-kernel.sh` 中，并且为该文件添加可执行权限：

```
chmod a+x build-kernel.sh
```

以后每次修改了代码后，就可以运行该脚本来编译内核了。

## 1.3 使用 Busybox 构建 initramfs <a href="#orgaece56d" id="orgaece56d"></a>

### 1.3.1 编译busybox <a href="#orgfa13f80" id="orgfa13f80"></a>

回到宿主机，下载 busybox 到工作目录并解压:

```
cd $HOME/linux
wget https://busybox.net/downloads/busybox-1.33.1.tar.bz2
tar -vxjf busybox-1.33.1.tar.bz2
```

回到编译内核的容器 `linux-builder` 中，对 busybox 进行编译配置：

```
mkdir -p /workspace/obj/busybox # 创建 busybox 的编译输出目录
cd /workspace/busybox-1.33.1
make O=../obj/busybox menuconfig
```

最后一条命令会打开配置目录，选中 `Settings ---> Build static binary (no shared libs)`, 如下图所示：

![](https://640510796-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Me-3_JXLYv-4hrEXyDb%2F-Me-5Nqpzgf8CaMOV64i%2F-Me-7f2FDXM_JT612SNW%2Fbusybox_cfg.png?alt=media\&token=6440ac55-2d90-4842-b8d8-92cee0e7b3fe)

然后通过如下命令编译并安装 busybox:

```
cd /workspace/obj/busybox/
make -j$(nproc)
make install
```

### 1.3.2 构造 initramfs <a href="#org8bfb4b3" id="org8bfb4b3"></a>

此处我们使用 busybox 构建一个极简的 initramfs, 能引导 Linux 启动并进入一个 shell 环境就足够。在容器中回到目录 `/workspace` 执行如下命令：

```
mkdir -p /workspace/initramfs/busybox
cd !$
mkdir -p {bin,sbin,etc,proc,sys,usr/{bin,sbin}}
cp -av /workspace/obj/busybox/_install/* .
```

此时我们已经将 busybox 生成的可执行文件全部拷贝到了对应目录，但还缺少一个 init 程序，可以简单写一个 shell 脚本来充当 init, 将如下内容写入文件 `/workspace/initramfs/busybox/init` 中：

```
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"

exec /bin/sh
```

为文件添加可执行权限：

```
chmod a+x /workspace/initramfs/busybox/init
```

通过如下命令将所有内容打包：

```
cd /workspace/initramfs/busybox
find . -print0 \
    | cpio --null -ov --format=newc \
    | gzip -9 > /workspace/obj/initramfs-busybox.cpio.gz
```

文件 `/workspace/obj/initramfs-busybox.cpio.gz` 便是最终的 initramfs, 该文件会在启动内核时作为参数传递给 qemu.

## 1.4 运行 Linux Kernel <a href="#org34ce8d0" id="org34ce8d0"></a>

重新启动一个容器，在该容器中启动编译好的内核：

```
docker run -it --name linux-runner -v $HOME/linux:/workspace linux-builder
```

&#x20;进入容器后运行如下命令启动内核：

```
qemu-system-x86_64 -kernel /workspace/obj/linux/arch/x86/boot/bzImage -initrd /workspace/obj/initramfs-busybox.cpio.gz -nographic -append "console=ttyS0"
```

成功启动并进入 shell:

![](https://640510796-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Me-3_JXLYv-4hrEXyDb%2F-Me-5Nqpzgf8CaMOV64i%2F-Me-8RG0CxunxktTI85z%2Flinux_boot.png?alt=media\&token=a46e9733-b033-457b-a4ce-fe874b91edd7)

&#x20;我们将运行内核的命令整理成一个脚本保存在文件 `start-kernel.sh` 中：

```
docker run -it --name linux-builder --rm -v $HOME/linux:/workspace linux-builder qemu-system-x86_64 -kernel /workspace/obj/linux/arch/x86/boot/bzImage -initrd /workspace/obj/initramfs-busybox.cpio.gz -nographic -append "console=ttyS0"
```

&#x20;并且为该文件添加可执行权限：

```
chmod a+x start-kernel.sh
```

&#x20;然后就可以运行该脚本来启动内核了。

## 1.5 调试 Linux Kernel <a href="#orgd23553d" id="orgd23553d"></a>

### 1.5.1 GDB

使用上一节的方式启动内核，但需要为 `qemu-system-x86_64` 添加参数 `-s -S`:

> &#x20;-s shorthand for -gdb tcp::1234&#x20;
>
> -S freeze CPU at startup (use ’c’ to start execution)

同时还需要将参数 `-append "console=ttyS0"` 修改为 `-append nokaslr`, 否则 qemu 在启动过程不会在断点处停止。我们创建一个启动脚本 `start-kernel-debugger.sh`, 包含如下内容：

```
docker run -it --name linux-debugger --rm -v $HOME/linux:/workspace linux-builder qemu-system-x86_64 -s -S -kernel /workspace/obj/linux/arch/x86/boot/bzImage -initrd /workspace/obj/initramfs-busybox.cpio.gz -nographic -append nokaslr
```

此时该容器不会有任何输出，因为参数 `-S` 让 qemu 暂时停止执行，需要通过 gdb 发送命令让其继续。接下来我们进入当前容器，并使用脚本 `/usr/local/bin/start-gdb.sh` 进入GDB：

```
docker exec -it linux-debugger bash start-gdb.sh
```

进入 gdb 命令行之后，连上 qemu 暴露的端口进行远程调试，然后就可以正常调试内核代码了：

![](https://640510796-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Me-3_JXLYv-4hrEXyDb%2F-Me-5Nqpzgf8CaMOV64i%2F-Me-8r_rn86s1VSyN9oO%2Fgdb_basic.png?alt=media\&token=4bb4a3e7-0f5d-47ef-8ddd-b34e38fc5dd1)

由于去掉了 `-append "console=ttyS0"`, 所以此时 qemu 的窗口不会输出任何日志，我们可以通过内核代码中附带的命令 `lx-dmesg` 来查看：&#x20;

![](https://640510796-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Me-3_JXLYv-4hrEXyDb%2F-Me-5Nqpzgf8CaMOV64i%2F-Me-8ziGVlow9YxQpqkw%2Fgdb_dmesg.png?alt=media\&token=35851687-6ad1-43f3-89f4-ebbada9e5a9f)

内核附带的命令都有前缀 `lx-`, 可以通过如下命令查看所有的命令：&#x20;

![](https://640510796-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Me-3_JXLYv-4hrEXyDb%2F-Me-5Nqpzgf8CaMOV64i%2F-Me-93tXf0CbMTC6IKvI%2Fgdb_lx.png?alt=media\&token=6c7e1726-e30d-4952-bc27-f46e26d489e9)

此时，就可以通过GDB来调试内核的所有代码了！

### 1.5.2 printk

GDB 的使用有一定的学习门槛，更便捷的方式是使用函数`printk` 打印日志来对代码进行追踪与调试，我们只需要在感兴趣的地方打印出日志，然后使用前面讨论过的方式重新编译内核并运行内核，就可以在启动日志中看到结果。

例如我们找到内核的入口函数，位于`init/main.c`文件中的函数`start_kernel()`, 在函数开头添加如下代码：

```
asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
{
  printk("*******************start kernel*********************");
  char *command_line;
  char *after_dashes;
  
  // 省略后续所有代码
  }
```

然后使用脚本`build-kernel.sh`编译内核，这次的编译会快很多，编译完成之后执行脚本`start-kernel.sh`, 便可以在启动日志的最前面看到我们添加的内容：

{% hint style="info" %}
脚本`build-kernel.sh`与`start-kernel.sh`请参见前文对应章节。
{% endhint %}

![](https://640510796-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-Me-3_JXLYv-4hrEXyDb%2F-Me-LW8FfbJMblMvZyJs%2F-Me-MI1EwOE6XNJgJxJL%2Fprintk_log.png?alt=media\&token=b8a706af-0ad2-41ed-8530-db785b0f4632)

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