第三章 内核编译与构建:从源码到可启动镜像
内核编译这件事,说简单也简单,说复杂也复杂。我见过不少工程师,配好 .config 之后直接 make,然后就去喝咖啡了。结果回来发现编译失败,或者编译出来的内核根本不能启动。嗯,这里面的门道,咱们得好好捋一捋。
3.1 交叉编译工具链配置
嵌入式开发,第一关就是交叉编译。说白了,就是在你的 x86 电脑上,编译出能在 ARM、MIPS 或 RISC-V 上运行的代码。这可不是装个 gcc 就完事的。
3.1.1 工具链的选择
我个人习惯用 Linaro 或 ARM 官方提供的预编译工具链。为什么?省事。自己从源码编译工具链?我曾经干过一次,花了整整两天,最后还踩了一堆坑。除非你有特殊需求,否则别折腾。
| 工具链类型 | 适用场景 | 获取方式 |
|---|---|---|
| Linaro GCC | ARM 32/64 位通用 | linaro.org 下载 |
| ARM GCC | ARM Cortex 系列 | developer.arm.com |
| Buildroot | 定制化根文件系统 | buildroot.org |
| Yocto | 企业级嵌入式 Linux | yoctoproject.org |
3.1.2 环境变量设置
工具链下载后,解压到某个目录,然后设置环境变量。我建议你写个脚本,别每次都手动 export。
# 设置交叉编译工具链路径
export CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH=arm
export PATH=$PATH:/opt/gcc-arm-9.2-2019.12-x86_64-arm-linux-gnueabihf/bin
# 验证工具链是否可用
arm-linux-gnueabihf-gcc --version
3.2 内核编译流程
内核编译,本质上就是三步:配置、编译、安装。但每一步都有讲究。你想想看,一个几千万行代码的项目,编译一次少则几分钟,多则几小时。要是中间出错了,那真是欲哭无泪。
3.2.1 配置内核
配置内核的方式有很多种,我列一下常用的:
- make menuconfig:基于 ncurses 的文本界面,最常用
- make xconfig:基于 Qt 的图形界面,更直观
- make defconfig:使用默认配置,适合快速测试
- make oldconfig:基于现有配置更新,适合升级内核版本
我个人偏爱 menuconfig。为什么?因为它轻量,在服务器上也能用,不需要图形桌面环境。
3.2.2 编译目标详解
编译内核时,你会看到 vmlinux、zImage、uImage 这些名词。它们到底是什么关系?
核心概念:vmlinux 是未压缩的 ELF 格式内核,zImage 是压缩后的内核镜像,uImage 是加了 U-Boot 头部的 zImage。
我画了一张图,帮你理清它们的关系:
3.2.3 编译命令实战
下面是一个完整的编译流程。我建议你第一次编译时,用 make -j4 这样的并行编译,但别开太多线程,否则容易把机器搞死。
# 清理之前的编译产物
make clean
# 配置内核(假设你已经有了 .config)
make menuconfig
# 编译内核镜像
make -j$(nproc) zImage
# 编译设备树
make -j$(nproc) dtbs
# 编译内核模块
make -j$(nproc) modules
# 生成 uImage(如果需要)
make -j$(nproc) uImage
make -j$(nproc) 可以自动使用所有 CPU 核心。但如果你在虚拟机里,建议减半,比如 make -j2。
3.3 模块编译与安装
内核模块,说白了就是可以动态加载的驱动程序。你想想看,如果每次改个驱动都要重新编译整个内核,那得多浪费时间?模块化就是为了解决这个问题。
3.3.1 模块编译
模块编译有两种方式:一种是随内核一起编译,另一种是单独编译。我建议你开发阶段用单独编译,这样快。
# 单独编译某个模块
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
# 编译并安装所有模块
make modules_install INSTALL_MOD_PATH=/path/to/rootfs
3.3.2 模块安装与依赖
模块安装后,需要更新依赖关系。我曾经遇到过模块加载失败,查了半天,结果是忘了跑 depmod。
# 安装模块到根文件系统
make modules_install INSTALL_MOD_PATH=/home/user/rootfs
# 更新模块依赖
depmod -a -b /home/user/rootfs
# 手动加载模块
insmod /lib/modules/5.10.0/kernel/drivers/mydriver.ko
# 自动加载模块(推荐)
modprobe mydriver
3.4 编译优化选项
编译优化,说白了就是在编译时间和运行性能之间找平衡。我见过有人为了追求极致性能,开了 -O3,结果编译出来的内核比 -O2 还慢。为什么会这样?因为 -O3 会做更多激进的优化,有时候反而会引入问题。
3.4.1 常用优化选项
| 优化级别 | 说明 | 适用场景 |
|---|---|---|
| -O0 | 不优化,编译最快 | 调试阶段 |
| -O1 | 基本优化 | 快速测试 |
| -O2 | 标准优化(内核默认) | 生产环境 |
| -Os | 优化代码大小 | 嵌入式设备 |
| -O3 | 激进优化 | 特定场景(慎用) |
3.4.2 内核特有的优化配置
除了 GCC 的优化选项,内核本身也有一些编译优化相关的配置。我挑几个重要的说说:
- CONFIG_CC_OPTIMIZE_FOR_SIZE:相当于 -Os,适合 Flash 小的设备
- CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE:相当于 -O2,默认选项
- CONFIG_TUNE_CPU:针对特定 CPU 优化,比如 Cortex-A7
- CONFIG_LTO:链接时优化,能进一步减小内核体积
我的建议:对于大多数嵌入式设备,用 -Os 就够了。别为了省那几百 KB 空间去开 LTO,调试起来太痛苦了。
3.4.3 调试与优化的平衡
调试信息和优化是矛盾的。你想想看,开了 -O2 之后,代码顺序都变了,单步调试时跳来跳去,根本看不懂。所以我的习惯是:
- 开发阶段:关优化,开调试信息(CONFIG_DEBUG_INFO)
- 测试阶段:开 -O2,保留部分调试信息
- 发布阶段:开 -Os,去掉调试信息
# 调试版内核配置
make menuconfig
# Kernel hacking -> Compile-time checks and compiler options
# -> Debug information (CONFIG_DEBUG_INFO) [Y]
# -> Optimize for performance (CONFIG_CC_OPTIMIZE_FOR_PERFORMANCE) [N]
# -> Optimize for size (CONFIG_CC_OPTIMIZE_FOR_SIZE) [N]
嗯,说到调试,我不得不提一个坑。有一次我在调试一个 USB 驱动,开了 -O2 之后,某些变量被优化掉了,gdb 里根本看不到。后来我加了 volatile 关键字才解决。所以,调试阶段千万别开高级优化。
好了,内核编译这块的内容就这些。记住,编译内核不是一锤子买卖,多试几次,找到最适合你项目的配置组合,才是正道。