在 MacOS 上直接编译 Linux Kernel 在我看来并不是一个很好的选择:
- 我不喜欢 MacOS 的第三方包管理工具 brew
- 我不希望在没有隔离的情况下安装一堆我不了解的工具
或者说,在 Ubuntu 上调试 Linux Kernel 才是一个更诱人的选择:
- apt 生态很好
- 基于 Ubuntu gcc 工具链编译 Linux Kernel 社区讨论更多,支持更完善
于是问题就变成了从哪找一台 Ubuntu ?
- 在云服务商那里以白嫖价格搞来的机器太小,大机器又觉得不值当
- 又不想白白浪费 Mac M1 Pro 的性能
OrbStack 是一个非常好的选择。
OrbStack is the fast, light, and easy way to run Docker containers and Linux. Develop at lightspeed with our Docker Desktop alternative.
OrbStack 是专用于 Mac 上的环境隔离工具:
- 可以无缝衔接 docker 命令,也可以直接创建 linux 虚拟机
- 其开销非常小,目前个人版是免费的
- 还有许多其他极其实用的功能,比如可以直接以 ssh 的形式进入虚拟机,带来类似远程开发的体验,也方便 VSCode 这类工具连接环境
下面是本文的脉络:
- 初始化 Ubuntu 开发环境
- 编译 Linux Kernel
- 基于 busybox 制作 root fs
- 启动 QEMU
- gdb 连接
- VS Code 连接
初始化 Ubuntu 开发环境
首先在我们的 Mac OS 上基于 OrbStack 创建 Linux 虚拟机。- ✗ orb version
- Version: 1.10.3 (1100300)
- Commit: 2b5dd5f580d80a3d2494b7b40dde2ef46813cfc5 (v1.10.3)
- ✗ orb create ubuntu:24.04 ubuntu-24-debug
复制代码 创建地飞快。
进入虚拟机: ssh ubuntu-24-debug@orb 。接下来我们所有的命令都是在虚拟机中操作的。- # 先看看我们的版本
- uname -a
- Linux ubuntu-24-debug 6.13.7-orbstack-00283-g9d1400e7e9c6 #104 SMP Mon Mar 17 06:15:48 UTC 2025 aarch64 aarch64 aarch64 GNU/Linux
- # 创建我们的 WorkDir ,下载 Linux Kernel
- mkdir -p ~/debug-linux-kernel-on-qemu/kernel_dev
- cd ~/debug-linux-kernel-on-qemu/kernel_dev
- git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
- # 我们选择和 Ubuntu 一致的 6.13 版本
- cd linux
- git checkout v6.13
复制代码 上面下载 linux kernel 基本是最慢的一步了。如果你的网速实在不给力,可以只下载特定的 tag 并且制定下 max-depth 。
接下来开始安装需要用到的工具:- sudo apt update
- sudo apt install -y build-essential flex bison libssl-dev libelf-dev libncurses-dev \
- qemu-system-aarch64 gdb dwarves busybox-static e2fsprogs bc
复制代码 这些工具的作用分别是:
- build-essential: 包含编译 C/C++ 程序所需的基础工具集,如 gcc, g++, make 等,是编译内核的前提。
- flex, bison: 分别是词法分析器和语法分析器的生成器,内核的构建过程依赖它们来处理 Kconfig 文件和代码中的某些部分。
- libssl-dev: 提供 OpenSSL 库的开发文件(头文件和库),内核编译时需要用到其中的加密和证书相关功能。
- libelf-dev: 提供操作 ELF (Executable and Linkable Format) 文件的库和头文件,内核编译和后续生成调试信息时需要。
- libncurses-dev: 提供 ncurses 库的开发文件,用于支持基于文本的图形界面,比如 make menuconfig。
- bc: 一个基础的命令行计算器,内核构建脚本中有时会用到它进行计算。
- qemu-system-aarch64: QEMU 模拟器,专门用于模拟 ARM64 (aarch64) 架构的完整系统。我们将用它来运行我们编译的内核。
- gdb: 大名鼎鼎的 GNU 调试器(GNU Debugger),是我们用来调试内核的主要工具。
- dwarves: 包含 pahole 等工具,用于读取和分析 DWARF 调试信息。内核编译时可能需要 pahole 来生成 BTF (BPF Type Format) 信息,这对于 eBPF 开发和一些现代调试技术很有帮助。
- busybox-static: 一个包含了许多标准 Unix 工具(如 ls, cat, mount, sh 等)的单个可执行文件。这里的 static 版本表示它是静态链接的,不依赖外部共享库,非常适合用来构建一个最小化的根文件系统(root filesystem)。
- e2fsprogs: 包含用于创建、检查和维护 ext2/ext3/ext4 文件系统的工具集,比如我们后面会用到的 mkfs.ext4。
编译 Linux Kernel
编译 Linux Kernel 过程也较为简单,唯一麻烦的就是配置。- cd ~/debug-linux-kernel-on-qemu/kernel_dev/linux
- make ARCH=arm64 defconfig
- make ARCH=arm64 menuconfig
复制代码 这里的 menuconfig 会让 console 变成可交互界面:
- 使用方向键进行移动
- 使用回车键进入子菜单,空格键选中/取消选中
- 这个 make ARCH=arm64 menuconfig 的核心目的是帮你修改内核配置文件 .config
为了方便后续调试,我们需要调整以下几个配置选项(如果找不到具体选项,可以在 menuconfig 界面中按 / 键,然后输入配置项名称,如 CONFIG_DEBUG_INFO 来搜索):
- 启用调试信息:
- 进入 Kernel hacking ---> Compile-time checks and compiler options --->
- 确保 Compile the kernel with debug info 选项被选中(显示为
- )。如果未选中(显示为 [ ]),按空格键切换。这将启用 CONFIG_DEBUG_INFO=y,让编译器在编译内核时加入 DWARF 调试符号。
- 在同一菜单下,确保 Provide GDB scripts for kernel debugging 选项也被选中(
- )。这将启用 CONFIG_GDB_SCRIPTS=y,会生成一些辅助 GDB 调试的脚本。
- 禁用 KASLR (可选但强烈推荐):
- 返回到 Kernel hacking 菜单 ---> (可能需要先退回主菜单再进入) Processor type and features --->
- 找到 Randomize the address of the kernel image (KASLR) 选项,按空格键取消选中(确保显示为 [ ])。这将设置 CONFIG_RANDOMIZE_BASE=n,禁用内核地址空间布局随机化(Kernel Address Space Layout Randomization, KASLR)。禁用 KASLR 后,内核每次加载到内存的基地址都是固定的,这极大地简化了在 GDB 中设置早期断点和理解内存地址的过程。
- 确保 Virtio 控制台支持:
- 进入 Device Drivers ---> Character devices ---> Virtual terminal --->
- 确保 Virtio console support 被选中(显示为 表示编译进内核,或 表示编译为模块)。通常 defconfig 会默认选中。这是为了让内核能够通过 QEMU 的 virtio 串口设备 (ttyAMA0) 输出信息。
完成以上修改后,使用方向键移动光标到屏幕底部的 < Save >,按回车键确认保存配置(默认会保存到 .config 文件),然后反复选择 < Exit > 退出 menuconfig。
操作后记得检查 .config 文件中是否符合你的预期:比如看看 CONFIG_DEBUG_INFO=y、 CONFIG_GDB_SCRIPTS=y 和 CONFIG_RANDOMIZE_BASE=n 是否存在且设置正确。
开始编译:这里的 -j8 表示使用 8 个线程并行编译,你可以根据自己机器的 CPU 核心数调整。速度非常快,我这里大概花了 10 分钟。并且在编译时一个 WARNING 都没有遇到,写过大型 C/C++ 项目的朋友知道这其实并不容易办到。
编译后我们会得到多个文件,对我们调试最重要的主要是两个:
- vmlinux: 位于内核源码根目录下。这是一个未经压缩、包含完整符号表和调试信息的 ELF 格式的内核可执行文件。gdb 需要加载这个文件来识别函数名、变量名、源代码行号等信息,是调试时必不可少的文件。
- arch/arm64/boot/Image: 这是一个经过压缩、去除了大部分调试信息、并且符合 ARM64 架构引导规范的内核镜像文件。QEMU(或真实的引导加载程序)实际加载并运行的是这个文件。它比 vmlinux 小得多,适合启动。
基于 busybox 制作 root fs
这一步是为 QEMU 虚拟机创建一个最小化的根文件系统 (root filesystem),让内核启动后能找到基本的运行环境和 shell。这是我踩坑最久的一步。
1. 创建临时目录和基本结构
首先,我们需要一个临时目录来构建文件系统的内容,并在其中创建标准 Linux 目录结构。- # 回到主工作目录
- cd ~/debug-linux-kernel-on-qemu
- # 创建并进入 rootfs 构建目录
- mkdir rootfs
- cd rootfs
- # 创建 Linux 基本目录结构
- mkdir -p bin sbin etc proc sys dev lib usr/bin usr/sbin
复制代码 这些目录是 Linux 系统运行所必需的,例如 bin 和 sbin 用于存放可执行命令,etc 用于存放配置文件,proc 和 sys 用于内核与用户空间交互,dev 用于存放设备文件。
2. 引入 BusyBox
busybox 将为我们的最小系统提供核心的用户态工具。- # 确认 busybox 是静态链接的(输出应包含 "statically linked")
- file /usr/bin/busybox
- # /usr/bin/busybox: ELF 64-bit LSB executable, ARM aarch64, version 1 (GNU/Linux), statically linked, ... stripped
- # 复制 busybox 到我们的 bin 目录
- cp /usr/bin/busybox ./bin/busybox
- # 使用 busybox --install 命令创建常用命令的符号链接
- # -s 表示创建符号链接 (symbolic link)
- ./bin/busybox --install -s ./bin
复制代码 执行 busybox --install -s ./bin 后,./bin 目录下会多出很多指向 busybox 的符号链接,例如 ls, sh, mount, echo 等。当内核启动后执行 /bin/sh 时,实际上是执行了 busybox,busybox 会根据被调用时的名称 (sh) 来模拟对应命令的行为。
3. 修正 BusyBox 链接路径 (重要)
默认的 busybox --install 创建的符号链接可能包含了绝对路径(如 ash -> /path/to/rootfs/bin/busybox)。当这个 rootfs 目录被制作成镜像并挂载到 QEMU 虚拟机内部时,这个绝对路径就不再有效了。我们需要的是相对链接(如 ash -> busybox)。
下面的脚本用于修正这个问题:
[code]# 创建修正脚本cat |