硬核拆解:U-Boot 的第一条指令到底做了什么?

电子说

1.4w人已加入

描述

玩嵌入式Linux的朋友,大概率都有过这样的体验:烧录完U-Boot,插上串口,看着屏幕上弹出 U-Boot 202x.xx 的打印信息,心里就踏实了——系统活了。

但很少有人想过,从芯片通电到这句打印出现,背后已经悄悄跑过了成千上万条指令。而这一切的起点,也是最神秘、最底层的那一步,就藏在arch/arm/cpu/armv8/start.S 这个文件里。

今天,我们就逐帧拆解这份 ARMv8 架构下 U-Boot 的“启动基因”,搞懂从通电到进入 C 语言世界(_main 函数)之前,CPU 到底在忙些什么。

(建议收藏,调试启动问题时,这篇就是你的“救命指南”)

u-boot

一、一切的起点:_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 启动时,遇到过最头疼的问题是什么?

审核编辑 黄宇

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 相关推荐
  • 热点推荐
  • u-boot

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分