对于概念性我们有了一定的了解以后,来瞅瞅UBoot的源码。
不过在这之前,整个图来回以一下前面讲的UBoot的启动跳转和硬件布局。
BootRom引导SPL,SPL引导U-Boot,然后U-Boot再引导内核。
下面我们来看看这个源码是什么样子的?
SPL代码追踪–start.S
从前面分析的u-boot-spl.lds链接文件可知,启动代码就是arch/arm/cpu/armv7/start.S。start.S主要做的事情就是初始化系统的各个方面。
从大的方面分,可以分成这几个部分:
1)设置CPU模式。 2)关闭看门狗。 3)关闭中断。 4)设置堆栈sp指针。 5)清除bss段。 6)异常中断处理。
/* * armboot - Startup Code for OMAP3530/ARM Cortex CPU-core * * Copyright (c) 2004 Texas Instruments <r-woodruff2@ti.com> * * Copyright (c) 2001 Marius Gr ger <mag@sysgo.de> * Copyright (c) 2002 Alex Züpke <azu@sysgo.de> * Copyright (c) 2002 Gary Jennejohn <garyj@denx.de> * Copyright (c) 2003 Richard Woodruff <r-woodruff2@ti.com> * Copyright (c) 2003 Kshitij <kshitij@ti.com> * Copyright (c) 2006-2008 Syed Mohammed Khasim <x0khasim@ti.com> * * SPDX-License-Identifier: GPL-2.0+ */ #include <asm-offsets.h> #include <config.h> #include <version.h> #include <asm/system.h> #include <linux/linkage.h> .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 #ifdef CONFIG_SPL_BUILD _undefined_instruction: .word _undefined_instruction _software_interrupt: .word _software_interrupt _prefetch_abort: .word _prefetch_abort _data_abort: .word _data_abort _not_used: .word _not_used _irq: .word _irq _fiq: .word _fiq _pad: .word 0x12345678 /* now 16*4=64 */ #else .globl _undefined_instruction _undefined_instruction: .word undefined_instruction .globl _software_interrupt _software_interrupt: .word software_interrupt .globl _prefetch_abort _prefetch_abort: .word prefetch_abort .globl _data_abort _data_abort: .word data_abort .globl _not_used _not_used: .word not_used .globl _irq _irq: .word irq .globl _fiq _fiq: .word fiq _pad: .word 0x12345678 /* now 16*4=64 */ #endif /* CONFIG_SPL_BUILD */ .global _end_vect _end_vect: .balignl 16,0xdeadbeef
首先使用.globl_start声明_start变量,并且告诉链接器此变量是全局的,外部可以访问,所以在arch/arm/cpu/armv7/sunxi/u-boot-spl.lds中,有用到此变量:
ENTRY(_start)
1、ENTRY(_start) 入口
即指定入口为_start,_start就是整个start.S的最开始,也是U-Boot代码的最开始。
_start: b reset
_start后面加上一个冒号“:”,表示一个标号,类似于C语言goto语句中的标号。
_start: b reset /* b是不带返回的跳转(bl是带返回的跳转),意思是无条件直接跳转到reset标号处执行程序*/ ldr pc, _undefined_instruction /*未定义指令异常向量,ldr的作用是, 将符号_undefined_instruction指向的地址的内容加载到pc*/ ldr pc, _software_interrupt /*软件中断向量*/ ldr pc, _prefetch_abort /*预取指令异常向量*/ ldr pc, _data_abort /*数据操作异常向量*/ ldr pc, _not_used /*未使用*/ ldr pc, _irq /*irq中断向量*/
这几行定义的内容对应于ARM处理器的7种异常处理模式。
第一行就是重启复位异常的处理。
#ifdef CONFIG_SPL_BUILD _undefined_instruction: .word _undefined_instruction _software_interrupt: .word _software_interrupt _prefetch_abort: .word _prefetch_abort _data_abort: .word _data_abort _not_used: .word _not_used _irq: .word _irq _fiq: .word _fiq _pad: .word 0x12345678 /* now 16*4=64 */ #else .globl _undefined_instruction _undefined_instruction: .word undefined_instruction .globl _software_interrupt _software_interrupt: .word software_interrupt .globl _prefetch_abort _prefetch_abort: .word prefetch_abort .globl _data_abort _data_abort: .word data_abort .globl _not_used _not_used: .word not_used .globl _irq _irq: .word irq .globl _fiq _fiq: .word fiq _pad: .word 0x12345678 /* now 16*4=64 */ #endif /* CONFIG_SPL_BUILD */
如果是编译SPL,那么CONFIG_SPL_BUILD生效:
ldr pc, _undefined_instruction _undefined_instruction: .word _undefined_instruction
那么这里是生效的,但是ldr pc,_undefined_instruction并无实际的执行动作。
如果是编译U-Boot,那么CONFIG_SPL_BUILD不生效:
ldr pc, _undefined_instruction .globl _undefined_instruction _undefined_instruction: .word undefined_instruction
而在后面的代码中可以看到:
/* * exception handlers */ .align 5 undefined_instruction: get_bad_stack bad_save_user_regs bl do_undefined_instruction
.word的用法是.word expr,用于分配一个字大小的内存单元,并用expr初始化字内存单元。
这里就相当于在_undefined_instruction处分配了一个32位的地址空间,里面放置着undefined_instruction。类似于C语言中的:
_undefined_instruction = &undefined_instruction
那么ldr pc,_undefined_instruction就相当于PC指到了undefined_instruction处,直接跳到异常处理函数了。
.global _end_vect _end_vect: .balignl 16,0xdeadbeef
这里定义了** _end_vect**,标志着向量表的结束。
接着使用.balignl做16位字节对齐。
不足之处是,用0xdeadbeef填充。其实用任何数据进行字节对齐的填充都是可以的,
这里选用0xdeadbeef是因为在十六进制下,只有abcdef这六个字母可用,从而表示出来的单词就非常少了,因此选择了这么一个有一定意思的单词。
接下来我们看实际的复位代码。
2、reset:
_start: b reset /* b是不带返回的跳转(bl是带返回的跳转),意思是无条件直接跳转到reset标号处执行程序*/
将CPU设置为SVC模式
/************************************************************************* * * Startup Code (reset vector) * * do important init only if we don't start from memory! * setup Memory and board specific bits prior to relocation. * relocate armboot to ram * setup stack * *************************************************************************/ #ifdef CONFIG_USE_IRQ /* IRQ stack memory (calculated at run-time) */ .globl IRQ_STACK_START IRQ_STACK_START: .word 0x0badc0de /* IRQ stack memory (calculated at run-time) */ .globl FIQ_STACK_START FIQ_STACK_START: .word 0x0badc0de #endif /* IRQ stack memory (calculated at run-time) + 8 bytes */ .globl IRQ_STACK_START_IN IRQ_STACK_START_IN: .word 0x0badc0de /* * the actual reset code */ reset: bl save_boot_params /* * disable interrupts (FIQ and IRQ), also set the cpu to SVC32 mode, * except if in HYP mode already */ mrs r0, cpsr and r1, r0, #0x1f @ mask mode bits teq r1, #0x1a @ test for HYP mode bicne r0, r0, #0x1f @ clear all mode bits orrne r0, r0, #0x13 @ set SVC mode orr r0, r0, #0xc0 @ disable FIQ and IRQ msr cpsr,r0
首先我们**定义了IRQ_STACK_START_IN,**这里存放的是0x0badc0de,这个其实是表示bad code,为了在程序运行时引起用户注意。
3、save_boot_params
reset: bl save_boot_params
接下来执行bl save_boot_params跳转指令。
在<arch/arm/cpu/armv7/start.S>中找到的save_boot_params的定义如下:
ENTRY(save_boot_params) bx lr @ back to my caller ENDPROC(save_boot_params) .weak save_boot_params
可以看到save_boot_params有一个.weak限定符,它表明save_boot_params是一个弱符号,也就是说如果没有重新定义save_boot_params,则**使用<arch/arm/cpu/armv7/start.S>中的save_boot_params。**如果其他地方有定义,则使用新的定义。在这里并没有其他的定义,所以save_boot_params是一个空函数。函数定义如下:
ENTRY(save_boot_params) bx lr @ back to my caller ENDPROC(save_boot_params) .weak save_boot_params
空函数之后的几行代码利用r0充当临时变量,以设定CPSR寄存器,将处理器模式设为SVC模式并禁用FIQ和IRQ。之所以将处理器模式设为SVC模式,有两个方面的考虑:
1)我们先简单地分析一下ARM处理器的7种模式:
- ·中止ABT和未定义UND模式。
- 中止ABT和未定义UND模式都是不太正常的模式,此处程序是正常运行的,所以不应该设置CPU为其中的任何一种模式,因此可以排除这两种模式。
- ·快中断FIQ和中断IRQ模式。
- 对于快中断FIQ和中断IRQ来说,U-Boot初始化的时候,也还没有中断要处理和能够处理的,而且即使是注册了中断服务程序后,能够处理中断,那么这两种模式也是自动切换过去的,因此,此处也不应该将CPU设置为其中的任何一种模式。
- ·用户USR模式。
- 虽然从理论上来说,可以设置CPU为用户USR模式,但是由于此模式无法直接访问很多的硬件资源,而U-Boot初始化时,就必须要去访问这类资源,所以此处可以排除,不能设置CPU为用户USR模式。
- ·系统SYS模式VS管理SVC模式。
- 首先,SYS模式和USR模式相比,所用的寄存器组都是一样的,但是增加了一些访问USR模式下不能访问的资源。而SVC模式本身就属于特权模式,本身就可以访问那些受控资源,而且相比SYS模式还多了些自己模式下的影子寄存器,所以,相对SYS模式来说,可以访问资源的能力相同,但是拥有更多的硬件资源。
从理论上来说,虽然CPU可以设置为SYS模式和SVC模式的任一种,但是从U-Boot方面考虑,其要做的事情是初始化系统的相关硬件资源,因此需要获取尽量多的权限,以方便操作硬件,初始化硬件。
从U-Boot的目的是初始化硬件的角度来说,CPU设置为SVC模式,更有利于其工作。因此,此处将CPU设置为SVC模式。
2)U-Boot作为一个BootLoader来说,最终的目的是启动Linux的内核,在做好准备工作(即初始化硬件,准备好内核和rootfs等)跳转到内核之前,本身就要满足一些条件,其中一个条件就是要求CPU处于SVC模式的。
因此,U-Boot在最初的初始化阶段,就将CPU设置为SVC模式也是最正确的。之所以禁用IRQ和FIQ,是因为在U-Boot引导阶段不需要使用IRQ和FIQ,禁用后可排除中断的影响。
/* * Setup vector: * (OMAP4 spl TEXT_BASE is not 32 byte aligned. * Continue to use ROM code vector only in OMAP4 spl) */ #if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD)) /* Set V=0 in CP15 SCTRL register - for VBAR to point to vector */ mrc p15, 0, r0, c1, c0, 0 @ Read CP15 SCTRL Register bic r0, #CR_V @ V = 0 mcr p15, 0, r0, c1, c0, 0 @ Write CP15 SCTRL Register /* Set vector address in CP15 VBAR register */ ldr r0, =_start mcr p15, 0, r0, c12, c0, 0 @Set VBAR #endif
这一段代码用于**设置协处理器中的寄存器SCTRL。**在《Cortex-A7 MPCore Technical Reference Manual》数据手册中有对SCTLR的介绍。SCTLR提供系统的顶层控制,包括内存系统。
该小节提供了访问SCTLR寄存器的方法:
MRC p15, 0, <Rt>, c1, c0, 0 ; Read System Control Register MCR p15, 0, <Rt>, c1, c0, 0 ; Write System Control Register
在代码中,使用的是r0:
bic r0, #CR_V @ V = 0中是设置SCTLR中的V位。
CR_V的定义在./arch/arm/include/asm/system.h中:
#define CR_V (1 << 13) /* Vectors relocated to 0xffff0000 */
V位(bit13)的说明如下:
- ·向量位。该位选择异常向量的基地址。
- ·0:异常向量的基地址为0x00000000。软件可以使用VBAR来重新映射该基地址。
- ·1:高位的异常向量的基地址为0xFFFF0000。该基地址不能被重映射。
因此,我们先将异常向量的基地址设定为0x00000000,接下来重新映射该基地址:
/* Set vector address in CP15 VBAR register */ ldr r0, =_start mcr p15, 0, r0, c12, c0, 0 @Set VBAR
在《ARM®Architecture Reference Manual ARMv7-A and ARMv7-R edition》数据手册中能查到读写VBAR的信息:
MRC p15, 0, <Rt>, c12, c0, 0 ; Read VBAR into Rt MCR p15, 0, <Rt>, c12, c0, 0 ; Write Rt to VBAR
这里我们将基地址映射为_start的地址。接着有几个函数的调用:
/* the mask ROM code should have PLL and others stable */ #ifndef CONFIG_SKIP_LOWLEVEL_INIT bl cpu_init_cp15 bl cpu_init_crit #endif bl _main
4、cpu_init_cp15
/************************************************************************* * * cpu_init_cp15 * * Setup CP15 registers (cache, MMU, TLBs). The I-cache is turned on unless * CONFIG_SYS_ICACHE_OFF is defined. * *************************************************************************/ ENTRY(cpu_init_cp15) /* * Invalidate L1 I/D */ mov r0, #0 @ set up for MCR mcr p15, 0, r0, c8, c7, 0 @ invalidate TLBs mcr p15, 0, r0, c7, c5, 0 @ invalidate icache mcr p15, 0, r0, c7, c5, 6 @ invalidate BP array mcr p15, 0, r0, c7, c10, 4 @ DSB mcr p15, 0, r0, c7, c5, 4 @ ISB /* * disable MMU stuff and caches */ mrc p15, 0, r0, c1, c0, 0 bic r0, r0, #0x00002000 @ clear bits 13 (--V-) bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM) orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align orr r0, r0, #0x00000800 @ set bit 11 (Z---) BTB #ifdef CONFIG_SYS_ICACHE_OFF bic r0, r0, #0x00001000 @ clear bit 12 (I) I-cache #else orr r0, r0, #0x00001000 @ set bit 12 (I) I-cache #endif mcr p15, 0, r0, c1, c0, 0 #ifdef CONFIG_ARM_ERRATA_716044 mrc p15, 0, r0, c1, c0, 0 @ read system control register orr r0, r0, #1 << 11 @ set bit #11 mcr p15, 0, r0, c1, c0, 0 @ write system control register #endif #ifdef CONFIG_ARM_ERRATA_742230 mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register orr r0, r0, #1 << 4 @ set bit #4 mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register #endif #ifdef CONFIG_ARM_ERRATA_743622 mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register orr r0, r0, #1 << 6 @ set bit #6 mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register #endif #ifdef CONFIG_ARM_ERRATA_751472 mrc p15, 0, r0, c15, c0, 1 @ read diagnostic register orr r0, r0, #1 << 11 @ set bit #11 mcr p15, 0, r0, c15, c0, 1 @ write diagnostic register #endif mov pc, lr @ back to my caller ENDPROC(cpu_init_cp15)
1、Invalidate L1 I/D
该函数的第一部分是Invalidate L1 I/D:
mov r0, #0 @ set up for MCR mcr p15, 0, r0, c8, c7, 0 @ invalidate TLBs mcr p15, 0, r0, c7, c5, 0 @ invalidate icache mcr p15, 0, r0, c7, c5, 6 @ invalidate BP array mcr p15, 0, r0, c7, c10, 4 @ DSB mcr p15, 0, r0, c7, c5, 4 @ ISB
在《Cortex-A7 MPCore Technical Reference Manual》中还可以查到mcr p15,0,r0,c8,c7,0是为了使TLB失效的,其中该操作会忽略Rt寄存器中的值,因此可以不往r0寄存器中写入0。
同样地,数据手册中的mcr p15,0,r0,c7,c5,0是为了使icache失效的;mcr p15,0,r0,c7,c5,6是为了使分支预测器失效的;mcr p15,0,r0,c7,c10,4执行Data Synchronization Barrier(数据同步屏障)操作,保证完成内存访问。mcr p15,0,r0,c7,c5,4执行**Instruction Synchronization Barrier(指令同步屏障)**操作,保证指令流的正确执行。
2、disable MMU stuff and caches
该函数的第二部分是disable MMU stuff and caches:
mrc p15, 0, r0, c1, c0, 0 bic r0, r0, #0x00002000 @ clear bits 13 (--V-) bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM) orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align orr r0, r0, #0x00000800 @ set bit 11 (Z---) BTB #ifdef CONFIG_SYS_ICACHE_OFF bic r0, r0, #0x00001000 @ clear bit 12 (I) I-cache #else orr r0, r0, #0x00001000 @ set bit 12 (I) I-cache #endif mcr p15, 0, r0, c1, c0, 0
mrc p15,0,r0,c1,c0,0用于读取SCTLR寄存器的值到r0中。bic r0,r0,#0x00002000指令将bit[13]V位清零,bic r0,r0,#0x00000007指令将bit[2:0]清零,然后orr r0,r0,#0x00000002指令将bit[1]置1。这样bit[2]C位的值为0,表示禁用D-cache。bit[1]A位的值为1,表明启用对齐的错误检查功能。bit[0]M位的值为0,**表示禁用地址转换功能,即禁用MMU。orr r0,r0,#0x00000800设置了bit[11]Z位,表示启动了程序流程的预测功能。bit[12]I位是开关I-cache的,对于引导ARM体系下的Linux,对I-cache的开关没有要求。
综合来看,通过对协处理器CP15的配置,可禁用MMU和Cache。
通过cpu_init_cp15,我们分析一下汇编语言中是如何实现函数调用的。
bl cpu_init_cp15实现函数调用,最后mov pc,lr实现函数的返回。
因为bl指令除了实现跳转到cpu_init_cp15之外,在跳转之前,还将下一条指令地址存放到lr寄存器中,这样,跳转的函数在执行完毕后,将lr再赋值给pc,就可以实现函数的返回,从而继续执行后续指令。
cpu_init_crit的定义如下。
5、cpu_init_crit
cpu_init_crit的定义如下。
/************************************************************************* * * CPU_init_critical registers * * setup important registers * setup memory timing * *************************************************************************/ ENTRY(cpu_init_crit) /* * Jump to board specific initialization... * The Mask ROM will have already initialized * basic memory. Go here to bump up clock rate and handle * wake up conditions. */ b lowlevel_init @ go setup pll,mux,memory ENDPROC(cpu_init_crit)
该函数直接调用lowlevel_init函数,lowlevel_init函数在arch/arm/cpu/armv7/lowlevel_init.S中,代码如下。
ENTRY(lowlevel_init) /* * Setup a temporary stack */ ldr sp, =CONFIG_SYS_INIT_SP_ADDR bic sp, sp, #7 /* 8-byte alignment for ABI compliance */ #ifdef CONFIG_SPL_BUILD ldr r9, =gdata #else sub sp, sp, #GD_SIZE bic sp, sp, #7 mov r9, sp #endif /* * Save the old lr(passed in ip) and the current lr to stack */ push {ip, lr} /* * go setup pll, mux, memory */ bl s_init pop {ip, pc} ENDPROC(lowlevel_init)
第19~23行,首先设定栈指针SP为CONFIG_SYS_INIT_SP_ADDR,然后确保SP是8字节对齐。
CONFIG_SYS_INIT_SP_ADDR在include/configs/sunxi-common.h中的定义如下。
47 /* DRAM Base */ 48 #define CONFIG_SYS_SDRAM_BASE 0x40000000 49 #define CONFIG_SYS_INIT_RAM_ADDR 0x0 50 #define CONFIG_SYS_INIT_RAM_SIZE 0x8000 /* 32 KiB */ 51 52 #define CONFIG_SYS_INIT_SP_OFFSET \ 53 (CONFIG_SYS_INIT_RAM_SIZE - GENERATED_GBL_DATA_SIZE) 54 #define CONFIG_SYS_INIT_SP_ADDR \ 55 (CONFIG_SYS_INIT_RAM_ADDR + CONFIG_SYS_INIT_SP_OFFSET)
其中,GENERATED_GBL_DATA_SIZE在include/generated/generic-asm-offsets.h中的定义如下:
10 #define GENERATED_GBL_DATA_SIZE 160 /* (sizeof(struct global_data) + 15) & ~15 @ */
因此CONFIG_SYS_INIT_SP_ADDR的值为0x8000–160=0x7F60。然后CONFIG_SPL_BUILD在arch/arm/lib/spl.c中定义了ldr r9,=gdata。
gd_t gdata __attribute__ ((section(".data")));
从定义中可以看出,gdata是一个结构体变量,存放在.data段中。所以这里我们将结构体的地址存放在r9中。在后面的处理中我们将看到gdata的位置。
接下来用下面的代码:
push {ip, lr} /* * go setup pll, mux, memory */ bl s_init pop {ip, pc}
在调用s_init函数之前做了push{ip,lr}压栈和pop{ip,pc}出栈操作。这里之所以做栈操作,是因为存在多级函数调用,需要保护现场。如果在C语言中,函数跳转前后要做的事情都是由C语言编译器实现的,包括查看汇编代码,还有类似的栈处理。
到这里了,辛苦了,快休息一下,下面接着分析s_init。
参考资料:
内容全部来自《深入理解BootLoader》