简易内核开发环境
近期尝试了一下写 Linux 内核代码,于是把折腾过程记录一下,方便以后参考。本文默认使用 x86_64 架构以及 Linux 作为宿主系统。
使用 QEMU 运行 Linux 内核
在开始写代码之前,我们需要先准备好模拟器用于运行 Linux。我这里用了QEMU,你也可以用 VirtualBox 之类的虚拟机。一般来说,我们需要一个内核可执行文件(内核本体)和一个 initramfs 镜像(提供用户态程序)。当内核被加载后,它会自动载入 initramfs 镜像,并执行其中的/init
程序。由于这一节的重点是 QEMU 配置,所以我直接使用宿主机的内核和 initramfs。以我的 Archlinux 系统为例,内核文件是/boot/vmlinuz-linux
,initramfs 文件是/boot/initramfs-linux.img
。使用以下命令启动 QEMU,按Ctrl-A x
退出。
1 | qemu-system-x86_64 \ |
-kernel
指定内核可执行文件。-initrd
指定 initramfs disk 镜像文件。-nographic -append "console=ttyS0"
禁用视频输出并使用串口作为终端。-m 512
设定内存。--enable-kvm
使用KVM。-cpu host
使用宿主机的 CPU 特性。
你应该能看到一些系统启动的信息以及无法挂载根分区的报错,这是正常现象,因为我们没有提供任何磁盘文件。你应该可以进入一个紧急修复 Shell, 执行一些简单的如 ls
cd
之类的命令。
创建自己的 initramfs
确保 QEMU 能够正常工作后,我们来创建自己 initramfs 替换掉上一步中的/boot/initramfs-linux.img
。这一节的内容主要来自 Creating a initramfs image from scratch 一文。initramfs 镜像本质上是一个经过压缩的 cpio 归档文件(cpio 约等于 tar)。根据内核版本的不同,你可以使用不同的压缩算法。我的内核比较新,所以可以使用lz4压缩。同时,我们直接下载使用编译好的busybox
可执行文件作为我们的 Shell。
1 | mkdir initramfs_root # 创建initramfs的根目录 |
这样,一个自定义的initramfs.cpio.lz4
文件就构建好了。用其替换掉/boot/initramfs-linux.img
1 | qemu-system-x86_64 \ |
这样你应该就能得到一条欢迎信息和一个 Shell。由于我们没有创建常用命令到busybox
的软链接,所以执行指令时需要加上busybox
的前缀。以下是我的运行结果。
Hello from initramfs
sh: can't access tty; job control turned off
/ # busybox ls
bin dev etc init lib mnt proc root sbin sys tmp var
/ #
编译自己的内核
替换掉 initramfs 镜像后,我们接着来替换内核。自己编译内核大概分三步:下载代码;配置内核;编译内核。Linux 的内核源代码可以在 https://www.kernel.org/ 下载。下载并解压到某个文件夹,比方说linux-5.5-rc5
。通常来说手动配置内核是个苦差事,于是我们直接使用默认配置就好。默认配置文件位于arch/x86/configs/x86_64_defconfig
。
linux-5.5-rc5$ make x86_64_defconfig # 使用64位默认配置
...此处省略若干行...
linux-5.5-rc5$ make -j10 # 请根据你电脑的核心数量调整
...此处省略若干行...
Kernel: arch/x86/boot/bzImage is ready (#1)
依葫芦画瓢,用这个编译好的bzImage
替换掉/boot/vmlinuz-linux
,你应该得到和之前一样的欢迎信息和 Shell。
Hello world! (不含 glibc)
使用 C 语言写 Hello world! 是一码事,不用 glibc 写 Hello world! 是另一码事。在我们创建的 initramfs 中,我们没有包含任何类似glibc
的 C 标准库,因此我们不能使用诸如printf()
之类的方便函数,需要直接使用系统调用进行输出。从汇编层面来说,要进行系统调用,只需要将系统调用号以及参数统统塞入一系列寄存器,然后执行syscall
指令即可。更详细的我在几年前的一篇《Programming with PTRACE, Part4 - 系统调用进阶》里有讲到。那么直接贴代码:
1 |
|
两个值得注意的点:使用_start()
而不是main()
;exit()
系统调用不能省。另外这个程序编译的时候也需要一些特殊的技巧,需要静态链接并且不需要标准库支持:
gcc -nostdlib -static helloworld.c -fno-stack-protector -o helloworld
将编译出的文件放入initramfs_root
并重新打包,然后在 QEMU 中运行就能看到效果了。
Hello from initramfs
sh: can't access tty; job control turned off
/ # /bin/helloworld
Hello world!
Challenge (Hello world from kernel space)
至此,我们已经知道了如何在 QEMU 中启动一个包含内核和用户空间程序的 Linux 系统,并且知道了如何利用系统调用让用户态程序输出字符串。有兴趣的读者可以尝试给内核添加一个新的系统调用,当被调用时向 kernel log 输出Hello kernel world!
。就像这样:
/ # /bin/helloworld_syscall
[ 4.621241] Hello kernel world!