要深入理解 U-Boot 加载内核和文件系统的代码实现,需要从命令解析→设备交互→镜像处理→内核跳转的全链路展开,结合具体函数调用和数据流转细节。以下基于 U-Boot 2023.07 版本,以 ARM 架构 + eMMC 存储为例,
一、命令解析:从bootcmd
到具体操作
U-Boot 的启动逻辑由main_loop
触发,位于common/main.c
:
// common/main.c
void main_loop(void)
{
// 环境变量初始化(从Flash读取或使用默认值)
env_relocate();
// 处理自动启动倒计时
if (bootdelay >= 0 && !abortboot(bootdelay)) {
// 执行bootcmd环境变量定义的命令序列
run_command_list(getenv("bootcmd"), -1, 0);
}
// 进入命令行交互(若未自动启动)
cli_loop();
}
- abortboot函数:在倒计时期间检测按键输入,若有按键则中断自动启动,进入命令行。
- run_command_list:将bootcmd字符串(如mmc dev 0; mmc read 0x80800000 0x200 0x1000; bootz 0x80800000 - 0x81000000)按分号拆分,逐个调用run_command执行。
关键代码片段解析
以下是 run_command_list
的核心实现逻辑(简化版):
int run_command_list(const char *cmd, int len, int flag)
{
const char *p = cmd;
int last_ret = 0;
int i;
if (len < 0)
len = strlen(cmd); // 自动计算长度
while (p < cmd + len) {
// 1. 查找命令分隔符(; 或 \n)
const char *q = p;
while (q < cmd + len && *q != ';' && *q != '\n')
q++;
// 2. 提取单条命令(p 到 q 之间的字符串)
int cmd_len = q - p;
char *buf = malloc(cmd_len + 1);
if (!buf)
return -1;
memcpy(buf, p, cmd_len);
buf[cmd_len] = '\0'; // 字符串结尾
// 3. 跳过命令前后的空格
char *cmd_str = buf;
while (*cmd_str && isspace(*cmd_str))
cmd_str++;
// 4. 执行单条命令(非空才执行)
if (*cmd_str) {
last_ret = run_command(cmd_str, flag); // 调用单命令执行函数
// 若为 bootd 模式且命令失败,停止执行
if (flag & CMD_FLAG_BOOTD && last_ret != 0) {
free(buf);
return last_ret;
}
}
// 5. 移动到下一条命令
free(buf);
p = q + 1; // 跳过分隔符
}
return last_ret; // 返回最后一条命令的执行结果
}
与其他函数的关联
-
run_command
run_command_list
依赖run_command
执行单条命令。run_command
的功能是:- 解析单条命令(如
mmc read 0x80800000 0x200 0x1000
); - 查找对应的命令处理函数(如
do_mmc
); - 传递参数并执行,返回执行结果。
- 解析单条命令(如
-
autoboot_command
在自动启动流程中,autoboot_command
调用run_command_list
执行bootcmd
环境变量中的命令序列,例如:void autoboot_command(void) { char *bootcmd = getenv("bootcmd"); if (bootcmd) run_command_list(bootcmd, -1, 0); // 执行bootcmd命令序列 }
典型应用场景
-
自动启动(
bootcmd
)
最常见的场景是执行bootcmd
中的命令序列,例如:bootcmd=mmc dev 0; mmc read 0x80800000 0x200 0x1000; bootz 0x80800000
run_command_list
会按分号拆分并依次执行mmc dev
、mmc read
、bootz
命令。 -
脚本执行
U-Boot 支持执行脚本文件(如.scr
格式),脚本中的多条命令通过run_command_list
逐条执行。 -
命令行批量输入
在 U-Boot 命令行中,用户可输入多条命令(用分号分隔),例如:=> setenv bootargs console=ttyS0,115200; saveenv; reset
这些命令会被
run_command_list
拆分并执行。
二、从 MMC 设备读取内核镜像
以mmc read
命令为例,代码位于cmd/mmc.c
,核心是调用 MMC 驱动读取数据:
// cmd/mmc.c
int do_mmc_read(cmd_tbl_t *cmdtp, int flag, int argc, char *const argv[])
{
struct mmc *mmc;
ulong addr, blk, cnt;
int dev, ret;
// 解析参数:mmc read <内存地址> <起始块> <块数>
dev = (argc >= 2) ? simple_strtoul(argv[1], NULL, 10) : 0;
addr = simple_strtoul(argv[2], NULL, 16); // 内核加载地址(如0x80800000)
blk = simple_strtoul(argv[3], NULL, 10); // 内核在MMC中的起始块
cnt = simple_strtoul(argv[4], NULL, 10); // 读取块数
mmc = find_mmc_device(dev); // 获取MMC设备句柄
if (!mmc) {
printf("MMC device %d not found\n", dev);
return 1;
}
// 调用MMC驱动读取数据到内存
ret = mmc->read(mmc, blk, cnt, (void *)addr);
if (ret) {
printf("MMC read failed: %d\n", ret);
return 1;
}
printf("Read %lu blocks from %d:%lu to 0x%lx\n", cnt, dev, blk, addr);
return 0;
}
- 底层驱动:
mmc->read
最终调用drivers/mmc/mmc.c
的mmc_bread
,通过 SD/eMMC 协议与硬件交互:// drivers/mmc/mmc.c int mmc_bread(struct mmc *mmc, lbaint_t start, lbaint_t blkcnt, void *buffer) { struct mmc_cmd cmd; struct mmc_data data; int ret; // 初始化MMC命令(读块操作) cmd.cmdidx = MMC_CMD_READ_SINGLE_BLOCK; // 单块读命令 cmd.cmdarg = start; // 起始块地址 cmd.resp_type = MMC_RSP_R1; data.src = buffer; // 内存缓冲区(内核将被读到这里) data.blocks = blkcnt; // 块数量 data.blocksize = mmc->read_bl_len; // 块大小(通常512字节) data.flags = MMC_DATA_READ; ret = mmc_send_cmd(mmc, &cmd, &data); // 发送命令到MMC控制器 return ret; }
三、解析内核镜像并验证
bootz
命令(用于 zImage)在加载内核后需验证镜像有效性,代码位于cmd/bootm.c
:
// cmd/bootm.c
int do_bootz(cmd_tbl_t *cmdtp, int flag, int argc, char *const argv[])
{
ulong kernel_addr, initrd_addr, initrd_end, fdt_addr;
int ret;
// 解析参数:bootz <内核地址> [initrd地址] [DTB地址]
ret = parse_bootz_args(argc, argv, &kernel_addr, &initrd_addr,
&initrd_end, &fdt_addr);
if (ret)
return ret;
// 验证zImage头部(魔数0x016f2818)
if (*(uint32_t *)kernel_addr != ZIMAGE_MAGIC) {
printf("Invalid zImage magic: 0x%08x\n", *(uint32_t *)kernel_addr);
return 1;
}
// 准备启动环境(如设置DTB参数)
ret = boot_prep(kernel_addr, initrd_addr, initrd_end, fdt_addr);
if (ret)
return ret;
// 执行内核启动
return do_bootz_exec(kernel_addr, initrd_addr, initrd_end, fdt_addr);
}
四、设备树(DTB)处理与参数传递
设备树是内核硬件配置的关键,U-Boot 需加载并传递其地址:
// cmd/bootm.c 中处理DTB
static int boot_prep(ulong kernel_addr, ulong initrd_addr,
ulong initrd_end, ulong fdt_addr)
{
if (fdt_addr) {
// 验证DTB头部(魔数0xd00dfeed)
if (fdt_check_header((void *)fdt_addr) != 0) {
printf("Invalid DTB at 0x%lx\n", fdt_addr);
return 1;
}
// 向DTB注入bootargs参数
char *bootargs = getenv("bootargs");
if (bootargs && fdt_setprop_string((void *)fdt_addr, 0,
"bootargs", bootargs)) {
printf("Failed to set bootargs in DTB\n");
return 1;
}
}
return 0;
}
五、最终跳转:移交控制权给内核
跳转逻辑由汇编实现(ARM 架构位于arch/arm/lib/bootm.c
),U-Boot 最终通过汇编指令跳转到内核在内存中的入口地址,严格遵循 ARM 架构的启动规范(如寄存器传递参数):
// arch/arm/lib/bootm.S
ENTRY(do_bootz_exec)
// 栈中参数布局:[0]内核地址, [4]initrd_end, [8]initrd_start, [12]fdt_addr
stmfd sp!, {r4-r11, lr} // 保存寄存器现场
// 1. 准备传递给内核的参数(ARMv7规范)
ldr r2, [sp, #12 + 4*8] // r2 = DTB地址(内核从这里获取硬件信息)
mov r1, #0 // r1 = 机器码(设备树模式下为0)
mov r0, #0 // r0 = 0(规范要求)
// 2. 禁用中断和MMU(确保内核启动环境干净)
mrs r3, cpsr // 读取当前程序状态寄存器
orr r3, r3, #0xc0 // 禁止IRQ和FIQ中断(bit7和bit6置1)
msr cpsr_c, r3 // 写入状态寄存器
// 3. invalidate缓存(避免脏数据影响)
mov r3, #0
mcr p15, 0, r3, c7, c5, 0 // invalidate I-cache
mcr p15, 0, r3, c7, c6, 0 // invalidate D-cache
// 4. 跳转到内核入口(zImage的解压函数)
ldr pc, [sp, #0 + 4*8] // pc = 内核地址(0x80800000),完成跳转
// 理论上不会返回,若返回则挂起
mov r0, #0
bl hang
ENDPROC(do_bootz_exec)
关键细节:
内核入口地址(如0x80800000)是zImage的起始地址,其内部包含解压逻辑,会将自身解压到更高的内存地址后执行真正的内核初始化。
六、文件系统挂载的间接参与
U-Boot 不直接参与文件系统加载,但通过以下方式为内核提供必要信息:
-
bootargs
环境变量:
定义在include/configs/xxx.h
(板级配置文件)中:#define CONFIG_BOOTARGS \ "console=ttyS0,115200 " \ "root=/dev/mmcblk0p2 " \ // 根文件系统位于eMMC第2分区 "rootfstype=ext4 " \ // 文件系统类型为ext4 "rw" // 读写模式挂载
内核启动后,解析
bootargs
中的root
参数,找到文件系统位置。 -
设备树中的存储设备节点:
U-Boot 加载的 DTB 中包含 eMMC 控制器和分区信息,例如:dts
mmc@f8007000 { // eMMC控制器节点 compatible = "arm,pl180"; reg = <0xf8007000 0x1000>; ... partitions { compatible = "fixed-partitions"; partition@0 { label = "boot"; reg = <0x0 0x200000>; }; partition@200000 { label = "rootfs"; reg = <0x200000 0x10000000>; }; }; };
内核通过该节点识别 eMMC 设备及分区,从而挂载
rootfs
分区。
全流程调用链总结
main_loop() → run_command_list("bootcmd") →
do_mmc_read() → mmc_read_blocks() → pl180_send_cmd_read() 【读取内核到内存】→
do_bootz() → bootz_verify_image() → boot_prepare_env() →
do_bootz_exec() 【汇编跳转至内核】→
内核解析DTB和bootargs → 挂载文件系统
每个环节都与硬件平台强相关(如存储设备驱动、架构特定的汇编跳转),但核心思想是 **“硬件初始化→加载镜像→传递参数→移交控制权”**。实际调试时,可通过 U-Boot 的printenv
、md
(内存查看)等命令验证各阶段是否正确执行。