U-Boot分析【学习笔记】(8)

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 跳回到标签所在位置,继续执行后面的代码

c 复制代码
reset:
	/* Allow the board to save important registers */
	b	save_boot_params
save_boot_params_ret:

start.S 从第一个标签 rset 开始执行,

b save_boot_params:

跳转到这个函数

此时我们阅读代码后面会发现一段:

c 复制代码
ENTRY(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(地址转换缓冲)在刚上电时,里面的电信号状态是随机的,也就是说存的是"垃圾数据"。

  1. mcr p15, 0, r0, c8, c7, 0 (Invalidate TLBs):
    TLB 是存放虚拟地址到物理地址映射的表格。
    如果不清空,CPU 可能会拿着旧的、错误的映射关系去读内存,导致程序崩溃。
  2. mcr p15, 0, r0, c7, c5, 0 (Invalidate icache):
    icache 是指令缓存。
    如果不清空,CPU 可能会运行之前残留的指令片段。
    invalidate BP array?
  3. BP 代表什么?
    Branch Predictor (分支预测器)。
    现代 CPU 为了跑得快,都有"预判"能力。当它看到一个跳转指令(比如 b 或 bl)时,它不会等代码真正执行到那一行,而是根据"历史经验"提前猜一下:"上次这里跳了,这次估计也要跳。" 然后它会提前把目标地址的代码加载进来。这个记录历史经验的表格就叫 BP Array。
    为什么要失效(Invalidate)它?
    在 U-Boot 刚启动或进行代码重定位(搬运)时,内存里的程序位置变了。
    如果 BP Array 里还存着旧的预判记录,CPU 就会根据"老经验"跳到错误的地址去。
  4. DSB (Data Synchronization Barrier):
    指令含义:"数据同步屏障"。
    它告诉 CPU:在我这条指令之前所有的内存访问(读/写)没完成之前,不准执行后面的指令。确保刚才的"清理动作"真的写进硬件里了。
  5. 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)

这里一次性清除了三个核心开关:

  1. Bit 0 - MMU 使能:关闭 MMU。此时 CPU 停止地址映射,进入"实地址模式"。你写的 0x87800000 就是内存条上真实的物理位置。
  2. Bit 1 为了方便顺便清理了,后面会改回来
  3. 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(指令缓存) 的开关不是死代码,而是可以由开发者在头文件中灵活配置的:

  1. 如果定义了 CONFIG_SYS_ICACHE_OFF:执行 bic 清除 Bit 12。这通常用于极其保守的调试阶段,或者某些硬件本身 Cache 有严重的 Errata(缺陷)时。
  2. 默认情况(#else):执行 orr 开启 Bit 12。
    为什么默认开启?
    指令是"只读"的。开启 I-Cache 能一次性从内存里预读一大串指令存放在 CPU 内部的极速空间里。这能让 U-Boot 的启动速度提升数倍,且不会像 Data Cache 那样引起数据不一致的风险。
  3. 清空与开启
    我们注意到之前
c 复制代码
mcr 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:

c 复制代码
mrc p15, 0, r1, c0, c0, 0    @ 读取 MIDR (主 ID 寄存器)

物理意义:这就像是在查 CPU 的"身份证"。

r3 (Variant):大版本号(比如 r2)。

r4 (Revision):小版本号(比如 p1)。

r2 (Combined):组合成版本号(比如 0x21 代表 r2p1)。

为什么要查? 因为有的 Bug 只存在于 r2p0 版本,到了 r2p1 硬件就修复了。软件得先确认自己跑在哪个版本上,再决定要不要打补丁。
(3)精准补丁(看版本号再打)

c 复制代码
cmp r2, #0x21                @ 检查版本是否小于 r2p1
bge skip_errata_454179       @ 若版本够新,硬件已修复,直接跳过
...                          @ 否则,执行补丁代码

3. 为什么它们操作的是 c15 寄存器?

你可能注意到了,这里频繁出现了 c15:

c 复制代码
mrc 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):"关键初始化"

它是内核配置与硬件板级配置之间的唯一接口。

  1. 为什么叫 "Critical"(关键)?
    此时的 CPU 虽然已经设置好了异常向量表(VBAR)并关闭了 MMU,但它现在的状态不佳:
    效率低:
    刚上电的 CPU 运行在默认频率(通常很低)
    空间小:
    目前的程序运行在 CPU 内部微小的 SRAM(或 L3 Cache 映射的空间)里。外面那个 512MB 甚至更大的 DDR 内存条,现在还没被使用。
  2. 注释里的干货: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 内存。
c 复制代码
b	lowlevel_init		@ go setup pll,mux,memory

这一行跳转,包含了三大硬件使命:

  1. PLL (Phase-Locked Loop,锁相环):
    动作:设置时钟倍频。
    意义:把 CPU 的频率拉高,让 U-Boot 运行效率提高。
  2. MUX (Multiplexing,引脚复用):
    动作:配置芯片引脚。
    意义:把引脚配置为特定功能。
  3. 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 里的地址填好了。
具体覆盖通用

  1. 链接脚本中的"点名"逻辑 :
c 复制代码
{
   *(.__image_copy_start)
   *(.vectors)
   CPUDIR/start.o (.text*)
  *(.text*)
}

这里规定了二进制镜像生成的物理顺序:

  1. *(.vectors):最前面放置的是中断向量表(包含了我们之前分析的 _start 标签)。
  2. CPUDIR/start.o (.text*):紧接着,链接器被强制要求放入 CPUDIR 路径下的 start.o。CPUDIR 是一个变量,在编译时由 Makefile 确定(对于 i.MX6ULL 来说,它指向 arch/arm/cpu/armv7)。
  3. (.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 文件。

相关推荐
噜噜噜阿鲁~1 小时前
python学习笔记 |10.1、面向对象编程-类和实例
笔记·python·学习
solicitous1 小时前
学习了解充电桩协议OCPP
学习·充电桩
风曦Kisaki1 小时前
# Linux运维Day02:LNMP架构部署、动静分离原理、Nginx地址重写、systemd服务管理
linux·运维·架构
Shadow(⊙o⊙)1 小时前
Linux进程地址空间——钻入Linux内核架构性剖析 硬核手搓!
java·linux·运维·服务器·开发语言·c++
大明者省1 小时前
乌邦托服务器系统www不同文件夹bird、infra建立隔离的虚拟环境
linux·运维·服务器
kobe_OKOK_1 小时前
ubuntu server设置 NTP 服务器
linux·服务器·ubuntu
不会编程的懒洋洋1 小时前
VisionPro 中 直方图 CogHistogramTool
图像处理·人工智能·笔记·计算机视觉·机器视觉·visionpro·康耐视
zzzsde1 小时前
【Linux】信号处理(3)信号处理&&valatile关键字
linux·运维·服务器·开发语言·算法
小夏子_riotous1 小时前
Kubernetes学习路径——5. Kubernetes 实战入门:Namespace、Pod、Label、Deployment 与 Service 全解析
学习·贪心算法·kubernetes