概述
在基于imx6ull平台的linux开发中,uboot的主要作用是为linux准备好运行环境,配置好硬件并将一些参数信息按照约定传给内核,然后跳转到内核运行。uboot本身是一个裸机程序主要功能是引导操作系统运行,功能其实并不复杂,只是开始要建立C运行环境,搬运代码到不同地方执行、兼容各种处理器架构、存储器、网络等功能代码非常晦涩。
imx系列处理器内部都固化了一段程序,这个程序根据启动引脚的配置,从不同存储介质约定的地址读取程序的配置信息(IVT),通过读取的信息可以找到用户程序并加载到指定内存运行;配置中还有一些处理器寄存器初始化数据,程序会将这些数据写入寄存器完成硬件的基本初始化功能,其中最主要的是处理器时钟和DDR控制器的配置数据,只有时钟和内存处理器配置完成后CPU才具备基本的运行用户程序运行的条件。在DDR初始化之前所有运行的程序只能使用处理器内部的SRAM,起使地址为0x00900000,大小为128K或256K,依据具体器件而定。uboot也不列外,在编译生成镜像的时候会在uboot.bin前面加上这些配置信息生成最终的uboot.imx镜像文件,IVT寄存器配置信息在board/freescale/xxx/imximage.cfg中,可以阅读doc/imx/mkimage/imximage.txt查看具体的镜像生成过程。
编译
uboot的编译时从make xxx_deconfig开始的,这个命令执行后会在uboot源码的根目录生成一个.config配置文件,xxx_deconfig和硬件电路板对应,不同板子可以创建自己的配置文件,根据实际情况修改uboot的配置。
uboot运行
了解程序启动过程必须先连接启链接文件,找到处理器的中断向量表所在的文件,进而从复位中断入开始代码运行之旅。链接文件和具体的处理器有关,查看源码根目录makefile,可以看到用户可以指定链接文件,如果没指定依次在固定目录查找。imx6ull使用的链接文件是arch/arm/cpu/u-boot.lds。
# If board code explicitly specified LDSCRIPT or CONFIG_SYS_LDSCRIPT, use
# that (or fail if absent). Otherwise, search for a linker script in a
# standard location.
ifndef LDSCRIPT
#LDSCRIPT := $(srctree)/board/$(BOARDDIR)/u-boot.lds.debug
ifdef CONFIG_SYS_LDSCRIPT
# need to strip off double quotes
LDSCRIPT := $(srctree)/$(CONFIG_SYS_LDSCRIPT:"%"=%)
endif
endif
# If there is no specified link script, we look in a number of places for it
#按优先级在不同目录查找连接文件
ifndef LDSCRIPT
ifeq ($(wildcard $(LDSCRIPT)),)
LDSCRIPT := $(srctree)/board/$(BOARDDIR)/u-boot.lds
endif
ifeq ($(wildcard $(LDSCRIPT)),)
LDSCRIPT := $(srctree)/$(CPUDIR)/u-boot.lds
endif
ifeq ($(wildcard $(LDSCRIPT)),)
LDSCRIPT := $(srctree)/arch/$(ARCH)/cpu/u-boot.lds
endif
endif
链接文件中有如下一段:
SECTIONS
{
. = ALIGN(4);
.text :
{
*(.__image_copy_start)
*(.vectors)
CPUDIR/start.o (.text*)
}
}
从这段中可以看出镜像的开头首先放了中断向量表和一个名为start.o两程序段,一般链接文件中具体指定名称的段代表一个文件或函数。.vectors段定义在arch/arm/lib/vectors.S中,这个文件定义了ARM架构的中断向量表,从文件所处的目录位置来看不同指令集的ARM中断向量表相同。
arch/arm/lib/vectors.S:
.macro ARM_VECTORS
#ifdef CONFIG_ARCH_K3
ldr pc, _reset
#else
b reset
#endif
#if !CONFIG_IS_ENABLED(SYS_NO_VECTOR_TABLE)
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
#endif
.endm
.section ".vectors", "ax" //定义.vectors段
#if defined(CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK)
/*
* Various SoCs need something special and SoC-specific up front in
* order to boot, allow them to set that in their boot0.h file and then
* use it here.
*
* To allow a boot0 hook to insert a 'special' sequence after the vector
* table (e.g. for the socfpga), the presence of a boot0 hook supresses
* the below vector table and assumes that the vector table is filled in
* by the boot0 hook. The requirements for a boot0 hook thus are:
* (1) defines '_start:' as appropriate
* (2) inserts the vector table using ARM_VECTORS as appropriate
*/
#include
#else
/*
*************************************************************************
*
* Exception vectors as described in ARM reference manuals
*
* Uses indirect branch to allow reaching handlers anywhere in memory.
*
*************************************************************************
*/
_start:
#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
.word CONFIG_SYS_DV_NOR_BOOT_CFG
#endif
ARM_VECTORS
#endif /* !defined(CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK) */
arch/arm/lib/vectors.S---->arch/arm/cpu/armv7/start.S
在文件vectors.S中我们看到定义了一个名为.vectors的段,这和链接文件中最开始放置的段名对应,因此程序从标号_start开始运行,imx6ull中没有定义CONFIG_SYS_DV_NOR_BOOT_CFG,因此第一行程序在ARM_VECTORS定义的宏中,我看到这个宏正是定义了ARM的中断向量表,表中的第一条指令复位后自动执行,跳转到reset函数中,函数实现在arch/arm/cpu/armv7/start.S文件中,这个文件和ARM的具体架构有关,我们下面一段一段分析这个文件中的代码。代码分析是建立在定义了CONFIG_SPL_BUILD宏的基础上,也就是使用SPL,如果不使用SPL则会省去relocate_code函数调用,也就是不用搬运代码了,会简单一些。不管是否使用SPL在复位发生时uboot一开始都运行在链接时指定的内存地址中,只是使用SPL时会搬运一次代码到DDR顶部运行。
.globl reset
.globl save_boot_params_ret
.type save_boot_params_ret,%function
#ifdef CONFIG_ARMV7_LPAE
.global switch_to_hypervisor_ret
#endif
reset:
/* Allow the board to save important registers */
b save_boot_params //跳转到参数保存函数,在本文件中,预留,没有实际操作
#endif
代码重定位
文件开头就定义了全局标号reset,中断向量表中b reset指令正是跳转到这里执行,要强调一下这条指令时一条相对寻址指令,也就处理器是通过PC寄存器加上一个偏移值来获取目标地址然后跳转过去的,目标地址和当前PC的值是相关的,只要他们相对位置固定这条指令总能正确执行。这个很重要,因为在编译uboot的时候并不确定它会在内存的哪个地址运行,如果采用绝对地址寻址,当uboot实际运行地址和编译时候指定的链接地址不同时程序会跳转到一个错误地址。这种相对寻址的指令就可以解决这个问题,让uboot可以运行在内存任意地址(要想实现这点还有其它一些工作要做,下面会讨论)。也许有人会想,我一个开始在给指定板子编译uboot时根据硬件就可以知道内存的地址信息,直接按照这个地址来编译镜像,加载uboot时也加载在这个地址不就行了吗。没错这样确实可以保证程序逻辑上可以正确运行,但是这样有时是做不到的。
- 有的处理器上电后并不能自动初始化DDR控制器,一般内部有一小段SRAM,上电后uboot的最前面的一段代码会被拷贝到这个小内存中运行,这段程序就是uboot的第一阶段引导程序,它的主要功能之一就是初始化主内存DDR,主内存初始化完成后会将所有uboot拷贝到DDR中运行。这就带来了问题uboot要保证在内部SRAM和DDR中都能正确运行,但链接时只能指定一个运行地址(一般为DDR范围内的地址)。
- uboot的主要作用是引导linux内核运行,内存中会存在uboot和linux内核两个可执行程序,他们之间的地址存在冲突的可能性,因此uboot在设计的时候会自动自我拷贝到DDR的最高地址处运行把低地址留给linux内核,也就是说即使在主内存中uboot也有可能运行在不同地址。
基于上述两点以及为了简化uboot的编译配置,uboot采用了位置无关的代码编译方式,即在编译代码时加入了-pie选项保证整个uboot的是位置无关的。
接着分析代码,首先跳转到save_boot_params处执行,这在当前本文件中定义,预留的函数接口没有实际操作然后跳回save_boot_params_ret处。接下来的操作就比较有意思了,主要功能是第一次重定位代码,这里说是第一次是因为后面的代码在搬运uboot后还会再次重定位一次。前文描述了编译器在编译uboot时使用了-pie选项这样镜像中会生成一个名为rel.dyn的段,这个段中包含了程序内所有需要重定位的全局变量和函数的地址信息。对于汇编程序可以精确的选择相对寻址的指令引用相关函数和变量,但是C语言程序要由编译器翻译成机器码没法精确要编译器使用指定的汇编指令进行相对寻址,而且当程序寻址范围比较大时就不可避免的使用绝对寻址指令,因此就设计了一套机制,编译器将所有需要绝对寻址的标号(函数和全局变量)的内存地址信息专门放到一个段中,运行代码本身要根据实际运行内存地址和编译链接地址的差值去修正该段中导出的全局标号的引用地址,当实际运行内存地址和编译链接地址一致时不需要修正。这个原理其实和操作系统常用的动态链接库加载时根据got段引用外部标号及导出内部标号异曲同工。原理是程序内部在引用全局标号时都不时直接以用,而是在将全局标号的地址信息放到一个中转表中,这个表每个条目记录着全局标号的实际内存地址,当引用全局标号时首先从中转表中拿到其内存地址,然后再通过这个内存地址引用真正的全局标号。而rel.dyn存在的意义就是方便修改中转表中的内容,达到重定位全局标号的目的。
如图,程序中定义了全局变量A,当我在代码其它地方引用这个变量时一般编译器会直接计算出A所在的地址直接操作内存,这样有可能会通过绝对地址引用A,当镜像运行地址和链接地址不一致时A的绝对地址已经改变,再用原来的链接地址去引用A必然时错误的。图中将地址A的地址放在了内存0x81370000中,假设A的链接地址是0x82150000,如果引用A时先从0x81370000地址处取得变量A的真实地址,再用这个地址去引用变量A,同样可以正确对A进项操作,当重定位后整个镜像的地址向下偏移了0x10000(从rel.dyn段所处的运行地址可以计算出),此时A的地址变为了0x81360000,如果还想正确引用A只要把0x81380000地址中A的指针改为0x81360000即可,这个过程就实现了代码的重定位。这里大家也许有个疑问,上述过程在获取变量A指针的过程中使用了地址0x81370000这个也不是绝对地址吗?这个编译器是这么处理的,编译器会把被引用的全局标号的地址放在引用它的代码附近的内存中,这样引用全局标号的指令很容易通过相对地址引用到这个内存进而获取全局标号的地址。之所以是这样是因为相对地址引用的范围是有限的,只能引用当前PC前后一定范围内的地址。至于编译器怎么实现将全局标号的地址放在引用这个标号的附近,这个其实很好办参考start.S中类似_rel_dyn_start_ofs的定义即可。
下面我们通过注释分析代码整个重定位流程:
save_boot_params_ret:
#ifdef CONFIG_POSITION_INDEPENDENT
/*
* Fix .rela.dyn relocations. This allows U-Boot to loaded to and
* executed at a different address than it was linked at.
*/
pie_fixup:
/* 获取标号reset的运行地址到r0 */
adr r0, reset /* r0 <- Runtime value of reset label */
/* 获取标号reset的链接地址到r0 */
ldr r1, =reset /* r1 <- Linked value of reset label */
/* 计算运行地址和link地址的偏移 */
subs r4, r0, r1 /* r4 <- Runtime-vs-link offset */
/* 如果为0,说明link地址和运行地址一致,不需要重定位直接退出 */
beq pie_fixup_done
/*
* 下面几行代码的作用是计算运行时rel.dyn段在内存中实际地址,只有获取这个段的
* 真实的起使地址才能依据其中的信息进行重定位。
*/
//获取pie_fixup标号的运行地址
adr r0, pie_fixup
//_rel_dyn_start_ofs链接时rel.dyn段相对pie_fixup标号的偏移
ldr r1, _rel_dyn_start_ofs
//计算rel.dyn运行时起始地址
/*
* rel.dyn(运行起始地址) = rel.dyn(链接起始地址)- pie_fixup(链接地址) + pie_fixup(运行地址)
*
* 个人认为觉得已经在r4中已经计算了运行地址相对link地址的偏移,直接用rel.dyn链接地址加上这个偏移也可以计算rel.dyn的起使运行地址,而且这样更直观一些。
*/
add r2, r0, r1 /* r2 <- Runtime &__rel_dyn_start */
ldr r1, _rel_dyn_end_ofs
//计算rel.dyn运行结束地址
add r3, r0, r1 /* r3 <- Runtime &__rel_dyn_end */
pie_fix_loop:
//获取rel.dyn段地址中的内容,参考上图执行后r0中的值是0x81370000
ldr r0, [r2] /* r0 <- Link location */’
//获取rel.dyn段地址接下来4个字节中的内容,参考上图执行后r1中的值是0x17即23
ldr r1, [r2, #4] /* r1 <- fixup */
//如果r1等于23则执行重定位
cmp r1, #23 /* relative fixup? */
bne pie_skip_reloc
/* relative fix: increase location by offset */
//将r0中的地址加上偏移地址获得实际运行时的有效地址,执行后r0值为0x81380000
add r0, r4
//获取0x81380000中的内容,值为0x82150000
ldr r1, [r0]
//0x82150000 + 0x10000, r0中的内容加上偏移后存回0x81380000
add r1, r4
str r1, [r0]
//更新运行时rel.dyn段所在地址0x82010000,中的内容,使其指向运行时变量A地址所在的内存
str r0, [r2]
add r2, #8
pie_skip_reloc:
//判断是否所有表项都修改完成,没完成则循环操作
cmp r2, r3
blo pie_fix_loop
pie_fixup_done:
=================================================
_rel_dyn_start_ofs:
.word __rel_dyn_start - pie_fixup
_rel_dyn_end_ofs:
.word __rel_dyn_end - pie_fixup
上面描述了重定位的原理和过程,不过start.S中的这次重定位rel.dyn一般是直接跳过的,因为从后面的一些代码来看在board_init_f函数调用返回后再次搬运代码之前,uboot中很多地方必须要求链接地址和运行地址一致,比如没有对board_init_f中调用的函数列表中的指针加上偏移值做修正。
关闭中断和切换到SVC模式
在引导过程中不需要开启中断以此关闭中断,同时进入SCV模式,这样可以时uboot运行在更高优先级可以更好的控制CPU,同时linux系统也是运行在这个模式下。
#ifdef CONFIG_ARMV7_LPAE
/*
* check for Hypervisor support
*/
mrc p15, 0, r0, c0, c1, 1 @ read ID_PFR1
and r0, r0, #CPUID_ARM_VIRT_MASK @ mask virtualization bits
cmp r0, #(1 << CPUID_ARM_VIRT_SHIFT)
beq switch_to_hypervisor
switch_to_hypervisor_ret:
#endif
/*
* disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode,
* except if in HYP mode already
*/
mrs r0, cpsr
and r1, r0, #0x1f @ mask mode bits
teq r1, #0x1a @ test for HYP mode
bicne r0, r0, #0x1f @ clear all mode bits
orrne r0, r0, #0x13 @ set SVC mode
orr r0, r0, #0xc0 @ disable FIQ and IRQ
msr cpsr,r0
重定位中断向量表关闭缓存和MMU
重定位中断向量表,中断向量指向arch/arm/lib/vectors.S中的中断向量表。ARM结构中的p15协处理器有一个VBAR寄存器,中断时处理器根据这个寄存器中的地址寻找中断向量表。不过这里设置的中断向量表基地址明显是有问题,把_start的链接地址写入到了VBAR中,这明显是错误的,应该把_start的运行地址写入VBAR。不过这个没关系,因为这里已经关闭了中断,而且后面在调用_main时还要重新设置。
#if !CONFIG_IS_ENABLED(SYS_NO_VECTOR_TABLE)
/*
* Setup vector:
*/
/* Set V=0 in CP15 SCTLR register - for VBAR to point to vector */
mrc p15, 0, r0, c1, c0, 0 @ Read CP15 SCTLR Register
bic r0, #CR_V @ V = 0
mcr p15, 0, r0, c1, c0, 0 @ Write CP15 SCTLR Register
#ifdef CONFIG_HAS_VBAR
/* Set vector address in CP15 VBAR register */
ldr r0, =_start
mcr p15, 0, r0, c12, c0, 0 @Set VBAR
#endif
#endif
cpu_init_cp15中主是关闭缓存和MMU,关闭MMU是逻辑程序和操作系统最主要的区别,因此我们认为uboot是一个复杂一些的裸机程序。
#if !CONFIG_IS_ENABLED(SKIP_LOWLEVEL_INIT)
#ifdef CONFIG_CPU_V7A
bl cpu_init_cp15
#endif
#if !CONFIG_IS_ENABLED(SKIP_LOWLEVEL_INIT_ONLY)
bl cpu_init_crit
#endif
#endif
bl _main
cpu_init_crit调用了lowlevel_init,这个函数在文件arch/arm/cpu/armv7/lowlevel_init.S中
ENTRY(cpu_init_crit)
/*
* Jump to board specific initialization...
* The Mask ROM will have already initialized
* basic memory. Go here to bump up clock rate and handle
* wake up conditions.
*/
b lowlevel_init @ go setup pll,mux,memory
ENDPROC(cpu_init_crit)
arch/arm/cpu/armv7/start.S---->arch/arm/cpu/armv7/lowlevel_init.S
lowlevel_init函数主要将栈指向内部IRMA中,imx6ull中定义了CONFIG_SPL_BUILD宏,其次将r9寄存器指向gdata数据结构,gdata中保存了很多全局变量。不过由于使用了SPL这些设置都意义不大,后面board_init_f还要重新设置。
.pushsection .text.s_init, "ax"
WEAK(s_init)
bx lr
ENDPROC(s_init)
.popsection
.pushsection .text.lowlevel_init, "ax"
WEAK(lowlevel_init)
/*
* Setup a temporary stack. Global data is not available yet.
*/
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr sp, =CONFIG_SPL_STACK
#else
ldr sp, =CONFIG_SYS_INIT_SP_ADDR
#endif
bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
#ifdef CONFIG_SPL_DM
mov r9, #0
#else
/*
* Set up global data for boards that still need it. This will be
* removed soon.
*/
#ifdef CONFIG_SPL_BUILD
ldr r9, =gdata
#else
sub sp, sp, #GD_SIZE
bic sp, sp, #7
mov r9, sp
#endif
#endif
/*
* Save the old lr(passed in ip) and the current lr to stack
*/
push {ip, lr}
/*
* Call the very early init function. This should do only the
* absolute bare minimum to get started. It should not:
*
* - set up DRAM
* - use global_data
* - clear BSS
* - try to start a console
*
* For boards with SPL this should be empty since SPL can do all of
* this init in the SPL board_init_f() function which is called
* immediately after this.
*/
bl s_init
pop {ip, pc}
ENDPROC(lowlevel_init)
.popsection
cpu_init_crit是个空函数,接着调用_main函数,其定义在arch/arm/lib/crt0.S文件中。
arch/arm/cpu/armv7/start.S---->arch/arm/lib/crt0.S
_main主要完成C语言环境的初始化工作,为调用c语言编写的board_init_f函数组做准备,这个函数会真正初始化SPL阶段的栈和gd全局数据结构。栈的基地址被设置成了CONFIG_TPL_STACK并做了8字节对齐处理,CONFIG_TPL_STACK在include/configs文件夹下板子对应的配置文件中定义。这里先将CONFIG_TPL_STACK临时作为堆栈的起始地址是因为后面会调用C函数了,必须有一个可用的堆栈。堆栈的设置并没有一步到位后面调用C函数后还会依据返回值重新设置第一阶段使用的最终堆栈。
上图展示了最终处理器内部SRAM的布局情况,从CONFIG_TPL_STACK地址开始向上依次为heap、gddata、栈,这些工作是由board_init_f_alloc_reserve、board_init_f_init_reserve两个C函数完成的,他们定义在/common/init/board_init.c文件中,board_init_f_alloc_reserve函数负责为heap和gddata预留内存空间,board_init_f_init_reserve负责从已经预留的内空间中为heap和gddata做些初始化工作,此后r9寄存器固定指向gd结构首地址。堆栈环境准备好了,接下来就是尽早调用串口初始化函数debug_uart_init,去实现再在文件include/debug_uart.h中,这是一个宏,最终调用board_debug_uart_init()、 _debug_uart_init()两个函数,imx6ull中board_debug_uart_init是个空函数,_debug_uart_init定义在drivers/serial/serial_mxc.c中,主要就是初始化串口相关寄存器,此后串口就可以正常打印log了。接下来清零bss段,这个通过宏CLEAR_BSS引用。至此SPL阶段的C语言环境完全准备就绪可以调用board_init_f完善gd数据结构及其余的基础初始化工作。
.macro CLEAR_BSS
ldr r0, =__bss_start /* this is auto-relocated! */
#ifdef CONFIG_USE_ARCH_MEMSET
ldr r3, =__bss_end /* this is auto-relocated! */
mov r1, #0x00000000 /* prepare zero to clear BSS */
subs r2, r3, r0 /* r2 = memset len */
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 */
strlo r2, [r0] /* clear 32-bit BSS word */
addlo r0, r0, #4 /* move to next */
blo clbss_l
#endif
.endm
/*
* entry point of crt0 sequence
*/
ENTRY(_main)
/* Call arch_very_early_init before initializing C runtime environment. */
#if CONFIG_IS_ENABLED(ARCH_VERY_EARLY_INIT)
bl arch_very_early_init
#endif
/*
* Set up initial C runtime environment and call board_init_f(0).
*/
#if defined(CONFIG_TPL_BUILD) && defined(CONFIG_TPL_NEEDS_SEPARATE_STACK)
ldr r0, =(CONFIG_TPL_STACK)
#elif defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
ldr r0, =(CONFIG_SPL_STACK)
#else
ldr r0, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
bic r0, r0, #7 /* 8-byte alignment for ABI compliance */
mov sp, r0
bl board_init_f_alloc_reserve
mov sp, r0
/* set up gd here, outside any C code */
mov r9, r0
bl board_init_f_init_reserve
#if defined(CONFIG_DEBUG_UART) && CONFIG_IS_ENABLED(SERIAL)
bl debug_uart_init
#endif
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_EARLY_BSS)
CLEAR_BSS
#endif
mov r0, #0
bl board_init_f
#if ! defined(CONFIG_SPL_BUILD)
/*
* Set up intermediate environment (new sp and gd) and call
* relocate_code(addr_moni). Trick here is that we'll return
* 'here' but relocated.
*/
ldr r0, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
bic r0, r0, #7 /* 8-byte alignment for ABI compliance */
mov sp, r0
ldr r9, [r9, #GD_NEW_GD] /* r9 <- gd->new_gd */
adr lr, here
#if defined(CONFIG_POSITION_INDEPENDENT)
adr r0, _main
ldr r1, _start_ofs
add r0, r1
ldr r1, =CONFIG_SYS_TEXT_BASE
sub r1, r0
add lr, r1
#endif
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
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
b relocate_code
here:
/*
* now relocate vectors
*/
bl relocate_vectors
/* Set up final (full) environment */
bl c_runtime_cpu_setup /* we still call old routine here */
#endif
#if !defined(CONFIG_SPL_BUILD) || CONFIG_IS_ENABLED(FRAMEWORK)
#if !defined(CONFIG_SPL_BUILD) || !defined(CONFIG_SPL_EARLY_BSS)
CLEAR_BSS
#endif
# ifdef CONFIG_SPL_BUILD
/* Use a DRAM stack for the rest of SPL, if requested */
bl spl_relocate_stack_gd
cmp r0, #0
movne sp, r0
movne r9, r0
# endif
#if ! defined(CONFIG_SPL_BUILD)
bl coloured_LED_init
bl red_led_on
#endif
/* call board_init_r(gd_t *id, ulong dest_addr) */
mov r0, r9 /* gd_t */
ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */
/* call board_init_r */
#if CONFIG_IS_ENABLED(SYS_THUMB_BUILD)
ldr lr, =board_init_r /* this is auto-relocated! */
bx lr
#else
ldr pc, =board_init_r /* this is auto-relocated! */
#endif
/* we should not return here. */
#endif
ENDPROC(_main)
_start_ofs:
.word _start - _main
arch/arm/cpu/armv7/start.S---->arch/arm/lib/crt0.S---->common/board_f.c
static const init_fnc_t init_sequence_f[] = {
setup_mon_len,
#ifdef CONFIG_OF_CONTROL
fdtdec_setup,
#endif
#ifdef CONFIG_TRACE_EARLY
trace_early_init,
#endif
initf_malloc,
log_init,
initf_bootstage, /* uses its own timer, so does not need DM */
event_init,
#ifdef CONFIG_BLOBLIST
bloblist_init,
#endif
setup_spl_handoff,
#if defined(CONFIG_CONSOLE_RECORD_INIT_F)
console_record_init,
#endif
#if defined(CONFIG_HAVE_FSP)
arch_fsp_init,
#endif
arch_cpu_init, /* basic arch cpu dependent setup */
mach_cpu_init, /* SoC/machine dependent CPU setup */
initf_dm,
#if defined(CONFIG_BOARD_EARLY_INIT_F)
board_early_init_f,
#endif
#if defined(CONFIG_PPC) || defined(CONFIG_SYS_FSL_CLK) || defined(CONFIG_M68K)
/* get CPU and bus clocks according to the environment variable */
get_clocks, /* get CPU and bus clocks (etc.) */
#endif
#if !defined(CONFIG_M68K)
timer_init, /* initialize timer */
#endif
#if defined(CONFIG_BOARD_POSTCLK_INIT)
board_postclk_init,
#endif
env_init, /* initialize environment */
init_baud_rate, /* initialze baudrate settings */
serial_init, /* serial communications setup */
console_init_f, /* stage 1 init of console */
display_options, /* say that we are here */
display_text_info, /* show debugging info if required */
checkcpu,
#if defined(CONFIG_SYSRESET)
print_resetinfo,
#endif
#if defined(CONFIG_DISPLAY_CPUINFO)
print_cpuinfo, /* display cpu info (and speed) */
#endif
#if defined(CONFIG_DTB_RESELECT)
embedded_dtb_select,
#endif
#if defined(CONFIG_DISPLAY_BOARDINFO)
show_board_info,
#endif
INIT_FUNC_WATCHDOG_INIT
misc_init_f,
INIT_FUNC_WATCHDOG_RESET
#if CONFIG_IS_ENABLED(SYS_I2C_LEGACY)
init_func_i2c,
#endif
#if defined(CONFIG_VID) && !defined(CONFIG_SPL)
init_func_vid,
#endif
announce_dram_init,
dram_init, /* configure available RAM banks */
#ifdef CONFIG_POST
post_init_f,
#endif
INIT_FUNC_WATCHDOG_RESET
#if defined(CONFIG_SYS_DRAM_TEST)
testdram,
#endif /* CONFIG_SYS_DRAM_TEST */
INIT_FUNC_WATCHDOG_RESET
#ifdef CONFIG_POST
init_post,
#endif
INIT_FUNC_WATCHDOG_RESET
/*
* Now that we have DRAM mapped and working, we can
* relocate the code and continue running from DRAM.
*
* Reserve memory at end of RAM for (top down in that order):
* - area that won't get touched by U-Boot and Linux (optional)
* - kernel log buffer
* - protected RAM
* - LCD framebuffer
* - monitor code
* - board info struct
*/
setup_dest_addr,
#ifdef CONFIG_OF_BOARD_FIXUP
fix_fdt,
#endif
#ifdef CONFIG_PRAM
reserve_pram,
#endif
reserve_round_4k,
arch_reserve_mmu,
reserve_video,
reserve_trace,
reserve_uboot,
reserve_malloc,
reserve_board,
reserve_global_data,
reserve_fdt,
reserve_bootstage,
reserve_bloblist,
reserve_arch,
reserve_stacks,
dram_init_banksize,
show_dram_config,
INIT_FUNC_WATCHDOG_RESET
setup_bdinfo,
display_new_sp,
INIT_FUNC_WATCHDOG_RESET
reloc_fdt,
reloc_bootstage,
reloc_bloblist,
setup_reloc,
#if defined(CONFIG_X86) || defined(CONFIG_ARC)
copy_uboot_to_ram,
do_elf_reloc_fixups,
#endif
clear_bss,
#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
!CONFIG_IS_ENABLED(X86_64)
jump_to_copy,
#endif
NULL,
};
void board_init_f(ulong boot_flags)
{
gd->flags = boot_flags;
gd->have_console = 0;
if (initcall_run_list(init_sequence_f))
hang();
#if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
!defined(CONFIG_EFI_APP) && !CONFIG_IS_ENABLED(X86_64) && \
!defined(CONFIG_ARC)
/* NOTREACHED - jump_to_copy() does not return */
hang();
#endif
}
board_init_f函数定义在文件common/board_f.c中,代码很简单就是顺序调用init_sequence_f列表中函数,参数boot_flags为0。
- setup_mon_len 将uboot从开始到bss段结束的长度信息设置到gd->mon_len
- fdtdec_setup 将设备树起始地址设置到gd->fdt_blob中,设备树放置的位置有多种方式,可以紧跟着uboot的__bss_end放置,也可以放在uboot中的__dtb_dt_spl_begin段中,用户也可以通过配置自己指定。这个函数还要校验设备树的有效性。
- initf_malloc 初始化gd中malloc使用的heap相关信息,基址除外,基址已经在board_init_f_init_reserve中设置
- initf_bootstage 记录引导阶段
- console_record_init:控制台输入输出缓冲区初始化
- arch_cpu_init:cpu相关初始化和具体器件有关,imx6ull这个函数定义在arch/arm/mach-imx/mx6/soc.c中
- initf_dm:设备模型初始化,这个比较复杂本文不做讨论
- board_early_init_f:板级初始化,imx6ull定义在board/freescale/mx6ullevk/mx6ullevk.c中,与板子相关
- env_init:环境变量输出化,uboot环境环境变量可以存储在很多介质中,每种类型的介质会提供一个struct env_driver类型的驱动结构来从这些介质访问保存的环境变量,这里就遍历驱动列表调用其初始化函数,如果没有唯一指定用哪个介质中的环境变量,还会设置uboot镜像中名为default_environment的环境变量,这个环境变量很多条目是在include/configs板级别头文件中定义的,所有使用的环境变量最终会用一个hash列表管理。
- display_options:到这里就在控制台打印出uboot的基本信息了
- dram_init:获取DDR大小到gd->ram_size中,imx6ull是通过读取DDR控制器中配置的信息获取大小的
- setup_dest_addr:初始化ddr相关信息,这些在后面代码重定位时会用到,知道ddr这些信息后就可以重新为堆、栈以及其它数据结构在ddr中分配内存,分配完还要把相关数据拷贝到新内存中,后面一系列函数都做这个工作。
- setup_reloc:计算uboot重定位的偏移地址,后面会根据这个偏移把uboot镜像拷贝到DDR中,同时拷贝老gd内容到新gd中。
至此board_init_f函数完成,可以看到这个函数主要时完成了gd数据结构的初始化、ddr内存分配和一些数据结构的搬运调整工作,uboot将被搬运到DDR的接近top的地址处。没有设计到太多硬件的初始化。
arch/arm/lib/crt0.S
#if ! defined(CONFIG_SPL_BUILD)
/*
* Set up intermediate environment (new sp and gd) and call
* relocate_code(addr_moni). Trick here is that we'll return
* 'here' but relocated.
*/
ldr r0, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
bic r0, r0, #7 /* 8-byte alignment for ABI compliance */
mov sp, r0
ldr r9, [r9, #GD_NEW_GD] /* r9 <- gd->new_gd */
adr lr, here //获取here当前运行地址
#if defined(CONFIG_POSITION_INDEPENDENT)
/* 计算当前运行地址和链接地址的偏移 */
adr r0, _main
ldr r1, _start_ofs
add r0, r1
ldr r1, =CONFIG_SYS_TEXT_BASE
sub r1, r0
add lr, r1
#endif
/* 获取gd->reloc_off值 */
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
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
b relocate_code
here:
/*
* now relocate vectors
*/
bl relocate_vectors
/* Set up final (full) environment */
bl c_runtime_cpu_setup /* we still call old routine here */
#endif
#if !defined(CONFIG_SPL_BUILD) || CONFIG_IS_ENABLED(FRAMEWORK)
#if !defined(CONFIG_SPL_BUILD) || !defined(CONFIG_SPL_EARLY_BSS)
CLEAR_BSS
#endif
# ifdef CONFIG_SPL_BUILD
/* Use a DRAM stack for the rest of SPL, if requested */
bl spl_relocate_stack_gd
cmp r0, #0
movne sp, r0
movne r9, r0
# endif
#if ! defined(CONFIG_SPL_BUILD)
bl coloured_LED_init
bl red_led_on
#endif
/* call board_init_r(gd_t *id, ulong dest_addr) */
mov r0, r9 /* gd_t */
ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */
/* call board_init_r */
#if CONFIG_IS_ENABLED(SYS_THUMB_BUILD)
ldr lr, =board_init_r /* this is auto-relocated! */
bx lr
#else
ldr pc, =board_init_r /* this is auto-relocated! */
#endif
/* we should not return here. */
#endif
ENDPROC(_main)
回到crt0.S中,开始的时候r9保存了gd老地址的指针,接下来做下面几个工作
- 重新初始化栈基地址寄存器,新地址在ddr中,由reserve_stacks函数分配
- 将r9指向ddr中gd的新地址,之前在setup_reloc中将老gd拷贝到了新的gd
- 调用relocate_code函数将uboot镜像搬运到ddr中,目标地址由reserve_uboot计算,在调用这个函数之前还有一件重要任务是计算函数的返回地址lr,因为从relocate_code返回时,ddr中新地址的uboot镜像已经就绪要跳到新地址运行而且要返回到新地址的here处继续向下执行,并不能重新从uboot开始处执行。这里计算lr比较复杂,因为涉及到三个地址之间的关系链接地址、当前运行地址、目标运行地址(一般这里链接地址、当前运行地址一致),gd->reloc_off是目标运行地址与链接地址的差值,因此返回地址计算公式为:
lr = gd->reloc_off-当前运行地址和链接地址的偏移 + here当前运行地址
即
lr = 目标运行地址和当前运行地址的偏移 + here当前运行地址
gd->reloc_off-当前运行地址和链接地址的偏移就等于目标运行地址和当前运行地址的偏移。
- relocate_code:这段代码和“代码重定位”一节中描述的功能完全一致,再次修正rel.dyn段是因为uboot镜像又被搬运到新地址了。
- relocate_vectors:重新设置中断向量表基地址,前文设置过一次但是是不正确的,搬运过代码再次设置。
- 前面已经清除过bss段,这里不用再清除
- spl_relocate_stack_gd: 这里又重新计算栈、堆、gd全局变量的地址如果deconfig文件中配置了CONFIG_SPL_STACK_R,这是因为不同配置的uboot可能需要更大的栈和堆,所以设置到ddr中,如果没有配置CONFIG_SPL_STACK_R则栈是在sram中,由board_init_f_alloc_reserve分配,但是gd和堆其实已经在ddr中,地址是由函数reserve_global_data、reserve_malloc分配,gd基地址后续在crt0.S文件中设置到r9寄存器中。配置CONFIG_SPL_STACK_R后gd会再次从ddr一个地址搬运到另外一个地址。个人觉得uboot对栈和gd的处理实在过于复杂,本来可以至多两次设置的事情,硬是搞成了3次,而且没看到对不同处理器的兼容性由任何帮助。
- board_init_r:这个函数也是完成一组函数的调用,最终根据环境变量的配置和终端的操作引导操作系统或启动hush(shell),至此uboot的c语言环境完全建立。
arch/arm/cpu/armv7/start.S---->arch/arm/lib/crt0.S---->common/board_r.c
void board_init_r(gd_t *new_gd, ulong dest_addr)
{
/*
* Set up the new global data pointer. So far only x86 does this
* here.
* TODO([email protected]): Consider doing this for all archs, or
* dropping the new_gd parameter.
*/
if (CONFIG_IS_ENABLED(X86_64) && !IS_ENABLED(CONFIG_EFI_APP))
arch_setup_gd(new_gd);
#if !defined(CONFIG_X86) && !defined(CONFIG_ARM) && !defined(CONFIG_ARM64)
gd = new_gd;
#endif
gd->flags &= ~GD_FLG_LOG_READY;
if (IS_ENABLED(CONFIG_NEEDS_MANUAL_RELOC)) {
for (int i = 0; i < ARRAY_SIZE(init_sequence_r); i++)
MANUAL_RELOC(init_sequence_r[i]);
}
if (initcall_run_list(init_sequence_r))
hang();
/* NOTREACHED - run_main_loop() does not return */
hang();
}
- 首先要重定位init_sequence_r中的函数指针,因为init_sequence_r数组中的函数地址还是链接时候的地址,直接调用肯定出错所以要加上偏移地址,uboot中这种修正很多,不过这里又一个疑问init_sequence_f中并没有做这个操作,唯一的解释是最后一次搬移代码之前链接地址和运行地址必须一致。
- 顺序调用init_sequence_r中的函数
这些函数本文就不具体分析了,放在另一片文章分析,函数最终调用run_main_loop进入主循环,自动执行环境变量bootcmd中的命令引导系统,如果在规定的时间内控制台输入回车则中断引导进入shell界面(hush)。