主流Bootloader的启动过程均可分为Stage1及Stage2两部分,一般来说,Stage1完成最基本的初始化操作、搬移代码及建立C语言运行环境;Stage2完成剩余的初始化操作。故Stage1一般使用汇编完成,Stage2一般使用C语言完成,不过这只是很粗略的情况,实际上Stage1中也会使用C语言代码。
下面以之前针对S3C2440移植好的U-Boot 2016.05为例,分析U-Boot中Stage1的代码。
程序入口点分析
要分析启动流程首先要找到程序入口点,也就是最先运行的代码位于何处,这是由链接过程决定的,故找到链接脚本文件u-boot.lds
:
1 | ENTRY(_start) |
可以看到,位于映像文件最前面的依次是.__image_copy_start
、.vectors
及arch/arm/cpu/arm920t/start.o
文件。
.__image_copy_start
位于映像文件的起始地址处,它在文件arch/arm/lib/sections.c
中被定义:
1 | char __bss_start[0] __attribute__((section(".__bss_start"))); |
这个变量的用处应该是用来进行代码定位的,也就是这个变量的地址就代表映像文件的起始地址,并不占用真实的存储空间,所以此处使用了一个0长度的数组,其他变量的用处也是一样的。U-Boot作者对此的说明是:
We need a 0-byte-size type for these symbols, and the compiler does not allow defining objects of C type ‘void’. Using an empty struct is allowed by the compiler, but causes gcc versions 4.4 and below to complain about aliasing. Therefore we use the next best thing: zero-sized arrays, which are both 0-byte-size and exempt from aliasing warnings.
.vectors
是中断向量表,对于ARM920t来说,使用的是位于arch/arm/lib/vectors.S
中的通用向量表:
1 | .globl _start |
可以看到,复位后会首先执行b reset
这条指令跳转至reset
处执行,而reset
标号正是位于arch/arm/cpu/arm920t/start.S
文件中:
1 | .globl reset |
所以一般都说,代码执行开始于start.S
文件,这也就是程序的入口点。下面就从这个文件开始分析。
start.S
start.S
的执行流程见下图:
lowlevel_init
是平台相关的,故它位于board/samsung/smdk2440/lowlevel_init.S
中。此处需要完成内存控制器的初始化工作,以便之后能正常使用SDRAM。对于S3C2440来说,就是初始化了BWSCON
、BANKCONn
、REFRESH
、BANKSIZE
、MRSRB
这些寄存器。
_main
是平台无关的,位于arch/arm/lib/crt0.S
中,crt的意思就是C-runtime startup Code,此文件的目的就是建立C语言运行环境,以便之后跳转到Stage2,下面就来分析此文件。
crt0.S
crt0.S
文件开头的注释中说明了_main
的执行序列,摘录如下:
This file handles the target-independent stages of the U-Boot start-up where a C runtime environment is needed. Its entry point is _main and is branched into from the target’s start.S file.
_main execution sequence is:
Set up initial environment for calling board_init_f().
This environment only provides a stack and a place to store the GD (‘global data’) structure, both located in some readily available RAM (SRAM, locked cache…). In this context, VARIABLE global data, initialized or not (BSS), are UNAVAILABLE; only CONSTANT initialized data are available. GD should be zeroed before board_init_f() is called.Call board_init_f().
This function prepares the hardware for execution from system RAM (DRAM, DDR…) As system RAM may not be available yet, , board_init_f() must use the current GD to store any data which must be passed on to later stages. These data include the relocation destination, the future stack, and the future GD location.Set up intermediate environment where the stack and GD are the ones allocated by board_init_f() in system RAM, but BSS and initialized non-const data are still not available.
For U-Boot proper (not SPL), call relocate_code().
This function relocates U-Boot from its current location into the relocation destination computed by board_init_f().Set up final environment for calling board_init_r().
This environment has BSS (initialized to 0), initialized non-const data (initialized to their intended value), and stack in system RAM. GD has retained values set by board_init_f().For U-Boot proper (not SPL), some CPUs have some work left to do at this point regarding memory, so call c_runtime_cpu_setup.
Branch to board_init_r().
其程序流程图如下:
全局变量gd
是一个struct global_data
类型的结构体,此结构体的定义位于include/asm-generic/global_data.h
文件中。此结构体保存了初始化阶段需要用到的各种全局变量,大约有100个左右,相关说明如下:
The following data structure is placed in some memory which is available very early after boot (like DPRAM on MPC8xx/MPC82xx, or some locked parts of the data cache) to allow for a minimum set of global variables during system initialization (until we have set up the memory controller so that we can use RAM).
Keep it SMALL and remember to set GENERATED_GBL_DATA_SIZE > sizeof(gd_t)
Each architecture has its own private fields. For now all are private
下面具体说明每一步的作用:
Step 1.1 初始化SP寄存器
1 | ldr sp, =(CONFIG_SYS_INIT_SP_ADDR) |
CONFIG_SYS_INIT_SP_ADDR
在板子的配置文件include/configs/smdk2440.h
中定义:
1 |
GENERATED_GBL_DATA_SIZE
是struct global_data
按16字节对齐后的大小。可以看到,这里是使用了SDRAM开头4K部分来作为临时堆栈。
Step 1.2 board_init_f_alloc_reserve
1 | mov r0, sp |
1 | ulong board_init_f_alloc_reserve(ulong top) |
为GD
结构体分配保留空间,16字节对齐。结构体的存储方式是从低地址向高地址生长,故此处返回的就是gd
的基地址。
Step 1.3 board_init_f_init_reserve
1 | mov r9, r0 |
简化版的函数实现:
1 | void board_init_f_init_reserve(ulong base) |
将gd
的基地址放到r9
中保存起来,之后将刚刚分配的保留空间清零完成初始化。
Step 2 board_init_f
1 | mov r0, #0 |
1 | void board_init_f(ulong boot_flags) |
board_init_f()
函数主要是调用initcall_run_list(init_sequence_f)
函数,而此函数的作用就是依次执行init_sequence_f
中的各函数指针,去除错误处理、Debug信息输出等部分后,简化版的函数体很简单:
1 | int initcall_run_list(const init_fnc_t init_sequence[]) |
下面来看一下init_fnc_t init_sequence_f[]
这个数组,init_fnc_t
是一个是用typedef
定义的函数指针类型:
1 | typedef int (*init_fnc_t)(void); |
init_sequence_f[]
数组中包含的就是需要执行的初始化函数列表,这里面的函数很多,这些函数的核心目的就是填充gd
结构体。其中比较重要的有下面这些(省略了其中大部分函数):
1 | static init_fnc_t init_sequence_f[] = { |
U-Boot自带的SMDK2410配置中,PLL时钟配置是在board_early_init_f
中完成的,不过在针对2440移植时将此部分代码移到了start.S文件中,故board_early_init_f
中只包含了GPIO端口初始化;
serial_init
及console_init_f
完成串口终端的初始化,在此之后串口终端才会有输出。
display_options
和print_cpuinfo
就是U-Boot运行时串口最先输出的那段版本信息。
dram_init
:对于2440来说,因为之前已经初始化好了DRAM,此处只是将DRAM大小写入gd
中,在show_dram_config
中将SDRAM大小信息输出至控制台。
setup_dest_addr
中根据RAM的大小计算了重定位开始地址,将RAM顶端地址存放在gd->relocaddr = gd->ram_top
中。
reserve_xxxxxx
依次计算重定位每一部分所需的RAM空间,并从gd->relocaddr
中减去这部分空间,其中reserve_uboot
是计算重定位U-Boot本身(.text + .bss)的地址;reserve_global_data
是计算重定位gd
的地址。从这里可以看出,重定位后的代码和数据位于RAM顶端。
setup_reloc
向gd
中填入了重定位地址信息,并完成了gd
本身的重定位,**gd->reloc_off
就是在这里计算出来的,代表重定位地址偏移量**:
1 | static int setup_reloc(void) { |
对于其他平台来说,最后还有一步jump_to_copy
,这里会实现代码跳转,也就是不会再返回此函数,不过对于ARM平台来说,是没有这一步的,执行完init_sequence_f[]
中的函数后会返回到board_init_f()
中,最后回到_main
中继续执行后面的代码。
Step 3 设置中间变量
1 | ldr sp, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */ |
这里面用到的gd
中的值都是在board_early_init_f()
中设置好的,其中有几句很重要的语句:
1 | adr lr, here |
here
就是下面那个标号,此处将here
的地址加上gd->reloc_off
这个偏移量后存放到lr
寄存器中,之后调用b relocate_code
命令返回后就会返回到lr
寄存器中的地址处。这就实现了将代码拷贝到SDRAM后从新的重定位地址处接着运行的目的。这也就是源码中这一段注释的含义:
Set up intermediate environment (new sp and gd) and call relocate_code(addr_moni). Trick here is that we’ll return ‘here’ but relocated.
设置好这些寄存器后调用relocate_code
实现代码重定位。
Step 4 relocate_code
位于/arch/arm/lib/relocate.S
中,使用汇编完成,简化版的代码如下:
1 | ENTRY(relocate_code) |
在这段代码中,使用ldmia
及stmia
指令,每次使用r10
及r11
两个寄存器进行复制,实现了将__image_copy_start
及__image_copy_end
中的代码复制到gd->reloc_off
处的目的。
这里省略了.rel.dyn
部分的重定位代码,这部分代码和以上代码差不多,只是还做了些额外处理。
最后调用bx lr
返回,此时会进行状态切换后接着运行行下面的程序,不过此时运行的程序已是位于重定位的地址处了。
之后执行的是bl relocate_vectors
,这个实现的是中断向量表的重定位,具体就不展开了。在此之后还调用了c_runtime_cpu_setup
,不过对于2440来说,这里没有做任何事情,可以跳过。
Step 5 清零BSS段
1 | ldr r0, =__bss_start /* this is auto-relocated! */ |
这一步建立C语言运行环境,实际上也就是清零BSS段,代码也很好懂,根据__bss_start
及__bss_end
的地址循环写入0即可。只是这有一个问题,注释中有说明:“this is auto-relocated!”,这说明__bss_start
及__bss_end
的地址已经是重定位后的新地址了,这是如何实现的呢?__bss_start
及__bss_end
的定义见本文最前面arch/arm/lib/sections.c
中的代码,是两个长度为0的数组,估计是在relocate_code
搬运代码过程中已经搬运了这两个0长度的代码了?这个问题还需要之后来仔细思考下。
Step 6 board_init_r
1 | bl coloured_LED_init |
在调用board_init_r
前还调用了两个函数:coloured_LED_init
及red_led_on
,不过在SMDK2410的BSP文件中没有实现这两个函数,目前使用的是common/board_f.c
中的空函数,相关函数还有下面这些:
1 | /************************************************************************ |
最后来看看board_init_r()
函数:
1 | void board_init_r(gd_t *new_gd, ulong dest_addr) |
initcall_run_list
函数在上面”2.board_init_f”小节中已经分析过了,就是依次执行传入数组中的各函数,此处传入了init_sequence_r
,这就是Stage2的初始化函数列表。
至此,Stage1结束,开始执行Stage2的初始化流程。