温州市网站建设_网站建设公司_数据统计_seo优化
2025/12/28 8:00:43 网站建设 项目流程

大家好,我是嵌入式兔哥。 🐰

最近有不少兄弟在准备大厂的嵌入式驱动面试,U-Boot 启动流程绝对是必问的“八股文”之首。但很多人只背得下来“第一阶段汇编、第二阶段C语言”,一被问到“重定位时 .rel.dyn 段是怎么修正的?”或者“内核启动前 r0/r1/r2 寄存器里存了什么?”就哑火了。

今天不整虚的,我基于 NXP i.MX6ULL 的源码分析,整理了四张核心调用栈视图。我们就顺着这四张图,把 U-Boot 从上电复位到移交控制权给 Linux 的全过程扒得干干净净。


🛠️ 第一阶段:汇编入口与硬件基础 (Reset & Low Level)

这一阶段的核心任务就一个字:“活”。CPU 刚上电,不仅 Cache 没开,连栈都没有,C 代码根本跑不起来。我们需要用汇编语言构建最基础的运行环境。

1. 入口与模式切换

一切的起点在u-boot.lds链接脚本指定的ENTRY(_start)。代码被加载到.text段地址(通常是0X87800000)。

  • 复位跳转arch/arm/lib/vectors.S中的_start也就是复位向量,第一条指令直接b reset

  • 模式设置:进入save_boot_params_ret后,第一件事就是修改CPSR 寄存器

    • 将 CPU 切换到SVC32 (Supervisor) 模式

    • 关键点:同时必须禁止 FIQ 和 IRQ 中断(CPSR bit 7-6 置 1)。在 Bootloader 阶段,我们不需要中断干扰,直到内核启动。

2. 硬件环境清理 (cpu_init_cp15)

在调用 C 语言之前,硬件环境必须是“纯净”的。

  • 关闭 MMU 和 Cache:这是面试常考点。为什么?因为此时还没建立页表,虚拟地址无法映射;且初始化阶段我们直接操作物理地址,开启 Cache 会导致数据一致性问题。

  • 清除 TLB:确保之前的地址转换记录被清空。

3. 建立临时栈 (lowlevel_init)

这是第一阶段的最后一步,也是为 C 语言铺路的这一步。

  • SP 指针赋值:我们将 SP 指针指向内部 RAM (OCRAM) 的地址0X0091FF00CONFIG_SYS_INIT_SP_ADDR)。注意,这时候还不敢用外部 DDR,因为 DDR 控制器还没初始化好!

  • 预留 GD 空间:U-Boot 核心全局变量global_data(GD) 也是放在这里的。代码中sp -= GD_SIZE(248字节),并将结果存入R9 寄存器

    • 📝记下来:在 ARM U-Boot 中,R9 寄存器永远指向 gd 结构体,千万别改它。

📦 第二阶段:前置 C 语言与代码重定位 (Board Init F & Relocation)

汇编做完了脏活累活,_main(arch/arm/lib/crt0.S) 终于接管了流程,开始进入 C 语言的世界。这一阶段的核心任务是:初始化 DDR,并将自己搬运到内存的高地址运行

1. Board Init F (Flash 运行阶段)

board_init_f是核心函数,它通过initcall_run_list执行了一系列初始化序列init_sequence_f

  • Serial Init:初始化串口,这时候你才能在终端看到打印信息。

  • DRAM Init:获取 DDR 大小(例如 512MB)。

  • 计算重定位地址:这是重头戏。U-Boot 会计算出 DDR 顶端的一个地址(如0X9FF47000),作为自己新的“豪宅”。

2. 代码自搬运 (Relocation)

面试中的“深水区”就在这里。为什么要重定位?为了把低地址腾出来给 Linux 内核使用。

  • Copy Looprelocate_code汇编函数会将 U-Boot 的代码段、数据段从源地址(0X87800000)完整拷贝到目的地址(0X9FF47000)。

  • Fix .rel.dyn (动态符号修正):这是最难理解的。代码拷贝后,程序里引用的全局变量地址还是老的(指向 0X8780…),访问就会出错。

    • 解决:U-Boot 遍历.rel.dyn段,取出每一个需要修正的 Label,给它加上一个Offset(新地址 - 旧地址 = 0X18747000)。

    • 这一步做完,代码才真正具备了在新地址运行的能力。

3. 环境切换

搬运完成后,_main里的汇编代码会:

  1. 重定位向量表:将 CP15 的 VBAR 寄存器指向新的 DDR 地址。

  2. Clear BSS:将 BSS 段清零。

  3. Jump:最后ldr pc, =board_init_r,直接飞到 DDR 中的 C 函数继续执行。


🚀 第三阶段:后置板级初始化与主循环 (Board Init R & Main Loop)

此时代码已经在 DDR 的高地址飞奔了,资源不再受限。board_init_r开始“满血”初始化。

1. 复杂外设初始化 (init_sequence_r)

  • Enable Cachesinitr_caches。现在代码在 DDR 跑了,必须开启 I-Cache 和 D-Cache,否则速度太慢。

  • MMC/Net/USB:初始化 EMMC 控制器、FEC 网卡驱动。这时候你才能用tftp下载内核,或者从 EMMC 读取镜像。

2. 进入主循环 (main_loop)

初始化全部结束后,进入死循环common/main.c

  • Bootdelay:读取环境变量bootdelay,开始倒计时(通常是 3 秒)。

  • Autoboot

    • 无人打断:执行bootcmd环境变量里的命令(通常是启动内核)。

    • 有人按下回车:进入cli_loop,启动 Hush Shell 解析器,等待用户输入命令(如printenv,bootz)。


🏁 第四阶段:启动 Linux 内核 (The Final Jump)

假设倒计时结束,执行bootz命令启动 Linux。这是 U-Boot 生命周期的终点。

1. 最后的准备 (do_bootm_states)

  • 关闭中断:在bootm_disable_interrupts中,再次确保中断彻底关闭。Linux 内核启动初期对中断极其敏感,如果有未处理的中断,内核会直接崩溃。

  • 设备树处理boot_prep_linux会解析bootargs环境变量(如 console设置、rootfs路径),并将这些信息注入到设备树(DTB)的/chosen节点中。这就解释了为什么 U-Boot 里的设置能传给内核。

2. 寄存器设置 (ABI 约定)

boot_jump_linux真正跳转前,必须满足 ARM Linux 的启动协议,设置三个通用寄存器:

  • R0 = 0:必须为 0。

  • R1 = Machine ID:机器 ID(现在设备树时代这个通常不重要了,但保留兼容)。

  • R2 = DTB 地址:最重要!告诉内核设备树在内存的哪个位置。

3. 移交控制权 (kernel_entry)

执行最后一句代码:

C

kernel_entry(0, machid, r2);

这实际上是将 PC 指针直接强制修改为内核的入口地址(images->ep)。也就是在这一瞬间,U-Boot 的使命彻底结束,Linux 内核开始接管世界。🌍


🐰 兔哥总结

兄弟们,U-Boot 看起来复杂,其实剥离掉细枝末节,就是一条线:

  1. 汇编阶段:关中断、关Cache、配堆栈 (SRAM)。

  2. 前置C阶段:算地址、搬代码、修符号 (Relocation)。

  3. 后置C阶段:开Cache、配网卡、读脚本 (Bootcmd)。

  4. 启动阶段:传参数、设寄存器、改PC指针 (Jump)。

把这四张图里的流程印在脑子里,下次面试官再问启动流程,你就直接把这几个关键函数和寄存器甩给他!

记得点赞收藏,我们下期讲讲 Linux 内核启动的第一行代码干了啥!👋

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询