若想在Linux内核上学习和开发,一个可以编译运行调试内核的环境必不可少。相信网上已经有不少相关文章了,但我还是遇到了不少麻烦,花费了几天时间才构建了一个令我比较满意的环境。
本文描述了一个从0到可运行环境的最短路径,且尽量只使用标准的工具,减小额外学习的负担。希望能帮助未来的其他人。特别地,本文包含了两个在其他文章中很少描述的内容,如何快速构建一个可在KVM中运行的.config
内核配置,和如何构建一个全功能的rootfs。使用本文描述的流程可以使你在写完代码后的最短时间内,启动一个使用修改后内核的Ubuntu发行版的qemu虚拟机。(顺便,在调试内核上用qemu虚拟机比其他vmware,hyper-v等方便多了)
环境
本文以下描述的操作均在一台安装Linux(Debian/Ubuntu发行版)的计算机上,并有权限启动KVM虚拟机。并不需要图形界面,所以在服务器上也能操作。
编译环境
最简单的方法应该是直接安装当前发行版的内核软件包的编译环境。虽然有些冗余,但应该是足够用了。即使要编译的内核版本不同,编译的依赖应该也不会有较大变化:
sudo apt-get build-dep linux
qemu虚拟机环境
qemu现在基本是调用Linux内核KVM功能的默认工具了。用它直接从命令行启动虚拟机相当方便。qemu-utils则提供了一些操作虚拟机磁盘镜像等所需的工具。
sudo apt install qemu-system-x86 qemu-utils
内核代码
有很多地方可以获取到内核的代码,比如官方仓库,GitHub镜像。如果你在大学,从教育网的镜像(如tuna)下载应该是最快的:
git clone https://mirrors.tuna.tsinghua.edu.cn/git/linux.git
编译配置(.config
)
Linux的编译配置相当复杂,有数以万计的配置项。但其中大多都是驱动程序,我们的目的只是在qemu中运行,需要的驱动程序只有少量非常固定的,其实可以用内核源码中自带的工具轻松获取一份可用的配置:
make defconfig
make kvm_guest.config
make mod2yesconfig
./scripts/config -d RANDOMIZE_BASE -e DEBUG_INFO -e GDB_SCRIPTS
首先获取一份当前架构(这里是x86_64)的默认配置,加入kvm需要的配置项,然后为了避免额外部署内核模块的麻烦,使用mod2yesconfig
将所有功能内置。最后,如果希望用gdb对这个内核进行调试的话,再加入一些调试所需的配置。之后我们会看到这些参数的作用。
完成后配置将保存在代码目录的.config
文件中。
编译内核
执行make
命令,并根据需要加入-j
参数指定并行度。没啥好说的,这一步一般都不会有什么问题,因为有很多机器人在背后不断测试编译过程的。
准备rootfs
有了内核之后,我们还需要在用户空间运行的程序来和内核交互,也就是我们平时在控制台运行的那些命令之类的。这里我直接使用Ubuntu构建好的镜像,这样在虚拟机中就能使用我们平常熟悉的各种命令,也有apt可用用来方便地安装额外的软件。Ubuntu提供了一些安装好的用于在虚拟机中使用的镜像,这里我使用的是Ubuntu focal (20.04)的最新发布版。这个镜像在使用时需要配合cloud init进行一些基本的配置,比如这里配置了默认的ubuntu用户的密码为asdf。
cat > user-data <<EOF
#cloud-config
password: asdf
chpasswd: { expire: False }
EOF
touch meta-data
sudo apt install cloud-image-utils
cloud-localds user-data.img user-data meta-data
这里得到的user-data.img
磁盘镜像中就包含了我们的配置
可以使用qemu测试一下,为了不修改原版镜像,我们以它为基础,创建一个新镜像:
qemu-img create -f qcow2 -b ubuntu-20.04-minimal-cloudimg-amd64.img test.img
qemu-system-x86_64 -cpu host --enable-kvm -m 1G \
--nographic \
-drive file=test.img,if=virtio \
-drive file=user-data.img,format=raw,if=virtio \
-device virtio-net-pci,netdev=net0 -netdev user,id=net0
这里使用虚拟串口和虚拟机通信。启动后可以用ubuntu账户和密码asdf登录,可以检查磁盘,网络等均已自动配置好。满意后删掉test.img即不会留下任何痕迹了。
启动自己编译的内核
有了这些准备后,启动刚刚编译好的内核就轻而易举了,得益于qemu的Direct Linux Boot功能,只需在刚才的命令上稍作修改:
qemu-system-x86_64 -cpu host -enable-kvm -m 1G \
-nographic \
-kernel /path/to/linux/arch/x86/boot/bzImage \
-append 'root=/dev/vda1 console=ttyS0' \
-drive file=test.img,if=virtio \
-drive file=user-data.img,format=raw \
-device virtio-net-pci,netdev=net0 -netdev user,id=net0
至此,你已经可以运行任何你修改过的内核代码了。可以运行uname -a
,确认现在运行的正是刚在编译出来的内核
gdb调试内核
可以直接使用gdb连接到qemu来调试里面的内核,可以说是非常方便了。做法也很简单,在之前的命令的基础上:
qemu-system-x86_64 ... -append '... nokaslr' -s -S
其中nokaslr
只有在之前编译时没有禁用RANDOMIZE_BASE
时才需要。之后启动gdb:
gdb vmlinux
(gdb) target remote :1234
(gdb) hbreak start_kernel
(gdb) c
Thread 1 hit Breakpoint 1, start_kernel () at init/main.c:925
这里有个奇妙的问题。在内核启动前,只有使用hbreak命令设置断点才有效,启动后则部分地方可以使用正常的break命令(start_kernel则似乎始终只有hbreak有效)。这个现象我尚未找到很全面的解释,但我推测和内核STRICT_KERNEL_RWX
配置有关。看上去这个配置在x86_64上目前是强制启动的(ARCH_OPTIONAL_KERNEL_RWX=n),在内核把自己的代码段设置为不可写入之后,gdb会自动切换到使用硬件断点,在内核启动前则会选择使用软件断点[1],而软件断点可能在qemu中本来就并不可用。
你可能会看到类似warning: File ".../vmlinux-gdb.py" auto-loading has been declined by ...
的提示,这正是GDB_SCRIPTS
编译配置的功能,按提示操作允许加载即可。这个脚本提供了很多调试内核的方便功能,均以lx-
开头,可用使用apropos lx
命令查看。
开发环境
这个问题不同人有自己的习惯。我由于之前用VS Code比较多,我也倾向于用它来阅读和修改内核代码。
具体来说,完成一次编译后,可以用内核代码树种的scripts/clang-tools/gen_compile_commands.py
脚本生成compile_commands.json
文件,然后安装VS Code的clangd插件,无需额外配置即可实现自动补全、定义跳转之类的功能。微软开发的C/C++插件应该也能用,但我感觉clangd响应速度更快,而且它是开源的。
总结
恭喜踏入Linux内核代码的殿堂,这是世界上最大最活跃的开源项目之一,每个版本都有超过1000个开发者参与其中。希望你也能称为其中一员,体会这个有着30年历史的,象征着开放,自由的开发者社区的魅力。
[1]: https://sourceware.org/gdb/onlinedocs/gdb/Set-Breaks.html
When GDB sets a breakpoint, it will try to use the target memory map to decide if software or hardware breakpoint must be used.