version : v1.0 「2023.4.27」
author: Y.Z.T.
摘要: 随记, 记录 I.MX6ULL 系列 SOC 的uboot 启动流程
⭐️ 目录
通过链接脚本可以找到程序的入口地址 ,
uboot
的最终链接脚本是u-boot.lds
, 是通过编译boot
生成的
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)
是整个函数的入口,_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
配置、选用不同的优化等级等等都会影响到这些值。
vectors.S
( 入口点:_start
)
- 代码当前入口点:
_star
存放在 文件arch/arm/lib/vectors.S
- _start 后面就是中断向量表
- 由
.section ".vectors", "ax"
可以知道 , 中断向量表这部分代码是存放在.vectors
段里面
u-boot.map
是uboot
的映射文件,可以从此文件看到某个文件或者函数链接到了哪个地址
段 .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
和 其他代码段的内容
Uboot 的启动流程可以大致分成两个阶段 :
- 第一阶段多使用汇编 , 主要完成一些板级的硬件初始化 , 如外设硬件初始化 , 如 DRAM , 串口 , 重定位等。
- 第二阶段通常使用C语言来实现 , 方便实现更加复杂的功能 , 主要完成
linux
内核 的启动
使用
ENTRY
和ENDPROC
两个宏来定义一个名为name
的函数 , 这个伪指令实现了指定一个入口的同时数据对齐,同时提供了一个函数入口 :ENTRY(name) ... ENDPROC(name)
这两个宏定义在#include
中
.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 @ 弱标号,如果别处有使用别处的定义,如果没有使用当前定义
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
模式
为了提高代码密度,减少ARM指令的数量,几乎所有的ARM指令都可以根据CPSR寄存器中的标志位,通过指令组合实现条件执行。
如:
B
,我们可以在后面加上条件码组成BEQ
、BNE
组合指令。BEQ
指令表示两个数比较,结果相等时跳转;BNE
指令则表示结果不相等时跳转bicne
指令表示 标志位Z= 0
的时候 , 执行清零指令 bic
ARM指令的条件码
BL跳转指令
格式 : BL{条件} 目标地址
作用 :
- 但跳转之前,会在寄存器
RL
(即R14
)中保存PC的当前内容- BL指令一般用在函数调用的场合
例
BL Label ;当程序无条件跳转到标号Label处执行时,同时将当前的PC值保存到R14中
... ; 子程序返回后接着从此处继续执行
CP15
协处理器一般用于存储系统管理,但是在中断中也会使用到,CP15
协处理器一共有16 个 32 位寄存器
( c0~c15 )
。CP15
协处理器的访问通过如下另个指令完成:
MRC: 将
CP15
协处理器中的寄存器数据读到ARM
寄存器中MCR: 将
ARM
寄存器的数据写入到CP15
协处理器寄存器中MCR{cond} p15,
,
- **cond:**指令执行的条件码,如果忽略的话就表示无条件执行
- opc1:协处理器要执行的操作码
- Rt:
ARM
源寄存器,要写入到CP15
寄存器的数据就保存在此寄存器中- CRn:
CP15
协处理器的目标寄存器- CRm: 协处理器中附加的目标寄存器或者源操作数寄存器,如果不需要附加信息就将
CRm
设置为C0
,否则结果不可预测- opc2:可选的协处理器特定操作码,当不需要的时候要设置为 0
例: 将 CP15
中 C0
寄存器的值读取到 R0
寄存器中,
MRC p15, 0, r0, c0, c0, 0
其中四个寄存器
c0
寄存器可以获取到处理器内核信息c1
寄存器可以使能或禁止 MMU、I/D Cache 等;c12
寄存器可以设置中断向量偏移 ( 如设置中断向量表偏移的时候就需要将新的中断向量表基地址写入 VBAR
中 )c15
寄存器可以获取 GIC (中断控制器)
基地址例 :
/*
* 设置中断向量表:
* (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
位置:
arch/arm/lib/vectors.S
上电启动后,代码执行到
_start
函数,调用reset
函数
reset
的函数目的是将处理器设置为SVC
模式,并且关闭FIQ
和IRQ
,然后设置中断向量以及初始化CP15
协处理器
_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
函数)
reset:
/* Allow the board to save important registers */
b save_boot_params
save_boot_params
也同样是直接跳转到 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
函数 ,
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
寄存器 ( CP15
的c1
寄存器)
️ save_boot_params_ret
函数调用路径
位置 :
arch/arm/cpu/armv7/lowlevel_init.S
lowlev el_init
函数主要完成如下功能
- 初始化一个临时堆栈 , 这个堆栈属于
IMX6ULL
的内部RAM
- 设置
r9
寄存器 , 用于保存GD
结构体的基地址- 这个临时堆栈 , 保留了
Global data
和GBL_DATA
的地址位置- 调用早期初始化函数
s_init
, 但对于IMX6ULL
来说相当于 空函数
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
计算:
这两个宏定义如下:
#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
则可以得到如下值
CONFIG_SYS_INIT_RAM_ADDR = IRAM_BASE_ADDR = 0x00900000 CONFIG_SYS_INIT_RAM_SIZE = 0x00020000 =128KB GENERATED_GBL_DATA_SIZE = 256
计算可得:
CONFIG_SYS_INIT_SP_OFFSET = 0x00020000 – 256 = 0x1FF00。 CONFIG_SYS_INIT_SP_ADDR = 0x00900000 + 0X1FF00 = 0X0091FF00,
️ sp指针8位对齐
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 中的任意 一 种 , 那么就会直接返回
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
就是个空函数
位置 :
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
控制台指令 ,或跳转到系统内核运行
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
内存区域
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;
}
2️⃣ 全局变量 global_data(gd)
uboot
中定义了一个指向gd_t
的指针gd
,gd
存放在寄存器r9
里面 ,因此gd
是个 全局变量#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
结构体
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_base
为gd
基地址 +gd
大小=0X0091FA00+248
=0X0091FAF8
- 并做16字节对齐 , 最终
gd->malloc_base=0X0091FB00
,这个也就是early malloc
的 起始地址
4️⃣ 通过 gd->bd
计算新的 gd
地址
涉及的代码
ldr r9, [r9, #GD_BD] // 获取gd -> bd的地址
sub r9, r9, #GD_SIZE // 减去bd 结构体占用的空间 即为 gd结构体的空间
通过前文 , 可以得到如下信息 :
r9
寄存器存放的是 一个指向gd_t
结构体 的指针gd
, 即r9寄存器存放的是gd
数据结构体 旧的 基地址 ( 片上 RAM, 不是DRAM)板信息
bd
是gd
结构体的第一个成员 , 即gd -> bd
的 首地址 与 gd结构体的 基地址 (即r9
寄存器保存的值) 是一致的typedef struct global_data { bd_t *bd; unsigned long flags; ... }gd_t;
gd
结构体的gd -> bd
成员在 调用board_init_f
函数的时候 就已经被重定位在DRAM
上了 ( 即 新地址 )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
的位置
5️⃣
位置 :
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
时 使用
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
(一个存放各个函数入口的数组)位置 :
common/board_f.c
功能 :
- 是一个存放了各个函数入口的数组
- 通过
initcall_run_list
来一系列初始化序列 , 设置GD
的各个成员的值- 用于初始化一系列外设,比如串口、定时器,或者打印一些消息等
去除条件编译后的init_sequence_f
如下 :
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
, 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
定义宏定义如下 :
#define TOTAL_MALLOC_LEN (CONFIG_SYS_MALLOC_LEN + CONFIG_ENV_SIZE)
CONFIG_SYS_MALLOC_LEN
为16MB=0X1000000
CONFIG_ENV_SIZE
=8KB=0X2000
- 因此
TOTAL_MALLOC_LEN=0X1002000
(即malloc
的区域大小为0X1002000
)
可以得到:
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字节调整之后结果如下:
gd->start_addr_sp=0X9EF44FB0 gd->bd=0X9EF44FB0 @ 调用完board_init_f这个函数之后 , 这个根据gd->bd, 来获取重定位后 , 新的gd的地址
6️⃣ reserve_global_data
函数
保留出
gd_t
的内存区域,gd_t
结构体大小为248B
完成后结果如下 :
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
相应的内存 , 这里不使能完成后结果如下
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 的起始地址和大小
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
重定位后的内存分配图
位置 :
arch/arm/lib/relocate.S
功能 :
- 代码拷贝 , 将
uboot
拷贝到DDR
中 ,即uboot
重定位 到DRAM
的高地址- 重定位就是
uboot
将自身拷贝到DRAM
的另一个地放去继续运行 (DRAM 的高地址处)
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
来解决重定位问题的
位置 :
arch/arm/lib/relocate.S
功能 :
relocate_vectors
函数用于重定位向量表
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)
位置 :
common/board_r.c
功能 :
- 在前面 的
board_init_f
函数并没有 对所有的外设进行初始化 , 还需要做一些后续的初始化工作- 这些后续初始化 工作就是由
board_init_r
函数来完成的- 跟前面的
board_init_f
函数一样也是通过 调用initcall_run_list
来运行初始化序列- 函数集合 init_sequence_r 用于存放一系列初始化函数
函数集合 init_sequence_r
如下所示 (已删去大量条件编译)
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, // 主循环 , 处理命令
}
在uboot内核启动流程中 , 已经完成了以下工作 :
设置
CPU
工作模式
- 禁用 中断 (
FIQ
、IRQ
)- 将
CPU
设置为SVC
模式给linux 内核传递参数 如
DRAM
的 起始地址和大小关闭
MMU
、关闭 数据cache
等
通过 bootz
启动linux
内核流程如下
启动
Linux
内核的时候会用到一个重要的全局变量 ,bootm_headers_t images;
bootm_headers_t
是个boot
头结构体,在文件include/image.h
中的定义 . 其中的os
成员变量是image_info_t
类型的,为 系统镜像信息#ifndef USE_HOSTCC image_info_t os; /* OS 镜像信息 */ ulong ep; /* OS 入口点 */
结构体
image_info_t
是系统 镜像信息结构体 ,具体如下: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
的不同阶段#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;
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
的状态执行不同的代码states & BOOTM_STATE_XXX
通过函数
bootm_os_get_boot_func
来查找系统启动函数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
内核
do_bootm_linux
函数位置
arch/arm/lib/bootm.c
功能 调用
boot_prep_linux
和boot_jump_linux
两个函数, 并最终启动linux
内核
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_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
函数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
的值
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); } //进入内核
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
)
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
内核的 参数信息。