最近在学习 Linux 相关的知识,琢磨着这光学不实践一下也不行,遂尝试编译一个 Linux 内核。
注 :
- Linux 5.14.12;
- qemu 6.1.0;
- busybox 1.31.1;
- ubuntu 20.4
编译 Linux 内核
首先想跑起一个 linux,第一步当然就是编译 linux 的内核。
- kernel.org(官网)下载源码
tarball 就表示 tar 包的意思
- tar Jxfv 名字 (J 表示要解压的是 .xz 格式的,这个可以通过 man tar 命令查看。x 是解压的意思。f 是指定文件名。v 是输出详细信息,也可以不加 v)
这里简单解释一下一些比较重要的目录的作用:
-
- /arch(架构和平台相关): linux 是可以支持很多平台的,比如x86、MIPS、ARM,linux要支持这些不同的架构,就必须有各种架构相关的模块代码,这些不同模块代码的实现了对架构硬件资源的管理,而linux内核本身只关注进程调度、内存管理、网络管理、文件系统、设备驱动等内容,内核实现上述功能需要的数据结构,由arch/xxx目录下的程序来构造和注册到内核,arch/xxx目录下的程序使用标准的数据结构和接口。
- /lib (内核常用方法): 比如常用算法,数据结构比如 llist,kobject 等
- /drivers (设备驱动): 比如 /drivers/net 下会放网卡相关的驱动, vlan 设备的驱动,网卡的驱动等
- /fs (文件系统相关): 比如 ext2,4 等
- /kernel (cpu 相关): 比如 cpu 调度相关代码,多核处理器的相关代码等
- /net (网络栈): 比如 ivp4,ipv6 等
-
make menuconfig,可以可视化地选择要编译带有哪些功能的内核(这个过程中可能会遇到很多错误,百度或者谷歌都很容易搜到)有些功能用不到的就可以给干掉,比如什么蓝牙,或者 LED 之类的功能。这里可以将自己不需要的功能全部干掉以保证编译出来的内核比较小。
*“” **号表示要编译到内核中,属于在编译内核的过程中就会被静态编译进去
**“M” **表示要编译成内核模块儿,但是不直接放到内核中,而是作为 .ko 文件,等内核运行起来了需要的时候再干进去
注:按空格就能控制 * 还是 M
- 选完之后会生成一个 .config
-
然后使用 make -j4 进行编译(-j 表示要使用 4核,至于机器有几个核,可通过 nproc 查看。几个常见问题:
-
- fatal error:xxx.h no such file or directory(使用 apt-cache search 报错的文件名去掉后缀 | grep dev,然后 apt-get install 这个包名)
- No rule to make target ‘debian/canonical-certs.pem‘, needed by ‘certs/x509_certificate_list‘ (vim .config 把 CONFIG_SYSTEM_TRUSTED_KEYS 这一项置为 "",然后重新编译)
- 这一套过程可能需要得半个小时到一个小时就很费劲,需要玩会儿手机
编译完会在源码目录下生成一个 vmlinux 目录,同时 arch/x86_64/boot 目录下会多一个 bzimage 文件, bzImage 就是压缩后的一个镜像,vmlinux 是未压缩的除此之外根据编译选项的不同还可以编译出其他格式的镜像,比如 make 执行 uimage 就会编译出 uboot 这种专门用于嵌入式设备上的镜像,另外 bzImage 是通过 gzip 压缩过的格式。
- 通过 make modules_install INSTALL_MOD_PATH=/root/ding-os/ (INSTALL_MOD_PATH 指定模块要导入的目录),命令执行完后这个目录下会有一个 lib 目录
/lib/modules/${linux 版本名}/kernel 下就是 linux 内核中的目录,其中有各种 .ko 文件
编译 qemu
- www.qemu.org 下载,官网打开可能有点慢,可尝试梯子
- 解压tar xvJf qemu-6.1.0.tar.xz
- 编译
./configure --prefix=/${你的 qemu 源码根目录}/qemu-6.1.0/run --target-list=x86_64-softmmu,x86_64-linux-user
--prefix 表示要交 qemu 安装到那个目录下, 也就是执行完 make install 之后会把东西 isntall 到这个目录中 --target-list 表示要构建的目标平台, 比如 --target-list=arm-softmmu 的话就会只编译 arm 平台
上面的命令中要编译 x86_64 平台的 system 和 user,user 选项编译出来的 qemu 可以直接执行用户态的程序,比如 ls 等等,而 system 编译出来的 qemu 是模拟一台真的物理机,包括像 rootfs 之类的东西都需要自己在手动进行加载或挂载
另外安装过程中可能会遇到一些问题。
常见的比如
ERROR: Cannot find Ninja
解决方法:apt-get install ninja-build
ERROR: Dependency "pixman-1" not found
解决方法:apt-get install libpixman-1-dev
ERROR: glib-2.22 gthread-2.0 is required to compile QEMU
解决方法:apt-get install libglib2.0-dev
等等,基本上碰到的问题都是可以百度或者谷歌到的~
- make & make install,过程大概十来分钟,完事儿之后就可以看到 qemu 源码的根目录下有个 run 目录
/bin 目录下就是 qemu 的可执行文件,刚才编译选项的 --target-list=x86_64-user 编译出来的 qemu-x86_64 目录也在这里,可以通过它直接执行用户态程序。
编译 Busybox
Busybox 是一个小巧的工具集,包含了 linux 下大部分的命令,也就是说,安装了这个工具集之后,就无须再一个个地安装一些 linux 的基本命令了。
- busybox.net/ 下载并解压 .bz2 结尾的文件
- make menuconfig 进入图形化界面
确保 “编译静态二进制” 的这项前面一定是 *,这样可以以静态编译的方式来进行编译
之后选择 Exit 后进行保存
- make & make install 安装完后会 默认安装到源码目录的 _install/ 目录下
镜像制作
在制作镜像之前,我们需要先简单了解一下 linux 的启动过程:
1、加电之后,先在主板上读取一段程序,这段烧录在主板上的程序就是 BIOS
2、 BIOS 通过去读取磁盘上0磁道1扇区的 512 字节,把这些加载到内存的某个地址上,这 512 字节就是 MBR。
3、 MBR 由于只有 512 字节,能做的事儿不多,所以 MBR 主要会去从磁盘上读取另一段儿代码,这段代码叫 bootloader,bootloader 主要就是用来加载操作系统的。
4、 bootloader 会把内核代码加载到内存中,然后会加载根文件系统,因为文件系统都是要挂载到某个目录上的
5、 但此时还没有任何一个目录,于是 bootloader 中会执行一个 initramdisk(或 initramfs)的程序,这个程序的作用就是在根目录下挂载一个根文件系统,也就是 rootfs。
6、 然后这个 rootfs 要分析物理机上有哪些硬件,要加载一些对应硬件的驱动,不过此时的 rootfs 还是只存在于内存中(虚拟 rootfs),接下来要真正将 rootfs 挂载到硬盘上(真实 rootfs)。
7、 此时内存中 rootfs 中的某个目录下需要有个 init 脚本,操作系统就会去运行这个 init 程序,init 脚本就会把所有的程序比如 shell,fireware 等等启动起来。
所以启动一个 Linux 比较主要的部分就是需要由 BIOS、MBR、Bootloader、initramdisk、rootfs 由于我们使用 qemu 的 system 模式(也就是上面 --target-list=x86_64-softmmu 指定的参数编译出来的 qemu),在 qemu 的这个模式中,BIOS 和 MBR 以及 Bootloader 都已经内置好了,所以我们只需要制作 initramdisk 程序以及根文件系统(虚拟 rootfs 以及 真实 rootfs)就好了。
- 先进入到最开始在编译 linux 内核时 “make modules_install INSTALL_MOD_PATH=/root/${your path}/” 中 INSTALL_MOD_PATH 指定的目录下
- 然后将镜像以及 busybox 编译出来的东西还有 qemu 编译出来的东西都拷贝过来
- 开始制作 initramdisk,也就是虚拟 rootfs。简单解释一下 ramdisk 是个啥:ramdisk 其实就是在内存也就是 ram 上虚拟出来的块儿设备,操作方法约等于操作磁盘,但是由于是内存,所以读写速度更快。
Linux 在启动的时候会先把一个提前准备好的比较小的 rootfs 加载进内存,然后作为一个临时的根文件系统进行 mount,之后会在这个根文件系统中执行 init 程序,init 程序中可以对一些硬件做一些检测,或者将网卡的驱动当成模块插入等。当 init 程序执行完毕之后就会挂载真正的 rootfs 到磁盘上,主要通过 switch_root 完成。
对于 initramdisk 需要用到的虚拟 rootfs 可以使用 busybox 来制作。首先进入到 busybox 目录,先创建出几个 rootfs 必须要有的目录,比如 dev,etc,proc,sys 等。
- 然后在 dev 目录下创建 console 字符设备,主设备号是 5,次设备号是 1。之后再创建一个 ram 块儿设备,主设备号是 1,次设备号是 0。
其中 console 设备是为了关联 tty 文件,以便可以在终端上看到各种输出。ram 是在内核启动时,会解压缩并将 initramdisk 的内容复制到 /dev/ram 中。
- 在 busybox 目录下创建一个 init 文件,内容如下:
#! /bin/sh
PATH="/bin:/sbin:/usr/bin:/usr/sbin"
mount -t sysfs sys /sys
mount -t proc proc /proc
echo "/sbin/mdev" > /proc/sys/kernel/hotplug
mdev -s
mount /dev/sda /rootfs
insmod /lib/e1000.ko
echo "" > /proc/sys/kernel/hotplug
umount -f /proc
umount -f /sys
echo "begin to switch rootfs."
exec /sbin/switch_root /rootfs /init
/bin/sh
这个脚本主要作用就是开启 hotplug 热插拔机制(echo 的 mdev 是 busybox 提供的一个工具,它可以在系统启东时候自动帮忙创建一些设备节点,比如启动之后 /dev 下的那堆 tty 设备文件)。然后将 /dev/sda 也就是系统检测到的第一块儿硬盘挂载到 /rootfs 下。随后插入 e1000 这块儿网卡设备的驱动。最后通过 switch_root 将根文件系统切换到真正的 /rootfs 并执行其下的 init 脚本。
- 有了这个 init 文件之后就可以对其进行归档了
cpio 可以简单理解成类似 zip 的压缩命令,它将一些文件归档为 cpio 格式的文件,内核可以自动对 cpio 格式的包进行解压缩
- 目前到这里 initramdisk 就算是创建好了,接下来还要创建真正的 rootfs。可以在 busybox 目录下创建一个 rootfs 姆露露,然后使用 dd 命令创建一块儿虚拟磁盘空间,并给它格式化为 ext4
- 接下来创建一个 rootfs 目录,并将刚刚创建的 rootfs.img 给 mount 到这个 rootfs 目录上
- 此时 rootfs 目录下应该啥也没有呢,直接把 busybox 下的东西拷贝进来
- 需要注意的是此时 rootfs 下也会有个 init 脚本,不过不同的是这个 init 已经是真是的 rootfs 可以执行的 init 了,因此一些在 initrd.img(也就是 busybox 文件) 中的 init 的一些命令已经不需要执行了,所以将 rootfs 目录下的 init 稍作修改
- 再然后手动创建一个 qemu 的启动脚本以方便我们启动内核
-kernel 指定内核镜像;
-initrd 指定刚刚生成启动脚本;
-append 是内核启动时候传递给内核一些参数,在内核运行过程中某些内核函数会用到;
-nographic 可以把结果都输出到终端上;
-hdb 指定根文件系统的磁盘镜像。
- 此时执行脚本,qemu 已经可以正常启动,并且一些基本的命令使用也没问题
注:此时启动后如果报类似“找不到 /dev/tty1” 或者是类似“执行 /init 失败”之类的问题的话,可以尝试看看 busybox/init 是否具有可执行权限。如果没有的话用 chmod 加一下,之后删掉 rootfs.img 和 initrd.img 然后重新从上面第 6 步开始。
-
不过此时的系统还没办法进行网络通信,我们还需要给它配置一下网络通信。按下 control + A + X 可以退出 qemu 的终端
-
编辑一下 cmd 启动脚本后面新增 -netdev tap,id=nd0,ifname=tap0 -device e1000,netdev=nd0
-netdev 指定网络后端,这使用 tap 设备,-device指定虚拟网络设备,这里使用英特尔的 e1000 网卡
- 之后在主机上的 /etc 下增加 qemu-ifdown 和 qemu-ifup 两个文件
由于启动 qemu 时使用 tap 和物理主机进行通信,所以在启动 qemu 的时候还需要在物理主机上创建出能和 qemu 通信的网桥设备。
ifup 文件的作用就是在物理主机上启动一个叫做 br0 的网桥,并把虚拟的 tap 设备绑定到这个 br0 网桥上,并把 br0 网桥给 up 起来,这样由于在 cmd 中也指定了 qemu 启动后要依赖的网络设备是 tap0,所以此时 qemu 就能和物理主机进行通信了
ifdown 的脚本中和它反着,就是把启动时候创建的网桥和 tap 都给 down 掉然后删除网桥(注:如果系统上没有 brctl 命令的话,需要 apt-get)
- 另外由于启动时指定的网卡名是 e1000,但是在编译内核的时候可能并没有把 e1000 直接编译进来,所以需要以模块的方式加载 e1000.ko将 lib 目录下的 e1000.ko 复制到 busybox/lib 的目录下
那这个 e1000.ko 是什么时候插入到内核的呢?其实在上面创建 busybox/init 脚本的时候,有一句是“insmod /lib/e1000.ko”,就是它来 insert 这个 e1000.ko 的。
- 这时执行 cmd 就没任何问题了(此时再次执行 cmd 命令,可能会报说无法找到 qemu-ifup 之类的错误,这是由于不同版本的 qemu 去加载 ifup 文件的默认路径可能会不一样导致的,只要把刚刚创建好的 qeum-ifup 和 qemu-ifdown 两个脚本拷贝到报错中对应的路径就可以了)
- 注意还有一个小问题,就是这个时候直接去 ping 主机的网桥地址可能会 ping 不通
此时在 qemu 里 ifconfig 看不到任何东西,但是 ifconfig -a 可以看到 lo 和 eth0
这是因为系统启动没有把 eth0 网卡给启动,手动把 eth0 给 up 起来就 ok 了
至此,一个最小的 Linux 内核就跑通了~
如果哪里有错误还请大佬们指正,感谢~