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 | OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") |
- 其中
ENTRY(_start)是整个函数的入口,_start在arch/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
- _start 后面就是中断向量表
- 由
.section ".vectors", "ax"可以知道 , 中断向量表这部分代码是存放在.vectors段里面
2.3.1.3 映射文件 u-boot.map
u-boot.map是uboot的映射文件,可以从此文件看到某个文件或者函数链接到了哪个地址
1 | 段 .text 的地址设置为 0x87800000 |
- 可以看到
.text的起始地址为0x87800000- 镜像启动地址 (
.__image_copy_start) 也是0x87800000vectors段 的起始地址也是0x87800000vectors段 后面则是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() 宏
使用
ENTRY和ENDPROC两个宏来定义一个名为name的函数 , 这个伪指令实现了指定一个入口的同时数据对齐,同时提供了一个函数入口 :
1
2
3 ENTRY(name)
...
ENDPROC(name)
这两个宏定义在#include <linux/linkage.h>中
1 | .globl save_boot_params |
2.3.2.0.2 为什么选择SVC 模式
通过 设置
CPSR寄存器 的bit0 ~ bit4五位来设置 处理器的工作模式
如下表所示:
在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,我们可以在后面加上条件码组成BEQ、BNE组合指令。 BEQ指令表示两个数比较,结果相等时跳转;BNE指令则表示结果不相等时跳转bicne指令表示 标志位Z= 0的时候 , 执行清零指令bic
ARM指令的条件码
BL跳转指令
格式 : BL{条件} 目标地址
作用 :
- 但跳转之前,会在寄存器
RL(即R14)中保存PC的当前内容- BL指令一般用在函数调用的场合
例
1 | 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:协处理器要执行的操作码
- Rt:
ARM源寄存器,要写入到CP15寄存器的数据就保存在此寄存器中- CRn:
CP15协处理器的目标寄存器- CRm: 协处理器中附加的目标寄存器或者源操作数寄存器,如果不需要附加信息就将
CRm设置为C0,否则结果不可预测- opc2:可选的协处理器特定操作码,当不需要的时候要设置为 0
例: 将 CP15 中 C0 寄存器的值读取到 R0 寄存器中,
1 | MRC p15, 0, r0, c0, c0, 0 |
其中四个寄存器
- 通过
c0寄存器可以获取到处理器内核信息 - 通过
c1寄存器可以使能或禁止 MMU、I/D Cache 等; - 通过
c12寄存器可以设置中断向量偏移 ( 如设置中断向量表偏移的时候就需要将新的中断向量表基地址写入VBAR中 ) - 通过
c15寄存器可以获取GIC (中断控制器)基地址
例 :
1 | /* |
2.3.2.1 阶段1 : 初始化外设硬件
2.3.2.1.1 uboot程序入口点 __start
位置:
arch/arm/lib/vectors.S上电启动后,代码执行到
_start函数,调用reset函数
reset的函数目的是将处理器设置为SVC模式,并且关闭FIQ和IRQ,然后设置中断向量以及初始化CP15协处理器
1 | _start: |
-
下面的
8~14 行就是是 中断向量表 -
可以看到 直接跳到
reset函数 (reset函数直接跳转到save_boot_params函数)1
2
3reset:
/* 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模式 ,并禁用IRQ和FIQ两个中断- 重定位 中断向量表 ,将其定位到
uboot的起始地址 ( 这里取0x8780 0000)- 调用
cpu_init_cp15函数 , 设置其他和CP15有关的设置(cache, MMU, tlb), 打开I-cache- 调用
cpu_init_crit函数 , 并最终生成一个属于IMX6ULL内部RAM的临时堆栈- 调用
_main函数 ,
1 | save_boot_params_ret: |
- 在第33行 , 调用
cpu_init_crit, 这个函数内部仅仅调用了lowlevel_init函数lowlevel_init用于创建一个IMX6ULL内部的 临时堆栈
补充:
🅰️ SCTLR寄存器 ( CP15的c1寄存器)
🅱️ save_boot_params_ret 函数调用路径
2.3.2.1.3 lowlevel_init函数
位置 :
arch/arm/cpu/armv7/lowlevel_init.S
lowlev el_init函数主要完成如下功能
- 初始化一个临时堆栈 , 这个堆栈属于
IMX6ULL的内部RAM- 设置
r9寄存器 , 用于保存GD结构体的基地址- 这个临时堆栈 , 保留了
Global data和GBL_DATA的地址位置- 调用早期初始化函数
s_init, 但对于IMX6ULL来说相当于 空函数
1 | ENTRY(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_ADDR和IRAM_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),这样被清除的地址不会与数据冲突
🆎 此时的堆栈内存情况
🔤 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 Flash、SDRAM- 初始化全局结构体变量
GD,在GD里有U-boot实际加载地址- 调用
relocate_code,将U-boot镜像从Flash复制到RAM- 从
Flash跳到内存RAM中继续执行程序BSS段清零,跳入bootcmd或main_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最后部分预留各数据的内存空间 (如uboot、malloc、gd、bd等) , 最终一个完整的内存 分配图 , 在后面重定位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 | ENTRY(_main) |
补充:
1️⃣ board_init_f_alloc_reserve 函数
位置 :
common/init/board_init.c函数功能如下:
- 留出早期的
malloc内存区域和gd内存区域
1 | ulong board_init_f_alloc_reserve(ulong top) |
-
其中
CONFIG_SYS_MALLOC_F_LEN=0X400 -
sizeof(struct global_data)=248(GD_SIZE值) -
完成后的 内存分配 :
2️⃣ 全局变量 global_data(gd)
uboot中定义了一个指向gd_t的指针gd,gd存放在寄存器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 | typedef struct global_data { |
3️⃣ board_init_f_init_reserve 函数
位置 :
common/init/board_init.c功能 :
- 用于初始化 gd , 即清零处理
- 设置
gd->malloc_base为gd基地址 +gd大小=0X0091FA00+248=0X0091FAF8- 并做16字节对齐 , 最终
gd->malloc_base=0X0091FB00,这个也就是early malloc的 起始地址
4️⃣ 通过 gd->bd 计算新的 gd 地址
涉及的代码
1 | ldr r9, [r9, #GD_BD] // 获取gd -> bd的地址 |
通过前文 , 可以得到如下信息 :
r9寄存器存放的是 一个指向gd_t结构体 的指针gd, 即r9寄存器存放的是gd数据结构体 旧的 基地址 ( 片上 RAM, 不是DRAM)板信息
bd是gd结构体的第一个成员 , 即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_BDGD_SIZE是gd结构体的大小 , 为248B为什么
gd->bd减去gd的大小就是 新的gd的位置
因为
gd新的地址 (即在DRAM中的地址) 是在bd数据的下面 ( 即 低地址位置 )图为 调用
board_init_f函数后 在DRAM中的 内存空间图![]()
5️⃣
2.3.2.1.5 board_init_f 函数
位置 :
common/board_f.c功能 :
初始化一系列外设,比如串口、定时器,或者打印一些消息等
初始化
gd的各个成员变量, 将uboot在DRAM最后面的地址区域 预留区域 , 方便后面拷贝
- 因为本质上
uboot是linux的引导文件,引导完成后linux会在DRAM前面的地址区域启动- 为了防止
linux启动后对uboot进行干扰,uboot会将自己重定位到DRAM最后面的地址区域
- 拷贝之前需要给
uboot各部分分配好内存位置和大小 ,比如gd应该存放到哪个位置,malloc内存池应该存放到哪个位置等- 这些信息都保存在
gd的 成员变量 中,因此首先要对gd的这些成员变量做初始化在
DRAM最后部分预留各数据的内存空间 (如uboot、malloc、gd、bd等) , 最终一个完整的内存 分配图 , 在后面重定位uboot时 使用
1 | void board_init_f(ulong boot_flags) |
- 形参
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 | static init_fnc_t init_sequence_f[] = { |
补充:
1️⃣ setup_dest_addr 函数
setup_dest_addr函数 主要用于设置目的地址 , 主要用于输出以下三个 值:
gd->ram_size( RAM的大小 ) 这里是0x20000000, 512MBgd->ram_top( RAM的最高地址 ) 这里是0x80000000+0x20000000=0xA0000000gd->relocaddr(重定位后的最高地址 ) 这里是0xA000000
2️⃣ reserve_mmu 函数
留出
MMU的TLB表的位置,分配MMU的TLB表内存以后会对gd->relocaddr做 64K 字节对齐完成之后的
gd->arch.tlb_size、gd->arch.tlb_addr和gd->relocaddr如下所示:
gd->arch.tlb_size:MMU的TLB表大小 (这里为0x4000)gd->arch.tlb_addr:MMU的TLB表起始地址,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
CONFIG_SYS_MALLOC_LEN为16MB=0X1000000CONFIG_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_t,bd_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].start和gd->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
🔟 重定位后的内存分配图
2.3.2.1.7 relocate_code 函数
位置 :
arch/arm/lib/relocate.S功能 :
- 代码拷贝 , 将
uboot拷贝到DDR中 ,即uboot重定位 到DRAM的高地址- 重定位就是
uboot将自身拷贝到DRAM的另一个地放去继续运行 (DRAM 的高地址处)
1 | ENTRY(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 | ENTRY(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 | init_fnc_t init_sequence_r[] = { |
2.3.2.2 阶段2 : bootz启动linux 内核
在uboot内核启动流程中 , 已经完成了以下工作 :
设置
CPU工作模式
- 禁用 中断 (
FIQ、IRQ)- 将
CPU设置为SVC模式给linux 内核传递参数 如
DRAM的 起始地址和大小关闭
MMU、关闭 数据cache等
通过 bootz 启动linux 内核流程如下
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
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
int state;
2.3.2.2.2 bootz 命令
bootz 命令完成以下的工作 :
do_bootz函数
bootz_start函数
- 在
bootz_srart函数中设置images的ep成员变量,也就是系统镜像的入口点 , 使用bootz命令启动系统的时候就会设置系统在DRAM中的存储位置,这个存储位置就是系统镜像的入口点,因此images->ep=0X80800000- 查询镜像文件是否为
linux镜像文件 , 以及用于查询设备树文件 (dbt) ,
- 调用函数
bootm_disable_interrupts关闭中断- 设置
images.os.os为IH_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_LINUXbootm_os_get_boot_func的返回值 就是 找到的Linux系统启动函数为do_bootm_linux
(见 ‘2.3.2.2.3’ 小节)
do_bootm_linux函数
do_bootm_linux函数最终会 跳转执行boot_prep_linux和boot_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(¶ms); 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_linux和boot_jump_linux两个函数, 并最终启动linux内核
1 | int do_bootm_linux(int flag, int argc, char * const argv[], |
可以看到 do_bootm_linux 函数 最终调用了 boot_prep_linux 和 boot_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应该是设备树的起始地址,而设备树地址保存在images的ftd_addr成员变量中- 如果不使用设备树的话,
r2应该是uboot传递给Linux的参数起始地址 , 即 环境变量bootargs的值
1 | static void boot_jump_linux(bootm_headers_t *images, int flag) |
2.3.2.2.4 补充
1️⃣ 内核镜像格式vmlinuz和zImage和uImage
-
uboot经过编译直接生成的elf格式的可执行程序是u-boot,这个程序类似于windows下的exe格式,在操作系统下是可以直接执行的。但是这种格式不能用来烧录下载。我们用来烧录下载的是u-boot.bin,这个东西是由u-boot使用arm-linux-objcopy工具进行加工(主要目的是去掉一些无用的)得到的。这个u-boot.bin就叫 镜像(image) ,镜像就是用来烧录到EMMC中执行的。 -
linux内核经过编译后也会生成一个elf格式的可执行程序,叫vmlinux或vmlinuz,这个就是原始的 未经任何处理加工 的原版内核elf文件;嵌入式系统部署时烧录的一般不是这个vmlinuz/vmlinux,而是要用objcopy工具去制作成烧录镜像格式(就是u-boot.bin这种,但是内核没有.bin后缀),经过制作加工成烧录镜像的文件就叫Image(制作把78M大的精简成了7.5M,因此这个制作烧录镜像主要目的就是缩减大小,节省磁盘)。 -
原则上
Image就可以直接被烧录到Flash上进行启动执行(类似于u-boot.bin),但是实际上并不是这么简单。实际上linux的作者们觉得Image还是太大了所以对Image进行了压缩,并且在image压缩后的文件的前端附加了一部分解压缩代码。构成了一个压缩格式的镜像就叫zImage。 -
uboot为了启动linux内核,还发明了一种内核格式叫uImage。uImage是由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/tools下cp 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存放的是块内存的基地址 ,这块内存中存放的是
uboot给Linux内核的其他参数。这些参数有内存的 起 始地址、内存大小、 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 | struct tag { |
可以看出 :
struct_tag结构体由struct tag_header+联合体union构成- 结构体
struct tag_header用来描述每个tag的头部信息,如tag的 类型 ,tag的 大小- 联合体
union用来描述每个传递给Linux内核的 参数信息。






