硬核拆解:U-Boot 的第一条指令到底做了什么? 电子说
玩嵌入式Linux的朋友,大概率都有过这样的体验:烧录完U-Boot,插上串口,看着屏幕上弹出 U-Boot 202x.xx 的打印信息,心里就踏实了——系统活了。
但很少有人想过,从芯片通电到这句打印出现,背后已经悄悄跑过了成千上万条指令。而这一切的起点,也是最神秘、最底层的那一步,就藏在arch/arm/cpu/armv8/start.S 这个文件里。
今天,我们就逐帧拆解这份 ARMv8 架构下 U-Boot 的“启动基因”,搞懂从通电到进入 C 语言世界(_main 函数)之前,CPU 到底在忙些什么。
(建议收藏,调试启动问题时,这篇就是你的“救命指南”)

一、一切的起点:_start 标号
整个 U-Boot 的启动,始于一个全局标号 _start,这是芯片上电/复位后,PC 指针(程序计数器)指向的第一个地址,相当于 U-Boot 的“出生证明”。
在_start 开头,有一段非常关键的条件编译,给 SoC 厂商留足了“自定义空间”:
#ifdef CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK#include#else b reset#endif
简单说:有些芯片要求引导头必须包含特殊签名或头部结构,厂商就可以在boot0.h 里做定制化操作;如果不需要,就直接跳转到reset 标号,进入下一步。
这一步看似简单,却是 U-Boot 适配不同芯片的关键“后门”。
二、位置无关:U-Boot 能“随处运行”的秘密
现代 U-Boot 有个很强大的特性——位置无关代码(PIC),也就是说,它的链接地址(编译时指定的地址)和实际加载地址(烧录到芯片的地址)可以不一样,怎么烧录都能正常启动。
这个特性的核心,就藏在pie_fixup 这段代码里:
pie_fixup: adr x0, _start // 读取 _start 的运行时实际地址 ldr x1, _TEXT_BASE // 读取编译时指定的链接地址 sub x9, x0, x1 // 计算实际地址与链接地址的偏移量 ...pie_fix_loop: // 遍历重定位表,给所有需要修正的地址加上偏移量
逻辑很直白:先算出“实际地址”和“预期地址”的差值(偏移量),再遍历整个重定位表,把所有需要重定位的位置都加上这个偏移量。
正是这几步操作,让 U-Boot 实现了“任意地址加载,随处运行”,极大降低了烧录和调试的门槛。
三、异常级别切换:从 EL3 降到 EL1 的“降权”操作
ARMv8 架构的 CPU 有 4 个异常级别(EL0~EL3),级别越高,权限越大。芯片上电时,通常会处于最高权限的 EL3,但 Linux 内核一般只需要运行在 EL1(或 EL2,用于虚拟化)。
所以,start.S 要做的关键一步,就是把 CPU 的异常级别“降下来”,用一个精妙的宏就能完成统一设置:
adr x0, vectorsswitch_el x1, 3f, 2f, 1f
这段代码的作用是:判断当前 CPU 的异常级别,然后执行对应级别的初始化:
•EL3(最高级别):设置向量表基地址(vbar_el3),配置 SCR_EL3 寄存器,开启 FP/SIMD 功能(浮点/向量运算)。
•EL2(虚拟化级别):设置 vbar_el2,同样开启 FP/SIMD。
•EL1(内核级别):设置 vbar_el1。
无论从哪个级别启动,最后都会统一落到同一个入口,同时初始化通用定时器频率(cntfrq_el0),为后续的驱动和计时功能提供精准时钟。
四、开启缓存 + 多核“唤醒”:让系统“跑起来”
芯片上电时,缓存(ICache/DCache)是默认关闭的——缓存虽能提升性能,但启动初期系统状态混乱,开启缓存会导致数据异常。所以 U-Boot 会在这里手动开启缓存:
#ifndef CONFIG_SYS_ICACHE_OFF mov x1, #CR_I // 开启 ICache#else mov x1, #0 // 关闭 ICache#endif
然后根据当前异常级别,将配置写入对应的 SCTLR_ELx 寄存器,同时清除中断掩码(DAIF),让系统能响应 SError(系统错误)。
如果是多核处理器(比如 Cortex-A53/A57),还要开启 SMP 一致性(多核协同工作的关键):
mrs x0, S3_1_c15_c2_1orr x0, x0, #0x40 // 置位 SMPEN 位msr S3_1_c15_c2_1, x0
这一步,就是让多核 CPU 从“各自为战”变成“协同工作”的基础。
五、硬件“补丁”:修复 ARM 处理器的 Errata
哪怕是 ARM 这样的巨头,其处理器也可能存在一些底层 bug(行业内叫 Errata),这些 bug 可能导致特定场景下的数据不一致、死锁或性能异常,需要通过固件补丁来修复。
start.S 中的 apply_core_errata 函数,就是干这个活的,由 Kconfig 中的 CONFIG_ARM_ERRATA_* 选项控制,针对 Cortex-A57 等处理器的经典 Errata 打补丁:
•Errata 828024:关闭 write-back no-allocate 的 non-allocate hint,避免数据缓存异常。
•Errata 826974:关掉 DMB 指令前的投机执行,防止指令执行顺序错乱。
•Errata 833471 / 829520 / 833069:调整分支预测器、FPSCR 寄存器刷新等行为,提升稳定性。
这些看似晦涩的寄存器操作,每一步都是为了让处理器更稳定、更可靠。
六、多核“点名”:主 CPU 唤醒所有副 CPU
完成底层配置后,就到了板级初始化的第一站——lowlevel_init,主要干三件核心事:
1.主 CPU 初始化 GIC 中断控制器(Distributor 和 CPU Interface),相当于给系统“装中断管家”。
2.如果开启了MULTIENTRY(多核启动),副 CPU 会先等待主 CPU 释放“自旋表”地址,然后切换到对应异常级别,等待主 CPU 调度。
3.主 CPU 继续执行,走向 master_cpu 标号。
这里有个很巧妙的设计:smp_kick_all_cpus 函数会通过 GIC 分发 SGI 0 号软中断,把所有副 CPU“踢醒”,让它们进入 U-Boot 启动流程。副 CPU 则在 slave_cpu 标号下“待命”,一旦检测到 CPU_RELEASE_ADDR 非零,就立即跳转执行。
这就是 U-Boot 多核启动的核心逻辑——主 CPU “点名”,副 CPU “应答”,协同启动。
七、终章:跳入 C 语言世界,奔向 _main
当所有底层配置、多核唤醒都完成后,主 CPU 就会执行关键的一步,跳入 C 语言的世界:
master_cpu: bl _main
至此,start.S 的使命就暂告一段落了。
接下来的工作,就交给arch/arm/lib/crt0.S 和board_init_f / board_init_r 完成:重定位 U-Boot 代码、清除 BSS 段、初始化串口、DRAM(内存)、外设,最终才能看到我们熟悉的 U-Boot 打印信息。
写在最后
回看这份 start.S,它就像一颗经过精密雕琢的钻石——没有多余的指令,每一个条件编译、每一次寄存器操作,都是为了把上电时混乱的系统初态,梳理成一个干净、稳定的运行环境。
它不仅是 U-Boot 的启动起点,更是理解整个 ARMv8 系统启动流程的“最佳教科书”。
如果你在调试嵌入式系统时,遇到“卡在启动早期”“多核不同步”“打开缓存就挂死”等问题,不妨回到这份代码里找找答案——真正的宝藏,往往就藏在最开始的地方。
最后,留言区聊聊:你在调试 U-Boot 启动时,遇到过最头疼的问题是什么?
审核编辑 黄宇
全部0条评论
快来发表一下你的评论吧 !