玩嵌入式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厂商留足了“自定义空间”:
#ifdefCONFIG_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的异常级别“降下来”,用一个精妙的宏就能完成统一设置:
adrx0, vectorsswitch_elx1,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会在这里手动开启缓存:
#ifndefCONFIG_SYS_ICACHE_OFF mov x1, #CR_I // 开启 ICache#else mov x1, #0 // 关闭 ICache#endif
然后根据当前异常级别,将配置写入对应的SCTLR_ELx寄存器,同时清除中断掩码(DAIF),让系统能响应SError(系统错误)。
如果是多核处理器(比如Cortex-A53/A57),还要开启SMP一致性(多核协同工作的关键):
mrsx0, 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启动时,遇到过最头疼的问题是什么?
审核编辑 黄宇
-
u-boot
+关注
关注
0文章
140浏览量
39964
发布评论请先 登录
深度解析 RK 平台 U-Boot 环境变量(env):原理、配置与实战
深度剖析U-Boot ADC Uclass:从架构到实战的全维度解析
S32G398 u-boot OCOTP 编程保险丝仅在复位后激活是为什么?
更新 U-boot 时出现的问题求解
更新固件后 U-boot 不运行怎么解决?
更新 SPL 和 U-Boot的提示和技巧
深入解析U-Boot image.c:RK平台镜像处理核心逻辑
深入解析RK3588 U-Boot板级文件:evb_rk3588.c核心逻辑拆解
U-Boot SPL核心文件spl.c深度解析:从启动流程到调试优化
深入解析U-Boot TPL代码:嵌入式启动的“第一棒”背后的秘密
深入解析U-Boot命令处理核心文件:功能、调试与开发价值
解析Rockchip平台U-Boot核心文件:boot_rkimg.c到底做了什么?
深入理解 RK3506 U-Boot 重定位:从代码到原理
硬核拆解:U-Boot 的第一条指令到底做了什么?
评论