9. start.S 分析
在上一篇文章U-Boot分析【学习笔记】(7)中,我们详细分析了 U-Boot 的物理版图 。通过对链接脚本的逐行分析,我们已经掌握了 U-Boot 是如何给 Makefile 收集到的成千上万个零件安排绝对物理坐标的 。
我们理清了中断向量表(.vectors)为何必须在 0 地址,也明白了 BSS 段与重定位表之间"过河拆桥"的空间复用原理 。
然而,站在架构师的角度审视,链接脚本(LDS)本质上只是静态的 。它规定了空间的划分,却不负责赋予这些空间生命。
那么这些地址标签,究竟是如何与 CPU 的跳转逻辑、内存的清零动作以及"搬家"的修正算法产生联系的? 今天我们要研究的对象,正是赋予链接脚本灵魂的文件 ------arch/arm/cpu/armv7/start.S (由于imx6ull 属于 armv7 框架,所以我们研究该目录下的 start.S 文件)
bash
.text :
{
*(.__image_copy_start)
*(.vectors)
CPUDIR/start.o (.text*)
*(.text*)
}
在 u-boot.lds 中,链接器指定的是 start.o。
既然是 .o 零件在排队,我们为什么要看 .S 源码呢?
源码(.S)是逻辑的唯一载体
.o 文件是二进制格式(ELF),里面全是机器码(如 E59F0010),人类很难直接阅读。
.S 文件:写着 ldr r0, =_start。它告诉了我们 CPU 为什么要加载这个地址,这里面包含了程序员的意图。
.o 文件:已经变成了 CPU 执行的指令流。
分析意义:如果你只看 .o,你只知道那里有一堆指令;但看 .S才能知道这些指令是为了设置异常向量表、关闭看门狗还是初始化 CP15。
9.1汇编语言基础
由于 start.S 文件是汇编代码,这里先对汇编语言进行一些简单的介绍,让读者能够更好的理解其中的意思。
汇编语言其实是机器码的"助记符"。在 start.S 中,我们主要会遇到三类语法:
伪指令、通用指令以及专门操作 CPU 核心的协处理器指令
伪指令(Directives)
这些指令不直接对应 CPU 动作,而是告诉汇编器如何组织代码。
.globl:声明全局符号。就像 C 语言的 extern,让链接脚本(LDS)能找到它。
.word:在当前位置存放一个 32 位的常量。
.section:定义一个段。这直接呼应了 LDS 里的 .vectors 或 .text。
通用数据处理指令这是代码逻辑的"骨架"。ARM 汇编遵循典型的"操作码 目标, 源1, 源2"格式:
| 指令 | 全称 (Full Name) | 核心语义 (Semantic) | 代码实例 (Example in start.S) |
|---|---|---|---|
| LDR | LoaD Register | 加载寄存器:从内存读取数据到寄存器。 | ldr r0, =_start (将链接脚本定义的起始地址加载到 r0) |
| STR | STore Register | 存储寄存器:将寄存器数据写入内存。 | str r0, [r1] (在初始化或重定位时将数据保存到目标地址) |
| MOV | MOVe | 移动:在寄存器之间复制数据。 | mov r0, #0 (给 r0 寄存器清零,准备后续操作) |
| BIC | Bit Clear | 位清除:根据掩码将指定位清零。 | bic r0, r0, #0x1f (清除 CPSR 的模式位,准备切换 CPU 模式) |
| ORR | Logical OR | 逻辑或:根据掩码将指定位置 1。 | orr r0, r0, #0x13 (将模式位设为 0x13,即强制进入 SVC 模式) |
| MRS | Move from Status register | 读状态寄存器:读出程序状态寄存器(CPSR)的值。 | mrs r0, cpsr (修改模式前,必须先读出当前的 CPU 状态) |
| MSR | Move to Status register | 写状态寄存器:将值写回状态寄存器(CPSR)。 | msr cpsr, r0 (将修改后的值写回,使 CPU 模式切换正式生效) |
| B | Branch | 跳转:直接跳转到目标地址。 | b reset (CPU 复位后直接跳转到 reset 初始化逻辑) |
| BL | Branch with Link | 带返回跳转:跳转并将返回地址存入 LR。 | bl cpu_init_cp15 (调用子函数,执行完后能跳回此处继续执行) |
协处理器指令(MCR/MRC)
start.S 中会看到大量类似 mrc p15, 0, r0, c1, c0, 0 这样的代码。
MRC:Move from Coprocessor。
读开关。把 CPU 内部核心配置(如 Cache、MMU)读到通用寄存器 r0。
MCR:Move to Coprocessor。
写开关。把修改好的 r0 写回核心配置。
遇到这类指令不要去背操作数,只需要查阅对应的 CPU 手册(如 Cortex-A7 Reference Manual),看对应的寄存器(如 SCTLR)某位定义的是什么功能即可。
标签(Labels)在汇编中,reset: 或 _start: 这样的标签本质上就是地址 。
当在 start.S 看到 ldr r0, =_start 时,CPU 实际上是在利用我们在链接脚本(LDS)里确定的那个"物理坐标"进行跳转。这就是静态图纸与动态执行的交汇点。
9.2 源码分析
从 start.S 的开头分析:
c
.globl reset
.globl save_boot_params_ret
#ifdef CONFIG_ARMV7_LPAE
.global switch_to_hypervisor_ret
#endif
reset:
/* Allow the board to save important registers */
b save_boot_params
save_boot_params_ret:
.
.
.
ENTRY(save_boot_params)
b save_boot_params_ret @ back to my caller
ENDPROC(save_boot_params)
.weak save_boot_params
c.globl reset含义:
这里定义了一个全局符号 reset
作用:能看到后面几行代码中有一个标签 reset,定义了这个全局符号后,链接脚本就能通过 b reset 跳回到标签所在位置,继续执行后面的代码
creset: /* Allow the board to save important registers */ b save_boot_params save_boot_params_ret:start.S 从第一个标签 rset 开始执行,
b save_boot_params:
跳转到这个函数
此时我们阅读代码后面会发现一段:
cENTRY(save_boot_params) b save_boot_params_ret @ back to my caller ENDPROC(save_boot_params) .weak save_boot_params.weak 是一个弱符号,它定义了 save_boot_params ,也就是它的前三行代码,内容是 b save_boot_params_ret
逻辑:b save_boot_params 让链接器现寻找有没有强符号定义的bsave_boot_params 函数,如果没有就会跳转到弱符号(.weak)定义的save_boot_params ,而这个弱符号定义的函数内容是b save_boot_params_ret,这时候我们发现 save_boot_params_ret 正好是 reset 标签执行 b save_boot_params 后的下一行。
也就是说,如果有特别定义的 boot 参数(即强符号定义)那就会跳转进去执行,如果没有就会回到 reset 标签的 save_boot_params_ret 继续往后执行 uboot 的主干

我们不需要每行汇编代码都看,代码中有注释,我们知道有什么用处即可,重点关注 b 跳转命令指向的函数,但是这里防止读者无法解释链接脚本(LDS)是怎么和硬件联系起来的,分析一个汇编代码的例子:
c
/*
* Setup vector:
* (OMAP4 spl TEXT_BASE is not 32 byte aligned.
* Continue to use ROM code vector only in OMAP4 spl)
*/
#if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))
/* Set V=0 in CP15 SCTLR register - for VBAR to point to vector */
mrc p15, 0, r0, c1, c0, 0 @ Read CP15 SCTLR Register
bic r0, #CR_V @ V = 0
mcr p15, 0, r0, c1, c0, 0 @ Write CP15 SCTLR Register
/* Set vector address in CP15 VBAR register */
ldr r0, =_start
mcr p15, 0, r0, c12, c0, 0 @Set VBAR
#endif
这段代码的作用是告诉CPU,当出现中断或异常时,应该跳转到什么地方执行。
其中最重要的是:
c
ldr r0, =_start
_start 是什么?
它是链接脚本 (LDS) 里定义的首行标签,代表了程序在内存中的起始位置(例如 0x87800000)。
代码含义:将链接脚本定义的 _start 起始地址加载到 r0 寄存器中
c
mcr p15, 0, r0, c12, c0, 0 @Set VBAR
MCR <coproc>, <opc1>, <Rt>, <CRn>, <CRm>, <opc2>
| 参数 | 全称 | 作用 |
|---|---|---|
<MCR> |
- | Move to Coprocessor from Register(将通用寄存器的值传给协处理器) |
<coproc> |
Coprocessor | 协处理器编号。在 ARMv7 中,我们主要操作 p15(系统控制协处理器),它负责内存管理、缓存控制等核心任务。 |
<opc1> |
Opcode 1 | 第一操作码。确定是大类功能(标准主要寄存器 or 厂商扩展) |
<Rt> |
Source Register | 源寄存器。这是一个 ARM 通用寄存器(如 r0)。存放着想要写入硬件的"数值"或"地址"。 |
<CRn> |
Target Register | 目标主寄存器。选定功能模块(如 c1 是控制位,c12 是异常向量) |
<CRm> |
Additional Register | 目标辅助寄存器。配合 CRn 使用,选定具体的参数组。 |
<opc2> |
Opcode 2 | 第二操作码。用来确定在同一组的多个寄存器中中选取哪一个。 |
1. 为什么需要操作码?(硬件背景)
通用寄存器(如 r0-r15)只有 16 个,但协处理器(如 CP15)内部的功能极其庞大(管理 MMU、Cache、分支预测、时钟、向量表等)。
由于指令长度有限(32位),无法给 CP15 里的每一个细微功能都分配一个唯一的寄存器编号。
于是,ARM 设计者采用了"多维坐标系"的方法来定位功能:
CRn: 大分类(主寄存器序号)。
CRm: 小分类(辅助寄存器序号)。
2. opc1 和 opc2: 具体的动作指令或功能过滤器。
opc1 (Opcode 1):决定了这条指令是要访问 "主要寄存器"(Primary Registers)还是 "厂商自定义扩展"。
对于 CP15 而言:ARM 架构标准规定,访问几乎所有标准的系统控制寄存器(包括 VBAR, SCTLR 等)时,opc1 必须设置为 0。
opc2 (Opcode 2):定义"细分功能"
当 CRn(主位置)和 CRm(次位置)已经确定后,可能还有多个功能在同一个位置。这时候就靠 opc2 来做最后的筛选。
例子:在 ARMv7 架构中,CRn=c12, CRm=c0 这个地址确实被分配给了多个与向量基址相关的寄存器。它们共享了"主坐标",必须靠 opc2 这个"副坐标"来定位 VBAR 寄存器。
总结:通用寄存器 --->协处理器
通用寄存器由Rt直接决定 ,即r0
协处理器由opc1,CRN,CRm,opc2共同决定 :
先是 opc1 确认是在主要寄存器中找,然后CRN,CRm,opc2决定了是哪个具体的寄存器。
整行代码的物理意义:将通用寄存器(r0)的值,传入异常向量基址寄存器VBAR中
9.3 cpu_init_cp15
c
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_cp15
根据之前的分析,链接器在编译阶段将寻找项目中是否有其他强符号定义(start.S中定义的就是强符号)的 cpu_init_cp15,有就跳进去执行,bl 会把当前行的下一行地址 记录在LR寄存器中,再进行跳转。
但我们发现,cpu_init_cp15 并没有被 .weak 弱符号定义,按下编译键时,链接器会直接弹出一个红色的错误:multiple definition of 'cpu_init_cp15'。
这说明如果链接器找到了其他的 cpu_init_cp15 函数,会终止编译,只有当项目中只有一个 cpu_init_cp15,链接器才会把 bl 指令的目标地址指向 start.S 中这个唯一的函数地址,在执行完后,在 cpu_init_cp15 的末尾会读取 LR 寄存器的值跳转回 bl cpu_init_cp15 的下一行继续执行。

也就是说先执行 cpu_init_cp15 函数,再向下执行 bl cpu_init_crit ;
接下来就分析 cpu_init_cp15 函数,它占据了一半的 start.S 文件,我们对其深入分析:
9.3.1 构建绝对纯净的底层运行环境
c
/*
* Invalidate L1 I/D
*/
mov r0, #0 @ set up for MCR
mcr p15, 0, r0, c8, c7, 0 @ invalidate TLBs
mcr p15, 0, r0, c7, c5, 0 @ invalidate icache
mcr p15, 0, r0, c7, c5, 6 @ invalidate BP array
mcr p15, 0, r0, c7, c10, 4 @ DSB
mcr p15, 0, r0, c7, c5, 4 @ ISB
在硬件层面,Cache(缓存)和 TLB(地址转换缓冲)在刚上电时,里面的电信号状态是随机的,也就是说存的是"垃圾数据"。
- mcr p15, 0, r0, c8, c7, 0 (Invalidate TLBs):
TLB 是存放虚拟地址到物理地址映射的表格。
如果不清空,CPU 可能会拿着旧的、错误的映射关系去读内存,导致程序崩溃。- mcr p15, 0, r0, c7, c5, 0 (Invalidate icache):
icache 是指令缓存。
如果不清空,CPU 可能会运行之前残留的指令片段。
invalidate BP array?- BP 代表什么?
Branch Predictor (分支预测器)。
现代 CPU 为了跑得快,都有"预判"能力。当它看到一个跳转指令(比如 b 或 bl)时,它不会等代码真正执行到那一行,而是根据"历史经验"提前猜一下:"上次这里跳了,这次估计也要跳。" 然后它会提前把目标地址的代码加载进来。这个记录历史经验的表格就叫 BP Array。
为什么要失效(Invalidate)它?
在 U-Boot 刚启动或进行代码重定位(搬运)时,内存里的程序位置变了。
如果 BP Array 里还存着旧的预判记录,CPU 就会根据"老经验"跳到错误的地址去。- DSB (Data Synchronization Barrier):
指令含义:"数据同步屏障"。
它告诉 CPU:在我这条指令之前所有的内存访问(读/写)没完成之前,不准执行后面的指令。确保刚才的"清理动作"真的写进硬件里了。- ISB (Instruction Synchronization Barrier):
指令含义:"指令同步屏障"
背景:CPU 不是一行行执行代码的,为了提速,CPU 在执行当前指令的同时,其实已经提前抓取(Fetch)了后面好几行的指令,并且等待处理。
如果不加 ISB 会发生什么?
动作 A:执行了指令,告诉硬件:"把缓存(icache)清空"
现实情况:虽然 icache 被清空了,但 CPU 可能已经提前抓好了 3-4 条"旧"指令(这些指令是从还没清空前的 icache 里读出来的)。
结果:CPU 会继续把这几条"过时"的指令跑完,才去读新的指令。这几条旧指令可能会导致系统跑飞。
9.3.2 SCTLR 寄存器的读-改-写
c
/*
* disable MMU stuff and caches
*/
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002000 @ clear bits 13 (--V-)
bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM)
orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align
orr r0, r0, #0x00000800 @ set bit 11 (Z---) BTB
#ifdef CONFIG_SYS_ICACHE_OFF
bic r0, r0, #0x00001000 @ clear bit 12 (I) I-cache
#else
orr r0, r0, #0x00001000 @ set bit 12 (I) I-cache
#endif
mcr p15, 0, r0, c1, c0, 0
核心流程:读 -> 改 -> 写
这里操作的是 CP15 协处理器的 c1 寄存器,即 SCTLR (System Control Register,系统控制寄存器)。其坐标为 (0, c1, c0, 0)。
读 (MRC):将硬件当前的配置"拍个照"存入 r0。
改 (BIC位清零/ORR逻辑或):在 r0 中修改特定的功能开关(位操作)。
写 (MCR):将改好的配置灌回硬件,设置立即生效。
c
bic r0, r0, #0x00002000 @ clear bits 13 (--V-)
物理意义:选择异常向量表的地址
在 ARM 架构中,异常向量表(存放中断处理函数入口的地方)有两个可选的默认地址:
低端向量表 (Low Vectors):基地址在 0x00000000。
高端向量表 (High Vectors):基地址在 0xffff0000。
Bit 13(即 V位)就是控制这个选择的开关:
V = 1:CPU 强制去 0xffff0000 找向量表。
V = 0:CPU 去低端找向量表,并且允许使用 VBAR 寄存器进行重定向。
我们在 9.2 中可知通过 mcr p15, 0, r0, c12, c0, 0 手动设置了 VBAR 为 _start 的地址,所以Bit 13要清零,关掉高端向量表锁定,释放 VBAR 的重定向权力。
c
bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM)
这里一次性清除了三个核心开关:
- Bit 0 - MMU 使能:关闭 MMU。此时 CPU 停止地址映射,进入"实地址模式"。你写的 0x87800000 就是内存条上真实的物理位置。
- Bit 1 为了方便顺便清理了,后面会改回来
- Bit 2 - Data Cache 使能:关闭数据缓存。
为什么要关? 在 U-Boot 搬运自己(Relocation)之前,如果不关 MMU 和 Date Cache,数据可能会被缓存在 Cache 里而没写进物理内存,或者地址映射发生混乱,导致搬运后程序直接跑飞。
c
orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align
为什么刚关掉(bic #7)又要开启(orr #2)?
在前面的 bic r0, r0, #0x00000007 中,程序员是为了图省事,一次性清空了 M/A/C 三个位(因为 7 = 111 2 7 = 111_2 7=1112)。但在 U-Boot 的设计中,虽然我们要关掉 MMU (M) 和 Data Cache ©,但我们希望开启对齐检查 (A)。
物理意义:开启对齐检查能帮助开发者在早期发现代码中的逻辑错误。如果代码尝试访问一个未对齐的地址,硬件报错能防止后续产生更难排查的随机数据错误。
c
orr r0, r0, #0x00000800 @ set bit 11 (Z---) BTB
全称:Branch Target Buffer (分支目标缓冲)。
物理意义:它是 分支预测器(前面提到的 BP) 的核心。
CPU 运行程序时,遇到 if-else 或跳转指令,BTB 会根据历史记录预测下一步去哪。
为什么要开?虽然我们要关掉数据缓存来保命(稳定性),但分支预测属于 CPU 内部的逻辑优化,开启它能让 U-Boot 运行速度提高,提升执行效率。
c
#ifdef CONFIG_SYS_ICACHE_OFF
bic r0, r0, #0x00001000 @ clear bit 12 (I) I-cache
#else
orr r0, r0, #0x00001000 @ set bit 12 (I) I-cache
#endif
代码里出现了 #ifdef,这说明 I-Cache(指令缓存) 的开关不是死代码,而是可以由开发者在头文件中灵活配置的:
- 如果定义了 CONFIG_SYS_ICACHE_OFF:执行 bic 清除 Bit 12。这通常用于极其保守的调试阶段,或者某些硬件本身 Cache 有严重的 Errata(缺陷)时。
- 默认情况(#else):执行 orr 开启 Bit 12。
为什么默认开启?
指令是"只读"的。开启 I-Cache 能一次性从内存里预读一大串指令存放在 CPU 内部的极速空间里。这能让 U-Boot 的启动速度提升数倍,且不会像 Data Cache 那样引起数据不一致的风险。- 清空与开启
我们注意到之前
cmcr p15, 0, r0, c7, c5, 0 (Invalidate icache)清空了指令缓存中的数据,但这并不意味着开启后就没有数据了。
清空 (Invalidate):是指清除旧数据。刚上电时,I-cache 里的电平状态是随机的,里面存的是"硬件垃圾"。如果不清空就直接开启,CPU 可能会把这些随机电平当成指令去执行,导致瞬间跑飞。
开启 (Enable):是指启动功能。开启后,CPU 才会开始把内存里真实、新的指令抓取到 Cache 中。
逻辑:先"格式化",再"投入使用"
c
mrc p15, 0, r0, c1, c0, 0
.
.
.
mcr p15, 0, r0, c1, c0, 0
这里就是核心流程中说的读和写,先把 p15 当前配置写入 r0中,然后对 r0 进行修改,配置好后再把 r0 的数据 写会 p15 中,完成配置。
9.3.3 ARM 勘误处理
c
#ifdef CONFIG_ARM_ERRATA_716044
mrc p15, 0, r0, c1, c0, 0 @ read system control register
orr r0, r0, #1 << 11 @ set bit #11
mcr p15, 0, r0, c1, c0, 0 @ write system control register
#endif
#if (defined(CONFIG_ARM_ERRATA_742230) || defined(CONFIG_ARM_ERRATA_794072))
mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register
orr r0, r0, #1 << 4 @ set bit #4
mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register
#endif
#ifdef CONFIG_ARM_ERRATA_743622
mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register
orr r0, r0, #1 << 6 @ set bit #6
mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register
#endif
#ifdef CONFIG_ARM_ERRATA_751472
mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register
orr r0, r0, #1 << 11 @ set bit #11
mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register
#endif
#ifdef CONFIG_ARM_ERRATA_761320
mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register
orr r0, r0, #1 << 21 @ set bit #21
mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register
#endif
#ifdef CONFIG_ARM_ERRATA_845369
mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register
orr r0, r0, #1 << 22 @ set bit #22
mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register
#endif
mov r5, lr @ Store my Caller
mrc p15, 0, r1, c0, c0, 0 @ r1 has Read Main ID Register (MIDR)
mov r3, r1, lsr #20 @ get variant field
and r3, r3, #0xf @ r3 has CPU variant
and r4, r1, #0xf @ r4 has CPU revision
mov r2, r3, lsl #4 @ shift variant field for combined value
orr r2, r4, r2 @ r2 has combined CPU variant + revision
#ifdef CONFIG_ARM_ERRATA_798870
cmp r2, #0x30 @ Applies to lower than R3p0
bge skip_errata_798870 @ skip if not affected rev
cmp r2, #0x20 @ Applies to including and above R2p0
blt skip_errata_798870 @ skip if not affected rev
mrc p15, 1, r0, c15, c0, 0 @ read l2 aux ctrl reg
orr r0, r0, #1 << 7 @ Enable hazard-detect timeout
push {r1-r5} @ Save the cpu info registers
bl v7_arch_cp15_set_l2aux_ctrl
isb @ Recommended ISB after l2actlr update
pop {r1-r5} @ Restore the cpu info - fall through
skip_errata_798870:
#endif
#ifdef CONFIG_ARM_ERRATA_801819
cmp r2, #0x24 @ Applies to lt including R2p4
bgt skip_errata_801819 @ skip if not affected rev
cmp r2, #0x20 @ Applies to including and above R2p0
blt skip_errata_801819 @ skip if not affected rev
mrc p15, 0, r0, c0, c0, 6 @ pick up REVIDR reg
and r0, r0, #1 << 3 @ check REVIDR[3]
cmp r0, #1 << 3
beq skip_errata_801819 @ skip erratum if REVIDR[3] is set
mrc p15, 0, r0, c1, c0, 1 @ read auxilary control register
orr r0, r0, #3 << 27 @ Disables streaming. All write-allocate
@ lines allocate in the L1 or L2 cache.
orr r0, r0, #3 << 25 @ Disables streaming. All write-allocate
@ lines allocate in the L1 cache.
push {r1-r5} @ Save the cpu info registers
bl v7_arch_cp15_set_acr
pop {r1-r5} @ Restore the cpu info - fall through
skip_errata_801819:
#endif
#ifdef CONFIG_ARM_ERRATA_454179
cmp r2, #0x21 @ Only on < r2p1
bge skip_errata_454179
mrc p15, 0, r0, c1, c0, 1 @ Read ACR
orr r0, r0, #(0x3 << 6) @ Set DBSM(BIT7) and IBE(BIT6) bits
push {r1-r5} @ Save the cpu info registers
bl v7_arch_cp15_set_acr
pop {r1-r5} @ Restore the cpu info - fall through
skip_errata_454179:
#endif
#ifdef CONFIG_ARM_ERRATA_430973
cmp r2, #0x21 @ Only on < r2p1
bge skip_errata_430973
mrc p15, 0, r0, c1, c0, 1 @ Read ACR
orr r0, r0, #(0x1 << 6) @ Set IBE bit
push {r1-r5} @ Save the cpu info registers
bl v7_arch_cp15_set_acr
pop {r1-r5} @ Restore the cpu info - fall through
skip_errata_430973:
#endif
#ifdef CONFIG_ARM_ERRATA_621766
cmp r2, #0x21 @ Only on < r2p1
bge skip_errata_621766
mrc p15, 0, r0, c1, c0, 1 @ Read ACR
orr r0, r0, #(0x1 << 5) @ Set L1NEON bit
push {r1-r5} @ Save the cpu info registers
bl v7_arch_cp15_set_acr
pop {r1-r5} @ Restore the cpu info - fall through
skip_errata_621766:
#endif
mov pc, r5 @ back to my caller
1. 物理意义:什么是 Errata(勘误/缺陷)?
Errata 在硬件领域指的就是"芯片设计缺陷"。
ARM 公司设计了一款 CPU 架构,厂家(如 NXP, 三星)把它造了出来。但在大规模使用后,发现某个特定版本的芯片在某种特定情况下会"卡死"或"算错",但是硬件没法改,于是通过软件方法解决。
软件打补丁:ARM 会发布一个文档,告诉开发者:"如果用的是 R2p1 版本的芯片,请把某某寄存器的第几位设为 1,这样就能绕过这个硬件 Bug。"
这段代码的本质: U-Boot 作为一个通用的引导程序,为了能在各种"有小瑕疵"的芯片上稳定运行,把这些已知的补丁全都预备好了。
2. 逻辑拆解:补丁是怎么打的?可以把这段代码分为三个逻辑层次:
(1)无差别补丁(只要定义了宏就打)代码开头那一堆:
c#ifdef CONFIG_ARM_ERRATA_716044 mrc p15, 0, r0, c1, c0, 0 @ 读 orr r0, r0, #1 << 11 @ 改:设置第11位绕过Bug mcr p15, 0, r0, c1, c0, 0 @ 写 #endif依然是"读-改-写"。只要在编译 U-Boot 时根据你的芯片型号定义了这些宏,这段代码就会把那些隐藏的"硬件开关"拨到安全位置。
(2)身份核查中间有一段代码在操作 r1 到 r4:
cmrc p15, 0, r1, c0, c0, 0 @ 读取 MIDR (主 ID 寄存器)物理意义:这就像是在查 CPU 的"身份证"。
r3 (Variant):大版本号(比如 r2)。
r4 (Revision):小版本号(比如 p1)。
r2 (Combined):组合成版本号(比如 0x21 代表 r2p1)。
为什么要查? 因为有的 Bug 只存在于 r2p0 版本,到了 r2p1 硬件就修复了。软件得先确认自己跑在哪个版本上,再决定要不要打补丁。
(3)精准补丁(看版本号再打)
ccmp r2, #0x21 @ 检查版本是否小于 r2p1 bge skip_errata_454179 @ 若版本够新,硬件已修复,直接跳过 ... @ 否则,执行补丁代码3. 为什么它们操作的是 c15 寄存器?
你可能注意到了,这里频繁出现了 c15:
cmrc p15, 0, r0, c15, c0, 1 @ Diagnostic Register (诊断寄存器)c15:在 ARM 协处理器定义中,通常预留给"实现定义(Implementation Defined)"的功能。
物理意义:这里面存放的通常是芯片内部的"秘密开关",专门用来控制那些非标准的、只有高级开发人员或修 Bug 时才需要的电路逻辑。
9.4 cpu_init_crit 引入
cpu_init_cp15 的最后 会执行 mov pc, r5 @ back to my caller继续执行 bl cpu_init_crit
c
ENTRY(cpu_init_crit)
/*
* Jump to board specific initialization...
* The Mask ROM will have already initialized
* basic memory. Go here to bump up clock rate and handle
* wake up conditions.
*/
b lowlevel_init @ go setup pll,mux,memory
ENDPROC(cpu_init_crit)
cpu_init_crit(Critical Initialization):"关键初始化"
它是内核配置与硬件板级配置之间的唯一接口。
- 为什么叫 "Critical"(关键)?
此时的 CPU 虽然已经设置好了异常向量表(VBAR)并关闭了 MMU,但它现在的状态不佳:
效率低:
刚上电的 CPU 运行在默认频率(通常很低)
空间小:
目前的程序运行在 CPU 内部微小的 SRAM(或 L3 Cache 映射的空间)里。外面那个 512MB 甚至更大的 DDR 内存条,现在还没被使用。- 注释里的干货:Mask ROM 的"交接棒"
The Mask ROM will have already initialized basic memory.
物理意义:
这里提到了一个隐形成员------Mask ROM(芯片厂家固化在硅片里的第一段代码)。
背景:
在 U-Boot 运行前,Mask ROM 已经做了一些最基础的操作(比如把 U-Boot 从 SD 卡或 NAND 拷贝到内存里)。
任务:
U-Boot 需要执行 lowlevel_init 去把时钟频率(Clock Rate)拔高,并真正初始化 DDR 内存。
cb lowlevel_init @ go setup pll,mux,memory这一行跳转,包含了三大硬件使命:
- PLL (Phase-Locked Loop,锁相环):
动作:设置时钟倍频。
意义:把 CPU 的频率拉高,让 U-Boot 运行效率提高。- MUX (Multiplexing,引脚复用):
动作:配置芯片引脚。
意义:把引脚配置为特定功能。- Memory (DDR 初始化):
动作:配置内存控制器。
意义:这是最重要的任务。一旦 lowlevel_init 完成,U-Boot 就可以从几百 KB 的CPU中的SRAM 迁移到DDR 里去了。
cpu_init_crit 就像是一个"接口",它本身不干活,而是负责把球传给特定开发板的实现函数 lowlevel_init。
&emsp由于 imx6ull 位ARMv7框架,所以我们编译的是 ARMv7 架构的 CPU,这个文件夹下的所有核心文件都会被编进最终的镜像里,而 armv7文件夹下 有 lowlevel_init.S 文件。其中有:
c
ENTRY(lowlevel_init)
.
.
.
ENDPROC(lowlevel_init)
在 Linux 和 U-Boot 的汇编体系中,ENTRY 并不是一个简单的注释,它是一个定义在 linux/linkage.h 里的宏。它的原型通常如下:
c#define ENTRY(name) \ .globl name; \ ALIGN; \ name:物理意义:
当写下 ENTRY(lowlevel_init) 时,预处理器在编译阶段会自动加上 .globl lowlevel_init。所以,虽然没看到 global 字样,但编译器已经把它变成了一个全局可见的符号 。
当编译 U-Boot 时,编译器会把每一个 .S 和 .c 文件都编译成一个个 .o 目标文件。
start.o 里写着:"我要跳到 lowlevel_init 这个地方,但我现在不知道它在哪。"
lowlevel_init.o 里写着:"我是 lowlevel_init,我在这里!"(这得益于我们刚才说的 ENTRY 宏提供的全局属性)。
最后,链接器(Linker)会把这些 .o 文件全部揉在一起。它发现 start.o 有个需求,刚好 lowlevel_init.o 能满足,于是它就把 bl lowlevel_init 里的地址填好了。
具体覆盖通用
- 链接脚本中的"点名"逻辑 :
c{ *(.__image_copy_start) *(.vectors) CPUDIR/start.o (.text*) *(.text*) }这里规定了二进制镜像生成的物理顺序:
- *(.vectors):最前面放置的是中断向量表(包含了我们之前分析的 _start 标签)。
- CPUDIR/start.o (.text*):紧接着,链接器被强制要求放入 CPUDIR 路径下的 start.o。CPUDIR 是一个变量,在编译时由 Makefile 确定(对于 i.MX6ULL 来说,它指向 arch/arm/cpu/armv7)。
- (.text):这是**"具体配置覆盖通用配置"**发生的关键! 这里的星号 * 代表"剩下的所有文件"。
"覆盖"是如何在 (.text) 中完成的?
在8.2 .text 代码段我们知道*(.text*)的作用是扫描 所有输入文件 (),从中提取出 所有以 .text 开头的代码段 (.text),并将它们按照链接顺序,整齐地摆放在当前输出文件的这个位置。
在汇编语言中,除非显式地使用 .section 指令指定其他段(比如 .data 或 .bss),否则编译器默认会将所有的指令(机器码)都放在 .text 段中,所以lowlevel_init 也属于 .text 代码段。
当链接器处理到 (.text ) 这一行时,它会去扫描所有被 Makefile 编译出来的 .o 文件。
"具体覆盖通用"的物理过程如下:
在 Makefile 中,如果具体 SoC(System on Chip 系统芯片) 目录(如 mx6)和通用目录(如 armv7)都存在 lowlevel_init.o,Makefile 的逻辑通常会只选择其中一个编译进 .o 列表。
链接阶段的"唯一性":即使两个目录下都存在同名的 lowlevel_init 符号,由于 ENTRY(lowlevel_init) 把它们都标记成了全局符号,链接器在处理到 (.text ) 时,会按照 Makefile 传递给它的 .o 文件序列 进行填充。
Makefile 会将具体的 SoC 目标文件总是排在通用 arch 库的前面并传给链接器,当 Makefile 传给链接器两个 lowlevel_init 时,前面的是具体的,后面是通用的,链接器接收了具体的 lowlevel_init 之后,后续出现的同名符号 lowlevel_init 将不再起作用。
以下是 U-Boot 构建系统中体现这种顺序的典型代码写法:
1. 顶层 Makefile 中的路径拼接在 U-Boot 的根目录 Makefile 中,系统会根据配置(make xxx_defconfig)确定 CPU、SOC、BOARD 等变量。随后,它会按照从"具体"到"通用"的顺序将这些目录加入到链接列表 libs-y 中。
shell# 简化示例:U-Boot 顶层 Makefile 的逻辑 # 这里的顺序决定了最终链接时的文件序列 # 1. 首先加入具体的 SoC 路径 (例如 mx6) libs-y += arch/$(ARCH)/cpu/$(CPU)/$(SOC)/ # 2. 然后加入通用的 CPU 架构路径 (例如 armv7) libs-y += arch/$(ARCH)/cpu/$(CPU)/ # 3. 接着加入其它的架构/板级路径 libs-y += arch/$(ARCH)/lib/ libs-y += board/$(VENDOR)/$(BOARD)/物理意义:当 make 执行时,它会先扫描 SOC 目录,将其内部编译生成的 built-in.o 放在列表前面,然后再放 CPU 目录下的 built-in.o。
2. 子目录 Makefile 中的"条件编译"
在 arch/arm/cpu/armv7/Makefile(通用路径)中,代码通常会写一个"后备开关",即:只有当特定 SoC 没搞定某项任务时,才编译通用的零件。
shell# arch/arm/cpu/armv7/Makefile 示例 # 默认情况下,如果定义了 >CONFIG_SYS_GENERIC_LOWLEVEL_INIT 宏 # 才把通用的 lowlevel_init.o 编进这个目录的 built-in.o 中 obj-$(CONFIG_SYS_GENERIC_LOWLEVEL_INIT) += lowlevel_init.o # 如果具体 SoC (如 mx6) 的头文件里没有定义这个宏 # 那么这个 lowlevel_init.o 根本就不会被生成,也就不会参与链接
不过 mx6/ 目录下并没有 lowlevel_init.S ,所以imx6ull用的是 armv7 目录下的通用 lowlevel_init.S 文件。