2. 嵌入式Linux内核编译与启动:内核源码获取、配置与编译、设备树基础、内核启动流程分析

好,咱们进入第二章。这一章是BSP开发的硬核基础——你得亲手把内核从源码变成能跑的二进制,还得搞明白它上电后是怎么活过来的。我见过不少工程师,设备树改错了、启动参数配漏了,结果板子死活起不来,最后查半天发现是基本功不扎实。咱们今天就把这些坑填平。

2.1 内核源码获取与版本管理

先聊源码。你从哪儿拿?我个人习惯直接从 kernel.org 拉LTS版本,或者用芯片厂商提供的BSP包。别用太新的主线内核做产品,除非你想天天追着Linus的补丁跑。

我的建议: 产品开发选LTS内核,比如5.10.y或6.1.y。厂商BSP通常基于某个LTS版本打补丁,兼容性最好。

获取方式很简单:

# 从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 负责:

  1. 初始化硬件(时钟、DDR、串口)
  2. 从存储介质加载内核镜像和设备树到内存
  3. 设置启动参数(通过设备树的 chosen 节点或ATAG)
  4. 跳转到内核入口点

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 本章知识体系

下面这张图总结了内核编译与启动的完整链路,你对照着看,思路会更清晰:

嵌入式Linux内核编译与启动流程 嵌入式Linux内核编译与启动流程 源码获取 内核配置 内核编译 设备树编写 设备树编译 生成dtb Bootloader 内核解压 start_kernel init进程启动 用户空间运行 💡 关键:每个阶段都有对应的调试手段。Bootloader看串口打印,内核解压看CONFIG_DEBUG_LL,start_kernel看early printk。 用户空间看init日志。逐层排查,没有解决不了的问题。

2.7 常见问题与避坑指南

问题1:内核编译报错 "No rule to make target"
原因通常是配置依赖没选全。比如你选了某个驱动,但它依赖的子系统没开。解决办法:make olddefconfig 自动补全依赖,或者用 make menuconfig 搜索依赖项。
问题2:板子启动后卡在 "Starting kernel ..."
我曾经被这个问题折磨了两天。后来发现是设备树里内存地址写错了,内核解压后直接跑飞。检查 reg = <0x80000000 0x10000000> 是否和硬件实际内存匹配。
问题3:设备树节点没生效
检查 status = "okay" 是否设置。很多外设在 dtsi 里默认是 disabled,你必须在板级 dts 里显式启用。另外,别忘了检查引脚复用配置(pinctrl)是否正确。

好了,这一章的内容就这些。内核编译和启动是BSP开发的基本功,你动手做一遍,比看十遍文档都管用。拿起你的板子,从拉源码开始,一步步走到用户空间。遇到问题别慌,看日志、查配置、问社区——这些都是我当年踩坑踩出来的经验。