4. 内核启动流程分析(U-Boot阶段)
好,咱们进入第四章。这一章我打算聊聊U-Boot——这个在嵌入式Linux世界里几乎绕不开的Bootloader。说实话,我最早接触U-Boot是在一个思科交换机项目上,当时看着启动日志里一串串的"U-Boot 2010.03"字样,心里还挺发怵的。后来啃了几个月源码,才慢慢摸清它的脾气。
U-Boot的全称是"Universal Boot Loader",它负责把内核从Flash或网络加载到内存,然后跳转执行。在思科固件里,U-Boot通常分两个阶段:SPL(Secondary Program Loader)和U-Boot proper。为什么要分两段?说白了,就是芯片上电后SRAM太小,装不下完整的U-Boot,只能先跑一个小巧的SPL,初始化DDR,再把完整的U-Boot加载进来。
4.1 U-Boot源码结构
先看看U-Boot的源码长什么样。我习惯把U-Boot源码目录比作一个工具箱,每个文件夹负责一类活儿。
| 目录 | 作用 | 我关注的点 |
|---|---|---|
| arch/ | 架构相关代码(ARM、MIPS、x86等) | start.S就在这里,CPU初始化核心 |
| board/ | 板级支持包,每个板子一个文件夹 | 思科经常魔改这里,加自己的硬件初始化 |
| common/ | 通用功能,如命令行、环境变量 | cli_hush.c是命令行解析器,我改过它 |
| drivers/ | 各种外设驱动(网卡、Flash、串口) | 网卡驱动最常出问题,尤其是PHY |
| include/ | 头文件和配置文件 | configs/下的头文件定义了板级参数 |
| lib/ | 库函数,如CRC、压缩解压 | zlib、lzma解压内核时会用到 |
| net/ | 网络协议栈(TFTP、NFS等) | 思科常用TFTP升级固件 |
我个人习惯,拿到一个思科固件后,先看board/和include/configs/这两个目录。为什么?因为板级初始化代码和配置宏都藏在这里。思科经常会在board/里塞一些私有初始化函数,比如初始化交换芯片、设置MAC地址等。你想想看,如果直接跳过去看通用代码,很容易漏掉关键信息。
核心要点:U-Boot源码的核心入口是arch/arm/cpu/armv7/start.S(ARM架构为例)。SPL和U-Boot proper共用同一份start.S,通过编译宏CONFIG_SPL_BUILD来区分行为。
4.2 启动流程:SPL → U-Boot proper
启动流程我画了一张图,你先看看整体脉络。
这张图把流程分成了四个阶段。我一个个拆开讲。
4.2.1 ROM Code阶段
芯片上电后,CPU会从固定的地址(比如0x00000000或0xFFFF0000)开始执行。这个地址里放的是芯片厂商固化的ROM Code。ROM Code会做最基本的初始化:设置栈指针、初始化时钟、检测启动介质(NAND、SD卡、SPI Flash等)。然后它会从启动介质的前几个扇区加载SPL到内部SRAM。
嗯,这里要注意:ROM Code是只读的,你改不了。思科能定制的是SPL和U-Boot proper。我曾经在一个项目里发现,某款思科交换机用的Marvell芯片,ROM Code会先检查GPIO电平来决定启动顺序。这个信息在数据手册里根本找不到,我是靠逻辑分析仪抓出来的。
4.2.2 SPL阶段
SPL的代码量很小,通常控制在4KB到32KB之间。它要完成三件事:
- 初始化DDR控制器——这是最关键的。没有DDR,你连U-Boot proper都放不下。
- 初始化串口——方便输出调试信息。我习惯在SPL里加一句打印,确认它跑起来了。
- 从Flash加载U-Boot proper到DDR——然后跳转过去。
SPL的入口是start.S,它会调用board_init_f()。这个函数在SPL阶段只做最基本的初始化。加载U-Boot proper时,SPL会读取Flash上的U-Boot镜像,校验CRC,然后复制到DDR的指定地址。跳转前,SPL会设置好栈指针和参数寄存器。
调试技巧:如果SPL阶段卡住了,多半是DDR初始化失败。我建议你在SPL里加一个LED闪烁函数,用肉眼观察执行到了哪一步。比串口打印更可靠,因为串口驱动可能还没初始化好。
4.2.3 U-Boot proper阶段
U-Boot proper的代码量就大多了,几十KB到几百KB不等。它的启动流程可以概括为:
- board_init_f()——初始化大部分外设:网卡、Flash、I2C、GPIO等。这个函数会调用一系列初始化函数,按优先级排列。
- relocate_code()——把U-Boot自身从Flash或低端DDR复制到高端DDR地址。为什么要重定位?因为低端地址要留给内核用。
- board_init_r()——进入主循环。初始化环境变量、启动命令行、等待用户输入。如果用户没干预,就执行bootcmd。
我遇到过最头疼的问题,是重定位后U-Boot跑飞了。后来发现是链接脚本里定义的基地址和实际加载地址不一致。思科有时候会修改board/下的链接脚本,你得仔细核对CONFIG_SYS_TEXT_BASE这个宏。
4.3 环境变量与bootcmd解析
环境变量是U-Boot的灵魂。说白了,它就是一组键值对,存储在Flash或EEPROM里。U-Boot启动时会从存储介质加载环境变量到内存,然后解析执行。
常用的环境变量有:
| 变量名 | 作用 | 典型值 |
|---|---|---|
| bootcmd | 自动启动时执行的命令 | run netboot; run usbboot; run mmcboot |
| bootargs | 传递给内核的命令行参数 | console=ttyS0,115200 root=/dev/mtdblock2 |
| ipaddr | 设备IP地址 | 192.168.1.100 |
| serverip | TFTP服务器IP | 192.168.1.1 |
| loadaddr | 内核加载地址 | 0x80008000 |
| fdtaddr | 设备树加载地址 | 0x88000000 |
bootcmd的解析流程是这样的:
- U-Boot启动后,先检查用户是否在倒计时内按了任意键。如果按了,进入命令行交互模式。
- 如果没按,U-Boot读取环境变量
bootdelay,等待指定秒数。 - 然后执行
bootcmd的内容。bootcmd可以是一串命令,用分号隔开。比如:tftp 0x80008000 zImage; tftp 0x88000000 dtb; bootz 0x80008000 - 0x88000000 - U-Boot会逐条解析执行这些命令。如果某条命令失败,默认会停止执行后续命令。
避坑指南:我曾经在分析一台思科路由器时,发现bootcmd里写的是run flashboot,但flashboot这个变量根本没定义。结果U-Boot一直卡在"Unknown command"提示上。后来我查了环境变量存储区,发现是Flash的某个坏块导致变量读取不全。所以,如果你遇到U-Boot启动后不加载内核,先检查环境变量是否完整。
环境变量的存储格式也值得一说。U-Boot用CRC32校验环境变量的完整性。存储结构是:
struct env_image_single {
uint32_t crc; // 整个环境变量的CRC32校验值
unsigned char data[]; // 环境变量数据,格式为"key=value\0"的拼接
};
解析时,U-Boot先读取CRC,然后计算实际数据的CRC,两者比对。如果不一致,就使用默认环境变量。思科有时候会故意修改默认环境变量,把一些调试功能藏起来。比如,我见过一台设备,默认环境变量里没有bootcmd,只有altbootcmd。你得在命令行里手动输入run altbootcmd才能启动。
最后提一句,bootargs这个变量特别重要。它决定了内核怎么挂载根文件系统。思科设备通常用mtdblock或ubi作为根文件系统。如果你在逆向分析时想进入单用户模式,可以在bootargs里加single或init=/bin/sh。嗯,这个技巧我屡试不爽。
好了,U-Boot阶段就聊这么多。下一章我们会进入内核启动的下一站——内核解压与初始化。到时候再细聊。
公众号:蓝海资料掘金营,微信deep3321