uboot介绍
uboot就是一段引导程序,在加载系统内核之前,完成硬件初始化,内存映射,为后续内核的引导提供一个良好的环境。uboot是bootloader的一种,全称为universal boot loader。
一、uboot的makefile
1.1 makefile整体解析过程
为了生成u-boot.bin这个文件,首先要生成构成u-boot.bin的各个库文件、目标文件。为了各个库文件、目标文件就必须进入各个子目录执行其中的Makefile。由此,确定了整个编译的命令和顺序。
1.2 makefile整体编译过程
- 首先,根据各个库文件、目标文件出现的先后顺序,依次进入各个子目录编译从而生成这些目标
- 然后,回到顶层目录,继续执行顶层Makefile的总目标,最后生成u-boot.bin。
uboot的编译分为两步:配置、编译。
(1)第一步:配置,执行make pangu_basic_defconfig进行配置,生成.config文件
(2)第二步:编译,执行make进行编译,生成u-boot.bin。
二、uboot启动流程
- uboot分为 uboot-spl 和 uboot 两个组成部分。
uboot启动分三个阶段
-
BL0
ROM上的固化程序(Boot Rom) -
BL1(u-boot-spl)
- 初始化部分时钟(和SDRAM相关)
- 初始化DDR(外部SDRAM)
- 从存储介质上(比如SD\eMMC\nand flash)将BL2镜像加载到SDRAM上
- 验证BL2镜像的合法性
- 跳转到BL2镜像所在的地址上
- BL2 (uboot)
- 初始化部分硬件,包括时钟、内存等等
- 加载内核到内存上
- 加载文件系统、atags或者dtb到内存上
- 根据操作系统启动要求正确配置好一些硬件
启动操作系统
2.1 uboot的链接文件(u-boot.lds)
链接文件的作用
-
指定代码段和数据段、只读数据段在内存中的存放地址;(地址具体为i.m6ull , 其他芯片可能不是 0X87800000)
-
u-boot.map 是 uboot 的映射文件,看到某个文件或者函数链接到了哪个地址,
-
__image_copy_start 为 0X87800000,而.text 的起始地址也是0X87800000。
-
vectors 段保存中断向量表,vectors 段的起始地址也是 0X87800000,说明整个 uboot 的起始地址就是 0X87800000,
-
这也是为什么我们裸机例程的链接起始地址选择 0X87800000 了,目的就是为了和 uboot 一致。
-
-
指定代码的入口地址;
- 连接文件中找到程序的入口点:_start, 其中_start 在文件 arch/arm/lib/vectors.S 。
2.2 uboot启动流程
第一阶段
-
SPL是Secondary Program Loader的简称,第二阶段程序加载器,这里所谓的第二阶段是相对于SOC中的Boot ROM来说的
-
Boot ROM会通过检测启动方式来加载第二阶段bootloader。uboot已经是一个bootloader了,那么为什么还多一个uboot spl呢?
-
这个主要原因是对于一些SOC来说,它的内部SRAM可能会比较小,小到无法装载下一个完整的uboot镜像,那么就需要spl,它主要负责初始化外部RAM和环境,并加载真正的uboot镜像到外部RAM(DDR)中来执行。
-
所以由此来看,SPL应该是一个非常小的loader程序,可以运行于SOC的内部SRAM中,它的主要功能就是加载真正的uboot并运行之。
通过uboot-spl编译脚本:u-boot/arch/arm/cpu/u-boot-spl.lds
所以uboot-spl的代码入口函数是_start
对应于路径u-boot/arch/arm/lib/vector.S的_start,后续就是从这个函数开始分析。
2.2.1. 由u-boot.lds中知道入口点是 _start , 进入_start函数:
有一条跳转指令b reset跳转到reset函数处去执行
2.2.2. reset函数处的代码如下:(spl的流程在reset中结束,reset中进入uboot)
reset 函数跳转到了 save_boot_params 函数
2.2.3. save_boot_params 函数
100 ENTRY(save_boot_params)
101 b save_boot_params_ret @ back to my caller
save_boot_params 函数也是只有一句跳转语句,跳转到 save_boot_params_ret 函数,save_boot_params_ret 函数代码如下:
38 save_boot_params_ret:
39 /*
40 * disable interrupts (FIQ and IRQ), also set the cpu to SVC32
41 * mode, except if in HYP mode already
42 */
43 mrs r0, cpsr
44 and r1, r0, #0x1f @ mask mode bits
45 teq r1, #0x1a @ test for HYP mode
46 bicne r0, r0, #0x1f @ clear all mode bits
47 orrne r0, r0, #0x13 @ set SVC mode
48 orr r0, r0, #0xc0 @ disable FIQ and IRQ
49 msr cpsr,r0
完成设置 CPU 处于 SVC32 模式(超级管理权限 / 保护模式),并且关闭FIQ 和 IRQ 这两个中断。(其他芯片还会关闭看门狗,但是im6ulll没有看门狗)
2.2.4. 然后返回_start函数接着执行
51 /*
52 * Setup vector:
53 * (OMAP4 spl TEXT_BASE is not 32 byte aligned.
54 * Continue to use ROM code vector only in OMAP4 spl)
55 */
56 #if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))
57 /* Set V=0 in CP15 SCTLR register - for VBAR to point to vector */
58 mrc p15, 0, r0, c1, c0, 0 @ Read CP15 SCTLR Register
59 bic r0, #CR_V @ V = 0
60 mcr p15, 0, r0, c1, c0, 0 @ Write CP15 SCTLR Register
61
62 /* Set vector address in CP15 VBAR register */
63 ldr r0, =_start
64 mcr p15, 0, r0, c12, c0, 0 @Set VBAR
65 #endif
第63行设置r0寄存器的值为_start,_start就是整个uboot的入口地址,其值为0X87800000,相当于 uboot 的起始地址,因此 0x87800000 也是向量表的起始地址。
第 64 行将 r0 寄存器的值(向量表值)写入到 CP15 的 c12 寄存器中,也就是 VBAR 寄存器。
因此第 58~64 行就是 设置中断向量表重定位的。 中断向量表的内容就是我们前面在reset下面看到的那些ldr xxxx指令
67 /* the mask ROM code should have PLL and others stable */
68 #ifndef CONFIG_SKIP_LOWLEVEL_INIT
69 bl cpu_init_cp15
70 bl cpu_init_crit
71 #endif
72
73 bl _main
完成上面操作后,会跳转到cpu_init_cp15和cpu_init_crit标号执行,最后转到_main函数去执行。
- cpu_init_cp15 用来设置 CP15 相关的内容,完成启动ICACHE,关闭DCACHE,关闭MMU和TLB 。
(ICACHE为指令缓存,可以不关闭,指令直接操作的硬件,实际的物理地址。但是DCACHE就必须要关闭,此时MMU没有使能,虚拟地址映射不成功,sdram无法访问,DCACHE无数据,这就可能导致读取到错误的数据) - cpu_init_crit 处的代码很简单,就是调用lowlevel_init进行初始化。
2.2.5. lowlevel_init 函数(初始化内存空间,为第二阶段准备ram)
lowlevel_init主要完成平台级和板级的初始化 ,如:时钟、内存、网卡、串口的初始化。
此时初始化SP指向 内存空间为IRAM(内部ram ,OCRAM 128K ,0x00900000)
2.2.6. _main函数
-
因为后面是C语言环境,首先是设置堆栈
-
初始化gd(下图中global date,内部ram) , 进行清零(同上内部ram)
-
调用 board_init_f 函数(将SP指针从内部IRAM,转移到外部DDR),主要用来初始化 DDR,定时器,完成代码拷贝等等
-
调用函数 relocate_code,也就是代码重定位函数,此函数负责将 uboot 拷贝到新的地方去
-
调用函数 relocate_vectors,对中断向量表做重定位
-
调用函数c_runtime_cpu_setup , 清除 BSS 段 , 。
bss段不占用空间,都是未初始化的全局变量或者已经初始化为零的变量,本来就是零,直接清零就好。不清零的话未初始化的变量可能会存在未知的数值。 -
设置函数 board_init_r 的两个参数 , 调用 board_init_r 函数 ,
-
board_init_r 函数打印一些列的信息到串口,然后会进入main_loop() 。main_loop会进行倒计时,如果此时按下回车就会进入uboot的shell交互界面,否则就会自动引导启动OS系统。
2.2.7. board_init_f 函数
-
重新设置环境(sp 和 gd) , 新的 sp 和 gd 将会存放到 DDR 中(外部),而不是内部的 RAM 了
-
uboot 会将自己重定位到 DRAM(DDR) 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中。这么做的目的是给 Linux 腾出空间,防止 Linuxkernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。
-
在拷贝之前肯定要给 uboot 各部分分配好内存位置和大小,比这些信息都保存在 gd 的成员变量中(从板子配置文件里读取),因此要对 gd 的这些成员变量做初始化。最终形成一个完整的内存“分配图”,在后面重定位 uboot 的时候就会用到这个内存“分配图”(外部)。
-
注:上电后芯片内部Boot ROM把uboot搬移到DRAM头部(0x87800000),重定位则再搬移到DDR后部
2.2.8. relocate_code 函数
- relocate_code 函数是用于代码拷贝
重定位就是 uboot 将自身拷贝到 DRAM 的另一个地放去继续运行(DRAM 的高地址处)。我们知道,一个可执行的 bin 文件,其链接地址和运行地址要相等,也就是链接到哪个地址,在运行之前就要拷贝到哪个地址去。现在我们重定位以后,运行地址就和链接地址不同了,这样寻址的时候不会出问题吗?
代码动态重定位原理
-
分析问题产生原因
r0 = gd->relocaddr = 0x9ff47000 , uboot重定位后的首地址
r1 = 0x87800000 源地址的首地址
r2 = 0x8785dc6c 源地址的结束地址
r4 = 0x9ff46000 - 0x87800000 = 0x18747000 偏移量
拷贝是从r1复制往r0粘贴 , 一次两个32位
当r1等于r2,拷贝完成当简单粗暴的将uboot从0x87800000拷贝到0x9ff47000 ,程序运行时地址和连接地址不同,发生错误。uboot解决方法是使用位置无关码,借用 .rel.dyn 段
-
使用位置无关码解重定位后和连接地址不同问题的原理
举例: board_init 函数会调用 rel_test,rel_test 会调用全局变量 rel_a
源代码static int rel_a = 0; void rel_test(void) { rel_a = 100; printf("rel_test\r\n"); } int board_init(void) { ... rel_test(); ... }
反汇编代码
8785dcf8 <rel_a>: 8785dcf8: 00000000 andeq r0, r0, r0 878042b4 <rel_test>: 878042b4: e59f300c ldr r3, [pc, #12] ; 878042c8 <rel_test+0x14> 878042b8: e3a02064 mov r2, #100 ; 0x64 878042bc: e59f0008 ldr r0, [pc, #8] ; 878042cc <rel_test+0x18> 878042c0: e5832000 str r2, [r3] 878042c4: ea00d64c b 87839bfc <printf> 878042c8: 8785dcf8 ; <UNDEFINED> instruction: 0x8785dcf8 878042cc: 87842aaf strhi r2, [r4, pc, lsr #21]
从反汇编代码中分析:
- 想要找到 rel_a 的地址,首先 r3 = pc + 12 = 0x878042b4 + 8 + 12 = 0x878042c8 。(由ARM 流水线决定 pc = 当前地址 + 8)
- 之后 r3 在0x878042c8 中存储数据为0x8785dcf8,即为rel_a地址
- 这里没直接读取rel_a , 而是借助0x878042c8 。 0x878042c8 就是Label
重定位后,地址变化
9ffa4cf8 <rel_a>: 9ffa4cf8: 00000000 andeq r0, r0, r0 9ff4b2b4<rel_test>: 9ff4b2b4: e59f300c ldr r3, [pc, #12] ; 878042c8 <rel_test+0x14> 9ff4b2b8: e3a02064 mov r2, #100 ; 0x64 9ff4b2bc: e59f0008 ldr r0, [pc, #8] ; 878042cc <rel_test+0x18> 9ff4b2c0: e5832000 str r2, [r3] 9ff4b2c4: ea00d64c b 87839bfc <printf> 9ff4b2c8: 8785dcf8 ; <UNDEFINED> instruction: 0x8785dcf8 9ff4b2cc: 87842aaf strhi r2, [r4, pc, lsr #21]
- 这时 Label中的值还是重定位之前的,必须要将8785dcf8换为重定位后的rel_a地址
- 重定位后的Label中的数据 = 0x878042c8(老的Label) + 0x18747000(uboot整体的偏移量) = 0x9ffa4cf8;
- 读取 rel_a 地址为 ,r3 = 0x9ff4b2b4 + 8 + 12 = 0x9ff4b2c8 ,即为Label地址,然后从Label中读取到 0x9ffa4cf8,为rel_a重定义后真是地址
-
uboot 中使用 .rel.dyn 段具体实现位置无关码的原理
完成这个功能在连接的时候需要加上”-pie”
.rel.dyn 段代码段
8785dcec: 87800020 strhi r0, [r0, r0, lsr #32] 8785dcf0: 00000017 andeq r0, r0, r7, lsl r0 …… 8785e2fc: 878042c8 strhi r4, [r0, r8, asr #5] 8785e300: 00000017 andeq r0, r0, r7, lsl r0
- .rel.dyn 段的格式,也就是两个 4 字节数据为一组。
高 4 字节是 Label 地址标识 0X17,低 4 字节就是 Label 的地址, - 第三行是878042c8,第四行是00000017 。说明878042c8是一个Label。正是上述分析中 存放 rel_a 地址的Label
- 在重定位后,rel_a 真是地址 = Label内数据 + offset(0x18747000)
- .rel.dyn 段的格式,也就是两个 4 字节数据为一组。
2.2.9 relocate_vectors函数
- 中断向量表重定位后的地址,就是重定位后uboot的首地址
2.2.7. board_init_r 函数
- 初始化一系列外设,比如串口、定时器,对uboot重定位后分配内存,或者打印一些消息等。