编译和运行新版本Linux内核

May 19,2025 linux kernel

outside-of-my-office

暮春时节,咖啡店外的街道郁郁葱葱。

对天天使用Linux系统的人来说,内核是个既神秘又强大的存在。编译内核也是探索和学习Linux的一种方式,虽然不大可能真的将自己编译的内核用在桌面或者生产系统,也不大有能力为Linux内核贡献代码,但是了解内核的基本组成还是挺有趣的。

本文使用RockyLinux9作为编译机,编译最新版的Linux内核 6.14.6 版本,并编译busybox提供基础的shell环境。之后层层深入,从构建initramfs开始、制作rootfs、最后使用GRUB引导内核启动结束。

基础环境:

编译的host系统:RockyLinux 9 Busybox版本:1.37.0
内核源码版本:6.14.6 QEMU的版本:qemu-kvm-9.0.0

不同发行版本的glibc等基础软件有差异,不是所有的系统都能编译最新版本内核,比如CentOS 7编译Linux 6的源码就有问题。

编译BusyboxLinux

创建用于编译的目录结构,下载BusyboxLinux的源码,安装编译的依赖:

$ mkdir build_linux
$ mkdir run_linux 
$ cd build_linux
$ wget https://mirrors.aliyun.com/linux-kernel/v6.x/linux-6.14.6.tar.xz
$ wget https://busybox.net/downloads/busybox-1.37.0.tar.bz2
$ sudo yum install -y gcc make glibc-static flex bison openssl-devel \
					ncurses-devel elfutils-libelf-devel

Linux官网 kernel.org 下载Linux源码比较慢,这里选择从阿里云的镜像源里下载Linux源码。

解压和编译过程中,目录会大小膨胀,建议使用至少有 10GB 可用空间的分区来做编译和运行的环境。

  • build_linux 目录,用来保存下载和编译源码。
  • run_linux 目录,用来保存Linux运行所需的文件。

此外,上面yum安装的基础依赖不一定就是全面的,如果后续编译遇到问题,可能还需要安装缺失的特定依赖。

编译安装 busybox

Busybox 是非常知名的工具软件,里面实现了各种常用的Linux命令,可以提供一个轻量的shell环境,用于和kernel交互。

为了方便使用,需要将 Busybox 编译成静态的二进制文件,之后运行时就没有依赖了, 编译静态二进制busybox的方法如下:

make menuconfig 时的交互选择界面中,勾选 Build static binary (no shared libs),路径是

Settings -->
	Build Options
    [*] Build static binary (no shared libs)` 

下面是具体的编译命令

$ tar xvf busybox-1.37.0.tar.bz2
$ cd busybox-1.37.0
$ make mrproper   
$ make menuconfig  # 一定要选择 Build static binary
$ make -j $(nproc)
  • make mrproper,清理之前构建的中间产物,虽然我们是新下载解压的源码,但是编译前清理一下是个好习惯
  • make menuconfig 交互式选择编译选项,只需要开启Build static binary即可
  • make -j $(nproc) 使用系统上所有的CPU核心进行编译,加快编译速度,也可以用-j指定具体编译并发数。

验证编译出来的busybox二进制文件是否静态,是否能正常使用

$ ldd busybox 
        not a dynamic executable
$ ./busybox pwd

如果没报错,说明busybox的编译正常。

编译Linux内核

和上面的编译过程一样,编译Linux时也可以在make menuconfig的过程中,自定义编译选项和模块。我一般会选择关闭虚拟化,多媒体和声卡,加快编译速度。

[ ] Virtualization  
Device Drivers
  < > Multimedia support
  < > Sound card support

做好选择之后退出并保存,会存为 .config 文件。接下来编译Linux源码的操作,和编译Busybox差不多。

$ tar xvf linux-6.14.6.tar.xz
$ cd linux-6.14.6
$ make mrproper  
$ make x86_64_defconfig   # 生成针对 x86_64架构的默认配置文件
$ make menuconfig     
$ make -j $(nproc)  
$ file arch/x86/boot/bzImage
arch/x86/boot/bzImage: Linux kernel x86 boot executable bzImage, version 6.14.6 () #...
$ ls -lh  arch/x86/boot/bzImage 
-rw-r--r--. 1 root root 13M May 17 10:41 arch/x86/boot/bzImage

如果make时遇到中报错:X.509 certificates to be preloaded into the system blacklist keyring

需要进行如下的menuconfig配置,将如下路径中的具体证书路径清空,保存,重新编译即可。

Cryptographic API
   > Certificates for signature checking
      > X.509 certificates to be preloaded into the system blacklist keyring   # 清空配置的证书路径

我的32核老服务器CPU在几分钟内就完成了编译,速度还是非常不错的。

生成的 bzImage 是用于启动引导的内核文件,是压缩格式的,保存的路径为 arch/x86/boot/bzImage。这个文件非常袖珍,只有13M大小,真是神奇。

make oldconfig 可以直接使用之前生成的 .config,而不用交互式选择模块。

使用新的Linux内核

QEMU是一款开源的仿真和虚拟化软件,可以模拟多种CPU架构和系统硬件。为简单起见,直接yum安装即可。

RockyLinux 9提供的qemu-kvm包集成了内核kvm虚拟化技术,可以提高QEMU模拟器的性能。 QEMU在不同发行版中内置的版本可能不一样,比如Debian 12中就推荐安装qemu-system-x86,并使用 qemu-system-x86_64 命令启动

$ sudo yum install -y qemu-kvm

将之前编译生成的busyboxbzImage文件都复制到run_linux 目录下,后续的生成的启动文件也都放在 run_linux 目录下。

$ cd run_linux
$ cp ../build_linux/busybox-1.37.0/busybox ./
$ cp ../build_linux/linux-6.14.6/arch/x86_64/boot/bzImage ./

initrd 、 initramfs、rootfs的区别

Linuxinitrd、initramfs、rootfs的区别:

  • initrd(initial ramdisk):通常是一个压缩的块设备映像,是initramfs的前身,早期Linux中有所使用。加载到内存后需要挂载为一个临时文件系统。启动过程较长,现在已经很少使用。
  • initramfs:基于内存的临时初始根文件系统,通常是cpio归档的格式,在系统启动过程中使用。在真正的rootfs被挂载之前,初始化硬件设备和加载必要的驱动(比如加载LVM模块),以便挂载真正的rootfs。系统启动过程结束后就不再需要,此时系统会切换到真正的rootfs。
  • rootfs:这里指Linux系统磁盘上的根文件系统,包含了启动后系统运行所需的目录和文件,比如/bin、/etc、/dev、/proc等。通常作为系统的主文件系统,所有其他文件系统都会在其上挂载。rootfs通常包含了系统的长期存储数据。一般用硬盘等介质。

GRUBmenuentry中可能还会使用initrd这个配置项名称,但指向的文件一般是nitramfs格式的。

initramfs启动

有些简单的内核调试可能是不需要硬盘和持久化的,这时用initramfs就很方便。

创建initramfs_dir,将busybox文件放到initramfs_dir/bin下,后续将initramfs_dir 目录打包成cpio格式的initramfs.img文件。等启动initramfs后,busybox在系统中的路径为 /bin/busybox。

$ mkdir -p initramfs_dir/bin
$ cp busybox initramfs_dir/bin/

创建启动文件initramfs_dir/init、内容如下

#!/bin/busybox sh

/bin/busybox mkdir /proc && /bin/busybox mount -t proc none /proc

/bin/busybox echo "Hello World."
/bin/busybox sh

脚本中做了一些简单的初始化操作,如果有需要,也可以做的更复杂些。

  • 首先创建了 /proc 目录,然后挂载,有这个目录了就能运行ps命令
  • 然后输出了“Hello World”
  • 最后启动一个交互式的 sh,提供给用户操作

init脚本中所有命令都是以busybox的方式运行的。下面为init文件添加可执行权限,它会在内核加载之后启动。

$ chmod +x initramfs_dir/init

为了方便测试,接下来创建Makefile,用来管理 initramfsQEMU的启动命令

initramfs:
	cd initramfs_dir && find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.img

run:
	/usr/libexec/qemu-kvm \
        -kernel bzImage \
        -initrd initramfs.img \
        -m 256M \
        -nographic \
        -append "earlyprintk=serial,ttyS0 console=ttyS0"
  • initramfs 目标用于创建新的 initramfs.img 文件
  • run 目标用于启动QEMU
    • -kernel-initrd参数指定了kernel文件和initramfs文件
    • -m 指定了QEMU虚拟机的内存
    • -nographic 指定不要启动图形界面
    • -append 指定虚拟机的串口等信息

注意:makefile中的缩进是tab,不能是空格

接下来正式打包 initramfs和运行编译好的Linux。

$ make initramfs   # 生成 initramfs.img 文件
$ make run
...
Hello World.
/ # busybox ls
bin   dev   init  proc  root

/ # busybox uname -a
Linux (none) 6.14.6 #1 SMP PREEMPT_DYNAMIC Sat May 17 10:40:54 EDT 2025 x86_64 GNU/Linux

/ # busybox ps -ef  | busybox  head -5
PID   USER     TIME  COMMAND
    1 0         0:00 {init} /busybox sh /init
    2 0         0:00 [kthreadd]
    3 0         0:00 [rcu_gp]
    4 0         0:00 [rcu_par_gp]

可以看到内核版本,以及1号进程都和我们预设的一样。

注意: 命令前都需要加上 busybox 命令。

退出QEMU的方式:先按Ctrl-a 后按x。

这样启动的QEMU虚拟机运行在内存中,重启后,系统中所有的配置和修改都会丢失。如果要对数据进行持久化,可以创建一块虚拟磁盘作为系统的rootfs。

接下来测试rootfs的方案。

rootfs启动

为了摆脱busybox ls 这种麻烦的命令行方式,接下来,完善rootfs的目录结构。进入busybox的编译目录,继续执行 make install命令。

$ cd build_linux/busybox-1.37.0/
$ make install
$ ls -l  _install/usr/bin/wget
lrwxrwxrwx. 1 appweb appweb 17 May 17 11:54 _install/usr/bin/wget -> ../../bin/busybox   # wget命令是到busybox的软连接

会生成 _install 目录,里面包含各类常用的命令文件,都以软连接的方式指向二进制文件 bin/busybox 。里面的文件已经具备了基本的目录结构。

创建rootfs目录和rootfs.img虚拟磁盘,对rootfs.img执行格式化和挂载,并将busybox新生成的 _install 目录下的内容复制到 rootfs目录下。

$ cd run_linux
$ mkdir rootfs
$ dd if=/dev/zero of=rootfs.img bs=1024k count=50
$ mkfs.ext4 rootfs.img
$ sudo mount rootfs.img rootfs/
$ sudo chown $UID rootfs   
$ cp -r ../build_linux/busybox-1.37.0/_install/* rootfs/
$ sudo mkdir rootfs/{proc,dev,etc}
$ sudo mkdir rootfs/etc/init.d/
  • 创建一个 50M 大小的文件,作为QEMU使用的虚拟磁盘
  • 先将虚拟磁盘格式化为ext4,再挂载到rootfs目录下
  • busybox_install 目录下的内容复制到 rootfs

继续为 rootfs 创建其它基础目录结构和文件。创建 rootfs/etc/inittab,启动后的路径为 /etc/inittab,定义系统的运行级别和该级别下启动的进程,内容如下

::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::ctrlaltdel:/bin/umount -a -r

inittab文件的第1列通常是id,第2列通常是数字0-6,但是我们这个没这么复杂,仅指定了3个动作触发相应的任务

  • sysinit 表示系统初始化阶段,指定执行 /etc/init.d/rcS 文件做初始化的任务
  • respawn表示如果进程终止,则启动一个登录的/bin/sh
  • ctrlaltdel 表示用户按下Ctrl+Alt+Del 组合键时执行的命令,卸载所有文件系统,并以只读方式重新挂载

创建 rootfs/etc/init.d/rcS,内容如下,会执行etc/fstab文件中指定的挂载操作

#! /bin/sh

/bin/mount -a

创建 rootfs/etc/fstab ,内容如下,挂载 proc 文件系统

proc            /proc   proc    defaults    0   0

etc/init.d/rcS赋予可执行权限。然后卸载 rootfs

$ sudo chmod +x rootfs/etc/init.d/rcS
$ sudo umount rootfs

编写新的 Makefile.rootfs,用于让QEMUrootfs的方式启动内核

run:
	/usr/libexec/qemu-kvm \
                -kernel bzImage \
                -m 256M \
                -nographic \
                -hda rootfs.img \
                -append "root=/dev/sda rw earlyprintk=serial,ttyS0 console=ttyS0 loglevel=8"

整体上和initramfsQEMU启动命令差不多,有细微差别

  • -hda rootfs.imgrootfs.img 作为额外硬盘挂载到QEMU虚拟机中。
  • 不需要 initrd 参数指定initramfs文件了。
  • -append中指定了kernel的参数
    • root=/dev/sda:指定根文件系统的位置。rw 或 ro:指定根文件系统以读写或只读模式挂载。
    • 还可以指定 init 程序:init=/path/to/init。没指定的话,默认会尝试/sbin/init/etc/init/bin/init/bin/sh 这些文件。

启动rootfs的虚拟机,使用make 命令的-f选项指定为rootfs编写的Makefile文件Makefile.rootfs

$ make run -f Makefile.rootfs

之后进去系统就可以直接执行命令了

~ # ls -l /bin/ls
lrwxrwxrwx    1 0        0                7 May 17 09:07 /bin/ls -> busybox

~ # df -hT
Filesystem           Type            Size      Used Available Use% Mounted on
/dev/root            ext4           41.8M     10.1M     28.3M  26% /
devtmpfs             devtmpfs      106.1M         0    106.1M   0% /dev

~ # free -m
             total       used       free     shared    buffers     cached
Mem:           227         15        211          0          0          2
-/+ buffers/cache:         13        213
Swap:            0          0          0

可以看到 ls 命令是到 busybox 文件的软连接。dffree命令也能正常显示

如此就具备了一个基本的Linux的雏形。此时修改系统的文件和配置,内容就会被持久化到虚拟磁盘中了,实际上就是 rootfs.img 这个文件中。

配置GRUB

GRUB 是Linux系统中常用的引导加载程序,负责在计算机启动时加载操作系统内核并移交控制权。与之相关的配置文件一般位于/boot/ 目录下。 接下来创建新的虚拟磁盘,并使用parted划分为2个分区,一个用于GRUB引导,一个用于主分区。

接下来,使用GRUB把我们的新Linux系统打造的更像样一些。

$ dd if=/dev/zero of=disk.img bs=1M count=50   # 50MB大小的磁盘

# 使用GPT分区
$ sudo parted disk.img mklabel gpt
$ sudo parted disk.img mkpart primary 1MiB 2MiB  # 第一个分区
$ sudo parted disk.img set 1 bios_grub on       # 标记为 BIOS Boot 分区
$ sudo parted disk.img mkpart primary ext4 2MiB 100%   # 第二个分区,根分区

$ sudo parted disk.img print  # 查看分区表
Number  Start   End     Size    File system  Name     Flags
 1      1049kB  2097kB  1049kB               primary  bios_grub
 2      2097kB  52.4MB  50.3MB  ext4         primary
  • 创建了50M大小的disk.img文件,作为后续QEMU挂载使用的虚拟磁盘。用parted标记为gpt格式
  • 划定了2个分区,1M的那个用于BIOS Boot分区,剩余的空间是rootfs的根分区,并格式化为ext4

将新创建的disk.img 也挂载到本地,这里使用 losetup 工具挂载。

$ sudo losetup -Pf disk.img  # 挂载
$ losetup -a      # 查看挂载,获取实际的盘符,可能是loop0,也可以是其他的编号

$ ls /dev/loop0*  
/dev/loop0  /dev/loop0p1  /dev/loop0p2   # 可以看到虚拟磁盘的设备和分区的设备了

$ sudo mkfs.ext4 /dev/loop0p2  # 谨慎确认后操作

$ mkdir disk 
$ sudo mount /dev/loop0p2 disk

注意: 必须确定 /dev/loop0 是我们新创建的虚拟盘,如果误操作到物理磁盘,会产生主机系统损坏的严重后果。

  • 挂载之后可以看到有/dev/loop0p1/dev/loop0p2 两个分区(前提是之前没有挂载过文件)。 用mount进行的虚拟磁盘mount也会占用设备编号,我的环境中是 loop0,在你的设备中不一定,操作前注意用losetup -a 检查下实际的盘符,并修改为自己环境的配置。
  • 挂载之后将根分区格式化为 ext4
  • 将格式化好的根分区挂载到本地新创建的disk目录,然后就可以往里面写数据了;

下面为新磁盘写入rootfs的内容,我们直接从之前的rootfs.img 里复制过来

$ sudo mount rootfs.img rootfs
$ sudo cp -r rootfs/* disk/

使用grub2-install 往虚拟磁盘中安装GRUB 2引导加载程序。并将 bzImage 内核文件复制到 boot 目录下

$ sudo grub2-install \
  --target=i386-pc \
  --boot-directory=disk/boot \
  /dev/loop0   # 操作前谨慎确认

$ sudo cp bzImage disk/boot/

注意: 必须确定 /dev/loop0 是我们新创建的虚拟盘,如果误操作到物理磁盘,会产生主机系统损坏的严重后果。

继续创建和编写GRUB 文件 disk/boot/grub2/grub.cfg,指定加载模块和内核文件与内核参数。

# Begin /boot/grub2/grub.cfg
set default=0
set timeout=5

insmod part_gpt
insmod ext2
set root=(hd0,2)

menuentry "Linux 6.14.6" {
    linux       /boot/bzImage   root=/dev/sda2 ro console=ttyS0,115200n8 earlyprintk=serial
}

卸掉挂载,然后用QEMU启动系统

$ sudo umount rootfs
$ sudo umount disk
$ /usr/libexec/qemu-kvm -hda disk.img  -nographic

很快就能看到GRUB界面了!

grub

GRUB结束后继续启动,就进入了内核启动阶段,最终就能看到一个不太完善的Linux系统了。

至此,我们自己编译的当前最新版本Linux内核就成功的通过GRUB引导了。

总结

首先,我们编译了一个Linux内核,又编译了busybox为新内核提供简单而又基础的shell环境。通过制作initramfsQEMU中快速启动新内核,方便快速测试和验证;通过虚拟磁盘构建rootfs的方式,实现虚拟机数据的持久化;最后,通过配置GRUB引导,使得这个新的系统看起来更加有模有样。

但实际上,busybox提供的功能十分有限,缺乏大量的基础软件,远远不能作为真实的系统使用。如要想要进一步深入了解如何构建一个真实的Linux发行版,建议查看Linux From Scratch,书中逐步讲解了如何从头编译一个完整Linux系统,这本书也不长,主要的篇幅都是编译过程,读起来并不吃力。

本篇博客就到这里为止。后续还有一篇博客,将讲解如何开发一个简单而又无用的Linux内核模块,并使用gdb对其进行调试。

total 0 comments