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
) 也是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() 宏
使用
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-Boo
t从当前位置 (片上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_BD
GD_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
=0xA0000000
gd->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=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_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_LINUX
bootm_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
内核的 参数信息。