# 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>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://s3.shizhz.me/s3e1.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
