要分析boot启动流程,首先要找到程序入口地址,可以通过编译uboot生成u-boot.lds,通过查看链接脚本u-boot.lds知道入口点是 arch/arm/lib/vectors.S 文件中的_start。
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)//代码当前入口点: _start, _start 在文件arch/arm/lib/vectors.S 中有定义
SECTIONS
{
. = 0x00000000;
. = ALIGN(4);
.text :
{
*(.__image_copy_start)//uboot 拷贝的首地址
*(.vectors)//vectors 段保存中断向量表
arch/arm/cpu/armv7/start.o (.text*)//将 arch/arm/cpu/armv7/start.s 编译出来的代码放到中断向量表后面
*(.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)//uboot 拷贝的结束地址
}
.rel_dyn_start :
{
*(.__rel_dyn_start)//.rel.dyn 段起始地址
}
.rel.dyn : {
*(.rel*)
}
.rel_dyn_end :
{
*(.__rel_dyn_end)//.rel.dyn 段结束地址
}
.end :
{
*(.__end)
}
_image_binary_end = .;//镜像结束地址
. = ALIGN(4096);
.mmutable : {
*(.mmutable)
}
.bss_start __rel_dyn_start (OVERLAY) : {
KEEP(*(.__bss_start));//.bss 段起始地址
__bss_base = .;
}
.bss __bss_base (OVERLAY) : {
*(.bss*)
. = ALIGN(4);
__bss_limit = .;
}
.bss_end __bss_limit (OVERLAY) : {
KEEP(*(.__bss_end));//.bss 段结束地址
}
.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.*) }
}
uboot启动主要分为两部分arch级初始化和板级初始化。下面具体分析各个初始化。
如上图所示,入口点是 arch/arm/lib/vectors.S 文件中的_start。_start 开始的是中断向量表,跳转到 reset 函数里面, reset 函数在 arch/arm/cpu/armv7/start.S 里面。 reset 函数跳转到了 save_boot_params 函数,save_boot_params 函数又跳转到 save_boot_params_ret 函数,该函数主要分为五个功能:
①设置CPU处于SVC模式,并且关闭FIQ和IRQ两个中断;
②设置向量表重定位;
③设置cp15寄存器(Cache,MMU,TLBs);
④配置关键寄存器和初始化
⑤跳转_main,进入板级初始化
cpu_init_crit 内部仅仅是调用了函数 lowlevel_init,该函数主要用于设置sp指针和r9寄存器。函数 lowlevel_init 在文件arch/arm/cpu/armv7/ lowlevel_init.S 中定义。
① 设置SP指向CONFIG_SYS_INIT_SP_ADDR=0X0091FF00,流程见上图。可结合下图自己分析
② 设置SP 8字节对齐
③ 设置gd(global data)大小为248B
④ sp地址(0x0091FE08)保存在r9寄存器中
⑤将ip和lr入栈
⑥ 跳转s_init//什么都没有做
⑦ 将入栈的ip和lr出栈,并将lr赋给pc
_main主要分为六个部分,简单来说主要是把iROM中的程序拷贝至DDR,然后载重定向,最后初始化各种外设。
board_init_f_alloc_reserve函数主要用于留出早期malloc和gd(global_data)内存区域;board_init_f_init_reserve函数主要功能初始化gd。两个函数主要功能如上图所示:
① 设置指针地址0X0091FF00;
② 减去0X400,248B。留出早期malloc内存区域和gd内存区域。再保留8B,以使得16字节对齐;
③ 初始化gd(清零),再做16字节对齐,最终sp指向0X0091FB00;
board_init_f函数主要用于初始化DDR,定时器,完成代码拷贝等
① 初始化外设;
② 初始化gd各个成员变量
commmon/board_f.c
board_init_f函数中的initcall_run_list函数主要用于调用一系列函数,值保存在 init_sequence_f函数中。
initcall_run_list函数的具体主要功能如下:可结合上图分析
1、gd->num_len=_bss_end-_start//uboot image大小,即代码长度,0X878A8E74-0x87800000=0XA8EF4
2、initf_malloc()//gd->malloc=CONFIG_SYS_MALLOC_F_LEN=0X400,内存池大小为0x400
3、arch_cpu_init()//初始化架构相关的内容,CPU级别操作
4、 initf_dm()//驱动模型一些初始化
5、 board_early_init_f()//初始化串口的IO配置(I.MX6ULL)
6、 timer_init()//初始化定时器(Cortex-A7内核)
…
14、 init_baud_rate()//根据环境变量baudrate设置gd->baudrate=115200
…
24、dram_init()//设置gd->ram_size=512MB 0X2000 0000B
…
44、set_up_addr()//设置地址gd->ram_zise=0X2000 0000;gd->ram_top=0XA0000000
(0X80000000+0X2000 0000);gd->relocadder=0XA0000000(重定位后最高地址)…
...
48、reserve_uboot//gd->mon_len=0X8EF4;gd->start_addr_sp=
0X9FF47000;gd->relocadder=0X9FF47000//uboot重定位后的起始地址
49、reserve_malloc//TOTAL_MALLOC_LEN=CONFIG_SYS_MALLOC_LEN(0X10000000B=16MB)+
CONFIG_ENV_SIZE(0X2000=8K)
50、reserve_board()//留出板子bd所占的内存区
52、reserve_global_data()//留出gd所占的内存区
…
55、resreve_stacks//留出栈空间,gd->start_addr_sp-16,然后16字节对齐
…
最终sp=gd->start_addr_sp=0X9EF44E90
…
61、setup_reloc//设置gd其他一些成员变量,供后面定位使用,并且将以前的gd拷贝到gd->new_gd处
最终uboot重定位后偏移为0X18747000(0X9FF47000-0X87800000),新的gd首地址0X9EF44EB8,新的sp首地址0X9EF44E90
relocate_code函数主要用于代码拷贝,在relocate_code函数之前还有语句ldr r0,[r9,#GD_RELOCADDR]
,即r0=gd-> relocaddr= 0X9FF47000,uboot重定位后的首地址。
relocate_code函数在arch/arm/lib/relocate.S中,下面结合代码分析该函数
1、ldr r1,=__image_copy_start//r1=0X8780000源地址起始地址
2、subs r4, r0, r1//r4=0X9FF47000-0X87800000=0X18747000 偏移
3、ldr r2, =__image_copy_end//r2=0X8785dc6c源地址结束地址
4、copy_loop: //拷贝,将uboot从源地址0X8780000拷贝至0X9FF47000
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
注意:直接将uboot从0X87800000拷贝至其他地方后,函数调用、全局变量引用可能会出问题。uboot采用位置无关码来处理该类问题(简单说采用相对地址寻址,而不是采用绝对地址寻址,并且重定位后需要将Label+offset)。在使用 ld 进行链接的时候使用选项”- pie” 生成位置无关的可执行文件。具体为.rel.dyn段。
5、
/*
* fix .rel.dyn relocations
*/
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 /* relative fixup? */
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
第5段的程序分析如下:
1、r2=__rel_dyn_start,也就是.rel.dyn 段的起始地址。
2、r3=__rel_dyn_end,也就是.rel.dyn 段的终止地址。
3、从.rel.dyn 段起始地址开始,每次读取两个 4 字节的数据存放到 r0 和 r1 寄存器中, r0 存放低 4 字节的数据,也就是 Label 地址; r1 存放高 4 字节的数据,也就是 Label 标志。
4、r1 中给的值与 0xff 进行与运算,其实就是取 r1 的低 8 位。
5、判断 r1 中的值是否等于 23(0X17)。//0X17就是判断是否是Label
6、如果 r1 不等于 23 的话就说明不是描述 Label 的,执行函数 fixnext ,否则的话继续执行下面的代码。
7、r0 保存着 Label 值, r4 保存着重定位后的地址偏移, r0+r4 就得到了重定位后的Label值。此时 r0 保存着重定位后的 Label 值。
8、读取重定位后 Label 所保存的变量地址,此时这个变量地址还是重定位前的地址,将得到的值放到 r1 寄存器中。
9、 r1+r4 即可得到重定位后的变量地址 。
10、重定位后的变量地址写入到重定位后的 Label 中。
11、比较 r2 和 r3,查看.rel.dyn 段重定位是否完成。
12、如果 r2 和 r3 不相等,说明.rel.dyn 重定位还未完成,因此跳到 fixloop 继续重定位 .rel.dyn 段。
relocate_vectors函数主要用于重定位向量表。relocate_vectors函数位于arch/arm/lib/relocate.S中,用于设置VBAR寄存器为重定位后的中断向量表起始地址
board_init_r函数与board_init_f函数类似,用于初始化一系列外设。 board_init_r函数位于commmon/board_r.c中,实际上
board_init_f函数并没有初始化所有的外设,还需要做一些后续工作,这些后续工作就是由函数board_init_r 函数来完成。
board_init_r函数中含有init_sequence_r函数,该函数用于初始化序列。而init_sequence_r函数又含有run_main_loop函数,用于进入uboot命令模式或启动linux内核。run_main_loop函数中最重要的是main_loop函数,该函数位于common/main.c中。下面分析main_loop函数
该函数主要功能如下:
1、打印启动进度
2、设置环境变量
3、cli_init()//初始化hush shell相关变量
4、run_preboot_environment_command()//获取环境变量prebooot的内容,preboot是一些预启动命令,一般不使用该环境变量
5、bootdelay_process()//获取bootdelay的值,然后保存到stored_bootdelay全局变量里面,获取bootcmd环境变量值,并且将其返回
6、autoboot_command(bootcmd)
---> abortboot(stored_bootdelay)//参数为bootdelay,该函数用于处理倒计时
---> abortboot_normal(bootdelay)//参数为bootdelay,该函数用于处理倒计时
7、cli_loop()//uboot命令处理函数 common/cli.c
---> parse_file_outer()//common/cli_hush.c
---> rcode = parse_stream_outer(&input, FLAG_PARSE_SEMICOLON);//hush shell 的命令解释器,负责接收命令行输入,然后解析并执行相应的命令
---> parse_stream()//命令解析
---> run_list()//运行命令
---> run_list_real()
---> run_pipe_real()
---> cmd_process()//处理命令,即执行命令。Uboot使用 U_BOOT_CMD 来定义一个命令。 CONFIG_CMD_XXX来使能uboot中的某个命令。U_BOOT_CMD最终是定义了一个cmd_tbl_t类型的变量,所有的命令最终都是存放在.u_boot_list段里面。cmd_tbl_t的cmd成员变量就是具体的命令执行函数,命令执行函数都是do_xxx。
---> find_cmd()//从.u_boot_list段里查找命令,当找到对应的命令以cmd_tlb_t类型返回
---> cmd_call()//cmdtp->cmd,直接引用cmd成员变量