启程!Linux内核开发之旅

创建时间:2021/11/28 下午11:43:26
编辑时间:2021/12/20 下午9:43:58
作者: huww98@163.com (huww98@163.com)
分类:Linux

若想在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.


返回文章列表

评论

NaturalBase 2022/1/24 下午9:40:46

想请教hww大哥一个问题,关于进程虚拟内存分配的。 在linux0.11中,每个进程分配64M地址,所以进程基址是64M*n,n是第几个进程,这样在从线性地址计算物理地址的时候,可以保证每个进程之间的地址是相互隔离的,并且进程内地址是连续的。 (我是看 https://www.bilibili.com/video/BV1d4411v7u7?p=23 ,38min左右,不知道这样有没有理解错) 但是现在的操作系统每个进程是4G空间,是不是一定要64位的系统才能用这种方式分配进程基址呢?还是现在有其他方式进行隔离呢?

回复

登录 / 注册 后发布评论