最近在看lkd和ulk,寻思配置一下编译环境能更好理解内容。
注意,使用容器的目的只是为了编译源码,因此,以下涉及编译相关的操作均为容器内操作,使用qemu运行为容器外操作,其他文件修改等操作在容器内外无明显影响,但是可能需要根据具体情况修改相对路径。
源码阅读环境
vim+ctags-universal+cscope
准备阶段
根据reddit的帖子,发现可以使用docker的debian/eol:sarge
编译此版本,它默认安装的gcc版本为3.3.5,同时又支持安装2.95,这是README推荐的版本。但是我发现3.3.5版本可以正常编译,所以直接用默认版本编译了。
使用以下指令拉取并运行镜像
1 |
|
在容器内补全编译环境
1 |
|
注意其指向的镜像源为archive.debian.org
。国内访问速度可能较慢。
一个奇怪的情况
我的运行环境为amd64,而帖子提示似乎只能构建32位的kernel。所以起初我认为指定运行i386平台比较稳妥。而且还可以在这个环境里编译一些静态链接的程序,能直接打包进initramfs中就能运行,比较方便。
所以我添加了--platform
参数来指定运行的平台。
1 |
|
启动后使用uname -m
查看后发现仍然是x86_64架构,与宿主机相同,这可能是docker的运行机制导致的。随后发现安装的软件均为x86架构的,而且软件均能正常运行。
但是其实我在最开始启动镜像时并未指定平台,根据我的环境,它应该默认启动amd64架构的镜像。检查后发现安装的软件同样为x86版本,也能正常运行。
这现象很奇怪,因为官网标记的这个镜像的架构只有linux/amd64
与linux/arm/v5
。使用docker manifest inspect debian/eol:sarge
发现这个镜像其实同时支持386、amd64和armv5。
官网显示的OS/ARCH为amd64的架构的构建指令中包含的下载链接也含有386标识:https://github.com/debuerreotype/docker-debian-eol-artifacts/tree/bdf1728b9b8153c87c06af5f90ae64ebab1aedb9/sarge/i386。
考虑到linux架构特殊,不像Windows需要专门的WoW64子系统才能在64位上运行32位软件。所以此镜像的标记为amd64平台的镜像实际上应该运行的平台为386。
初次编译kernel
在容器内进入linux2.6.11的根目录,我的是/ulk/linux-2.6.11
,运行
1 |
|
生成默认配置,目前环境应该只支持构建i386架构的kernel,如果尝试构建x86_64架构,会报错cc1: error: code model 'kernel' not supported in the 32 bit mode
,可能需要在i386平台的容器内使用x86_64的交叉编译器进行编译,但是我不会在这个上古环境中安装,所以先跳过了。
编译内核镜像生成bzImage。
1 |
|
启用initramfs
只生成内核镜像,不启动一个shell的话几乎无法使用,所以我们要在编译内核镜像时让它打包一个cpio进镜像并以initramfs方式启动,方便测试我们编译的内核。
执行make ARCH=i386 menuconfig
(不确定此时加不加ARCH的区别,但是我加了肯定不会错)进入可视化配置菜单,按照如下配置修改
- Device drivers
- block devices Initramfs source file(s)
其中需要填写一个initramfs的路径,我这里填写的路径是容器中的路径,为/ulk/initramfs.cpio
。
构建initramfs
创建目录initramfs
并进入,编写你想要的initramfs的结构。
我的initramfs配置
创建设备文件
创建目录mkdir initramfs/{bin,dev,etc,proc,sys}
我这里主要使用的是busybox的1.16.1
的i486
版本(这是我能找在官网找到的最老的符合需求的版本了),下载下来复制到bin/busybox
,然后chmod +x bin/busybox
在dev
中创建以下文件
1 |
|
如果想要使用串口,需要启用kernel配置CONFIG_SERIAL_8250
和CONFIG_SERIAL_8250_CONSOLE
,其他具体的忘了。
编写init
由于目前处于初级阶段,所以init无需写的很复杂,生成足够的工具就足够了。
1 |
|
为了降低包体积,我们只在bin下创建busybox程序和一些dev文件并打包,其他程序在执行init程序时动态生成。
最终目录结构
1 |
|
打包initramfs到bzImage
接下来进入initramfs目录,打包成cpio
1 |
|
注意,在容器中,此cpio文件位于/ulk/initramfs.cpio
,与之前在menuconfig中填写的路径相同。
接下来进行构建
1 |
|
不出意外的话即可构建成功,并生成bzImage到./arch/i386/boot/bzImage
接下来在宿主机中使用qemu运行
1 |
|
自动化构建
我们可以使用makefile简化打包cpio->重新编译bzImage->启动qemu这个过程,以下是参考
1 |
|
由于makefile需要拉起qemu,所以需要在宿主机中运行。而kernel构建过程位于容器中,构建完成后又要通过docker cp
复制出来bzImage。此过程中我们可能并不知道kernel源码目录,进行依赖分析可能较为困难,因此只能将bzImage标记为phony。
由qemu指定initramfs启动来减少编译时间
我们之前的思路是直接将initramfs打包进kernel镜像,便于移动和启动,但是后续开始写makefile后应该分别管理,从而加快编译速度。(其实是第一次打算使用-initrd时不知道为什么没有成功,才先采用打包的方法的。)
这是一个优化后的makefile
1 |
|
修复init拉起sh时的报错
如果在init脚本中直接拉起sh,会报错"bin/sh: can't access tty; job control turned off"
并关闭高级控制,例如<C-c>
、fg
、bg
等无法使用,经过查找发现一个解决方式是修改init脚本,添加setsid cttyhack sh
。
1 |
|
编译可以在此内核版本运行的代码
lkd书中有添加一个新的系统调用的例子sys_foo
,为了能够测试,我们需要编译一个foo_syscall.c
调用新添加的系统调用并输出。
1 |
|
由于我们的initramfs中什么编译工具都没有,自然无法在里边编译这个程序。我们需要在容器中编译它,目前有两种方法:给initramfs添加动态库,或者将此程序静态链接。但是无论如何选择,都得编译musl库。
经过挑选,我找到了一个相对远古的但是看起来比较稳定的musl版本:1.0.0。但是构建动态库时会报错且无法生成动态库:
1 |
|
查到一次提交修复了此问题,日期为2016-01-31 00:40:33 -0500
,所以需要往后找新一些的版本。1.1.24编译的动态库会产生段错误,可能太新了。最终发现可以正常编译1.1.15版本。
构建musl库
1 |
|
我们需要的文件位于lib/libc.so
,将他复制到initramfs/lib/
(可能需要创建lib目录)。另外,我们还需要ld.so
,但是暂且按下不表。
写一个hello world,使用musl-gcc hello_world.c -o hello_world
进行编译,发现可以运行,而且它的dynamic linker是/lib/ld-musl-i386.so.1
。
1 |
|
我感觉它太长了,所以接下来在配置initramfs中的ld.so
时给它改短点。
配置ld.so
我们先ln -s initramfs/lib/libc.so initramfs/lib/ld.so
(ls -l发现musl的ld.so实际上是libc.so的软链接,甚至只有ld.so没有libc.so应该也可以运行?不知道这种用法是否在musl的方案中),此时,假如我们进入qemu虚拟机,它的ld.so位于/lib/ld.so
,libc.so位于/lib/libc.so
。
但是我们在容器中使用musl-gcc编译出来的可执行程序的动态链接器的路径是/lib/ld-musl-i386.so.1
,当我们把这个可执行程序放在initramfs中显而易见由于ld.so的位置不同,动态链接的程序是不能正常运行的,因此需要修改它的动态链接器。目前我采用编译时修改dynaic-linker实现,当然对于已经编译的ELF文件,有nixos项目的patchelf可用。
1 |
|
可以直接在容器中alias一个short ld.so name musl-gcc
:alias sldmgcc="musl-gcc -Wl,-dynamic-linker=/lib/ld.so"
经过测试,作为一个动态链接的可执行程序,它可以在initramfs中运行。
此时的initramfs结构
1 |
|
编译自定义系统调用测试程序
我们前文展示了一个调用自定义系统调用的源码
1 |
|
_syscall0(long, foo)
是lkd书中展示的创建一个返回THREAD_SIZE
的syscall:sys_foo
。
我们在前文已经配置好了动态链接和编译环境,接下来可以着手编译这个测试程序了。
linux/unistd.h
这个头文件属于linux源码的一部分,我们需要用到它的一些宏和定义等,所以我们需要在编译时手动指定头文件的导入目录
1 |
|
1 |
|
将编译结果放入initramfs重新打包,启动qemu测试,可以看到成功执行代码。
1 |
|
总结
linux2.6.11版本有些老旧,它并不支持很多现在的功能,而且很多实现细节上也与当下经过探索过的、更好的实现细节相去甚远:例如mutex lock,在此版本仍然利用binary semaphore实现,没有上锁后只有所有者才能解锁的概念,且尚未实现completion variable等针对更加细节场景优化方案。
另外我记得lkd一书是对着2.6.33讲解的,有部分内容存在差异,但是大体上和2.6.11差别较小。
总的来说此版本在代码组织,编译过程上相较于现在的内核没有较大的变化,还是值得学习的。