uboot启动流程分析

FS4412 SOC的启动过程

uboot启动流程分析_第1张图片
在图中有

Cortax A9:其是芯片核心,也就是中央处理器——CPU。

Internal Rom:是一个只读存储器,里面存储了代码,总大小为64K。它的功能是用于读写pin脚,其作用是用来告诉系统从何处去读取uboot代码。uboot的代码可以从Nand、SD/MMC、eMMC、USB OTG等地方去启动。

也就说uboot的代码可以存储在外部的存储介质里面,通过拨动Opertaing Mode Pin(拨码开关),来选择从其中某个介质里去读取。而这些读取的驱动代码就集成在iROM之中。

其次iROM还有一个功能是读取外部的代码,将其读入内部的Internal SRAM之中,也就是BL1和BL2(未画)。BL代码就是我们自己写的BootLoader代码,然后程序计数器(PC指针)就会从iROM移动到Internal SRAM内部,执行我们的BootLoader代码。

Internal SRAM:集成在芯片内部的内存。执行SRAM的代码,也就是开始执行我们的BootLoader代码,它会完成一些硬件的初始化工作,如:初始化时钟和我们上面的Dram Controller控制器等,这样我们才能够使用DRAM内存。接着将OS加载到DRAM之中。

DRAM:uboot初始化结束之后,会将控制器交给DRAM之中的OS,这时候我们的可访问内存空间的大小就由256K扩展到外部的DRAM大小。

总结一下:

  • 芯片的启动过程就是由iROM开始,读取BL到SRAM。
  • 然后执行Internal SRAM的BootLoader代码,初始化硬件,加载OS到DRAM之中。
  • 接着BootLoader将控制器交给DRAM的OS,至此整个SOC的启动流程的分析完成了。

(传统的)u-boot的启动流程

链接脚本

​ 在进行uboot的链接启动流程分析之前,我们需要先看一下uboot的链接脚本。在C语言中,程序可以分为以下几个部分:

  1. 代码段(.text)
  2. 数据段(.data)
  3. 只读数据段(.rodata)
  4. 未初始化的数据段(.bss)
  5. 堆和栈

其中堆和栈属于动态区域,在程序运行时动态分配和释放。而代码段、数据段、只读数据段在链接之后产生。

一个C语言程序分为映象和运行时两种状态:

  1. 映象只包含代码段、数据段、只读数据段;
  2. 运行时还将包含动态形成的堆和栈区域;

而链接脚本的作用就是:

  1. 指定代码段和数据段、只读数据段在内存中的存放地址;
  2. 指定代码的入口地址;

如:uboot-2012.04.01的uboot的lds文件,其部分代码如下:

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
    . = 0x00000000;

    . = ALIGN(4);
    .text :
    {
        __image_copy_start = .;
        CPUDIR/start.o (.text)
                board/samsung/smdk2440/libsmdk2440.o (.text)
        *(.text)
    }  
    
    . = ALIGN(4);
    .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }

    . = ALIGN(4);
    .data : { 
        *(.data)
    }   

    . = ALIGN(4);

    . = .;
    __u_boot_cmd_start = .;
    .u_boot_cmd : { *(.u_boot_cmd) }
    __u_boot_cmd_end = .;
	
	...
}

这里简要说明一下:

从上面的代码我们可以看到有**.text段,其指定了所以uboot文件的.text段的存放位置**。往下是**.rodata段,其指定只读数据的存放位置**,紧跟着.text段,并且进行了4字节的对齐操作。再往下是数据段.data的存放位置。其中有一个__u_boot_cmd_start,它充当一个标号的作用,其指定了boot_cmd的起始地址,cmd也就是我们在boot的shell中使用的那些指令,而__u_boot_cmd_end指定的是结束地址。

uboot程序的入口地址,在第3行的ENTRY(_start)处指定了

uboot代码追踪

我们可以使用arm-linux-nm u-boot显示我们生成的uboot文件的符号信息。节选部分如下:

...
33f9964c T xyzModem_stream_open
33f993c0 T xyzModem_stream_read
33f9905c T xyzModem_stream_terminate
...

有了这个命令后,我们就可以很方便的查找到我们的uboot启动代码的地址,通过arm-linux-nm u-boot | grep _start,我们找到我们的起始地址,其输出信息如下面:

33fada14 A __bss_start
33fad4bc A __u_boot_cmd_start
33f80044 T _armboot_start
33f80048 T _bss_start
33f80000 T _start
33f80184 t _start_armboot
33fb2cbc b bin_start_address
33fadaf8 b mem_malloc_start
33f9daf8 t setup_start_tag

(这个uboot用的是韦东山提供的uboot-1.1.6版本,打了补丁可能位置不一样,不过不影响)

知道了地址,我们怎么定位文件的位置呢?

可以通过arm-linux-addr2line来定位,其可以将地址转成行号输出显示出来,其参数如下:

  -b --target=<bfdname>  Set the binary file format
  -e --exe=<executable>  Set the input file name (default is a.out)
  -i --inlines           Unwind inlined functions
  -j --section=<name>    Read section-relative offsets instead of addresses
  -s --basenames         Strip directory names
  -f --functions         Show function names
  -C --demangle[=style]  Demangle function names
  -h --help              Display this information
  -v --version           Display the program's version

我们可以通过arm-linux-addr2line -e u-boot 33f80000找到我们起始地址所在的文件,以及其在文件中的位置,输出如下:

/home/binwatson/uboot/u-boot/u-boot-1.1.6/cpu/arm920t/start.S:56

uboot的代码流程

uboot启动流程分析_第2张图片

上图为uboot的启动流程代码:

  1. 首先会先执行start.S,其主要完成的工作将CPU设置SVC模式(保护模式)、关闭中断看门狗等;
  2. 接着会执行lowlevel_init.S的代码,主要完成一些列初始化工作;
  3. 接着开始进入C语言程序,在进行C语言之前需要先开辟一个栈空间,由ctr0.S来完成;
  4. 进入C语言程序后,执行board_init_f()函数,进行自搬移操作前期的内存分配;
  5. 自搬移由crt0.S来完成,然后第二次初始化C语言的运行环境,接着程序就完全进入C语言环境执行;
  6. board_init_r()会进行MMC和网络初始化,然后进入自启动模式,如果在倒计时之前按下回车就进入main loop循环,也就是我们看到的uboot的shell界面。

uboot源码分析

​ 了解了整个uboot大致的启动流程后,我们就可以开始着手进行uboot的源代码分析了。我们从start.S文件开始分析,也就是uboot的入口地址_start,其节选代码如下:

...
.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
...

在第3行有一条跳转指令b reset跳转到reset标号处去执行,在reset下面的那些ldr xxxx指令是用于后面设置中断向量表使用的。reset标号处的代码如下:

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

从注释中我们就可以看到,其作用就是关闭中断,将CPU设置为SVC32模式。

接着_start的代码往下走:

/*
 * 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

    /* 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

从注释中我们可以看到,行2~14大致完成内容是设置SCTRL寄存器,也就是系统控制器以及设置中断向量表,这个中断向量表的内容就是我们前面在reset下面看到的那些ldr xxxx指令,这个需要借助CPU的协处理器CP15进行设置,基体细节不深究。

完成上面操作后,会跳转到cpu_init_cp15和cpu_init_crit标号执行,最后转到_main函数去执行

我们一步一步进行分析,先看cpu_init_cp15标号处的代码:

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_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
	...
	
ENDPROC(cpu_init_cp15)

从代码的注释我们可以看出,该标号处的代码主要完成关闭缓存、关闭虚拟内存MMU的作用,以及设置了diagnostic寄存器。

接着我们回到reset处的cpu_init_crit继续往下追踪:

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)

cpu_init_crit处的代码很简单,就是调用lowlevel_init进行初始化

由流程图和注释我们可以知道,lowlevel_init主要完成板级相关的一些初始化工作,如:时钟、内存、网卡、串口的初始化

接着我们回到reset处的_main继续往下追踪。_main标号定义在文件arch/arm/lib/crt0.S里,可以通过arm-linux-nm和arm-linux-address2line进行定位。

下面我们对_main进行追踪:

/*
 * Set up initial C runtime environment and call board_init_f(0).
 */

#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
    ldr sp, =(CONFIG_SPL_STACK)
#else
    ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
#endif
    bic sp, sp, #7  /* 8-byte alignment for ABI compliance */
    sub sp, #GD_SIZE    /* allocate one GD above SP */
    bic sp, sp, #7  /* 8-byte alignment for ABI compliance */
    mov r8, sp      /* GD is above SP */
    mov r0, #0
    bl  board_init_f

#if ! defined(CONFIG_SPL_BUILD)

从注释里我们可以看出,_main首先初始化了一下C的运行环境,接着调用board_init_f函数。除此以外进行sp寄存器的设置,也就是第10行处的代码,还有在栈空间里为GD变量分配了内存。接着我们继续追踪board_init_f:

void board_init_f(ulong bootflag)
{
    bd_t *bd;
    init_fnc_t **init_fnc_ptr;
    gd_t *id;
    ulong addr, addr_sp;

    ...
        
    for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
       if ((*init_fnc_ptr)() != 0) {
           hang ();
        }
    }
    
    ...
}

其中有两个最重要的全局变量bd_t *bdgd_t *id它们用于存储一些信息,这两个全局变量的存储空间就是在调用board_init_f之前已经在栈中进行分配了。

board_init_f主要完成两个工作,一是对gd_t *id进行初始化,二是它会调用一些列的函数(在第10行处的代码)完成一些列工作。GD(也就是gd_t *id)主要是记录一些地址信息,用于后续的自搬移操作使用。

接着我们回到_main,接着board_init_f继续往下看:

    bl  board_init_f

#if ! defined(CONFIG_SPL_BUILD)

/*
 * Set up intermediate environment (new sp and gd) and call
 * relocate_code(addr_moni). Trick here is that we'll return
 * 'here' but relocated.
 */

    ldr sp, [r8, #GD_START_ADDR_SP] /* r8 = gd->start_addr_sp */
    bic sp, sp, #7  /* 8-byte alignment for ABI compliance */
    ldr r8, [r8, #GD_BD]        /* r8 = gd->bd */
    sub r8, r8, #GD_SIZE        /* new GD is below bd */

    adr lr, here
    ldr r0, [r8, #GD_RELOC_OFF]     /* lr = gd->start_addr_sp */
    add lr, lr, r0
    ldr r0, [r8, #GD_RELOCADDR]     /* r0 = gd->relocaddr */
    b   relocate_code

在最后一行有一个b relocate_code跳转指令,从名字我们就可以知道,其完成的工作就是对代码进行自搬移操作,接着我们继续往下看:

here:

/* Set up final (full) environment */

    bl  c_runtime_cpu_setup /* we still call old routine here */

    ldr r0, =__bss_start    /* this is auto-relocated! */
    ldr r1, =__bss_end      /* this is auto-relocated! */

    mov r2, #0x00000000     /* prepare zero to clear BSS */

clbss_l:cmp r0, r1          /* while not at end of BSS */
    strlo   r2, [r0]        /* clear 32-bit BSS word */
    addlo   r0, r0, #4      /* move to next */
    blo clbss_l

    bl coloured_LED_init
    bl red_led_on

    /* call board_init_r(gd_t *id, ulong dest_addr) */
    mov r0, r8          /* gd_t */
    ldr r1, [r8, #GD_RELOCADDR] /* dest_addr */
    /* call board_init_r */
    ldr pc, =board_init_r   /* this is auto-relocated! */

    /* we should not return here. */

#endif

自搬移结束后,就准备进入C语言环境运行了。

从注释中可以看到设置了bss段,也就是对bss段进行了清零,标号clbss_l标号处的循环代码。

之后就会通过board_init_r标号,跳转到C语言函数,我们继续追踪board_init_r:

void board_init_r(gd_t *id, ulong dest_addr)
{
    ulong malloc_start;
#if !defined(CONFIG_SYS_NO_FLASH)
    ulong flash_size;
#endif
	
	...
	
	/* main_loop() can return to retry autoboot, if so just run it again. */
    for (;;) {
        main_loop();
    } 
    
    ...
}    

board_init_r会打印一些列的信息到串口,然后会进入main_loop()循环,main_loop会进行倒计时,如果此时按下回车就会进入uboot的shell交互界面,否则就会自动引导启动OS系统。

总结

uboot启动流程分析_第3张图片

uboot的启动如下,假设我们使用SD卡进行启动:

  1. iROM会将SD卡内的BL1加载到iRAM中,然后将控制器交给BL1;

  2. BL1会将BL2以及填充,一共16K的代码,加载到iRAM中;

    uboot由两部分组成,一部分是SPL完成硬件相关操作,另一部分是uboot代码。

  3. 接着SPL部分会将uboot代码加载DRAM之中,此时uboot位于DRAM的内存地址0x43E00000处;

  4. uboot加载需要加载Linux系统的uImage到内存,一般加载到0x4008000处,为了防止把uboot自己给覆盖了,所以uboot还需要进行一次自搬移操作;[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
    uboot启动流程分析_第4张图片

    图中的Global data就是我们前面看到的GD,它记录一些列地址信息,包括uboot自搬移后存储的地址,malloc的堆空间起始地址等;

  5. 然后CPU跳转新的uboot地址执行,这时我们就能看到串口打印一些列控制信息了。

至此uboot的启动流程大致分析就结束了。

你可能感兴趣的:(BootLoader,linux)