IMX6ULL学习笔记(四) —— uboot 启动流程

IMX6ULL 学习笔记


version : v1.0 「2023.4.27」

author: Y.Z.T.

简介: 随记, 记录 I.MX6ULL 系列 SOC 的uboot 启动流程



2.3 Uboot启动流程

2.3.1 链接脚本 u-boot.lds

2.3.1.1 链接脚本

通过链接脚本可以找到程序的入口地址 , uboot的最终链接脚本是 u-boot.lds , 是通过编译boot生成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
. = 0x00000000;
. = ALIGN(4);
.text :
{
*(.__image_copy_start)
*(.vectors)
arch/arm/cpu/armv7/start.o (.text*)
*(.text*)
}
. = ALIGN(4);
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
. = ALIGN(4);
.data : {
*(.data*)
}
. = ALIGN(4);
. = .;
. = ALIGN(4);
.u_boot_list : {
KEEP(*(SORT(.u_boot_list*)));
}
. = ALIGN(4);
.image_copy_end :
{
*(.__image_copy_end)
}
.rel_dyn_start :
{
*(.__rel_dyn_start)
}
.rel.dyn : {
*(.rel*)
}
.rel_dyn_end :
{
*(.__rel_dyn_end)
}
.end :
{
*(.__end)
}
_image_binary_end = .;
. = ALIGN(4096);
.mmutable : {
*(.mmutable)
}
.bss_start __rel_dyn_start (OVERLAY) : {
KEEP(*(.__bss_start));
__bss_base = .;
}
.bss __bss_base (OVERLAY) : {
*(.bss*)
. = ALIGN(4);
__bss_limit = .;
}
.bss_end __bss_limit (OVERLAY) : {
KEEP(*(.__bss_end));
}
.dynsym _image_binary_end : { *(.dynsym) }
.dynbss : { *(.dynbss) }
.dynstr : { *(.dynstr*) }
.dynamic : { *(.dynamic*) }
.plt : { *(.plt*) }
.interp : { *(.interp*) }
.gnu.hash : { *(.gnu.hash) }
.gnu : { *(.gnu*) }
.ARM.exidx : { *(.ARM.exidx*) }
.gnu.linkonce.armexidx : { *(.gnu.linkonce.armexidx.*) }
}

  • 其中 ENTRY(_start) 是整个函数的入口,_startarch/arm/lib/vectors.S 中有定义
  • 注意.text 代码段的内容
    • __image_copy_start ( uboot拷贝的首地址 )
    • vectors 段用于保存 中断向量表
    • arch/arm/cpu/armv7/start.o (.text*) 意思是将 arch/arm/cpu/armv7/start.o 编译出来的代码放到中断向量表后面
    • *(.text*) 用于存放其他的代码段

与地址有关的变量

变量 数值 描述
__image_copy_start 0x87800000 uboot 拷贝的首地址
__image_copy_end 0x8785dd54 uboot 拷贝的结束地址
__rel_dyn_start 0x8785dd54 .rel.dyn 段起始地址
__rel_dyn_end 0x878668f4 .rel.dyn 段结束地址
_image_binary_end 0x878668f4 镜像结束地址
__bss_start 0x8785dd54 .bss 段起始地址
__bss_end 0x878a8e74 .bss 段结束地址

除了__image_copy_start 的值 , 其他变量 每次编译的时候可能会变化,如果修改了 uboot 代码、修改了 uboot 配置、选用不同的优化等级等等都会影响到这些值。



2.3.1.2 vectors.S ( 入口点:_start )
  • 代码当前入口点:_star 存放在 文件 arch/arm/lib/vectors.S

image-20230511210751928


  • _start 后面就是中断向量表
  • .section ".vectors", "ax" 可以知道 , 中断向量表这部分代码是存放在 .vectors 段里面

2.3.1.3 映射文件 u-boot.map
  • u-boot.mapuboot 的映射文件,可以从此文件看到某个文件或者函数链接到了哪个地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
段 .text 的地址设置为 0x87800000
0x0000000000000000 . = 0x0
0x0000000000000000 . = ALIGN (0x4)

.text 0x0000000087800000 0x3cd64
*(.__image_copy_start)
.__image_copy_start
0x0000000087800000 0x0 arch/arm/lib/built-in.o
0x0000000087800000 __image_copy_start
*(.vectors)
.vectors 0x0000000087800000 0x300 arch/arm/lib/built-in.o
0x0000000087800000 _start
0x0000000087800020 _undefined_instruction
0x0000000087800024 _software_interruptp
0x0000000087800028 _prefetch_abort
0x000000008780002c _data_abort
0x0000000087800030 _not_used
0x0000000087800034 _irq
0x0000000087800038 _fiq
0x0000000087800040 IRQ_STACK_START_IN
arch/arm/cpu/armv7/start.o(.text*)
.text 0x0000000087800300 0xb0 arch/arm/cpu/armv7/start.o
0x0000000087800300 reset
0x0000000087800304 save_boot_params_ret
0x0000000087800340 c_runtime_cpu_setup
0x0000000087800350 save_boot_params
0x0000000087800354 cpu_init_cp15
0x00000000878003a8 cpu_init_crit
*(.text*)
.text 0x00000000878003b0 0x24 arch/arm/cpu/armv7/built-in.o
0x00000000878003b0 lowlevel_init
  • 可以看到 .text 的起始地址为 0x87800000
  • 镜像启动地址 (.__image_copy_start) 也是 0x87800000
  • vectors段 的起始地址也是 0x87800000
  • vectors 段 后面则是 arch/arm/cpu/armv7/start.s 和 其他代码段的内容


2.3.2 U-boot启动流程

Uboot 的启动流程可以大致分成两个阶段 :

  • 第一阶段多使用汇编 , 主要完成一些板级的硬件初始化 , 如外设硬件初始化 , 如 DRAM , 串口 , 重定位等。
  • 第二阶段通常使用C语言来实现 , 方便实现更加复杂的功能 , 主要完成linux 内核 的启动


2.3.2.0 补充信息
2.3.2.0.1 ENTRY() 和 ENDPROC() 宏

使用ENTRYENDPROC两个宏来定义一个名为name的函数 , 这个伪指令实现了指定一个入口的同时数据对齐,同时提供了一个函数入口 :

1
2
3
ENTRY(name)
...
ENDPROC(name)

这两个宏定义在#include <linux/linkage.h>

1
2
3
4
5
6
7
8
9
.globl  save_boot_params
.align 4 @4字节对齐
save_boot_params:
bx lr @ 带模式的返回

.type save_boot_params STT_FUNC; @ 说明该标识是函数
.size save_boot_params, .-save_boot_params @ 计算整个函数的大小

.weak save_boot_params @ 弱标号,如果别处有使用别处的定义,如果没有使用当前定义


2.3.2.0.2 为什么选择SVC 模式

通过 设置CPSR寄存器 的bit0 ~ bit4 五位来设置 处理器的工作模式

如下表所示:

image-20230515173401632

在uboot的启动流程中选择SVC 模式

  • 7种模式中,除用户usr模式外,其它模式均为 特权模式

  • 中止ABT和未定义UND模式

    • 因为此时程序是正常运行的 , 所以不应该设置CPU为这两种模式的其中任何一种
  • 快中断FIQ和中断IRQ模式

    • 对于快中断FIQ和中断IRQ来说,此处uboot初始化的时候,中断已经被禁用
    • 即使是注册了终端服务程序后,能够处理中断,那么这两种模式,也是自动切换过去的
    • 所以,此处也不应该设置为这两种模式中的其中任何一种模式
  • 用户USR模式

    • 访问uboot初始化,就必须很多的硬件资源 , 而用户模式 USR非特权模式 不能访问系统所有资源, 所以CPU也不能设置成USR
  • 系统SYS模式 和 管理SVC模式

    • SYS模式和USR模式相比,所用的寄存器组,都是一样的,但是增加了一些访问一些在USR模式下不能访问的资源
    • SVC模式本身就属于特权模式,本身就可以访问那些受控资源 , 相比 SYS 多了 专属寄存器 R13(sp)R14(lr) 以及 备份程序状态寄存器SPSR_svc
    • 所以 , 相对SYS模式来说,可以 访问资源的能力相同,但是拥有 更多的硬件资源
    • 因为在初始化 uboot 的过程中 , 要做的事情是初始化系统相关硬件资源,需要 获取尽量多的权限,以方便操作硬件,初始化硬件 , 所以最终选择 SVC模式


2.3.2.0.3 条件执行指令

为了提高代码密度,减少ARM指令的数量,几乎所有的ARM指令都可以根据CPSR寄存器中的标志位,通过指令组合实现条件执行。

 如:

  • 无条件跳转指令B,我们可以在后面加上条件码组成BEQBNE组合指令。
  • BEQ指令表示两个数比较,结果相等时跳转;
  • BNE指令则表示结果不相等时跳转
  • bicne 指令表示 标志位Z= 0 的时候 , 执行清零指令 bic

ARM指令的条件码

image-20220910151306942


BL跳转指令

格式 : BL{条件} 目标地址

作用 :

  • 但跳转之前,会在寄存器RL(即R14)中保存PC的当前内容
  • BL指令一般用在函数调用的场合

1
2
BL Label  ;当程序无条件跳转到标号Label处执行时,同时将当前的PC值保存到R14中
... ; 子程序返回后接着从此处继续执行

2.3.2.0.4 CP15协处理器

CP15 协处理器一般用于存储系统管理,但是在中断中也会使用到,CP15 协处理器一共有

16 个 32 位寄存器 ( c0~c15 )CP15 协处理器的访问通过如下另个指令完成:

  • MRC:CP15 协处理器中的寄存器数据读到 ARM 寄存器中

  • MCR:ARM 寄存器的数据写入到 CP15 协处理器寄存器中

    1
    MCR{cond} p15, <opc1>, <Rt>, <CRn>, <CRm>, <opc2>
    • cond: 指令执行的条件码,如果忽略的话就表示无条件执行
    • opc1:协处理器要执行的操作码
    • RtARM 源寄存器,要写入到 CP15 寄存器的数据就保存在此寄存器中
    • CRnCP15 协处理器的目标寄存器
    • CRm: 协处理器中附加的目标寄存器或者源操作数寄存器,如果不需要附加信息就将CRm 设置为 C0,否则结果不可预测
    • opc2:可选的协处理器特定操作码,当不需要的时候要设置为 0

例:CP15C0 寄存器的值读取到 R0 寄存器中,

1
MRC p15, 0, r0, c0, c0, 0

其中四个寄存器

  • 通过 c0 寄存器可以获取到处理器内核信息
  • 通过 c1 寄存器可以使能或禁止 MMU、I/D Cache 等;
  • 通过 c12 寄存器可以设置中断向量偏移 ( 如设置中断向量表偏移的时候就需要将新的中断向量表基地址写入 VBAR 中 )
  • 通过 c15 寄存器可以获取 GIC (中断控制器) 基地址

例 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* 设置中断向量表:
* (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))

/* 在CP15 SCTLR寄存器中设置V=0,并 用 VBAR 重新定位向量表 */
mrc p15, 0, r0, c1, c0, 0 @ 将CP15协处理器的 C1寄存器值读到r0寄存器
bic r0, #CR_V @ 将SCTLR寄存器的bit13位V 清零 , (即此时向量表基地址为 0X00000000,软件可以重定位向量表)
mcr p15, 0, r0, c1, c0, 0 @ 将CP15协处理器的 C1寄存器值写到r0寄存器

/* 在CP15 VBAR寄存器中设置向量地址 */
ldr r0, =_start
mcr p15, 0, r0, c12, c0, 0 @重定位向量表 将VBAR寄存器值设置为 _start , 即整个uboot 的入口地址
#endif



2.3.2.1 阶段1 : 初始化外设硬件
2.3.2.1.1 uboot程序入口点 __start

位置: arch/arm/lib/vectors.S

上电启动后,代码执行到 _start 函数,调用 reset 函数

reset 的函数目的是将处理器设置为SVC模式,并且关闭FIQIRQ,然后设置中断向量以及初始化 CP15 协处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_start:

#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
.word CONFIG_SYS_DV_NOR_BOOT_CFG
#endif

b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
  • 下面的 8~14 行 就是是 中断向量表

  • 可以看到 直接跳到reset 函数 (reset 函数直接跳转到save_boot_params 函数)

    1
    2
    3
    reset:
    /* Allow the board to save important registers */
    b save_boot_params
  • save_boot_params 也同样是直接跳转到 save_boot_params_ret 函数



2.3.2.1.2 save_boot_params_ret函数

位置 : arch/arm/lib/vectors.S


save_boot_params_ret 函数主要完成以下功能:

  • 当前处理器模式不为 HYP模式时 , 将处理器模式设置为 SVC模式 ,并禁用IRQFIQ两个中断
  • 重定位 中断向量表 ,将其定位到uboot 的起始地址 ( 这里取0x8780 0000)
  • 调用cpu_init_cp15 函数 , 设置其他和CP15有关的设置(cache, MMU, tlb) , 打开I-cache
  • 调用 cpu_init_crit 函数 , 并最终生成一个属于 IMX6ULL 内部RAM的临时堆栈
  • 调用 _main函数 ,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
save_boot_params_ret:
/*
* 当前系统不处于 HYP 模式时
* 禁用中断(FIQ和IRQ),也将cpu设置为SVC (管理)模式
*/
mrs r0, cpsr @ 读cpsr的值 , 并保存到 r0寄存器中
and r1, r0, #0x1f @ 使用位与操作 , 提取 CPSR寄存器的 bit0 ~ bit4 四位, 即用于设置 处理器工作模式的四位
teq r1, #0x1a @ 检查当前是否是 HYP模式 , 使用teq将 r1 与 0x1a进行异或运算 ,并将结果更新 CPSR标志位
bicne r0, r0, #0x1f @ 当 CPSR寄存器的标志位Z != 1 (即之前运算结果不为0 , 即不处于HYP模式),清除r0的低5位
orrne r0, r0, #0x13 @ 设置处理器模式为 SVC模式
orr r0, r0, #0xc0 @ 禁用 FIQ 和 IRQ (SPCR寄存器的 I为和F位 控制IRQ和FIQ,设置为1则禁用)
msr cpsr,r0 @ 将寄存器的值写回CPSR

/*
* 设置中断向量表
* c1寄存器 的bit13位是 'V' (向量表控制位),
* 为0时,向量表基地址为0x00000000(可重定位),
* 为1时,向量表基地址为0xFFFF0000(不可重定位)
*/
#if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))
mrc p15, 0, r0, c1, c0, 0 @ 读取CP15协处理器的c1寄存器,即SCTLR
bic r0, #CR_V @ CR_V = (1 << 13) 所以是清除c1寄存器 的bit13位(V)
mcr p15, 0, r0, c1, c0, 0 @ 写SCTLR

/* 在CP15协处理器的 VBAR寄存器(C12)中 设置向量表的重定位地址 , */
ldr r0, =_start @ 设置向量表的重定位地址 , 即整个uboot起始地址 (0x8780 0000)
mcr p15, 0, r0, c12, c0, 0 @ 将r0的值写入 VBAR
#endif

/* the mask ROM code should have PLL and others stable */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_cp15 @ 调用cpu_init_cp15函数, 用来设置和CP15有关的设置(cache, MMU, tlb),打开I-cache
bl cpu_init_crit @ 调用cpu_init_crit函数 , 再调用lowlevel_init函数
#endif

bl _main @ 调用_main函数,
  • 在第33行 , 调用cpu_init_crit , 这个函数内部仅仅调用了lowlevel_init函数
  • lowlevel_init 用于创建一个IMX6ULL内部的 临时堆栈

 补充:

🅰️ SCTLR寄存器 ( CP15c1寄存器)

20211223110110


🅱️ save_boot_params_ret 函数调用路径


image-20230515204430974



2.3.2.1.3 lowlevel_init函数

位置 : arch/arm/cpu/armv7/lowlevel_init.S


lowlev el_init 函数主要完成如下功能

  • 初始化一个临时堆栈 , 这个堆栈属于 IMX6ULL的内部 RAM
  • 设置r9寄存器 , 用于保存 GD结构体的基地址
  • 这个临时堆栈 , 保留了 Global dataGBL_DATA 的地址位置
  • 调用早期初始化函数 s_init , 但对于IMX6ULL来说相当于 空函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ENTRY(lowlevel_init)

/* 设置一个临时堆栈, 暂时还没有Global data(全局数据GD) , 但留出GD的大小*/
ldr sp, =CONFIG_SYS_INIT_SP_ADDR @ 将sp指针指向 系统初始化指针地址(0X0091FF00) ,定义如后文
bic sp, sp, #7 @ 对sp指针进行8字节对齐 ,对齐原理如后文所示

#ifdef CONFIG_SPL_DM
mov r9, #0 @条件编译不成立 , 未使用
#else

/* 预留出全局数据(GD)的大小 */
#ifdef CONFIG_SPL_BUILD
ldr r9, =gdata
#else
sub sp, sp, #GD_SIZE @ 将sp指针减去 GD的大小(GD_SIZE = 248)
bic sp, sp, #7 @ 将指针进行8字节对齐 (此时SP = 0X0091FF00-248=0X0091FE08)
mov r9, sp @ 将SP指针地址保存在 r9寄存器, 此时r9保存着 dg 结构体的基地址
#endif
#endif

/* 将旧的lr(通过ip传递)和当前的lr保存到堆栈中 */
push {ip, lr} @ 将ip和lr压栈

/* s_init:
* 调用最早期的init函数。这应该只做最基本的初始化,它不应该做以下的事情:
*
* - 设置DRAM
* - 使用全局数据(global_data)
* - 清除BSS段
* - 尝试启动控制台
*/
bl s_init @ 调用s_init , 对于 IMX6ULL来说是空函数
pop {ip, pc} @ 将lr出栈并赋给pc,将ip出栈赋给ip
ENDPROC(lowlevel_init)


补充:

🅰️ 宏CONFIG_SYS_INIT_SP_OFFSET和 宏 CONFIG_SYS_INIT_SP_ADDR 计算:

这两个宏定义如下:

1
2
3
4
5
6
7
#define CONFIG_SYS_INIT_RAM_ADDR     IRAM_BASE_ADDR       (这里是RAM基地址, 取0x00900000)
#define CONFIG_SYS_INIT_RAM_SIZE IRAM_SIZE (这里是RAM的大小, 取0X20000 = 128KB)

#define CONFIG_SYS_INIT_SP_OFFSET \ (值取 0x1FF00)
(CONFIG_SYS_INIT_RAM_SIZE - GENERATED_GBL_DATA_SIZE) (GENERATED_GBL_DATA_SIZE = 256)
#define CONFIG_SYS_INIT_SP_ADDR \ (值取 0X0091FF00)
(CONFIG_SYS_INIT_RAM_ADDR + CONFIG_SYS_INIT_SP_OFFSET)
  • IRAM_BASE_ADDRIRAM_SIZE 两个宏都定义在 arch/arm/include/asm/arch-mx6/imx-regs.h

  • GENERATED_GBL_DATA_SIZE 宏定义在 include/generated/generic-asm-offsets.h

  • GENERATED_GBL_DATA_SIZE 的含义为 (sizeof(struct global_data) + 15) & ~15

  • 则可以得到如下值

    1
    2
    3
    CONFIG_SYS_INIT_RAM_ADDR = IRAM_BASE_ADDR = 0x00900000
    CONFIG_SYS_INIT_RAM_SIZE = 0x00020000 =128KB
    GENERATED_GBL_DATA_SIZE = 256
  • 计算可得:

    1
    2
    CONFIG_SYS_INIT_SP_OFFSET = 0x00020000 – 256 = 0x1FF00。
    CONFIG_SYS_INIT_SP_ADDR = 0x00900000 + 0X1FF00 = 0X0091FF00,

🅱️ sp指针8位对齐

1
bic sp, sp, #7     @ sp指针8位对齐
  • 实现 8位对齐 的原理就是将最低三位清零因为 #7 对应 (0111),清除后就可以被 8 (1000)整除
  • 不过前提是栈地址要 向下生长 ( FD | ED),这样被清除的地址不会与数据冲突

🆎 此时的堆栈内存情况

image-20230515221338851


🔤 s_init 函数

位置: arch/arm/cpu/armv7/mx6/soc.c

  • s_init函数里面 , 代码会判断CPU类型

  • 如果 CPU 为 MX6SX、MX6UL、MX6ULL 或 MX6SLL 中的任意 一 种 , 那么就会直接返回

    1
    2
    3
    if (is_cpu_type(MXC_CPU_MX6SX) || is_cpu_type(MXC_CPU_MX6UL) ||
    is_cpu_type(MXC_CPU_MX6ULL) || is_cpu_type(MXC_CPU_MX6SLL))
    return;
  • 所以对 I.MX6UL/I.MX6ULL 来说,s_init 就是个空函数


2.3.2.1.4 _main函数

位置 : arch/arm/lib/crt0.S

_main 函数主要完成以下功能

  • 初始化C语言运行环境、堆栈设置
  • 各种板级设备初始化、初始化NAND FlashSDRAM
  • 初始化全局结构体变量GD,在GD里有U-boot实际加载地址
  • 调用relocate_code,将U-boot镜像从Flash复制到RAM
  • Flash跳到内存RAM中继续执行程序
  • BSS段清零,跳入bootcmdmain_loop交互模式

_main 执行顺序 :

  • 设置 可以调用board_init_f() 的初始C运行环境

    • 这个运行环境只提供 一个堆栈 和一个用来存储GD (global data) 结构体的 位置
    • 堆栈和储存位置都位于 RAM中( 如SRAM , 锁定缓存等)中 , 在这种情况下, 变量 GD 无论是否初始化(BSS段) 都不能使用
    • 只有 常量初始化的数据才能可用 , GD 在被 board_init_f() 调用前 ,应该先被清零 ( 调用board_init_f_init_reserve 函数 清零GD)
  • 调用board_init_f() 函数

    • 这个函数 从系统 外部 RAM (如DRAM, DDR …) 执行硬件准备 , 初始化一系列外设,比如串口、定时器,或者打印一些消息等
    • 因为此时 , 系统RAM还不可用 , board_init_f() 函数必须 使用当前的GD 变量来储存必须传递到后续阶段的 任何数据 , 所以 初始化 gd 的各个成员变量
    • 这些数据包括 : 重定位的 目的地址未来的堆栈未来的GD的内存位置 , 在DRAM最后部分预留各数据的内存空间 (如 ubootmallocgdbd等) , 最终一个完整的内存 分配图 , 在后面重定位 uboot时 使用
  • 设置中间环境 , 在DRAM的最后预留 各数据的内存空间 ,方便后面重定位

    • 其中 堆栈GD 是由board_init_f()在系统RAM (DRAM) 中分配的 ,
    • 但是BSS段和已初始化的 非const数据仍不可用
  • 调用 relocate_code 函数对uboot 进行真正的数据拷贝 和重定位 (不是 SPL)

    • 这个函数将U-Boot从当前位置 (片上RAM) 重新定位到由board_init_f()计算的重定位目的地 (DDR)
    • 对于SPL, board_init_f()只返回(到crt0)。在SPL中没有代码重定位。
  • 设置 能够 调用board_init_r()的最终环境 , 这个环境 存在以下条件 :

    • BSS段 ( 已初始化为0 )
    • 已初始化的非 const 数据 ( 初始化为预期值)
    • 在DRAM 上的堆栈
    • GD 保留了board_init_f() 设置的值
  • 调用 c_runtime_cpu_setup 函数 设置关于 CPU 此时的一些 内存设置

  • 调用board_init_r函数

    • 进行一些后续的初始化操作 , 如初始化 emmc、中断、环境变量等
    • board_init_r 中读取 uboot控制台指令 ,或跳转到系统内核运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
ENTRY(_main)

/* 设置初始C运行时环境并调用board_init_f(0) */
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr sp, =(CONFIG_SPL_STACK)
#else
ldr sp, =(CONFIG_SYS_INIT_SP_ADDR) @ 设置sp指针指向CONFIG_SYS_INIT_SP_ADDR (即0x0091FF00)
#endif
#if defined(CONFIG_CPU_V7M) @ 条件不成立
mov r3, sp
bic r3, r3, #7
mov sp, r3
#else
bic sp, sp, #7 @ sp指针8字节对齐
#endif
mov r0, sp @ 将sp保存到r0寄存器 , 此时r0 = 0x0091FF00
bl board_init_f_alloc_reserve @ (具体见补充 '1' ) 参数通过r0传递,作用是留出早期的 malloc 内存区域和 gd 内存区域
mov sp, r0 @ r0保存着返回值,将sp 设置为返回值 , 即0x0091FA00

/* 在这里设置 gd的值, 在所有c代码之外 */
mov r9, r0 @ (具体见 补充 '2') 设置gd (r9)指向 0x0091FA00(r0),因为 r9 寄存器存放着全局变量 gd 的地址
bl board_init_f_init_reserve @ (具体见 补充 '3')用于初始化 gd,即清零处理 , 设置early malloc起始地址 为 (gd即地址 + gd的大小)

mov r0, #0 @ 设置r0为0 ,用于传递参数0 方便后面调用 board_init_f(0) 即形参boot_flags = 0,
bl board_init_f @ (具体见'2.3.2.1.5'小节)初始化DDR, 定时器,串口, 预留各数据在DRAM中的内存空间等,

#if ! defined(CONFIG_SPL_BUILD)

/* 设置中间环境(新的sp和gd)并调用 relocate_code (addr_moni)
* 最后设置 lr 寄存器为 here ,后面执行其他函数如relocate_code 等, 返回的话 就会返回到here这个地址
*/
ldr sp, [r9, #GD_START_ADDR_SP] @ sp = r9 + GD_START_ADDR_SP 即(sp = gd->start_addr_sp) ,因为r9是 结构体gd的基地址
@ gd->start_addr_sp = 0x9EF44E90 ,这是属于DDR的地址,说明新的sp和gd放在ddr中而不是内部RAM

#if defined(CONFIG_CPU_V7M) @ 条件不成立
mov r3, sp
bic r3, r3, #7
mov sp, r3
#else
bic sp, sp, #7 @ sp做8字节对齐
#endif
ldr r9, [r9, #GD_BD] @(具体见 补充 '4')将 gd->bd 的数据读入r9寄存器, r9存放的是就的gd基地址,通过 gd->bd计算新的gd地址
sub r9, r9, #GD_SIZE @ 计算 gd 的新地址

adr lr, here @ 将 lr 寄存器 赋值为 here , 这样后面执行其他函数返回的时候就返回到 下面53行here符号的地方
ldr r0, [r9, #GD_RELOC_OFF] @ r0 = gd->reloc_off GD_RELOC_OFF = 68
add lr, lr, r0 @ 因为要重定位代码, 要把uboot拷贝到DDR的最后空间去 , 所以lr 中的here要使用重定位后的位置
#if defined(CONFIG_CPU_V7M)
orr lr, #1 @ 条件不成立 , 这行不运行
#endif
ldr r0, [r9, #GD_RELOCADDR] @ (r0 = gd->relocaddr, relocaddr保存uboot的目的地址)赋值后 , r0保存着 uboot 要拷贝的目的地址
b relocate_code @ (具体见 '2.3.2.1.7' 小节)调用relocate_code 代码重定位函数 , 赋值将uboot 拷贝到新的地址
here:

/* 开始重定位向量表 */
bl relocate_vectors @ (具体见 '2.3.2.1.8' 小节)调用 relocate_vectors ,重定位中断向量表

/* 设置最终(完整)环境 */

bl c_runtime_cpu_setup @ 配置协处理器 ,关闭icache
#endif

#if !defined(CONFIG_SPL_BUILD) || defined(CONFIG_SPL_FRAMEWORK) @条件不成立
# ifdef CONFIG_SPL_BUILD
/* 如果请求,使用DRAM堆栈为其余的SPL堆栈 */
bl spl_relocate_stack_gd
cmp r0, #0
movne sp, r0
movne r9, r0
# endif

/********************************* 下面这段代码用于清除 BSS段 ********************************/
ldr r0, =__bss_start /* bss段开始地址*/

#ifdef CONFIG_USE_ARCH_MEMSET
ldr r3, =__bss_end /* bss段结束地址 */
mov r1, #0x00000000 /* 将r1 赋 零用于清除 bss段 */

subs r2, r3, r0 /* r2 = r3-r0 , r2为bss段的长度 */
bl memset
#else
ldr r1, =__bss_end /* this is auto-relocated! */
mov r2, #0x00000000 /* prepare zero to clear BSS */

clbss_l:cmp r0, r1 /* while not at end of BSS */
#if defined(CONFIG_CPU_V7M)
itt lo
#endif
strlo r2, [r0] /* clear 32-bit BSS word */
addlo r0, r0, #4 /* move to next */
blo clbss_l
#endif

/**********************************************************************/

#if ! defined(CONFIG_SPL_BUILD)
bl coloured_LED_init
bl red_led_on
#endif
/* 设置 board_init_r(gd_t *id, ulong dest_addr) 两个参数 ,用r0、r1传参 */
mov r0, r9 @ 第一个参数是gd , 所以读取r9保存到r0
ldr r1, [r9, #GD_RELOCADDR] @ 第二个参数是目的地址 , 所以 r1= gd->relocaddr

/* 调用 board_init_r 函数*/
#if defined(CONFIG_SYS_THUMB_BUILD) @条件不成立
ldr lr, =board_init_r
bx lr
#else
ldr pc, =board_init_r @ (具体见 '2.3.2.1.9' 小节5)调用board_init_r函数 ,继续完成初始化工作
#endif
/* we should not return here. */
#endif

ENDPROC(_main)

补充:

1️⃣ board_init_f_alloc_reserve 函数

位置 : common/init/board_init.c

函数功能如下:

  • 留出早期的 malloc 内存区域和 gd 内存区域

1
2
3
4
5
6
7
8
9
10
11
ulong board_init_f_alloc_reserve(ulong top)
{
/* 预留早期 malloc的内存区域 */
#if defined(CONFIG_SYS_MALLOC_F)
top -= CONFIG_SYS_MALLOC_F_LEN;
#endif
/* LAST : 保留 GD 内存区域(四舍五入到16字节的倍数) */
top = rounddown(top-sizeof(struct global_data), 16);

return top;
}
  • 其中CONFIG_SYS_MALLOC_F_LEN=0X400

  • sizeof(struct global_data)=248 ( GD_SIZE 值)

  • 完成后的 内存分配 :

    image-20230516151408214



2️⃣ 全局变量 global_data(gd)

uboot 中定义了一个指向 gd_t 的指针 gdgd 存放在寄存器 r9 里面 ,因此 gd是个 全局变量

1
2
3
4
5
#ifdef CONFIG_ARM64
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("x18")
#else
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r9")
#endif

gd_t结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct global_data {
bd_t *bd;
unsigned long flags;
unsigned int baudrate;
unsigned long cpu_clk; /* CPU clock in Hz! */
unsigned long bus_clk;
/* We cannot bracket this with CONFIG_PCI due to mpc5xxx */
unsigned long pci_clk;
unsigned long mem_clk;
#if defined(CONFIG_LCD) || defined(CONFIG_VIDEO)
unsigned long fb_base; /* Base address of framebuffer mem */
#endif
//.......................//

#ifdef CONFIG_DM_VIDEO
ulong video_top; /* Top of video frame buffer area */
ulong video_bottom; /* Bottom of video frame buffer area */
#endif
}gd_t;


3️⃣ board_init_f_init_reserve 函数

位置 : common/init/board_init.c

功能 :

  • 用于初始化 gd , 即清零处理
  • 设置 gd->malloc_basegd 基地址 + gd 大小=0X0091FA00+248=0X0091FAF8
  • 并做16字节对齐 , 最终gd->malloc_base=0X0091FB00,这个也就是 early malloc起始地址


4️⃣ 通过 gd->bd 计算新的 gd 地址

涉及的代码

1
2
ldr	r9, [r9, #GD_BD]				// 获取gd -> bd的地址
sub r9, r9, #GD_SIZE // 减去bd 结构体占用的空间 即为 gd结构体的空间

通过前文 , 可以得到如下信息 :

  • r9寄存器存放的是 一个指向 gd_t结构体 的指针 gd , 即r9寄存器存放的是 gd 数据结构体 旧的 基地址 ( 片上 RAM, 不是DRAM)

  • 板信息 bdgd结构体的第一个成员 , 即 gd -> bd首地址 与 gd结构体的 基地址 (即r9寄存器保存的值) 是一致的

    1
    2
    3
    4
    5
    typedef struct global_data {
    bd_t *bd;
    unsigned long flags;
    ...
    }gd_t;
  • gd结构体的 gd -> bd 成员在 调用 board_init_f 函数的时候 就已经被重定位在 DRAM上了 ( 即 新地址 )

    1
    2
    3
    4
    5
    6
    7
    8
    if (initcall_run_list(init_sequence_f))		// 调用initcall_run_list函数来运行初始化序列
    ....

    static init_fnc_t init_sequence_f[] = { // init_sequence_f是initcall_run_list 的传入参数 (一个存放各个函数入口的数组)
    ...
    reserve_board, // 在DRAM留出板子 bd 所占的内存区 , 完成后 gd -> bd = 0X9EF44FB0
    ...
    }
  • 两个宏其中 GD_BD = 0 ; GD_SIZE = 248

    • 因为bd 是 gd结构体 的第一个成员 所以 gd -> bd = r9 + GD_BD
    • GD_SIZEgd结构体的大小 , 为248B
  • 为什么 gd->bd 减去 gd 的大小就是 新的 gd 的位置

    • 因为 gd新的地址 (即在DRAM中的地址) 是在 bd数据的下面 ( 即 低地址位置 )

    • 图为 调用 board_init_f 函数后 在DRAM 中的 内存空间图

      image-20230517172742492


5️⃣


2.3.2.1.5 board_init_f 函数

位置 : common/board_f.c

功能 :

  • 初始化一系列外设,比如串口、定时器,或者打印一些消息等

  • 初始化 gd 的各个成员变量, 将ubootDRAM 最后面的地址区域 预留区域 , 方便后面拷贝

    • 因为本质上 ubootlinux 的引导文件,引导完成后 linux 会在 DRAM 前面的地址区域启动
    • 为了防止 linux 启动后对 uboot 进行干扰,uboot 会将自己重定位到 DRAM 最后面的地址区域
    • 拷贝之前需要给 uboot 各部分分配好内存位置和大小 ,比如 gd 应该存放到哪个位置,malloc 内存池应该存放到哪个位置等
    • 这些信息都保存在 gd成员变量 中,因此首先要对 gd 的这些成员变量做初始化
  • DRAM最后部分预留各数据的内存空间 (如 ubootmallocgdbd等) , 最终一个完整的内存 分配图 , 在后面重定位 uboot时 使用


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void board_init_f(ulong boot_flags)
{
#ifdef CONFIG_SYS_GENERIC_GLOBAL_DATA // 条件不成立
/*
* 对于某些架构来说, 全局变量在调用这个函数之前就被被初始化和使用,所以应该保存全局变量的数据
* 对于这些架构,应该定义CONFIG_SYS_GENERIC_GLOBAL_DATA这个宏,并在重定位之前使用这里的堆栈来承载全局数据
*/
gd_t data;

gd = &data;

zero_global_data();
#endif

gd->flags = boot_flags; // 初始化 gd->flags=boot_flags=0
gd->have_console = 0;

if (initcall_run_list(init_sequence_f)) // (具体见 补充'2.3.2.1.6'小节)通过 initcall_run_list函数来运行初始化序列 , 传入参数是init_sequence_f (一个存放各个函数入口的数组)
hang();

#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
!defined(CONFIG_EFI_APP)
/* NOTREACHED - jump_to_copy() does not return */
hang();
#endif

/* Light up LED1 */
imx6_light_up_led1();
}
  • 形参 boot_flags 是通过 r0传递的 , r0 = boot_flags = 0
  • 调用initcall_run_list函数来运行 初始化序列 , 传入参数是init_sequence_f (一个存放各个函数入口的数组)


2.3.2.1.6 init_sequence_f 数组

位置 : common/board_f.c

功能 :

  • 是一个存放了各个函数入口的数组
  • 通过initcall_run_list 来一系列初始化序列 , 设置GD的各个成员的值
  • 用于初始化一系列外设,比如串口、定时器,或者打印一些消息等

去除条件编译后的init_sequence_f如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
static init_fnc_t init_sequence_f[] = {
setup_mon_len, // 设置gd->mon_len ,此处为 __bss_end -_start = 0xA8E74, 即整个代码的长度
initf_malloc, // 数初始化gd中跟malloc有关的成员变量,比如 malloc_limit (malloc内存池大小), 这里会设置
initf_console_record, // 对于 IMX6ULL来说 是空函数
arch_cpu_init, // 基本的arch CPU相关设置
initf_dm, // 驱动模型的一些初始化
arch_cpu_init_dm, // 函数未实现
mark_bootstage, // 设置某些标记
board_early_init_f, // 板子相关的
timer_init, // 初始化定时器
board_postclk_init, // 对于 I.MX6ULL 来说是设置 VDDSOC 电压
get_clocks, // get_clocks 函数用于获取一些时钟值,I.MX6ULL 获取的是 sdhc_clk 时钟(即SD卡外设时钟)
env_init, // 设置 gd 的env_addr成员,即环境变量的保存地址
init_baud_rate, // 初始化波特率,根据环境变量baudrate来初始化 gd->baudrate
serial_init, // 初始化串口
console_init_f, // 设置 gd->have_console 为 1,表示有个控制台,同时将之前暂存在缓冲区中的数据通过控制台打印出来
display_options, // 通过串口输出一些信息, 这里是uboot 的版本信息
display_text_info, // 打印一些文本信息,如果开启 UBOOT 的 DEBUG 功能的话就会输出 text_base、bss_start、bss_end
print_cpuinfo, // 打印CPU信息(和运行速度)
show_board_info, // 用于打印板子信息
INIT_FUNC_WATCHDOG_INIT // 初始化看门狗,对于 I.MX6ULL 来说是空函数
INIT_FUNC_WATCHDOG_RESET// 复位看门狗,对于 I.MX6ULL 来说是空函数
init_func_i2c, // 用于初始化 I2C
announce_dram_init, // 输出字符串 “DRAM:”
dram_init, // 配置可用RAM组,并非真正的初始化 DDR,只是设置gd->ram_size ,即DDR的大小(如 512MB)
post_init_f, // 完成一些测试,初始化 gd->post_init_f_time
INIT_FUNC_WATCHDOG_RESET// 复位看门狗,对于 I.MX6ULL 来说是空函数
testdram, // 测试 DRAM,空函数
INIT_FUNC_WATCHDOG_RESET// 复位看门狗,对于 I.MX6ULL 来说是空函数
INIT_FUNC_WATCHDOG_RESET// 复位看门狗,对于 I.MX6ULL 来说是空函数
/*
* 到这里为止 , 已经映射了DRAM并开始工作,
* 可以开始重定位代码 并继续从 DRAM 运行
*
* 在RAM末端预留内存(按顺序从上到下):
* - mmu的TLB表 (reserve_mmu = 0x4000 = 16KB , 64K字节对齐)
* - 跟踪调试的内存 (reserve_trace = 0)
* - uboot 所占用的内存区域 (reserve_uboot = 0xA8EF4 , 4K字节对齐)
* - malloc 区域 (reserve_malloc = 0x01002000 =16MB + 8KB)
* - 板子bd结构体的内存 (reserve_board = 80B)
* - `gd_t` 的内存区域 (240B)
* - 栈空间 (16字节对齐)
*/
setup_dest_addr, // (补充 '1')设置目的地址,设置gd->ram_size; gd->ram_top; gd->relocaddr 这三个值
reserve_round_4k, // 对 gd->relocaddr 做 4KB 对齐 , 这里的值0XA0000000,已经是 4K 对齐了,所以调整后不变
reserve_mmu, // (补充 '2')留出 MMU 的 TLB 表的位置, 分配完后会对 gd->relocaddr 做 64K 字节对齐
reserve_trace, // 留出跟踪调试的内存,I.MX6ULL 没有用到
reserve_uboot, // (补充 '3')留出重定位后的 uboot 所占用的内存区域, 大小由gd->mon_len 所指定, 分配完后做 4K字节对齐
reserve_malloc, // (补充 '4')留出 malloc 区域, 调整 gd->start_addr_sp 位置;malloc 区域由宏TOTAL_MALLOC_LEN定义
reserve_board, // (补充 '5')留出板子 bd 所占的内存区,bd 是结构体 bd_t,bd_t 大小为80字节 , 后续根据 gd->bd 计算出新的 gd 的位置 ,用于uboot重定位
setup_machine, // 设置机器 ID,linux 启动的时候会和这个机器 ID 匹配,如果匹配的话 linux 就会启动正常 ; IMX6ULL使用设备树,所以此函数无效
reserve_global_data, // (补充 '6')保留出 gd_t 的内存区域,gd_t 结构体大小为 248 字节
reserve_fdt, // 留出设备树相关的内存区域, I.MX6ULL 的 uboot 没有用到,所以此函数无效
reserve_arch, // 空函数
reserve_stacks, // (补充 '7')留出栈空间, 先对 gd->start_addr_sp 减去 16,然后做 16 字节对齐,如果使能IRQ的话也要留出对应内存 ,这里没有使用
setup_dram_config, // (补充 '8')设置gd->bd->bi_dram[0].start 和 gd->bd->bi_dram[0].size,后面会传递给 linux内核, 告诉 linux DRAM 的起始地址和大小
show_dram_config, // 显示 DRAM 的配置
display_new_sp, // 显示新的 sp 位置,即 gd->start_addr_sp 存放的值
INIT_FUNC_WATCHDOG_RESET
reloc_fdt, // 重定位 fdt,没有用到
setup_reloc, // (补充 '9') 设置 gd 的其他一些成员变量,供后面重定位的时候使用,并且将以前的 gd 拷贝到 gd->new_gd 处
NULL,
};


补充:

1️⃣ setup_dest_addr 函数

setup_dest_addr 函数 主要用于设置目的地址 , 主要用于输出以下三个 值:

  • gd->ram_size ( RAM的大小 ) 这里是 0x20000000 , 512MB
  • gd->ram_top ( RAM的最高地址 ) 这里是 0x80000000 + 0x20000000 = 0xA0000000
  • gd->relocaddr (重定位后的最高地址 ) 这里是0xA000000


2️⃣ reserve_mmu 函数

留出 MMUTLB 表的位置,分配 MMUTLB 表内存以后会对 gd->relocaddr 做 64K 字节对齐

完成之后的 gd->arch.tlb_sizegd->arch.tlb_addrgd->relocaddr 如下所示:

  • gd->arch.tlb_size : MMUTLB表大小 (这里为 0x4000)
  • gd->arch.tlb_addr : MMUTLB 表起始地址,64KB 对齐以后 ( 这里为 0x9FFF0000)
  • gd->relocaddr : relocaddr 地址 ( 这里为 0x9FFF0000)


3️⃣ reserve_uboot 函数

  • 留出重定位后的 uboot 所占用的内存区域 ,
  • uboot 所占用大小由gd->mon_len 所指定,留出 uboot 的空间以后还要对 gd->relocaddr 做 4K 字节对齐
  • 并且重新设置 gd->start_addr_sp

完成之后, gd->mon_len , gd->start_addr_sp , gd->relocaddr 如下所示:

  • gd->mon_len : uboot 所占的大小 ( 这里为 0xA8EF4)
  • gd->start_addr_sp : 重设 gd->start_addr_sp 指针 ( 这里为 0x9FF47000)
  • gd->relocaddr : relocaddr 地址 ( 这里为 0x9FF47000)


4️⃣ reserve_malloc 函数

  • reserve_malloc 函数 留出 malloc 区域,
  • 调整 gd->start_addr_sp 位置,
  • malloc 区域由宏TOTAL_MALLOC_LEN 定义

宏定义如下 :

1
#define TOTAL_MALLOC_LEN (CONFIG_SYS_MALLOC_LEN + CONFIG_ENV_SIZE)
  • CONFIG_SYS_MALLOC_LEN16MB=0X1000000
  • CONFIG_ENV_SIZE=8KB=0X2000
  • 因此 TOTAL_MALLOC_LEN=0X1002000 (即malloc 的区域大小为 0X1002000)

可以得到:

1
2
TOTAL_MALLOC_LEN=0X1002000
gd->start_addr_sp=0X9EF45000 @0X9FF47000-16MB-8KB=0X9EF45000


5️⃣ reserve_board 函数

  • reserve_board 函数,用于留出板子 bd 所占的内存区
  • bd 是结构体 bd_tbd_t 大小为 80字节

调整之后结果如下:

1
2
gd->start_addr_sp=0X9EF44FB0
gd->bd=0X9EF44FB0 @ 调用完board_init_f这个函数之后 , 这个根据gd->bd, 来获取重定位后 , 新的gd的地址


6️⃣ reserve_global_data 函数

保留出 gd_t 的内存区域,gd_t 结构体大小为 248B

完成后结果如下 :

1
2
gd->start_addr_sp=0X9EF44EB8    @0X9EF44FB0-248=0X9EF44EB8
gd->new_gd=0X9EF44EB8


7️⃣ reserve_stacks 函数

  • reserve_stacks 函数 用于 留出栈空间
  • 先对 gd->start_addr_sp 减去 16 , 然后做 16字节对齐
  • 如果使能 IRQ 的话还要留出 IRQ 相应的内存 , 这里不使能

完成后结果如下

1
gd->start_addr_sp=0X9EF44E90    


8️⃣ setup_dram_config 函数

  • setup_dram_config 函数 用于设置 dram信息
  • 即设置 gd->bd->bi_dram[0].startgd->bd->bi_dram[0].size 两个成员
  • 用于后续传递给 linux 内核 , 告诉linux DRAM 的起始地址和大小
1
2
gd->bd->bi_dram[0].start=0x80000000
gd->bd->bi_dram[0].size=0x20000000
  • 即传递给linux内核 , DRAM 的起始地址为 0x80000000 , 大小为 0X20000000(512MB)


9️⃣ setup_reloc 函数

  • setup_reloc 函数 用于设置 gd其他一些成员变量 , 供后面重定位的时候使用
  • 并将之前的 gd拷贝到 gd->new_gd处
  • 重定位后 , uboot 的新地址为 0X9FF4700 ;
  • 新的gd首地址为 0X9EF44EB8 ;
  • 新的 sp为0X9EF44E90


🔟 重定位后的内存分配图

image-20230517172742492



2.3.2.1.7 relocate_code 函数

位置 : arch/arm/lib/relocate.S

功能 :

  • 代码拷贝 , 将uboot 拷贝到 DDR中 ,即uboot重定位 到 DRAM的高地址
  • 重定位就是 uboot 将自身拷贝到 DRAM 的另一个地放去继续运行 (DRAM 的高地址处)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
ENTRY(relocate_code)

/************************************** 获取各个地址 ********************************************/

ldr r1, =__image_copy_start @ r1保存寄存器源地址 , 即0x8780 0000 , (__image_copy_start)在链接文件中 , 使用零长度数组标记代码段
subs r4, r0, r1 @ 保存偏移量 , r0为 gd->relocaddr = 0x9FF4 7000 (即uboot拷贝的首地址) r4 = r0-r1 为偏移量
beq relocate_done @ 判断r4是否为0, 即r0 - r1 运算结果 z = 0 ,如果是,则说明不用拷贝,直接执行relocate_done函数
ldr r2, =__image_copy_end @ r2=__image_copy_end, 使用r2保存 拷贝之前的代码结束地址 (片上RAM)

/*************************完成拷贝工作 , 拷贝 r1到r2这段地址的内容, 并写到目的地址 r0中去 *********************/
copy_loop:
ldmia r1!, {r10-r11} @ 从r1 开始 即(__image_copy_start) , 拷贝2个32位数据到 r10和r11中 , 拷贝完成后 ,r1的值会更新
stmia r0!, {r10-r11} @ 将r10和 r11的值写到目的地址 r0 即(gd->reloc_of), 写完后 , r0的值会更新
cmp r1, r2 @ 比较r1 和 r2是否相等 , 即确定是否拷贝完成
blo copy_loop @ 没有则跳转回 copy_loop 继续拷贝 (检查CPSR 寄存器C 标志位是否为0)


/*********************** 重定位.rel.dyn 段 , .rel.dyn 段是存放.text 段中需要重定位地址的集合 ***********/

ldr r2, =__rel_dyn_start @ r2 =__rel_dyn_start, 即 .rel.dyn 段的起始地址
ldr r3, =__rel_dyn_end @ r3 =__rel_dyn_end,
fixloop:
ldmia r2!, {r0-r1} @ 从起始地址开始 , 每次取两个 4字节数据放到r0和r1寄存器, r0存放低4字节(即Label 地址); r1存放高4字节(即Label 标志)
and r1, r1, #0xff @ 取r1的低8位
cmp r1, #23 @ 判断r1 中的值是否等于 23(0x17)
bne fixnext @ r1 不等于 23说明不是描述 Label的,执行fixnext,否则的话就继续执行下面的代码

/* relative fix: increase location by offset */
add r0, r0, r4 @ r0 保存着 Label 值,r4 保存着重定位后的地址偏移,r0+r4 就得到了重定位后的Label 值
ldr r1, [r0] @ 读取重定位后 Label 所保存的变量地址
add r1, r1, r4 @ r1+r4 可得到重定位后的变量地址 ,
str r1, [r0] @ 重定位后的变量地址写入到重定位后的 Label 中
fixnext:
cmp r2, r3 @ 比较 r2 和 r3,查看.rel.dyn 段重定位是否完成
blo fixloop @ 如果 r2 和 r3 不相等,说明.rel.dyn 重定位还未完成 ,继续重定位 .rel.dyn段

relocate_done:

#ifdef __XSCALE__

/*在xscale上,icache必须无效并且写缓冲区耗尽, 即使禁用缓存*/

mcr p15, 0, r0, c7, c7, 0 @ 禁用 icache (指令 cache)
mcr p15, 0, r0, c7, c10, 4 @ 将写缓冲区耗尽
#endif


#ifdef __ARM_ARCH_4__
mov pc, lr
#else
bx lr
#endif

ENDPROC(relocate_code)

补充:

1️⃣ 重定位后 寻址会不会有问题

重定位以后,运行地址就和链接地址不同了 , 但寻址的时候却不会出问题 , 原因如下:

  • 首先 uboot 函数寻址时使用到了 bl 指令,而 bl 指令时位置无关指令
  • bl 指令是相对寻址的 (pc+offset) ,因此 uboot 中函数调用是与 绝对位置 无关的
  • 其次函数对变量的访问没有直接进行,而是使用了一个第三方偏移地址,叫做 Label
  • 这个第三方偏移地址就是实现 重定位 后运行不会出错的重要原因
  • uboot 对于重定位后链接地址和运行地址不一致的解决方法就是 采用位置无关码,
  • 在使用 ld 进行链接的时候使用选项“-pie”生成位置无关的可执行文件生成一个.rel.dyn 段,uboot 就是靠这个.rel.dyn 来解决重定位问题的


2.3.2.1.8 relocate_vectors 函数

位置 : arch/arm/lib/relocate.S

功能 : relocate_vectors 函数用于重定位向量表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ENTRY(relocate_vectors)

#ifdef CONFIG_CPU_V7M @ 是 Cortex-M 内核执行的语句 ,因此条件无效

ldr r0, [r9, #GD_RELOCADDR] @ r0 = gd->relocaddr
ldr r1, =V7M_SCB_BASE
str r0, [r1, V7M_SCB_VTOR]
#else

#ifdef CONFIG_HAS_VBAR @ 支持向量表偏移则条件成立 , 这里条件成立

/*如果ARM处理器有安全扩展,使用VBAR重新定位异常向量。*/

ldr r0, [r9, #GD_RELOCADDR] @ gd->relocaddr为重定位后的 uboot首地址
mcr p15, 0, r0, c12, c0, 0 @ 将r0的值写入 CP15 的VBAR寄存器中 , 即将新的向量表写入到 寄存器 VBAR中
#else @ VBAR是向量表基地址寄存器。设置中断向量表偏移的时候就需要 将新的中断向量表基地址写入 VBAR 中

/* 将重新定位的中断向量表复制到正确的地址, 在CP15 的c1寄存器的 V 位给出了中断向量表的基地址 0x00000000*/
ldr r0, [r9, #GD_RELOCADDR] @ r0 = gd->relocaddr , 目的地址
mrc p15, 0, r2, c1, c0, 0 /* V bit (bit[13]) in CP15 c1 */
ands r2, r2, #(1 << 13)
ldreq r1, =0x00000000 /* If V=0 */
ldrne r1, =0xFFFF0000 /* If V=1 */
ldmia r0!, {r2-r8,r10}
stmia r1!, {r2-r8,r10}
ldmia r0!, {r2-r8,r10}
stmia r1!, {r2-r8,r10}
#endif
#endif
bx lr

ENDPROC(relocate_vectors)


2.3.2.1.9 board_init_r函数

位置 : common/board_r.c

功能 :

  • 在前面 的 board_init_f函数并没有 对所有的外设进行初始化 , 还需要做一些后续的初始化工作
  • 这些后续初始化 工作就是由 board_init_r 函数来完成的
  • 跟前面的 board_init_f 函数一样也是通过 调用 initcall_run_list 来运行初始化序列
  • 函数集合 init_sequence_r 用于存放一系列初始化函数

函数集合 init_sequence_r 如下所示 (已删去大量条件编译)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
init_fnc_t init_sequence_r[] = {
initr_trace, // 初始化和调试跟踪有关的内容
initr_reloc, // 设置 gd->flags,标记重定位完成。
initr_caches, // 初始化 cache,使能 cache
initr_reloc_global_data, // 初始化重定位后 gd 的一些成员变量
initr_barrier, // I.MX6ULL 未用到
initr_malloc, // 初始化 malloc
initr_console_record, // 初始化控制台相关的内容,I.MX6ULL 未用到,空函数。
bootstage_relocate, // 启动状态重定位
initr_bootstage, // 初始化 bootstage
board_init, // 板级初始化,包括 74XX 芯片,I2C、FEC、USB 和 QSPI 等。这里执行的是 mx6ull_alientek_emmc.c 文件中的 board_init 函数。
stdio_init_tables, // stdio 相关初始化
initr_serial, // 初始化串口
initr_announce, // 与调试有关,通知已经在 RAM 中运行
INIT_FUNC_WATCHDOG_RESET
INIT_FUNC_WATCHDOG_RESET
INIT_FUNC_WATCHDOG_RESET
power_init_board, // 初始化电源芯片
initr_flash, // 对于 I.MX6ULL 此函数无效
INIT_FUNC_WATCHDOG_RESET
initr_nand, // 如果有NAND的话 初始化 NAND
initr_mmc, // 如果有emmc的话 初始化emmc
initr_env, // 初始化环境变量
INIT_FUNC_WATCHDOG_RESET
initr_secondary_cpu, // 初始化其他 CPU 核,I.MX6ULL 只有一个核,因此此函数没用
INIT_FUNC_WATCHDOG_RESET
stdio_add_devices, // 各种输入输出设备的初始化,如 LCD driver,I.MX6ULL 使用 drv_video_init 函数初始化 LCD
initr_jumptable, // 初始化跳转表
console_init_r, // 控制台初始化,初始化完成以后此函数会调用 stdio_print_current_devices 函数来打印出当前的控制台设备
INIT_FUNC_WATCHDOG_RESET
interrupt_init, // 初始化中断
initr_enable_interrupts, // 使能中断
initr_ethaddr, // 初始化网络地址,也就是获取 MAC 地址。读取环境变量 "ethaddr" 的值
board_late_init, // 板子后续初始化,如果环境变量存储在 EMMC 或者 SD 卡中的话 , 此函数会调用 board_late_mmc_env_init 函数初始化 EMMC/S
INIT_FUNC_WATCHDOG_RESET
INIT_FUNC_WATCHDOG_RESET
INIT_FUNC_WATCHDOG_RESET
initr_net, // 初始化网络设备
INIT_FUNC_WATCHDOG_RESET
run_main_loop, // 主循环 , 处理命令
}



2.3.2.2 阶段2 : bootz启动linux 内核

在uboot内核启动流程中 , 已经完成了以下工作 :

  • 设置 CPU 工作模式

    • 禁用 中断 (FIQIRQ)
    • CPU设置为 SVC模式
  • 给linux 内核传递参数 如 DRAM起始地址和大小

  • 关闭 MMU 、关闭 数据cache


通过 bootz 启动linux 内核流程如下

20220213220954

2.3.2.2.1 images 全局变量

启动 Linux 内核的时候会用到一个重要的全局变量 ,

1
bootm_headers_t images;

bootm_headers_t 是个 boot 头结构体,在文件 include/image.h 中的定义 . 其中的os 成员变量是 image_info_t 类型的,为 系统镜像信息

1
2
3
#ifndef USE_HOSTCC
image_info_t os; /* OS 镜像信息 */
ulong ep; /* OS 入口点 */

结构体 image_info_t 是系统 镜像信息结构体 ,具体如下:

1
2
3
4
5
6
7
typedef struct image_info {
ulong start, end; /* blob 开始和结束位置*/
ulong image_start, image_len; /* 镜像起始地址(包括 blob)和长度 */
ulong load; /* 系统镜像加载地址*/
uint8_t comp, type, os; /* 镜像压缩、类型,OS 类型 */
uint8_t arch; /* CPU 架构 */
} image_info_t;

下面的 11 个***宏定义*** 表示 U-BOOT 的不同阶段

1
2
3
4
5
6
7
8
9
10
11
12
#define BOOTM_STATE_START 				(0x00000001)
#define BOOTM_STATE_FINDOS (0x00000002)
#define BOOTM_STATE_FINDOTHER (0x00000004)
#define BOOTM_STATE_LOADOS (0x00000008)
#define BOOTM_STATE_RAMDISK (0x00000010)
#define BOOTM_STATE_FDT (0x00000020)
#define BOOTM_STATE_OS_CMDLINE (0x00000040)
#define BOOTM_STATE_OS_BD_T (0x00000080)
#define BOOTM_STATE_OS_PREP (0x00000100)
#define BOOTM_STATE_OS_FAKE_GO (0x00000200) /*'Almost' run the OS*/
#define BOOTM_STATE_OS_GO (0x00000400)
int state;


2.3.2.2.2 bootz 命令

bootz 命令完成以下的工作 :

  • do_bootz 函数

    bootz_start 函数

    • bootz_srart 函数中设置 imagesep 成员变量,也就是系统镜像的入口点 , 使用 bootz 命令启动系统的时候就会设置系统在 DRAM 中的存储位置,这个存储位置就是系统镜像的入口点,因此 images->ep=0X80800000
    • 查询镜像文件是否为 linux 镜像文件 , 以及用于查询设备树文件 ( dbt) ,

    • 调用函数 bootm_disable_interrupts 关闭中断
    • 设置 images.os.osIH_OS_LINUX,也就是设置系统镜像为 Linux ( 后面会用到 images.os.os 来挑选具体的启动函数 )

    do_bootm_states 函数

    • do_bootz 函数的最后 调用 了 do_bootm_states 函数 , 用于根据不同的 BOOT 状态执行不同的代码段,判断 BOOT 的状态 , 然后根据BOOT的状态执行不同的代码

      1
      states & BOOTM_STATE_XXX
    • 通过函数 bootm_os_get_boot_func 来查找系统启动函数

      1
      boot_fn = bootm_os_get_boot_func(images->os.os);
      • 参数 images->os.os 就是系统类型 , 即之前设置的 IH_OS_LINUX
      • bootm_os_get_boot_func 的返回值 就是 找到的 Linux 系统启动函数为 do_bootm_linux

    • (见 ‘2.3.2.2.3’ 小节) do_bootm_linux 函数

      • do_bootm_linux 函数最终会 跳转执行 boot_prep_linuxboot_jump_linux 函数

      • boot_prep_linux 主要用于 处理环境变量 bootargs , bootargs 保存着 传递给 linux内核的参数

        static void boot_prep_linux(bootm_headers_t *images)
        {
            char *commandline = getenv("bootargs");      //从环境变量中获取 bootargs 的值
        
          。。。。。。。
                setup_board_tags(&params);      
                setup_end_tag(gd->bd);    //将 tag 参数保存在指定位置
            } else {
                printf("FDT and ATAGS support not compiled in - hanging\n");
                hang();
            }
            do_nonsec_virt_switch();
        }
        
      • boot_jump_linux 函数 , 保存机器ID (如果不使用设备树的话这个机器 ID 会被传递给 Linux

        内核) , 并最终调用 kernel_entry 函数 ,进入Linux内核



2.3.2.2.3 do_bootm_linux 函数

位置 arch/arm/lib/bootm.c

功能 调用boot_prep_linuxboot_jump_linux 两个函数, 并最终启动 linux 内核

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int do_bootm_linux(int flag, int argc, char * const argv[],
bootm_headers_t *images)
{
/* No need for those on ARM */
if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
return -1;

if (flag & BOOTM_STATE_OS_PREP) {
boot_prep_linux(images);
return 0;
}

if (flag & (BOOTM_STATE_OS_GO | BOOTM_STATE_OS_FAKE_GO)) {
boot_jump_linux(images, flag);
return 0;
}

boot_prep_linux(images);
boot_jump_linux(images, flag);
return 0;
}

可以看到 do_bootm_linux 函数 最终调用了 boot_prep_linuxboot_jump_linux 两个函数



补充:

1️⃣ boot_jump_linux 函数

位置 : arch/arm/lib/bootm.c

功能 :

  • 保存机器 ID,如果不使用 设备树 的话这个机器 ID 会被传递给 Linux , linux内核会查找 是否存在 与这个ID匹配的项目,那么 Linux 内核就会启动 ( 如果使用 设备树 的话 ,这个 ID 就无效了 )

  • 调用 kernel_entry 函数进入 Linux 内核

    • kernel_entry 函数 并不是 uboot 定义的 , 而是Linux 内核定义的 , Linux 内核镜像文件的第一行代码就是函数 kernel_entry 函数 , 因此要首先获取 kernel_entry 函数

      1
      kernel_entry = (void (*)(int, int, uint))images->ep;
      • images->ep 保存着 Linux 内核镜像的起始地址 , 起始地址保存的是 Linux 内核的第一行代码
    • Linux 内核一开始是 汇编代码,因此函数 kernel_entry 就是个汇编函数 , 向汇编函数传递参数要使用 r0、r1 和 r2 (参数数量不超过3个的时候)

      • kernel_entry 函数 有三个参数 zero,arch,params
      • 第一个参数 zero 为 0
      • 第二个参数为机器 ID
      • 第三个参数 ATAGS 或者 设备树(DTB) 首地址,ATAGS 是传统的方法,用于传递一些命令行信息啥的,如果使用设备树的话就要传递设备树(DTB)。
      • 当使用设备树时 , r2 应该是设备树的起始地址,而设备树地址保存在 imagesftd_addr 成员变量中
      • 如果不使用设备树的话,r2 应该是 uboot 传递给 Linux 的参数起始地址 , 即 环境变量 bootargs 的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static void boot_jump_linux(bootm_headers_t *images, int flag)
{
unsigned long machid = gd->bd->bi_arch_number;      //获取机器id (在 board/samsung/jz2440/jz2440.c 中设置,为 MACH_TYPE_SMDK2410(193))
char *s;
void (*kernel_entry)(int zero, int arch, uint params);
unsigned long r2;
int fake = (flag & BOOTM_STATE_OS_FAKE_GO);

kernel_entry = (void (*)(int, int, uint))images->ep;    //获取 kernel的入口地址,此处应为 30000000

s = getenv("machid");         //从环境变量里获取机器id (本例中还未在环境变量里设置过机器 id)
if (s) {             //判断环境变量里是否设置机器id
strict_strtoul(s, 16, &machid);     //如果设置则用环境变量里的机器id
printf("Using machid 0x%lx from environment\n", machid);
}

debug("## Transferring control to Linux (at address %08lx)" \
"...\n", (ulong) kernel_entry);
bootstage_mark(BOOTSTAGE_ID_RUN_OS);
announce_and_cleanup(fake);

if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len)
r2 = (unsigned long)images->ft_addr;
else
r2 = gd->bd->bi_boot_params;     //获取 tag参数地址,gd->bd->bi_boot_params在 setup_start_tag 函数里设置
if (!fake) kernel_entry(0, machid, r2); }   //进入内核


2.3.2.2.4 补充

1️⃣ 内核镜像格式vmlinuzzImageuImage

  • uboot经过编译直接生成的elf格式的可执行程序是u-boot,这个程序类似于windows下的exe格式,在操作系统下是可以直接执行的。但是这种格式不能用来烧录下载。我们用来烧录下载的是u-boot.bin,这个东西是由u-boot使用arm-linux-objcopy工具进行加工(主要目的是去掉一些无用的)得到的。这个u-boot.bin就叫 镜像(image) ,镜像就是用来烧录到 EMMC 中执行的。

  • linux内核经过编译后也会生成一个elf格式的可执行程序,叫vmlinuxvmlinuz,这个就是原始的 未经任何处理加工 的原版内核elf文件;嵌入式系统部署时烧录的一般不是这个vmlinuz/vmlinux,而是要用objcopy工具去制作成烧录镜像格式(就是u-boot.bin这种,但是内核没有.bin后缀),经过制作加工成烧录镜像的文件就叫Image(制作把78M大的精简成了7.5M,因此这个制作烧录镜像主要目的就是缩减大小,节省磁盘)。

  • 原则上Image就可以直接被烧录到Flash上进行启动执行(类似于u-boot.bin),但是实际上并不是这么简单。实际上linux的作者们觉得Image还是太大了所以对Image进行了压缩,并且在image压缩后的文件的前端附加了一部分解压缩代码。构成了一个压缩格式的镜像就叫zImage

  • uboot为了启动linux内核,还发明了一种内核格式叫uImageuImage是由zImage加工得到的,uboot中有一个工具,可以将zImage加工生成uImage。注意:uImage不关linux内核的事,linux内核只管生成zImage即可,然后uboot中的mkimage工具再去由zImage加工生成uImage来给uboot启动。这个加工过程其实就是在zImage前面加上64字节的uImage头信息 即可。

  • 原则上uboot启动时应该给他uImage格式的内核镜像,但是实际上uboot中也可以支持zImage,是否支持就看x210_sd.h中是否定义了LINUX_ZIMAGE_MAGIC这个宏。可以看出:有些uboot是支持zImage启动的,有些则不支持。但是所有的uboot肯定都支持uImage启动。

  • 如果直接在kernel底下去make uImage会提供mkimage command not found。解决方案是去uboot/toolscp mkimage /usr/local/bin/,复制mkimage工具到系统目录下。再去make uImage即可。

  • 因此如果通过uboot启动内核,Linux必须为uImage格式 ( 或部分支持zImage)。



2️⃣ 给内核传递参数

怎么从uboot 跳转 内核启动

只要 直接修改PC寄存器的值为Linux内核所在的地址 , 这样CPU就会从内核所在的地址 去取指令 , 从而执行内核代码

为什么要给内核传递参数呢?

uboot启动的第一阶段 , uboot 基本完成了 硬件的初始化 , 但内核 对于此时 开发板的环境 一无所知 , 所以要启动 Linux 内核 , uboot 必须要给 内核传递一些必要的信息 , 来告知内核 当前所处的环境


如何给内核传递参数

  • uboot 通过寄存器 r0、r1 和 r2 将参数传递给内核

例如

  • uboot机器ID通过R1传递给内核Linux内核运行的时候,首先就从R1中读取机器ID来判断是否支持当前机器。这个机器ID实际上就是开发板 CPU的ID,每个厂家生产出一款CPU的时候都会给它指定一个唯一的ID ( 当然使用设备树的话, 情况会有所不同)
  • *R2存放的是块内存的基地址 ,这块内存中存放的是ubootLinux内核的其他参数。这些参数有内存的 起 始地址、内存大小、 Linux 内核启动后挂载文件系统的方式等信息 。很明显,参数有多个,不同的参数有不同的内容,为了让Linux内核能精确的解析出这些参数,双方在传递参数的时候要求参数在存放的时候需要 按照双方规定的格式存放


3️⃣ 参数结构

  • 在 uboot 和 内核传递参数的过程中 , 除了约定好参数存放的地址外,还要规定参数的结构。Linux2.4.x以后的内核都期望以标记列表 (tagged_list)的形式来传递启动参数。
  • 标记,就是一种数据结构;标记列表,就是挨着存放的多个标 记。标记列表以标记 ATAG_CORE 开始,以标记 ATAG_NONE 结束。

标记数据结构 tag

标记的数据结构为tag,它由一个tag_header结构和一个联合(union)组成。tag_header结构表示标记的 类型及长度,比如是表示内存还是表示命令行参数等。对于不同类型的标记使用不同的 联合(union) ,比如表示内存时使用tag_mem32,表示命令行时使用 tag_cmdline。 ( 具体见 arch\arm\include\asm\setup.h )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;

/*
* Acorn specific
*/
struct tag_acorn acorn;

/*
* DC21285 specific
*/
struct tag_memclk memclk;
} u;
};

可以看出 :

  • struct_tag结构体由struct tag_header+联合体union构成
  • 结构体struct tag_header用来描述每个tag的头部信息,如tag类型tag大小
  • 联合体union用来描述每个传递给Linux内核的 参数信息


-------------已经到底啦! -------------