uboot启动分析
uboot
是一个操作系统引导程序,多用于嵌入式设备上linux
操作系统的引导。它是一个裸机程序,启动流程对其他裸机系统的启动都具有参考意义。下面,我们一起学习一下uboot
的启动流程。
uboot
的启动流程如下图所示:
我们对启动流程进行分析时,首先需要分析其链接脚本uboot.lds
,从中了解大概的镜像布局,来知道程序的入口,以及一些特殊的地址变量定义,段等。
uboot
程序的最开始为_start
,从_start
开始,是中断向量表的定义,这是大多数嵌入式裸机程序的共识,它们总是以复位中断为入口,进入整个系统。在uboot
的_start
标号开始处,我们可以看到复位中断的中断处理程序入口b reset
,这样就来到了reset
复位程序处。
reset
程序是系统启动或复位后执行的程序,因此,其第一步就是设置处理器为SVC
,关闭FRQ
和IRQ
,以确定处理器的工作模式(SVC
:处于特权模式,且有自己的影子寄存器;关中断:中断尚没有配置,防止程序中断引起的程序异常);第二步,立马就进行中断向量表的设置,使中断能够执行,这对用户来说是一个高优先级的配置任务;第三步,也是做一些需要尽快确定的系统状态,比如关闭MMU
,TLBs
,D-cache
,打开I-cache
等相关的操作,以及初始化栈指针使得C语言函数能够调用;这样,复位后基本的极简环境就设置好了,我们可以进一步执行uboot
相关程序,进入_main
。
_main
就是uboot
的进一步初始化了,其有了前面搭建的极简环境。第一步初始化早期malloc
,gd
全局数组(gd
全局数组用于保存uboot
的一些重要信息,例如镜像的开始地址,镜像的重装载地址,malloc
地址等等,这在后面再详细展开说明),调用board_init_f
来执行一系列的函数来初始化硬件和gd
结构体。其中,硬件初始化主要是初始化RAM(SRAM,DDR)
,因为后面重定位uboot
需要对RAM
进行读写操作,是必要的初始化;其主要的就是进行gd
结构体的初始化,完成了完整的内存布局,和其中各种变量的赋值。第二步就是对自己进行重定位,这一部分挺重要的,主要就是需要先将自己拷贝到重定位地址,然后修改.rel.dyn
段的位置无关码,最后对环境进行重新配置。因为运行地址进行了变化,所以程序必须是位置无关的,在编译的时候加上-pie
选项就可以将它编译成位置无关的,其会将程序中的绝对地址的地址都收集到一个特殊的段rel.dyn
中,那么,在进行重定位的时候,只需将该段中记录的地址的数进行一个修改即可,详细过程在后文讲述。第三步,调用board_init_r
函数来执行一系列函数,进行最终的uboot
初始化,包括malloc
,各种板级初始化等,最后调用run_main_loop
,进入uboot
命令行处理程序。
run_main_loop
在开始时会进入倒计时,倒计时结束则会执行bootcmd
环境变量中的命令,启动linux
之类的系统;在倒计时被按键中断的话会进入cli_loop
命令行处理程序,接收用户的指令执行。这部分就是uboot
的用户应用功能了,启动流程就到此结束。
以上就是uboot
启动流程的全部内容了,关于linux
启动方面不属于启动流程的内容,是uboot
实现的功能,在以后的文章中说明。下面,就上面提到的几个中间进行展开介绍。
gd全局变量
gd
全局变量贯穿整个uboot
启动,在前面的启动流程中我们看到了其中存储了重要的地址信息,暗含了内存布局,下面是它的结构体原型中的某些项,我们一起来体会一下它究竟是哪些内容:
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;
...
unsigned long have_console; /* serial_init() was called */
...
unsigned long env_addr; /* Address of Environment struct */
unsigned long env_valid; /* Checksum of Environment valid? */
unsigned long ram_top; /* Top address of RAM used by U-Boot */
unsigned long relocaddr; /* Start address of U-Boot in RAM */
...
unsigned long mon_len; /* monitor len */
unsigned long irq_sp; /* irq stack pointer */
unsigned long start_addr_sp; /* start_addr_stackpointer */
unsigned long reloc_off;
struct global_data *new_gd; /* relocated global data */
...
#ifdef CONFIG_TIMER
struct udevice *timer; /* Timer instance for Driver Model */
#endif
const void *fdt_blob; /* Our device tree, NULL if none */
void *new_fdt; /* Relocated FDT */
unsigned long fdt_size; /* Space reserved for relocated FDT */
struct jt_funcs *jt; /* jump table */
char env_buf[32]; /* buffer for getenv() before reloc. */
...
#ifdef CONFIG_SYS_MALLOC_F_LEN
unsigned long malloc_base; /* base address of early malloc() */
unsigned long malloc_limit; /* limit address */
unsigned long malloc_ptr; /* current address */
#endif
...
} gd_t;
从原型中我们可以看到,这个global_data
结构体就如它的名字一样,它应该是记录了uboot
所有用到的全局变量,其内容也比我们前面看到的要丰富,涵盖栈,malloc
,ram_top
等程序运行的环境信息,以及用于重定位的信息reloc_off
等,还有用于指导uboot
运行的各种变量例如时钟,事件,标志位等等。
在uboot
启动中,和gd
密切相关的就是环境信息和重定位信息了,依据环境信息搭建了内存布局,为程序运行提供了环境;重定位信息使得uboot
可以根据它来将自己重定位到RAM
高地址的地方。
uboot重定位
在uboot
的启动流程中,我们可以看到其中有一步是将自己重定位到RAM
的高地址处,为什么呢?uboot
它是一个引导程序,它的核心功能就是引导像linux
这样的系统启动,那么它就需要加载引导的镜像文件到内存中。为了给镜像文件留出足够的RAM
,同时进行保证内存布局的连续性,那么最好就是uboot
处于RAM
的最高地址处,下面的空间留给镜像。而uboot
的运行地址原本是uboot
程序的编译者在编译的时候指定的,其具有不确定性。即uboot
无法决定其运行地址,因此它选择在自己运行时自己将自己重定位到RAM
高地址处,既然无法决定自己的出身,那么就逆天改命。
但是,重定位要注意什么呢?重定位的话,就意味着这个程序最终会在不同的地址上运行,也即其运行地址是可变的,而由于只有链接地址和运行地址相同时,程序才能正确运行,那么其链接地址就必须有一定的办法可以改变(其在编译的时候确定),这就要求其指令必须是位置无关的。举个例子,如果有某条指令取x8729 0862
地址上的数据,那么如果当将程序重定位到偏移为0x1000 0000
处的位置,再去取0x8729 0862
上的数据,就出错了,因为原来的数据现在的地址变成了0x9729 0862
,这就是位置有关代码,即存在绝对地址。可以发现,当访问变量时就会出现位置有关代码,还是很常见的。在解决这类问题时,现在应该是有多种机制,uboot
中是在编译时指定pie
选项,它会将代码中出现的绝对地址所在的地址收集到一个特殊的段.rel.dyn
中,这样,在进行重定位的时候,除了将代码拷贝到重定位地址以外,还需取出.rel.dyn
段中的项,将其作为地址(需要加上偏移,对应到重定位代码的对应地址),修改该地址上的值,进行链接地址的修改。
代码重定位后,有一个重要的一点,怎么从当前的位置转到重定位之后的位置呢?其实现在在系统中已经有两个uboot
镜像了,且两个的运行地址和链接地址都是相对应的,都可以正常运行,但是我们处于重定位之前的镜像中。uboot
的做法是定义一个标号,将该标号地址加上偏移,那么就对应于重定位之后的镜像的标号处,在进行一个跳转即可转到重定位之后的代码。
另外,在重定位之后,因为uboot
的运行地址发生了变化,相应的一系列的环境像栈,malloc
,中断向量表也需要重新配置。不过,这里就确定了uboot
的最终环境。
下面是重定位部分相关的源码,可以看看实际的代码是怎么实现的,就会更加理解上面将的概念:
uboot.lds
链接脚本定义了.rel*
段位.rel.dyn
段,并定义了段的开始和结束的地址变量。
SECTIONS
{
...
.rel_dyn_start :
{
*(.__rel_dyn_start)
}
.rel.dyn : {
*(.rel*)
}
.rel_dyn_end :
{
*(.__rel_dyn_end)
}
...
}
-pie的作用
我们从.rel.dyn
段反推回去,看看位置无关代码做了什么。
-
首先,我们将
uboot
反汇编:arm-linux-gnueabihf-objdump -D -m arm u-boot > u-boot.dis vim u-boot.dis
-
查找
.rel.dyn
段: -
我们可以看到它以
8
个字节为一组,高四字节0x00000017
标识这是一个lable
(即需要重定位的对象),低四个字节是这个lable
的地址。 -
查看
lable
地址上的内容,即0x87800020
: -
可以看到,该地址上是一个地址,且有标号
_undefined_instruction
标识。并且在代码中有ldr pc, [pc, #20]
,相对寻址的方式,将该地址上的数送到pc
指针上。 -
查看
0x87800060
内容:可以看到该处是一个函数,且函数名和上面的标号查一个
_
。
综上,我们就可以推理出-pie
的作用,它对指令中所有的绝对地址(位置有关代码),都采用相对寻址的方式,将绝对地址存在别处,用相对寻址从别处加载进来。且在别处定义的时候同时定义一个标号,标号即为实际变量或函数名加上一个_
前缀。且在编译的时候,会抽出所有的别处定义的地址放到.rel*
段中,以供访问。关于如何抽取的,可能是抽取所有的_
前缀的标签,再判断是否是吧。
relocate_code调用的上下文
这里需要注意它设置好了lr为重定位之后的here
,跳转使用的是b
。
/* 【设置新栈】 */
ldr sp, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
/* 【确定here地址,用于从当前镜像转到重定位后的镜像中】 */
ldr r9, [r9, #GD_BD] /* r9 = gd->bd */
sub r9, r9, #GD_SIZE /* new GD is below bd */
adr lr, here
ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off */
add lr, lr, r0
#if defined(CONFIG_CPU_V7M)
orr lr, #1 /* As required by Thumb-only */
#endif
/* 【重定位relocate_code】 */
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
b relocate_code
here: /* 【here,relocate_code结束会跳到重定位后的这里】 */
/* 【重定位中断向量表】 */
bl relocate_vectors
bl c_runtime_cpu_setup /* we still call old routine here,没啥 */
#endif
/* 【清除bss段,略】 */
relocate_code
ENTRY(relocate_code)
/* r1拷贝的开始 r2拷贝的结束 r4偏移 */
ldr r1, =__image_copy_start /* r1 <- SRC &__image_copy_start */
subs r4, r0, r1 /* r4 <- relocation offset */
beq relocate_done /* skip relocation */
ldr r2, =__image_copy_end /* r2 <- SRC &__image_copy_end */
/* 【拷贝到重定位地址】 */
copy_loop:
ldmia r1!, {r10-r11} /* copy from source address [r1] */
stmia r0!, {r10-r11} /* copy to target address [r0] */
cmp r1, r2 /* until source end address [r2] */
blo copy_loop
/* 【根据.rel*段修改其中的链接地址】 */
ldr r2, =__rel_dyn_start /* r2 <- SRC &__rel_dyn_start */
ldr r3, =__rel_dyn_end /* r3 <- SRC &__rel_dyn_end */
fixloop:
ldmia r2!, {r0-r1} /* (r0,r1) <- (SRC location,fixup) */
and r1, r1, #0xff
cmp r1, #23 /* 判断是否是0x17 */
bne fixnext
/* relative fix: increase location by offset */
add r0, r0, r4
ldr r1, [r0]
add r1, r1, r4
str r1, [r0]
fixnext:
cmp r2, r3
blo fixloop
relocate_done:
...
/* 【跳转到lr中,即重定位后的here】 */
#ifdef __ARM_ARCH_4__
mov pc, lr
#else
bx lr
#endif
ENDPROC(relocate_code)
小结
对uboot
的启动分析就到此结束,从中我们也有一些程序编写上的收获。
我们可以看到,程序启动的过程不是一蹴而就的,它其中有很多次的初始化,因为有的环境必须依赖于某些环境。u_boot
最开始最首先就是处理器模式的设置,关中断,设置中断向量表,栈指针,这样程序基本可以运行了,且可以调用C
函数。后来进行了board_init_r
初始化,这里它也分的很清楚,因为它需要进行代码的重定位,因此不急着做uboot
所有硬件的初始化,只是做了必要的RAM
初始化,然后对gd
进行设置。最后重定位之后,才最终确定了环境,重新设置sp
,malloc
等等,并进行uboot
运行环境的全部初始化,包含各种硬件等等,在board_init_r
里。在理解这些时一定要知道程序在这里是要干什么,需要依赖什么环境,需要什么才配置什么,而不是一次做完。