Uboot启动流程

Start.S:Uboot第一阶段入口

1、 设置CPU模式为SVC模式(breset)

2、 关闭中断

3、 定义部分全局变量的值,例如:_sdagentflag、_logosize、_arm2size等

4、 Cpu_init_crit:设置重要的寄存器

5、 将(_start)代码copy至sram中,其中_start定义在开始阶段

6、 设置堆栈:先预留出malloc空间,再留出bdinfo空间,最后设置栈空间(其中用到的部分宏定义在uboot-xxx/include/configs/xx.h中)

7、 清除bss

8、 跳转至start_armboot

部分注解:

扫描二维码关注公众号,回复: 5744403 查看本文章
1       .balignl 16,0xdeadbeef

                表明剩下的代码需要16字节对齐,不对齐的用deadbeef填充,或者常用的dabc0de

2       bic     r0,r0,#0x1f

                表明清除r0寄存器的bit[4-0]位,再将结果重新保存入r0

3       orr     r0,r0,#0xd3

                将r0与0xd3进行‘或’运算后保存至r0中

4、     首先,sys模式和usr模式相比,所用的寄存器组,都是一样的,但是增加了一些访问一些在usr模式下不能访问的资源。而svc模式本身就属于特权模式,本身就可以访问那些受控资源,而且,比sys模式还多了些自己模式下的影子寄存器,所以,相对sys模式来说,可以访问资源的能力相同,但是拥有更多的硬件资源

所以,从理论上来说,虽然可以设置为sys和svc模式的任一种,但是从uboot方面考虑,其要做的事情是初始化系统相关硬件资源,需要获取尽量多的权限,以方便操作硬件,初始化硬件。从uboot的目的是初始化硬件的角度来说,设置为svc模式,更有利于其工作。因此,此处将CPU设置为SVC模式(放在最开始也是为了方便后续的硬件操作)。


/*

  *           修订:2017-1211

  *           内容:新增uboot启动第一阶段指令级分析以及各部分内容存放图

  *          

  */

注:第一阶段需结合多个文件一起分析,最重要的两个文件:

uboot.lds:Uboot链接脚本,主要用于“分配区域”,即指定各个部分存放的位置,其中

                      指定了Uboot的开始执行处:ENTRY(_start)

head.S:    uboot启动的入口,以_start为开始,以b start_armboot结束

head.S详细分析如下:

1、根据lds文件得知,先走_start代码,即b reset:通过设置cpsr寄存器使arm切换到SVC模式,具体原因见上文。

注:系统复位和上电以后都会从这里开始执行。b指令是只跳转不返回的,因此如何能够执行后面的异常向量对应的跳转代码呢?

原因节选自:http://blog.csdn.net/it_114/article/details/6260707

毕设笔记

1.对ARM异常(Exceptions)的理解

所有的系统引导程序前面中会有一段类似的代码,如下:

.globl_start                   ;系统复位位置

_start: b      reset            ;各个异常向量对应的跳转代码

        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                      ;快速中断异常

从中我们可以看出,ARM支持7种异常。问题时发生了异常后ARM是如何响应的呢?第一个复位异常很好理解,它放在0x0的位置,一上电就执行它,而且我们的程序总是从复位异常处理程序开始执行的,因此复位异常处理程序不需要返回。那么怎么会执行到后面几个异常处理函数呢?看看书后,明白了ARM对异常的响应过程,于是就能够回答以前的这个疑问。当一个异常出现以后,ARM会自动执行以下几个步骤:

(1)把下一条指令的地址放到连接寄存器LR(通常是R14),这样就能够在处理异常返回时从正确的位置继续执行。

(2)将相应的CPSR(当前程序状态寄存器)复制到SPSR(备份的程序状态寄存器)中。从异常退出的时候,就可以由SPSR来恢复CPSR。

(3) 根据异常类型,强制设置CPSR的运行模式位。

(4)强制PC(程序计数器)从相关异常向量地址取出下一条指令执行,从而跳转到相应的异常处理程序中。

至于这些异常类型各代表什么,我也没有深究。因为平常就关心reset了,也没有必要弄清楚。

ARM规定了异常向量的地址:

   b      reset            ; 复位 0x0

ldr pc, _undefined_instruction ;未定义的指令异常 0x4

       ldr     pc,_software_interrupt     ;软件中断异常   0x8

       ldr     pc,_prefetch_abort          ;预取指令    0xc

       ldr     pc,_data_abort              ;数据        0x10

       ldr     pc,_not_used                 ;未使用      0x14

       ldr     pc,_irq                      ;慢速中断异常   0x18

        ldr   pc,_fiq                      ;快速中断异常    0x1c

这样理解这段代码就非常简单了。碰到异常时,PC会被强制设置为对应的异常向量,从而跳转到相应的处理程序,然后再返回到主程序继续执行。

这些引导程序的中断向量,是仅供引导程序自己使用的,一旦引导程序引导Linux内核完毕后,会使用自己的中断向量。嗬嗬,这又有问题了。比如,ARM发生中断(irq)的时候,总是会跑到0x18上执行啊。那Linux内核又怎么能使用自己的中断向量呢?原因在于Linux内核采用页式存储管理。开通MMU的页面映射以后,CPU所发出的地址就是虚拟地址而不是物理地址。就Linux内核而言,虚拟地址0x18经过映射以后的物理地址

就是0xc0000018。所以Linux把中断向量放到0xc000 0018就可以了。

MMU的两个主要作用:

(1)安全性:规定访问权限

(2) 提供地址空间:把不连续的空间转换成连续的。

第2点是不是实现页式存储的意思?

.globl_start ;系统复位位置

_start: b reset ;各个异常向量对应的跳转代码

ldr pc, _undefined_instruction ;未定义的指令异常

……

_undefined_instruction:

.word undefined_instruction

也许有人会有疑问,同样是跳转指令,为什么第一句用的是 b reset;而后面的几个都是用ldr?

为了理解这个问题,我们以未定义的指令异常为例。

当发生了这个异常后,CPU总是跳转到0x4,这个地址是虚拟地址,它映射到哪个物理地址取决于具体的映射。

ldr pc, _undefined_instruction 

相对寻址,跳转到标号_undefined_instruction,然而真正的跳转地址其实是_undefined_instruction

的内容——undefined_instruction。那句.word的相当于:

_undefined_instruction dw undefined_instruction (详见毕设笔记3)。

这个地址undefined_instruction到底有多远就难说了,也许和标号_undefined_instruction在同一个

页面,也许在很远的地方。不过除了reset,其他的异常是MMU开始工作之后才可能发生的,因此

undefined_instruction的地址也经过了MMU的映射。

在刚加电的时候,CPU从0x0开始执行,MMU还没有开始工作,此时的虚拟地址和物理地址相同;另一方

面,重启在MMU开始工作后也有可能发生,如果reset也用ldr就有问题了,因为这时候虚拟地址和物理

地址完全不同。

因此,之所以reset用b,就是因为reset在MMU建立前后都有可能发生,而其他的异常只有在MMU建立之后才会发生。用b reset,reset子程序与reset向量在同一页面,这样就不会有问题(b是相对跳转的)。如果二者相距太远,那么编译器会报错的

2、异常向量处理函数:ldrpc, __异常类型,此处以第一条为例分析:

                ldr pc, _undefined_instruction

即,将标号_undefined_instruction表示的内容(undefined_instruction)加载到pc指针处(跳到该处执行),在head.S找到如下:

                undefined_instruction:

                                get_bad_stack

                                bad_save_user_regs

                                bl do_undefined_instruction

get_bad_stack:

.macro.endm之间的代码段定义为宏。

主要用于“获取系统异常情况下出错的堆栈           

bad_save_user_regs:

                保存用户模式寄存器

之后通过bl跳转指令跳到对应的函数执行。该函数只做两件事:打印reg,重启系统。

3、系统在执行reset处的代码后,由于不带返回,因此继续向后执行:bl cpu_init_crit

主要工作:通过设置寄存器相应位的值关闭MMUcache

关闭MMU和cache原因:http://blog.csdn.net/MingLLu/article/details/50585557

1.cache的定位

cache是位于主存(即是内存)与CPU内部的寄存器之间的一个存储设施,用来加快cpu与内存之间数据与指令的传输速率,从而加快处理的速度。

------------------------------------------------------------------------------------------

2.cache的作用

------------------------------------------------------------------------------------------

根据cache的定位可以看出来,它是用来加快cpu从内存中取出指令的速度,但我们都知道,在设备上电之初,我们的内存初始化比较慢一拍,当cpu初始化了,但内存还没准备好之后,就对内存进行数据读,那么势必会造成了指令取址异常,系统就会挂了。所以,在u-boot的上电之初,就得关闭掉数据cache,指令的cache关闭与不关闭没有太大的关系。

------------------------------------------------------------------------------------------

3.为啥要关闭MMU呢?

------------------------------------------------------------------------------------------

mmu在设备上电之初是没有任何作用的,也就是说,在u-boot的初始化之初执行汇编的那一段代码中,包括后面的初始化一些具体的外设时,访问的都是实际的地址,mmu的打开起不到任何的意义,为了不影响启动之初对程序的启动,关闭掉mmu设备是常用的做法。

4、cpu_init_crit返回后向下执行stack_setup。Stack_setup工作仅仅是设置sp寄存器的值,该值由_TEXT_BASE减去mallocsize和gd结构体的长度,再根据情况减去irq和fiq的长度,最后减去12字节的空间(为异常向量的空间?)

疑问:

1relocate的代码负责将uboot拷贝到_TEXT_BASE处,此处被注掉,原因不知?

        preloder在运行时会先初始化DRAM,再将UbooteMMC拷贝至DRAM中,然后再启动Uboot,因此无需再次拷贝uboot

 

2)没有relocate代码,那start_armboot是在何时搬移至_TEXT_BASE处的?原先就放在那的么?

通过反汇编arm-none-linux-gnueabi-objdumpu-boot可以看出,<_start>入口运行地址就是06f00000和_TEXT_BASE一致。说明Uboot已经被放置在正确的位置(由preloder拷贝)

 

补充:为何链接文件中指定_start0x000000,而map文件里面是6f00000

答案参见:http://blog.csdn.net/qiaoliang328/article/details/5891913

                uboot-83xx/config.mk中有:LDFLAGS+= -Ttext $(TEXT_BASE)

5、clear_bss

核心是调用memset清空bss段

BSS(Block Started by Symbol)段是可执行文件中的一种数据段。通常ARM编译器生成的可执行文件由两部分数据组成,分别是代码段和数据段。代码段又分为可执行代码段(text)和只读数据段(rodata);数据段又分为初始化数据段(data)和未初始化数据段(bss)。

6、ldr r10, =CFG_PAGETABLE_ADDRESS

       mov r0, r10

       bl createPageTable

获取页表地址,通过赋值给r0得以作为createPageTable的第一个入参(r1,r2对应函数的第二、三个参数,此处createPageTable只有一个入参,故只需修改r0即可)

通过代码查看为类似的页表映射,但具体实现不明确。可参考:http://blog.csdn.net/groundhappy/article/details/54889677

7、使用BX命令跳转至_armboot_start。

通过 _armboot_start:. word TEXT_BASE  指定_armboot_start的位置,

在./uboot-83xx/board/ac83xx_evb/config.mk中可知TEXT_BASE的位置为0x06F00000,且注释为“用于外部SRAM启动时的情况”

补充:

1)通过查看uboot-83xx/u-boot.map可以看出,uboot代码是从0x06f00000处开始,并且长度为0x4ef64

2)通过u-boot.map可知“.global”是4个字节的变量,“.word”是8个字节

3)通过代码和System.map文件得到uboot分布图如下:


 

4)正常启动时_sdagentflag0,插卡升级时_sdagentflag1_sdagentflag何时被初始化?

      Start.S中将_sdagentflag初始化为0x0,但插卡升级时,_sdagentflag如何变为1 的


Start_armboot():Uboot第二阶段入口

1、初始化gd_t结构体
2、一次调用init_sequnence数组中定义的函数,用于一些初始化
3、清零CONFIG_SYS_Malloc区
4、mmc_initialize(gd->bd):初始化mmc
5、env_relocate():初始化系统环境变量(参数),将其重定位到内存中特定的位置
6、stdio_init():初始化平台上的设备并挂接在链表上
7、jumptable_init():初始化系统各接口的跳转表
8、check_partition():检查系统分区信息
补充
{
    1)check_BCB_Valid():
       从mmc中的0x1000+4*1024处读取4*1024大小的块,通过比较读取出的头信息和长度是否符合来判断。
 
    2)在readpartitioninfofromflash_ext()中调用check_partition_Valid():
      从mmc中的0x1000+8*1024处读取1块数据(partitionHead),比较读取出的头信息是否符合来判断,同时读取partitionHead后的blocknct块的数据,用相同的方法计算长度是否符合。
 
    3)通过一个while循环,遍历所有由步骤2)读取出的分区信息,核心工作就是获取各 
       个分区的“分区起始地址”“分区大小(区分KB和MB)”“分区名”,最后判断
       kernel的类型,对全局环境变量g_bootcmd进行填充,用于下述A步骤正常启动
       流程的需要====>run_command(getenv(“bootcmd”))
}
 
9、misc_init_r():
A)DTB:从g_dtbPartitionAddr(0x1f00000)地址读取DTB到FDT_LOAD_ADDR(0x600000)处,size(0x7c00)。
问:读取操作do_mmcops中指定了读取的地方为mmc 0(一块开发板有多个存储设备,例如SDEMMC等,当前情况0表示emmc2表示外接SD卡)、读取的地址g_dtbPartitionAddr以及读出数据的放置地址FDT_LOAD_ADDR,按照uboot流程的描述为将image文件从emmc读取到DRAM中,如何确定FDT_LOAD_ADDRg_dtbPartitionAddr是在DRAM还是在emmc上?
猜测1
    之前start.S中的createPageTale函数将DRAM地址进行了映射,且其页表在mmc中。所以可能由于其地址不同表示不同存储设备。
猜测2
    Uboot运行在DRAM上,因此默认地址FDT_LOAD_ADDR即指DRAM地址,而读操作中的起始地址由于指定了mmc read操作,所以由do_mmcops函数将输入地址g_dtbPartitionAddr指定为emmc的地址。
 
2:此处的FDT_LOAD_ADDR(0x600000)与之前TEXT_BASE表示的0x6f00000是同一类型的地址么?包括后续加载LOGOMETAZONEimage时候的地址。若一致且均表示DRAM的地址,那么为何放置在那?也由此推出,为何将uboot放在0x6f00000而不是DRAM的起始地址。
 
 
B)调用get_reserve_mem_fdt_dts():
   步骤1)依次遍历g_reserve_mem数组各元素的第一个成员(DTB节点名),g_reserve_mem初始化时一共10个节点,从FDT_LOAD_ADDR中读取DTB数据(步骤A已将DTB读入FDT_LOAD_ADDR中),调用fdt_path_offset()接口获取DTB节点,再获取该节点的“reg”属性,用于获取其起始地址和大小。
    步骤2)内存STATIC_RSV_MEM_INFO_START_ADDR(0xf00000)处存放了一个整形数值,该数值用于表明系统当前有多少DTB节点。0xf00000+sizeof(int)后的地址STATIC_RSV_MEM_STRUCT_START_ADDR为DTB节点信息结构体数组的起始地址。
步骤1)中每遍历一个名称的DTB节点并获取其reg属性后就更新本地内存下的DTB节点信息结构体数组中对应的reg属性(起始地址/大小)。
     步骤3)如果没有定义dvp,则对应的DTB节点信息结构体的起始地址和大小均设置为0,方便后续fdt_mod_rsv_mem_node_for_zImage()删除节点和收回内存。
 
    该函数的核心工作是获取dts下的各个设备在DRAM中的起始地址和size(包括METAZONEarm2等的起始地址和大小),便于后续读取对应image时放置在DRAM中正确的位置。
        
DTS/DTB/FDT 原理参考PPThttps://wenku.baidu.com/view/7a2dd75b04a1b0717ed5dd62.html
 
TIPS:http://blog.csdn.net/zhaoqiwen1106/article/details/49854025
1)设备树的主要优势:对于同一SOC的不同主板,只需更换设备树文件.dtb即可实现不同主板的无差异支持,而无需更换内核文件。
2)bootloder需要支持将设备树的数据结构传给内核。(所以下述调用内核时第三个参数是FDT_LOAD_ADDR
3)设备树包含DTC(device tree compiler),DTS(device tree source和DTB(device tree blob)。且DTC编译DTS和DTSI可得DTB。
4)由于一个SOC可能有多个不同的电路板,而每个电路板拥有一个 .dts。这些dts势必会存在许多共同部分,为了减少代码的冗余,设备树将这些共同部分提炼保存在.dtsi文件中,供不同的dts共同使用。.dtsi的使用方法,类似于C语言的头文件,在dts文件中需要进行include .dtsi文件。当然,dtsi本身也支持include 另一个dtsi文件。
5)DTC编译.dts生成的二进制文件(.dtb),bootloader在引导内核时,会预先读取.dtb到内存,进而由内核解析。
 
 
 
C)当系统正常启动时,_sdagentflag标志位0,才会走步骤c,步骤c工作:
METAZONE:从g_metazonePartitionAddr(0x12d800*512)地址读取METAZONE到0xFE00000处
 
METAZONE:从g_metazonePartitionAddr(0x12dc00*512)地址读取METAZONE到0xFE10000处
具体实现较其他image文件的读入操作更复杂,不明白
D)fdt_set_cmdline_for_zImage():
        通过dts的chosen node节点,向zImage传递参数。
具体参见:http://blog.csdn.net/zhaoqiwen1106/article/details/49854025
的chosen node描述
 
E)Logo: 从g_logoPartitonAddr(0x24f00000)读取到0xC500000, image size= 0x4b0200 
 
F)ARM2Image_Read:从g_arm2PartitionAddr(0x800000)读取到(0x120c0000), image size= 0x6d8200 
 
G)启动时TrustZoneImage_Read: 从g_tzPartitionAddr(0x400000)读取到(0x100000), image size= 0x6d8200       
 
H)u4ARM2Start():启动arm2
 
10、测试wakeup是否被按下
        若按下则调用do_rsd_upgrade()走“拷贝升级”流程:
            查找是否有可用的U盘
            查找是否有可用的SD卡
            找到可用的升级设备后,走同一流程:do_extsdcard_or_udisk_upgrade
               当找到可以升级的文件后:读分区信息、去除写保护、按对应分区信息进行升               
               级、更新对应的分区信息、重启写保护
11、未按下wakeup键,则跳过直接走后面的流程:判断_sdagentflag标志是否为1,为1表明启用“做卡升级”。
 
12、“做卡升级”的入口为do_sdagent(),通过run_command(“sdagent”,0)进入。流程。
    注:U_BOOT_CMD(“xxx”,  ,  ,do_xx…)用于定义run_command和对应命令的入口函数,此处为U_BOOT_CMD(sdagent,  ,  ,do_sdagent);
 
13、若_sdagentflag标志为0,则表明走正常启动流程,进入main_loop()
 
main_loop():主要功能有初始化启动时间、初始化modem(串口传输协议)、设置bootdelay、读取和解析command。
 
1)计算启动次数限制(启动次数限制往往用于实际产品中)
2)u_boot_harsh_start():设置第二种解析命令的方式
3)install_auto_complete():安装命令行自动补全功能
4)获取bootdelay,并在该时间段内判断是否有用户按下abort key(’Enter’键):
   
A、若无abort key按下则走正常启动流程
执行run_command(getenv(“bootcmd”))
其中getenv(“bootcmd”)可以获取默认的环境参数default_environment[“bootcmd”],得到“CONFIG_MMC_BOOTCMD”:
 
#define CONFIG_MMC_BOOTCMD “mmc read 0 0x6000000 0x6000 0x2000\; mmc read 0 0x7000000 0x8000 0x1000\;bootm 0x6000000”
Uboot即会执行这三条命令。
1)mmc read 0 0x6000000 0x6000 0x2000命令表示从mmc 0中偏移为0x6000的地方读取0x2000字节的数据到0x6000000(uboot运行在DRAM所以此处指DRAM)处;
2)mmc read 0 0x7000000 0x8000 0x1000命令表示从mmc 0中偏移为0x8000的地方读取0x1000字节的数据到0x7000000处。
3)bootm 0x6000000同样由U_BOOT_CMD指定其命令对应的函数为do_bootm()
 
 
(注:正常情况下会按照上述流程走到bootm命令===>调用do_bootm(),但是在前期check_partition()期间会将”bootz 命令”赋值给全局变量’g_bootcmd’最终在进入main_loop之前通过setenv(“bootcmd”, g_bootcmd)提前设置好环境,当Uboot走到步骤A)时getenv(“bootcmd”)获取到的命令不再是以”bootm 0x600000”结尾,而是以”bootz”结尾(命令对应函数为do_bootz()),所以在启动流程的log中发现直接走do_bootz流程)
 
 
B、若abort key按下则进入命令行模式
若有按键按下,则条件if(bootdelay() >=0 && s &&  !abortboot(bootdelay))不满足直接跳出向下走。命令行模式下则会一直做“读命令”和“解析命令”两件事。
 
do_bootz()
1)bootz中直接将KERNEL_LOAD_ADDR(0x1008000)表示的值强转为函数指针theKernel:
        theKernel = (void *)(KERNEL_LOAD_ADDR)
 
2)将g_tz_mem_addr(0x100000)表示的值做同样处理,转化为Trustzone函数指针:
        initTrustZone = (void (*)(int, int, uint, void*))g_tz_mem_addr;
 
3)获取完接口之后调用cleanup_before_linux()进行启动前的预处理(关中断、关cache)。
 
4)若定义了CONFIG_TRUSTZONE_SURPPORT则执行:
        initTrustZone(0, machid, FDT_LOAD_ADDR, theKernel);
   否则执行:
        theKernel(0, machid, FDT_LOAD_ADDR);
  最终跳到对应的地址执行后续启动内核的流程。
 
 
do_sdagent()
1、检查升级工具版本信息
2、对设备本身mmc和SD卡进行初始化
3、从SD卡中的g_ImageStartAddr/512 – 1的位置读取1个block的数据(升级的头信息,
   包括:是否format标记、是否全升级标记、是否擦除mmc标记等)
4、根据标记的值进行不同的设置,例如:清除写保护,擦除mmc等
5、若不为全升级,则先获取分区信息
6、进行do{}while()操作,直到分区升级完毕为止。
        a)从设备mmc中读取一块的数据,清除写保护
        b)检查分区的类型,通常为raw类型,核心升级接口为emmc_write_raw_image()
        c)写入新的分区后更新分区信息
        d)重新写保护
        e)写入datazone等信息
 
 

 

附:do_bootmdo_bootm_linux流程
do_bootm()
1、检查参数;
2、调用boot_start():
        1)通过boot_get_kernel()获取有效的内核镜像
        2)检查获取的image是否合适
3、启动准备(停用一些功能,例如关闭cache和中断)
4、通过boot_load_os()加载系统。(主要包括:获取系统镜像类型、根据类型对镜像进行
   不同的处理(解压),然后重定位到指定的地点)
5、加载完成后调用boot_os[images.os.os],即do_bootm_linux()
 
do_bootm_linux():进入内核前的入口,与do_bootz()相仿
1、获取machid
2、设置memory tag
3、设置启动命令行tag
4、存在ramdisk时设置initrdtag
5、cleanup_before_linux()
6、最后同do_bootz(),若定义了CONFIG_TRUSTZONE_SUPPORT则直接调用initTrustZone,否则调用theKernel(0,machid,images->ft_addr),其中第一个参数必须为0,第二个参数为机器类型ID,通常为ARM,第三个参数为启动参数列表在内存中的位置。正常情况下为调用initTrustZone。
注:可见bootz流程比bootm流程主要缺少的是读取kernel镜像文件到对应位置的步骤。因此需提前将kernel放入对应的位置。

猜你喜欢

转载自blog.csdn.net/liangzaiEvil/article/details/78775567