2. 嵌入式Linux内核编译与启动:内核源码获取、配置与编译、设备树基础、内核启动流程分析
好,咱们进入第二章。这一章是BSP开发的硬核基础——你得亲手把内核从源码变成能跑的二进制,还得搞明白它上电后是怎么活过来的。我见过不少工程师,设备树改错了、启动参数配漏了,结果板子死活起不来,最后查半天发现是基本功不扎实。咱们今天就把这些坑填平。
2.1 内核源码获取与版本管理
先聊源码。你从哪儿拿?我个人习惯直接从 kernel.org 拉LTS版本,或者用芯片厂商提供的BSP包。别用太新的主线内核做产品,除非你想天天追着Linus的补丁跑。
获取方式很简单:
# 从kernel.org拉取LTS 5.10
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.200.tar.xz
tar -xf linux-5.10.200.tar.xz
# 或者用git管理,方便回退
git clone --depth 1 --branch v5.10 git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
版本管理上,我吃过亏。有一次项目组有人直接改了源码目录里的文件,没做版本标记,结果三个月后谁也说不清改了什么。后来我强制要求:所有内核修改必须基于git,每次编译前打tag。你想想看,出了问题能快速二分查找,多省心。
2.2 内核配置:从零到能跑
配置内核是门手艺活。说白了,就是告诉内核:你要支持哪些硬件、哪些功能、哪些驱动。配置不对,板子要么起不来,要么跑得慢。
2.2.1 配置方法对比
| 方法 | 命令 | 适用场景 |
|---|---|---|
| 默认配置 | make defconfig |
快速生成通用配置,通常需要二次裁剪 |
| 厂商配置 | make <board>_defconfig |
芯片厂商提供的板级配置,最省事 |
| 菜单配置 | make menuconfig |
交互式,逐项选择,适合精细调优 |
| 保存/加载 | make savedefconfig |
生成最小化配置文件,便于版本管理 |
我一般这么干:先用厂商的 defconfig 打个底,再用 menuconfig 微调。调完后执行 make savedefconfig,把精简后的配置提交到git。这样配置文件只有几百行,而不是上万行。
2.2.2 关键配置项
嗯,这里要注意几个必选项,少了任何一个板子都起不来:
- CONFIG_OF:设备树支持,必须开启
- CONFIG_CMDLINE:内核启动参数,比如指定根文件系统位置
- CONFIG_DEVTMPFS:设备节点自动创建,现代系统必备
- CONFIG_BLK_DEV_INITRD:支持initramfs,用于早期用户空间
CONFIG_DEVTMPFS_MOUNT,结果系统启动后 /dev 目录是空的,所有设备都找不到。查了整整半天才发现是少挂了一个文件系统。所以,配置完一定要检查这个。
2.3 内核编译:让代码变成二进制
配置搞定,接下来编译。命令不多,但参数有讲究。
# 设置交叉编译工具链
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
# 编译内核镜像
make -j$(nproc) zImage
# 编译设备树
make -j$(nproc) dtbs
# 编译内核模块
make -j$(nproc) modules
# 安装模块到目标根文件系统
make modules_install INSTALL_MOD_PATH=/path/to/rootfs
-j$(nproc) 是并行编译,能充分利用多核CPU。我第一次编译内核时不知道这个参数,单线程跑了快一个小时。后来加了 -j8,十分钟搞定。
编译产物主要有三个:
- zImage:压缩后的内核镜像,通常放在
arch/arm/boot/下 - *.dtb:编译好的设备树二进制文件,在
arch/arm/boot/dts/下 - *.ko:内核模块,分散在各个子目录中
2.4 设备树基础:硬件描述语言
设备树(Device Tree)是嵌入式Linux的硬件描述语言。说白了,就是用文本文件告诉内核:你的板子上有哪些外设、它们连在哪个地址上、用哪个中断号。
2.4.1 设备树文件结构
一个典型的设备树节点长这样:
/dts-v1/;
#include "imx6ul.dtsi"
/ {
model = "MyBoard";
compatible = "mycompany,imx6ul-board";
chosen {
stdout-path = &uart1;
};
memory {
device_type = "memory";
reg = <0x80000000 0x10000000>; // 256MB RAM
};
&uart1 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
};
};
这里有几个关键概念:
- dtsi:芯片级设备树头文件,定义SoC内部外设
- dts:板级设备树,包含具体板子的硬件信息
- chosen:传递启动参数给内核
- memory:描述内存布局
- status:控制外设是否启用,
okay表示启用,disabled表示禁用
2.4.2 设备树编译与反编译
# 编译dts为dtb
dtc -I dts -O dtb -o myboard.dtb myboard.dts
# 反编译dtb为dts(调试用)
dtc -I dtb -O dts -o myboard.dts myboard.dtb
dtc 反编译看看dtb内容对不对。我遇到过编译时路径写错,结果dtb里全是零的情况。反编译一看,立马发现问题。
2.5 内核启动流程分析
内核启动流程,说白了就是一段从汇编到C、从单核到多核、从裸机到操作系统的进化史。我把它分成四个阶段:
2.5.1 第一阶段:引导加载程序(Bootloader)
U-Boot 或别的 bootloader 负责:
- 初始化硬件(时钟、DDR、串口)
- 从存储介质加载内核镜像和设备树到内存
- 设置启动参数(通过设备树的
chosen节点或ATAG) - 跳转到内核入口点
2.5.2 第二阶段:内核解压与早期初始化
内核入口是 stext(ARM架构),用汇编完成:
- 设置页表,开启MMU
- 解压内核(如果是zImage)
- 跳转到
start_kernel(C语言入口)
2.5.3 第三阶段:核心初始化
start_kernel 是C语言的起点,它干的事可多了:
- 初始化调度器、内存管理、中断系统
- 解析设备树,创建平台设备
- 调用各个子系统的初始化函数
- 创建并启动
init进程(PID=1)
2.5.4 第四阶段:用户空间启动
init 进程启动后,根据启动参数挂载根文件系统,执行 /sbin/init 或 /init。至此,系统进入用户空间,你终于能看到登录提示符了。
Kernel panic 或 Unable to handle kernel NULL pointer dereference 通常能直接告诉你问题在哪。我调试时最常用的命令就是 dmesg | grep -i error。
2.6 本章知识体系
下面这张图总结了内核编译与启动的完整链路,你对照着看,思路会更清晰:
2.7 常见问题与避坑指南
原因通常是配置依赖没选全。比如你选了某个驱动,但它依赖的子系统没开。解决办法:
make olddefconfig 自动补全依赖,或者用 make menuconfig 搜索依赖项。
我曾经被这个问题折磨了两天。后来发现是设备树里内存地址写错了,内核解压后直接跑飞。检查
reg = <0x80000000 0x10000000> 是否和硬件实际内存匹配。
检查
status = "okay" 是否设置。很多外设在 dtsi 里默认是 disabled,你必须在板级 dts 里显式启用。另外,别忘了检查引脚复用配置(pinctrl)是否正确。
好了,这一章的内容就这些。内核编译和启动是BSP开发的基本功,你动手做一遍,比看十遍文档都管用。拿起你的板子,从拉源码开始,一步步走到用户空间。遇到问题别慌,看日志、查配置、问社区——这些都是我当年踩坑踩出来的经验。