U-Boot 启动流程分析
本文基于ARM vexpress-a9虚拟板卡和u-boot-2016-09源码,对u-boot的ARMv7架构启动过程代码进行分析,主要梳理从第一条指令的执行到进入main loop的整个过程
直接上图,这张图就是本文的所有内容了,涵盖了从CPU加载的第一条指令到进入main loop,不同文件的部分用虚线框出来了,文件内的不同函数跳转用带颜色的背景圈出来,红色处理部分代表汇编,黄色处理部分代表C函数
本文通过对u-boot中ARMv7架构的启动代码跟踪,试图回答以下问题
代码是从哪里开始执行的?
启动后的最开始部分做了什么?
ARMv7启动时是如何设置异常向量的?
ARMv7启动时是如何设置MMU、Cache和TLB的?
在boot阶段为什么要设置一个临时栈?作用是什么?
为了能够执行C函数,需要准备什么?
CONFIG_SYS_INIT_SP_ADDR在什么位置?是怎么确定的?
u-boot中的GD是什么?
为什么要进行u-boot的重定向?具体是如何操作的?
启动入口
如何知道程序启动的第一条指令在哪里呢?最准确的方法是查看链接脚本u-boot.lds文件
ENTRY(_start)
告诉了编译器汇编程序的入口是_start
标签,在arch/arm/lib/vectors.S文件中定义了_start
汇编程序
_start
的第一条指令是一个分支跳转语句,跳转到reset标签执行,后面是7条设置pc寄存器为几个异常处理handle的操作
_start
的这一段代码如何理解呢?这里有两个层面的意义,第一个层面是程序从_start
开始执行,第一条语句就跳转到reset开始执行,这是启动代码的流程设计。要想理解第二层面,需要了解ARMv7体系结构中异常处理的相关知识,ARM定义了8种异常,当CPU运行过程中出现发生某种异常时,就会停止正常的程序执行,转而跳转到异常处理流程中,而每种异常具体应该怎么处理,是由软件定义的,软件通过异常向量表vector来执行不同的处理,而异常向量表本质上是在内存中定义的几个函数handle,需要通过设置专用的寄存器来告诉CPU向量表在哪里,_start
标签开头这一段内容,就是定义的异常向量表,包括第一条指令跳转到reset,也是向量表的一部分,除了正常启动从这里执行,当程序发生复位异常时,也会跳转到这里执行。ARM异常处理详细介绍后续文章会专门介绍
reset处理
reset
标签定义在arch/arm/cpu/armv7/start.S中,第一条指令是跳转到save_boot_params
执行
save_boot_params
的定义也在start.S中,没有做任何操作直接返回了
接下来这一段汇编,将cpsr的bit[4:0]设置为了0x13,即0b10011,将cpsr的bit[6~7]设置为1,将CPU模式设置为SVC模式,禁用FIQ和IRQ
这里有几个知识点
ARM处理器模式
使用MRS和MSR来读出CPSR内容以及修改CPSR
通过设置CPSR来修改处理器模式
通过设置CPSR来enable/disable FIQ和IRQ
这些内容涉及到ARM体系结构处理器模式和程序状态寄存器相关内容,后续文章会专门介绍。经过这段汇编之后,处理器变为了SVC高级权限模式,因为启动过程后续还需要对系统进行很多高权限的设置,必须让CPU处于高级权限模式。而关FIQ和IRQ也是由于u-boot启动部分代码比较关键,外部中断可能会干扰代码的正常执行,而boot阶段又不需要中断触发什么事件,因此关闭中断
然后接下来一段汇编,通过注释可以看出是在设置异常向量表vector
具体是怎么操作的呢?这里需要ARM体系结构协处理器CP15相关知识,协处理器CP15的作用很多,异常向量设置、MMU、Cache等很多重要的内容都和CP15有关,后文也会看到,CP15协处理器有很多个特殊寄存器用于实现特殊功能,而操作CP15中寄存器的方法就如同上图代码一样,通过MRC读出和MCR写入,并在固定位置指定cp15协处理器和c1寄存器,c1寄存器是SCTLR寄存器,其中的V标志位用于决定CPU的异常向量表位于低地址还是高地址(0xFFFF0000),代码中将其设置为0,则默认的向量表在0x00000000,而具体真正的向量表在哪里,又是由cp15的c12寄存器VBAR来决定。代码中可以看出,将c12设置为了_start
标签,这正好与上文对_start
的第二层解释对应上了
接下来跳转了两个处理部分cpu_init_cp15
和cpu_init_crit
cpu_init_cp15
定义如下
通过注释可以看到这段汇编代码关闭了L1的数据和指令Cache,关闭了MMU stuff和cache,具体的操作都是通过设置cp15的c1、c7、c8来完成的。因为u-boot的启动过程会操作RAM设置堆栈、全局数据结构和代码重定向,如果使用了MMU,还需要设计页表,如果使用了cache,很多RAM相关的操作都需要进行额外的cache操作,增加了代码处理,而MMU和cache对于u-boot而言不是关键作用,因此启动过程将其关闭
cpu_init_crit
的处理代码如下,跳转到了lowlevel_init
lowlevel_init
在arch/arm/cpu/armv7/lowlevel_init.S中
注释中说设置了一个临时的栈空间,代码中将sp设置为了CONFIG_SYS_INIT_SP_ADDR
,然后跳过了一个GD_SIZE
的大小,将lr压栈是为了保存lr寄存器,因为后续调用C函数s_init
会修改lr寄存器,如果不先保存的话,cpu_init_crit
返回不到reset
里了
s_init
符号在vexpress-a9中是未定义的,从u-boot编译生成的System.map中查找不到该符号,说明s_init
不存在,在其他CPU中可能会进行一些额外的初始化
至此,reset
的大致内容已经梳理完成,总结一下,主要做了以下几件事
将CPU模式切换到SVC模式
关闭FIQ和IRQ
禁用MMU
禁用Cache
整个过程如下图所示
_main
start.S中reset
的最后一条语句是跳转到_main
标签,_main
定义在arch/arm/lib/路径下,_main
标签的注释信息说的比较详细,清楚的列出了_main
标签的执行过程,大致内容如下
为调用
borad_init_f()
准备一个初始的环境,这个初始环境只是在本地内存(RAM)中提供一个栈空间和一个用于存储全局数据(GD)的地方,在这个环境中,只有全局变量有效,BSS段无法访问。GD在调用board_init_f()
之前必须被清零调用
board_init_f()
,该函数为在DDR/DRAM中运行程序准备硬件环境。由于board_init_f()
执行时,DDR/DRAM还无法访问,该函数必须使用GD来存储数据,GD中包含重定向目的、未来将使用的栈和未来GD应该存放的位置在DDR/SDRAM中设置初始环境,包括在DDR/SDRAM中的栈和GD,但是BSS和初始化的非常量数据仍然不可用
对于正式U-Boot(非SPL),调用
relocate_code()
,该函数将U-Boot从当前位置重定向到由board_init_f()
计算的新位置对于SPL,
board_init_f()
直接返回,不进行任何操作为调用
board_init_r()
准备最后的环境,该环境将BSS变量初始化为0,初始化非常量数据,初始化DDR/DRAM中的栈,对于正式U-Boot(非SPL),某些CPU还有一些关于内存的工作要做,会调用
c_runtime_cpu_setup()
跳转到
board_init_r()
borad_init_f()准备
先来看第一阶段的代码处理,该阶段是为board_init_f()
做准备的
不考虑SPL的情况下,该代码首先将sp寄存器赋值为CONFIG_SYS_INIT_SP_ADDR
,然后将sp指向的地址进行了对齐操作,再将sp赋值给r0,调用了board_init_f_alloc_reserve
,这是个C函数,r0作为入参和出参,内部应该是对r0做了修改,退出后再将r0赋值给sp和r9,又进入了board_init_f_init_reserve
来看看board_init_f_alloc_reserve
内做了什么,该函数定义在common/init/board_r.c中
注释里对该函数的解释是为GD分配了一个预定的空间,注意这里的空间是在RAM中,这个GD空间的起始地址top
是由r0决定,而返回的地址是GD的底部bottom
,即计算好了GD的大小,将GD的结束位置作为返回值赋值给r0
结合注释和代码就比较好理解了,top
即传入的r0,也就是要给GD分配的高地址,代码中将top减去了GD的大小作为地址返回
然后将r0赋值给sp,再将r9赋值给sp,此时sp和r9都指向了GD的底部,再调用board_init_f_init_reserve
,该函数也在common/init/board_r.c中,定义如下
该函数的作用也比较好理解,用一个指针指向GD底部,然后对GD空间全部赋值为0,然后将指向GD底部的r0修改到指向GD的顶部
做一个阶段性的总结,board_init_f()
之前的这一段代码,主要的作用是在RAM中为GD分配了一段空间,并初始化为0
整个过程如图所示
borad_init_f()函数
接下来就到了board_init_f()
函数中,该函数定义如下
主要是调用initcall_run_list
来调用init_sequence_f
数组中的所有函数,这里有一个小细节,即全局数据结构GD的指针gd
是怎么定义的,在arch/arm/include/asm/global_data.h中
因为这里分析的不是ARM64架构代码,因此gd
指针的定义为r9寄存器,前面分析,r9指向了GD的底部
init_sequence_f
的定义如下,定义了很多函数
比较重要的是在dram_init
函数中对DRAM进行了初始化,然后在setup_dest_addr
函数中对gd->relocaddr
进行了赋值
可以看到是将重定向地址赋值到了DRAM的顶部,也就是高地址
然后在setup_reloc
中将GD拷贝到了DRAM中
代码重定向
代码重定向定义在汇编relocate_code
中,文件为arch/arm/lib/relocate.S
可以看到首先是使用ldmia
和stmia
指令,将u-boot的代码段拷贝到目标地址,也就是DRAM的高地址,这里的__image_copy_start
和__image_copy_end
在u-boot的链接文件中定义了,只将代码段包括在内
然后将__rel_dyn段进行拷贝
向量表重定向
向量表重定向定义在汇编relocate_vectors
中,文件也是arch/arm/lib/relocate.S
可以看到将向量表拷贝到了高地址,并设置了CP15的c1寄存器
C运行时设置
如果定义了关闭ICache,则在此处禁用指令cache,没有做什么特殊的操作
board_init_r()函数
board_init_r()函数定义在common/board_r.c中,定义如下
主要是遍历并调用init_sequence_r
函数,该数组与init_sequence_f
类似,也是定义了一组初始化函数,最后一个执行函数就是run_main_loop
,也就进入了主循环
至此已经完成了本文对ARMv7系列u-boot启动流程的大致过程梳理
总结
这里对ARMv7系列vexpress-a9虚拟板的u-boot启动流程进行一个总结概述,整个启动过程大致流程如下
启动的入口由u-boot.lds链接脚本可知是
_start
,定义在arch/arm/lib/vectors.S中,该位置定义了ARM的异常响亮表,第一个指令就是处理reset异常reset
定义在arch/arm/cpu/armv7/start.S中,设置ARM处理器模式为SVC模式,以获取操作CPU内部寄存器的权限,然后禁用了IRQ和FIQ,以防止不必要的中断打断。这几个操作都是通过设置cpsr完成的接下来通过设置CP15协处理器禁用了MMU和Cache,u-boot的处理并不需要这些部件的参与,以免增加额外的cache一致性维护代码
reset
初级阶段完成,跳转到arch/arm/lib/crt0.S中的_main
标签至此程序一直在RAM/Flash中运行,由于u-boot编译的bss段链接地址并不在此时的运行地址中,无法使用bss段,而u-boot为了存储一些中间变量,在RAM中通过设置一个临时栈空间,让C函数能够进行嵌套调用,并在这里临时栈的临近位置初始化了一个全局结构体GD
调用
board_init_f
C函数完成DDR初始化以及重定向地址计算,包括GD、栈等内容的重定向地址,这些信息也都是保存到RAM中的GD里根据GD里的重定向地址进行重定向操作,主要是将u-boot代码段、rel_dyn段、异常向量表、栈空间和GD本身都重定向到DDR中的高地址里,然后跳转到DDR中执行
调用
board_init_r
C函数完成后半部分初始化,并最终进入主循环
本文主要是对整个流程进行初步的分析,让读者对u-boot的启动流程有一个整体的认识,GD结构体的内容、重定向的详细过程以及board_init_f
和board_init_r
的细节处理,后文会进行专项分析