第 3 章 从源码构建 6.x Linux 内核(第二部分)
本章叙事线索:在上一章,我们像准备手术台一样配置了内核。现在手术刀要下去了——我们要切开代码,编译它,安装它,并看着它在 GRUB 的引导下苏醒。这不仅是敲命令,这是在亲手制造一个操作系统的灵魂。
我们在上一章走到一半停下了。那时候,我们已经学会了如何获取内核源码(不管是用 tar 解压还是用 git 直接拉取),理解了那个如同迷宫般的源码树结构,并且——这或许是最重要的一步——搞定了内核配置,生成了一份属于我们自己的 .config 文件。你甚至还试着自己往配置菜单里加了一项。
但这些只是准备工作。
现在,我们要开始真正的制造过程。剩下的这四个步骤,将把一堆文本文件变成一个甚至能让机器飞起来的内核镜像:
- 编译内核镜像和模块(Step 4):这是重头戏,也是 CPU 最发热的时候。
- 安装内核模块(Step 5):编译出来的
.ko文件得放到它们该去的地方。 - 生成 initramfs 和配置引导程序(Step 6):解决那个经典的「鸡生蛋」问题,并告诉 BIOS/UEFI 去哪里找新的内核。
- 定制 GRUB 与最终验证(Step 7):确保开机时能看到我们的新内核,并确认它真的按我们预想的方式工作。
作为这一章的尾声,我们还会跨越架构的限制,尝试为另一块板子——大名鼎鼎的树莓派(Raspberry Pi)——交叉编译一个内核。
准备好了吗?这次我们不再只是看说明书,而是要开始拧螺丝了。
3.1 编译内核镜像和模块
如果你只是作为一个最终用户来看待这件事,编译内核其实简单得令人发指。
只要确保你在内核源码树的根目录下,敲下 make,然后——就可以去泡杯咖啡了。真的,就这一个命令。kbuild 系统会自动处理剩下的所有事情:它会编译出内核镜像,编译出所有你配置为模块(m)的组件,甚至在嵌入式系统中还会顺带编译出设备树 Blob(DTB)。
第一次编译会花点时间,这很正常。现在的 Linux 内核代码库庞大得惊人,估计有 2500 到 3000 万行源代码(SLOC)。这实际上是个极其消耗内存和 CPU 的活儿,以至于有些人甚至拿内核编译来当压力测试工具!
当然,make 后面可以跟不同的目标。如果你输入 make help,你会看到一大串选项。我们在上一章用过它来查看配置目标,现在我们用它来看看构建目标。
我们在上一章已经设置过环境变量 LKP_KSRC 指向我们的源码目录,这里直接用:
$ cd ${LKP_KSRC}
$ make help
[...]
Other generic targets:
all - Build all targets marked with [*]
* vmlinux - Build the bare kernel
* modules - Build all modules
[...]
Architecture specific targets (x86):
* bzImage - Compressed kernel image (arch/x86/boot/bzImage)
[...]
$
请注意这里:执行 make all(或者光敲 make,因为它是默认目标)会构建上面带 * 号的三个目标。它们分别代表什么?
- vmlinux:这是未压缩的内核镜像文件。它体积巨大,尤其在开启了调试信息的时候。我们通常不直接用它启动,但在内核调试时,它是无价之宝——千万别删了它。
- modules:所有被标记为
m(模块)的配置项,会被编译成.ko(Kernel Object)文件,暂时存放在源码树里。 - bzImage:这是 x86 架构特有的压缩内核镜像(big zImage)。这才是引导加载程序真正要加载进内存、解压并跳转执行的文件。
这里有个经常被问到的问题:既然我们启动用的是 bzImage,那要 vmlinux 有什么用?
想象一下,bzImage 是打包好的快递盒,里面的东西是压缩过的,方便运输。而 vmlinux 是那个摊开在桌子上的所有零件清单。如果你要调试内核崩溃,你需要那个未压缩、带满符号信息的 vmlinux。没有它,你看到的只是一堆让人头晕的地址,而不是函数名。
并行编译:榨干你的 CPU
现在的 make 工具很聪明,它支持多进程并行构建。如果你还在用单线程 make,那你是在浪费你的机器性能。
你可以通过 -jn 选项来控制并行度,其中 n 是并行任务的上限。一个通用的经验法则(Heuristic)是:
n = CPU 核心数 * 系数
这个系数通常是 2。如果你的系统核心数极多(成百上千),系数可以降到 1.5。当然,这里指的核心数最好是支持 SMT(Simultaneous Multi-Threading,同时多线程,也就是 Intel 的超线程技术)的逻辑核心数。
怎么知道你的机器有几个核心?用 nproc 就行:
$ nproc
4
这是我的虚拟机配置,分配了 4 个核心。所以,我们可以把并行数设为 8(4 * 2)。
$ make -j8
💡 副作用预警:编译内核极其消耗 CPU 和内存。
如果你在虚拟机里开图形界面编译,可能会遇到系统卡顿甚至突然把你登出的情况。这是因为内存耗尽了。
- 建议:切换到多用户文本模式(runlevel 3 或
multi-user.target)再编译。在 systemd 下用sudo systemctl isolate multi-user.target即可。- 或者:给虚拟机多分点内存。内存现在不贵,但这比看着编译到一半报错要省钱得多。
- 最佳实践:通过
ssh连接到虚拟机进行编译,同时把输出重定向到文件,方便排错:make -j8 2>&1 | tee out.txt
那些让人讨厌的依赖报错
当你满心欢喜地敲下 make -j8 后,有时候并不会一帆风顺。你会遇到类似这样的报错:
warning: Cannot use CONFIG_STACK_VALIDATION=y, please install libelf-dev
[...]
make[1]: *** No rule to make target 'debian/canonical-revoked-certs.pem'
第一个问题是少装了 libelf-dev。在 Ubuntu 上,sudo apt install libelf-dev 就能解决。
第二个问题更有趣,也更具迷惑性。它会在编译进行一会儿后突然蹦出来,导致构建失败。问题的根源在于一个名为 CONFIG_SYSTEM_REVOCATION_KEYS 的配置项。
在最近的 Ubuntu 系统上,这个配置项默认指向了一个在 vanilla kernel(纯源码)里不存在的文件。最简单的修复方法就是关掉它:
# 使用脚本工具禁用该配置项
scripts/config --disable SYSTEM_REVOCATION_KEYS
# 验证一下
$ grep CONFIG_SYSTEM_REVOCATION_KEYS .config
# CONFIG_SYSTEM_REVOCATION_KEYS is not set
现在,再次运行 make -j8。这次应该能一口气跑到底了。
编译完成后的产物:你要找的三个文件
如果一切顺利,屏幕最后会滚动出类似这样的信息:
LD vmlinux
SYSMAP System.map
[...]
BUILD arch/x86/boot/bzImage
Kernel: arch/x86/boot/bzImage is ready (#3)
这时候,在源码树的根目录下,你应该能找到三个关键文件(还有很多其他的,但这三个最重要):
$ ls -lh vmlinux System.map
-rw-rw-r-- 1 c2kp c2kp 4.8M May 16 16:12 System.map
-rwxrwxr-x 1 c2kp c2kp 704M May 16 16:12 vmlinux
$ file vmlinux
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
看到那个 vmlinux 的大小了吗?704MB。这就是包含了所有调试符号和元数据的原始内核镜像。
而那个 System.map,是内核符号表。它记录了内核里的函数名和变量名对应的内存地址。这在调试 OOPS 或者崩溃时非常重要。
至于我们真正要启动的那个压缩镜像——bzImage,它藏在架构特定的目录里:
$ ls -lh arch/x86/boot/bzImage
-rw-rw-r-- 1 c2kp c2kp 12M May 16 16:12 arch/x86/boot/bzImage
$ file arch/x86/boot/bzImage
arch/x86/boot/bzImage: Linux kernel x86 boot executable bzImage, RO-rootX, swap_dev 0x6, Normal VGA
12MB。这才是最终会被塞进内存里的东西。
这里有个小技巧:内核的 Makefile 里内置了一些验证目标,可以帮你确认当前的内核版本或镜像名称,省得你敲错路径:
$ make kernelrelease kernelversion image_name
6.1.25-lkp-kernel
6.1.25
arch/x86/boot/bzImage
好了,镜像有了。现在,我们需要把它支撑起来——安装那些被编译成模块的组件。
3.2 安装内核模块
在编译阶段,所有标记为 m 的内核模块都被编译成了 .ko 文件,散落在源码树的各个角落。但是,仅仅编译出来是不够的。系统在启动时需要在一个「大家都知道的地方」去寻找这些模块。
这个「大家都知道的地方」就是:/lib/modules/$(uname -r)/。
模块去哪了?
在安装之前,我们先看看源码里到底生成了哪些模块。用 find 命令找找看:
$ find . -name "*.ko"
./crypto/crypto_simd.ko
./crypto/cryptd.ko
[...]
./fs/binfmt_misc.ko
./fs/vboxsf/vboxsf.ko
它们现在只是源码树里的普通文件。为了让系统在启动和运行时能加载它们,我们需要执行安装步骤。
执行安装
安装很简单,只需一个命令,但必须用 root 权限,因为我们要往 /lib/modules 下写东西:
$ sudo make modules_install
[...]
INSTALL /lib/modules/6.1.25-lkp-kernel/kernel/arch/x86/crypto/aesni-intel.ko
SIGN /lib/modules/6.1.25-lkp-kernel/kernel/arch/x86/crypto/aesni-intel.ko
[...]
DEPMOD /lib/modules/6.1.25-lkp-kernel
$
注意看输出里发生了什么:
- INSTALL:模块被拷贝到了
/lib/modules/6.1.25-lkp-kernel/kernel/目录下的对应路径中。 - SIGN:如果你的系统开启了内核模块签名(
CONFIG_MODULE_SIG,这是一个很强的安全特性),安装过程会对模块进行签名。如果开启了强制签名(CONFIG_MODULE_SIG_FORCE),那些没签名或签名不对的模块将被拒绝加载。 - DEPMOD:最后,系统运行了
depmod工具。它的作用是分析模块之间的依赖关系(比如模块 A 可能依赖模块 B),并生成modules.dep等元文件,确保加载时顺序正确。
现在,去看看那个目录:
$ ls /lib/modules
5.19.0-40-generic/ 5.19.0-41-generic/ 6.1.25-lkp-kernel/
每个已安装的内核都有一个专属文件夹。再看看我们的新内核下面有什么:
$ ls /lib/modules/6.1.25-lkp-kernel/kernel/
arch/ crypto/ drivers/ fs/ lib/ net/ sound/
这就是我们刚刚编译好的所有模块,它们已经就位,随时待命。
⚠️ 警告:别把宿主机搞挂了
交叉编译时的陷阱
如果你是在交叉编译(比如在 x86 上给 ARM 编译),千万不要直接运行 sudo make modules_install,除非你设置了 INSTALL_MOD_PATH。否则,你会把你宿主机上的模块覆盖掉,或者把 ARM 的模块混杂进 x86 的目录里,这会导致系统极其不稳定,甚至起不来。
正确做法是设置一个安装根目录:
export STG_MYKMODS=../staging/rootfs/my_kernel_modules
make INSTALL_MOD_PATH=${STG_MYKMODS} modules_install
这样,所有的模块都会被安装到 ${STG_MYKMODS}/lib/modules/ 下,和你的宿主机完全隔离。这在嵌入式开发中是标准操作。
3.3 生成 initramfs 镜像与引导设置
现在内核有了,模块有了。还差临门一脚。
在 x86 架构上,这一步通常包含两个部分:生成 initramfs(早期内存文件系统)镜像,以及更新引导加载程序(GRUB)的配置。
为什么要搞个 initramfs?这是一个好问题,我们稍后会详细拆解它。现在,先让我们把这一步跑通。在 x86_64 的 Ubuntu 上,这一切通常只需要一个命令:
$ sudo make install
INSTALL /boot
run-parts: executing /etc/kernel/postinst.d/dkms 6.1.25-lkp-kernel /boot/vmlinuz-6.1.25-lkp-kernel
run-parts: executing /etc/kernel/postinst.d/initramfs-tools 6.1.25-lkp-kernel /boot/vmlinuz-6.1.25-lkp-kernel
update-initramfs: Generating /boot/initrd.img-6.1.25-lkp-kernel
[...]
run-parts: executing /etc/kernel/postinst.d/zz-update-grub 6.1.25-lkp-kernel
Sourcing file `/etc/default/grub'
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-6.1.25-lkp-kernel
Found initrd image: /boot/initrd.img-6.1.25-lkp-kernel
done
正如你所见,make install 做了很多幕后工作。它调用了 /etc/kernel/postinst.d/ 下的一堆脚本,其中就包括 update-initramfs(用来生成那个 .img 文件)和 update-grub(用来修改 GRUB 的菜单)。
生成了什么?
现在 /boot 目录下多了几个关键文件:
/boot/vmlinuz-6.1.25-lkp-kernel:这是bzImage的副本,是压缩后的内核镜像。/boot/initrd.img-6.1.25-lkp-kernel:这就是刚刚生成的 initramfs 镜像。/boot/System.map-6.1.25-lkp-kernel:符号表的副本。
你可能会问:initramfs 到底是个什么东西?为什么非得有它?
理解 initramfs 框架:解开「鸡生蛋」的死结
这是一个比它看起来要深刻的问题。
想象一下,你正在维护一个 Linux 发行版。你的用户可能会把根文件系统格式化成各种奇奇怪怪的类型——ext4、btrfs、f2fs,甚至是加密的 LUKS 分区。
内核本身是很精简的。为了保持灵活性,这些具体的文件系统驱动(比如 f2fs.ko)通常被编译成内核模块,而不是直接塞进内核镜像里。
这就导致了一个经典的「鸡生蛋」问题:
- 内核启动了,在内存里跑着。
- 它想要挂载根文件系统(比如 f2fs 格式的)。
- 为了挂载 f2fs,它需要加载
f2fs.ko驱动模块。 - 但是,
f2fs.ko文件本身就躺在那个还没被挂载的根文件系统里(通常是/lib/modules/.../fs/f2fs/f2fs.ko)。
死循环了。
initramfs 就是那个解结的人。
它是一个极其精简的、包含在内存里的文件系统镜像。它里面塞满了挂载真实根文件系统所需的最基本的工具:驱动模块(f2fs.ko)、加密库(用于解密密码)、脚本以及 /sbin/init 程序。
流程是这样的:
- Bootloader (GRUB) 加载内核镜像(
vmlinuz)和 initramfs 镜像(initrd.img)进内存。 - 内核启动,解压 initramfs 到一个临时的 RAM 磁盘。
- 内核将这个 RAM 磁盘作为临时的根文件系统挂载。
- initramfs 里的脚本运行,加载必要的硬件驱动和文件系统驱动(比如
f2fs.ko)。 - 一旦准备就绪,脚本执行
pivot_root操作——把根文件系统从 RAM 切换到真实的磁盘分区。 - 系统继续启动,执行真正的
/sbin/init(systemd 或 SysVinit)。
这就是为什么即使你的硬盘是加密的,你也能在开机时看到那个输入密码的小框体——那是 initramfs 提供的用户空间环境在运行。
偷看 initramfs 里面有什么
别被这玩意儿忽悠了,它其实就是个压缩包。在 Ubuntu 上,你可以用 lsinitramfs 命令看看里面都有啥:
$ lsinitramfs /boot/initrd.img-6.1.25-lkp-kernel | head -n 20
.
kernel
bin
conf/initramfs.conf
etc
lib64
libx32
run
sbin
scripts
usr
usr/bin/cpio
usr/bin/dd
[...]
usr/lib/modules/6.1.25-lkp-kernel/kernel/fs/f2fs/f2fs.ko
[...]
看,里面有 usr/lib/modules,也就意味着,那个让我们能挂载真实根文件系统的 f2fs.ko 就在这里面。一旦 pivot_root 完成,这个临时文件系统的使命就结束了,它的内存会被回收。
3.4 定制 GRUB 引导程序
内核和 initramfs 都在 /boot 里待命了,GRUB 配置也更新了。现在,我们需要搞定最后一点交互层面的东西:让 GRUB 在开机时显示菜单。
默认情况下,现代的 GRUB 往往为了追求启动速度和所谓的「干净体验」,会直接启动最新的内核,不给你任何选择的机会。这对于开发调试来说是灾难——万一新内核起不来,你连回退的机会都没有。
强制显示菜单
我们要修改 GRUB 的配置文件。请注意,这些操作是在你的目标系统(也就是那台跑着 Ubuntu 的虚拟机或物理机)上进行的。
-
备份配置文件(这是个好习惯):
sudo cp /etc/default/grub /etc/default/grub.orig -
编辑文件:
sudo vi /etc/default/grub -
修改关键行: 为了让菜单每次都显示,找到
GRUB_TIMEOUT_STYLE,把它改成menu;或者如果有一行GRUB_HIDDEN_TIMEOUT_QUIET=true,把它改成false。同时,设置一下超时时间(也就是如果你不按键,它等多久才自动启动默认项):
GRUB_TIMEOUT=3GRUB_TIMEOUT_STYLE=menuGRUB_HIDDEN_TIMEOUT_QUIET=false -
更新 GRUB: 别忘了这一步,改了配置不生效是新手常犯的错误。
sudo update-grub
指定默认启动的内核
GRUB 默认会启动列表里的第 0 个内核(通常是最新安装的那个)。如果你想稳妥一点,想让它默认启动发行版自带的旧内核,可以这样修改:
GRUB_DEFAULT="Advanced options for Ubuntu>Ubuntu, with Linux 5.19.0-42-generic"
这语法有点像路径,指明了子菜单和具体的菜单项。改完记得再次运行 sudo update-grub。
3.5 点火启动:见证你的新内核
万事俱备。现在是心跳加速的时刻了——重启系统。
$ sudo reboot
当虚拟机(或物理机)重启时,按住 Shift 键(或者如果你用的是 UEFI,可能得按 Esc,这取决于固件)。你应该会看到 GRUB 的菜单界面。
选择 "Advanced options for Ubuntu",然后选刚才那个带你编译标记的内核(比如 Ubuntu, with Linux 6.1.25-lkp-kernel),按回车。
如果你把 quiet splash 从内核参数里删掉(按 e 键编辑启动项就能删),你还能看到漫天飞舞的内核启动日志。那是一种很原始的浪漫。
验证:它真的是我们的内核吗?
进了系统,别光顾着高兴。先验证一下我们是不是真的在跑刚才编译出来的内核。
$ uname -r
6.1.25-lkp-kernel
这还不够。还记得上一章我们在配置里改过 CONFIG_HZ 吗?我们把它改成了 300。让我们确认一下这个配置确实生效了。内核源码里有个脚本叫 extract-ikconfig,它能从内核镜像里把配置信息抠出来:
$ ${LKP_KSRC}/scripts/extract-ikconfig /boot/vmlinuz-6.1.25-lkp-kernel | grep -E "LOCALVERSION|CONFIG_HZ"
CONFIG_LOCALVERSION="-lkp-kernel"
[...]
CONFIG_HZ_300=y
CONFIG_HZ=300
完美。或者,既然我们在配置里开启了 CONFIG_IKCONFIG_PROC,我们也可以直接从 /proc 文件系统里查:
$ gunzip -c /proc/config.gz | grep -E "LOCALVERSION|CONFIG_HZ"
CONFIG_LOCALVERSION="-lkp-kernel"
CONFIG_HZ=300
这一刻,你可以确信:你掌控了这台机器的内核。
3.6 跨越架构:为树莓派交叉编译内核
如果你的工作只在 x86 服务器上,那你已经可以毕业了。但作为折腾内核的人,你迟早会遇到嵌入式设备。这里我们拿树莓派 4(Raspberry Pi 4 Model B,ARM64 架构)练练手。
为什么不在树莓派上直接编译? 因为树莓派性能相对较弱,编译内核可能要花好几个小时。而交叉编译是在你的高性能 x86 主机上为 ARM 生成代码,几分钟就能搞定。这才是嵌入式开发的标准姿势。
Step 1:准备源码
我们需要树莓派官方维护的内核源码。选一个工作目录:
export RPI_STG=~/rpi_work
mkdir -p ${RPI_STG}/kernel_rpi
cd ${RPI_STG}/kernel_rpi
git clone --depth=1 --branch=rpi-6.1.y https://github.com/raspberrypi/linux.git
这里我们克隆的是 rpi-6.1.y 分支,正好和我们在 x86 上编译的主线版本保持一致(都是 LTS)。
Step 2:安装交叉编译工具链
现在的 Debian/Ubuntu 系统里,交叉编译器都已经打包好了,不需要你自己去折腾什么 crosstool-ng。
我们需要 aarch64(ARM 64位)的工具链:
$ sudo apt install gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu
装好后,你会发现 /usr/bin/ 下多了一堆带前缀的工具:aarch64-linux-gnu-gcc、aarch64-linux-gnu-ld 等等。
这个前缀 aarch64-linux-gnu- 就是所谓的 Toolchain Prefix(工具链前缀)。我们需要把它告诉内核的 Makefile。
Step 3:配置与编译
这里的关键是告诉 Makefile 两个环境变量:
ARCH=arm64:我们要编译的是 ARM64 架构。CROSS_COMPILE=aarch64-linux-gnu-:用哪个工具链来编译。
先清理一下,然后加载树莓派 4 的默认配置(bcm2711_defconfig 是针对博通 2711 芯片的配置,也就是树莓派 4、400 用的):
cd ${RPI_STG}/kernel_rpi/linux
make mrproper
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- bcm2711_defconfig
如果你需要微调配置,依然可以用 menuconfig,记得带上架构参数:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig
最后,开火编译:
make -j8 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- all
这次生成的文件名字变了。在 ARM64 上,压缩内核镜像不叫 bzImage,叫 Image.gz,存放在 arch/arm64/boot/ 下。
$ ls -lh arch/arm64/boot/Image.gz
-rw-rw-r-- 1 c2kp c2kp 7.9M Jun 21 13:24 arch/arm64/boot/Image.gz
这就是你要拷贝到树莓派 SD 卡上的东西。当然,别忘了还要把编译出来的模块安装到某个目录,然后打包拷过去。
打包成 deb:更优雅的交付方式
如果你想把编译好的内核给另一台机器(或另一块板子)装上,最简单的方法不是手动拷贝文件,而是打包成 deb 包。
内核 Makefile 甚至贴心地提供了这个目标:
make -j8 ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- bindeb-pkg
这会在上一级目录生成好几个 .deb 文件:
linux-image-*.deb:内核镜像和模块。linux-headers-*.deb:头文件。linux-libc-dev_*.deb:用户空间库。
你只需要把这些文件拷贝到目标机器上,执行 sudo dpkg -i *.deb,就完成了安装。是不是很酷?
3.7 本章回响
我们这一章干了很多事。
表面上看,我们是在学习怎么敲 make、怎么改 GRUB 配置、怎么用交叉编译器。但实际上,我们是在建立一种关于「系统构建」的直觉。
你现在应该明白,一个操作系统并不是一块铁板。它是分层的:
- 最底层是Bootloader (GRUB),它是第一个被唤醒的守门人,负责把内核拉进内存。
- 然后是内核镜像,它是心脏,但它往往太笨重,需要把一部分功能(如文件系统驱动)剥离出去。
- 为了解决剥离带来的依赖问题,我们有了 initramfs,它是那个在真实世界挂起之前,先把场地铺平的临时工。
- 最后是内核模块,它们是按需加载的插件,让内核既保持精简,又具有扩展性。
还记得我们这一章开头说的那个直觉吗?——「从源码构建内核就是制造灵魂」。现在你可以补充一句:这个灵魂不仅要有躯干(内核镜像),还要有四肢(模块),以及一个助产士来确保它顺利降生。
下一章,我们将进入一个更微观的世界。我们将不再只是构建内核,而是开始向内核里注入代码。我们将学习如何编写 Linux 内核模块(LKM)。如果说这一章是学会了怎么造车,那么下一章,我们要学的就是怎么改装发动机。
练习题
练习 1:understanding
题目:在内核构建过程中,vmlinux、bzImage 和 vmlinuz 这三个文件在用途和状态上有何本质区别?如果在 x86 系统的 /boot 目录下只看到 vmlinuz 而没有 bzImage,这是否意味着构建失败?
答案与解析
答案:vmlinux 是未压缩的内核 ELF 可执行文件,包含调试符号,体积巨大,主要用于调试,不直接用于启动;bzImage 是 x86 架构特有的“大压缩内核镜像”(big zImage),经过压缩,是 Bootloader 实际加载并解压到内存执行的文件;vmlinuz 是与 vmlinux 对应的压缩版本名称('z' 代表压缩),通常就是 bzImage 或其复制/链接。在 /boot 目录下看到 vmlinuz 是正常的,这通常就是 bzImage 的副本或符号链接,仅从文件名差异不能判断构建失败。
解析:考察对核心产物名称的理解。vmlinux 是编译链接后的原始产物,虽然它是 ELF 格式但包含太多元数据和符号不适合直接引导。bzImage(boot zImage)是为了解决早期内存限制而设计的压缩引导镜像。vmlinuz 只是一个命名习惯(vmlinux + z),在大多数现代发行版中,/boot/vmlinuz-xxx 实际上就是从编译产物的 bzImage 复制过来的。因此,只要能生成 vmlinuz,通常意味着 bzImage 已成功生成。
练习 2:application
题目:假设你正在为一台嵌入式 ARM 设备交叉编译内核模块。为了避免覆盖宿主开发机的模块,你需要将模块安装到临时目录 /tmp/rootfs/lib/modules。请写出实现这一目标的 make 命令,并解释系统为何无法直接加载这些未安装在 /lib/modules 下的模块。
答案与解析
答案:命令:make INSTALL_MOD_PATH=/tmp/rootfs modules_install。解析:使用 INSTALL_MOD_PATH 环境变量可以指定模块安装的根路径,这样模块会被安装到 /tmp/rootfs/lib/modules/<kernel-version>/ 目录下。系统无法直接加载是因为模块加载工具(如 modprobe)会依据 /lib/modules/$(uname -r) 下的 modules.dep 等依赖文件来查找模块,且内核默认的安全机制(模块签名验证路径)也指向标准系统路径。
解析:考察 INSTALL_MOD_PATH 的应用。在交叉编译或系统构建中,我们通常不希望污染宿主环境。通过修改安装前缀,我们可以将产物打包到根文件系统镜像中。此外,这涉及到 depmod 的工作原理,它生成的索引文件必须与模块的实际加载路径匹配,否则即使手动指定 .ko 文件路径,也可能因依赖缺失而加载失败。
练习 3:thinking
题目:Linux 系统启动时,为什么需要 initramfs(Initial RAM Filesystem)?如果将磁盘驱动程序编译进内核(y)而不是作为模块(m),是否可以完全抛弃 initramfs?请从根文件系统挂载的“鸡生蛋”问题角度进行分析。
答案与解析
答案:initramfs 的存在是为了解决驱动依赖的“鸡生蛋”问题:内核必须加载磁盘驱动才能读取和挂载根文件系统,但如果驱动程序文件本身就存储在根文件系统的 /lib/modules 中,内核就无法读取它。initramfs 是一个在内存中的微型文件系统,Bootloader 将其加载到内存后,内核可以直接访问其中的驱动模块,从而挂载真正的根文件系统。即使将磁盘驱动编译进内核,可以简化引导过程,但在某些复杂场景(如 LVM 逻辑卷、加密根文件系统、网络挂载 NFS)下,仍然需要 initramfs 中的用户空间工具(如 cryptsetup、lvm)来辅助完成根文件系统的准备和切换(pivot_root),因此通常不能完全抛弃它。
解析:这是一个批判性思考题。核心在于理解内核空间与用户空间工具的协作。虽然将驱动编译进内核(y)确实能让内核在启动初期识别硬件,但在处理现代存储堆栈(如 RAID、LUKS 加密、复杂的设备映射)时,仅靠内核内部的静态代码往往不够,还需要用户空间的脚本来配置环境。initramfs 提供了一个极简的 Linux 环境,在切换根文件系统之前运行这些必要的准备工作。因此,除非是极其简单的单分区 ext4 启动盘,否则 initramfs 通常是必须的。
要点提炼
编译 Linux 内核的核心在于执行 make 命令,它会根据 .config 配置生成 vmlinux(含调试符号的未压缩镜像)、bzImage(x86 架构下实际用于启动的压缩镜像)以及 .ko 内核模块。由于内核代码量庞大(约 3000 万行),构建过程极度消耗 CPU 和内存,因此通常使用 make -j$(nproc) 进行并行编译以提升效率。此外,编译过程中可能会因缺少依赖库(如 libelf-dev)或配置冲突(如 CONFIG_SYSTEM_REVOCATION_KEYS 指向不存在文件)而报错,需根据提示调整环境或配置。
编译出的内核模块需要通过 sudo make modules_install 安装到系统的 /lib/modules/$(uname -r)/ 目录下,这样系统才能在运行时找到并加载它们。这一步不仅拷贝文件,还会通过 depmod 分析模块间的依赖关系并生成映射文件。值得注意的是,在进行交叉编译(例如在 x86 主机上为 ARM 设备编译)时,必须设置 INSTALL_MOD_PATH 变量指定安装根目录,否则会错误地覆盖宿主机的模块,导致系统崩溃。
为了让系统能够挂载真实的根文件系统,必须生成 initramfs(早期用户空间),它是解决内核启动时“鸡生蛋”依赖问题的关键。因为内核本身通常不包含文件系统驱动(如 f2fs、LUKS 加密驱动),而这些驱动文件恰恰存放在尚未挂载的硬盘上,initramfs 作为一个临时的内存文件系统,负责在启动初期加载必要的驱动和执行 pivot_root 切换,最终将控制权交给真实的磁盘系统。
引导新内核需要将编译好的镜像和 initramfs 拷贝至 /boot 目录,并更新 GRUB 配置(通常通过 sudo make install 自动完成)。为了防止新内核无法启动导致系统“变砖”,建议修改 /etc/default/grub 将 GRUB_TIMEOUT_STYLE 设为 menu 以强制显示开机选单,并善用 GRUB_DEFAULT 指定默认启动项。重启后,不仅可以通过 uname -r 查看版本,还能利用 /proc/config.gz 验证特定内核参数(如 CONFIG_HZ)是否按预期生效。
当目标设备性能较弱(如树莓派)时,应在高性能主机上进行交叉编译。这需要安装对应的交叉工具链(如 gcc-aarch64-linux-gnu),并在 make 命令中显式指定 ARCH(如 arm64)和 CROSS_COMPILE 变量。编译完成后,使用 bindeb-pkg 目标可以将内核和模块打包成 .deb 文件,这是一种比手动拷贝文件更优雅、更便于在目标设备上部署和管理的交付方式。