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_cp15cpu_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标签的执行过程,大致内容如下

  1. 为调用borad_init_f()准备一个初始的环境,这个初始环境只是在本地内存(RAM)中提供一个栈空间和一个用于存储全局数据(GD)的地方,在这个环境中,只有全局变量有效,BSS段无法访问。GD在调用board_init_f()之前必须被清零

  2. 调用board_init_f(),该函数为在DDR/DRAM中运行程序准备硬件环境。由于board_init_f()执行时,DDR/DRAM还无法访问,该函数必须使用GD来存储数据,GD中包含重定向目的、未来将使用的栈和未来GD应该存放的位置

  3. 在DDR/SDRAM中设置初始环境,包括在DDR/SDRAM中的栈和GD,但是BSS和初始化的非常量数据仍然不可用

  4. 对于正式U-Boot(非SPL),调用relocate_code(),该函数将U-Boot从当前位置重定向到由board_init_f()计算的新位置

  5. 对于SPL,board_init_f()直接返回,不进行任何操作

  6. 为调用board_init_r()准备最后的环境,该环境将BSS变量初始化为0,初始化非常量数据,初始化DDR/DRAM中的栈,

  7. 对于正式U-Boot(非SPL),某些CPU还有一些关于内存的工作要做,会调用c_runtime_cpu_setup()

  8. 跳转到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

可以看到首先是使用ldmiastmia指令,将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_fC函数完成DDR初始化以及重定向地址计算,包括GD、栈等内容的重定向地址,这些信息也都是保存到RAM中的GD里

  • 根据GD里的重定向地址进行重定向操作,主要是将u-boot代码段、rel_dyn段、异常向量表、栈空间和GD本身都重定向到DDR中的高地址里,然后跳转到DDR中执行

  • 调用board_init_rC函数完成后半部分初始化,并最终进入主循环

本文主要是对整个流程进行初步的分析,让读者对u-boot的启动流程有一个整体的认识,GD结构体的内容、重定向的详细过程以及board_init_fboard_init_r的细节处理,后文会进行专项分析