Uboot启动流程分析(五)


1、前言

在前面的文章《Uboot启动流程分析(四)》,链接如下:

已经对board_init_f() 函数作出了简单的分析,该函数对一些早期的外设进行了初始化,例如调试串口,并填充了gd_t结构体中的成员变量,最主要的是对整个DRAM的内存进行了分配,以便uboot的重定位,接下来,先回顾一下_main函数的大概流程,如下:

_main
    |
    board_init_f_alloc_reserve-->reserve gd and early malloc area
    |
    board_init_f_init_reserve-->initialize global data
    |
    board_init_f-->initialize ddr,timer...,and fill gd_t
    |
    relocate_code-->relocate uboot code
    |
    relocate_vectors-->relocate vectors
    |
    board_init_r-->calling board_init_r

在_main函数中,调用完了board_init_f()函数后,将DRAM的内存分配好,填充gd_t结构体成员变量,接下来,就是调用relocate_code()函数重定位uboot代码,调用relocate_vectors()函数重定位中断向量表,本篇文章将简单分析uboot的大概重定位过程。

2、uboot段相关变量

在分析relocate_code函数之前,先来总结一下相关的uboot段相关变量,这些段的地址在uboot代码重定位的时候需要用到,将uboot源码进行编译后,会在源码根目录生成u-boot.lds链接文件和u-boot.map内存映射文件,通过这两个文件,可以寻找到uboot段的地址,一些重要的段地址如下表格所示:

变量 数值 描述
 _start  0x87800000  uboot入口地址
 __image_copy_start  0x87800000  uboot拷贝的开始地址
 __image_copy_end  0x87868960  uboot拷贝的结束地址
 __rel_dyn_start  0x87868960 .rel.dyn段开始地址 
 __rel_dyn_end  0x87871af0 .rel.dyn段结束地址 
 _image_binary_end  0x87871af0  
 __bss_start  0x87868960 .bss段开始地址 
 __bss_end  0x878b4c34 .bss段结束地址 

在上面寻找到的变量值中,除了_start和__image_copy_start值不会该变,当修改了uboot的源码或改变了编译条件,其他的变量都可能会发生改变,因此分析时,一定要以实际编译时进行uboot分析。

3、relocate_code函数

在调用relocate_code函数之前的分析,可以参考《文章Uboot启动流程分析(二)》,链接如下所示:

也就是_main函数执行的第二部分,relocate_code函数会传入一个参数,该参数为gd->relocaddr,也就是uboot重定位的目的地址。

接下来,对relocate_code函数进行分析,该函数用于重定位uboot,函数的定义在下面的汇编文件中:

uboot/arch/arm/lib/relocate.S

relocate_code函数的定义如下所示:

/*
 * void relocate_code(addr_moni)
 *
 * This function relocates the monitor code.
 *
 * NOTE:
 * To prevent the code below from containing references with an R_ARM_ABS32
 * relocation record type, we never refer to linker-defined symbols directly.
 * Instead, we declare literals which contain their relative location with
 * respect to relocate_code, and at run time, add relocate_code back to them.
 */

ENTRY(relocate_code)
    ldr    r1, =__image_copy_start/* r1 <- SRC &__image_copy_start */ //r1保存源image开始地址
    //r0 = gd->relocaddr,r4 = r0 - r1 = 0x8ff3b000 - 0x87800000 = 0x873b000
    subs    r4, r0, r1 /* r4 <- relocation offset */ //r4 = gd->reloc_off,保存偏移地址
    beq    relocate_done    /* skip relocation */ //如果r0和r1相等,则不需要uboot重定位
    ldr    r2, =__image_copy_end  /* r2 <- SRC &__image_copy_end */  //r2保存源image结束地址

copy_loop:
    ldmia    r1!, {r10-r11}  /* copy from source address [r1] */ //拷贝uboot代码到r10和r11
    stmia    r0!, {r10-r11}  /* copy to  target address [r0] */ //将uboot代码写到目的地址
    cmp    r1, r2     /* until source end address [r2] */ //判断uboot是否拷贝完成
    blo    copy_loop  //循环拷贝

    /*
     * fix .rel.dyn relocations //修复.rel.dyn段
     */
    ldr    r2, =__rel_dyn_start    /* r2 <- SRC &__rel_dyn_start */
    ldr    r3, =__rel_dyn_end    /* r3 <- SRC &__rel_dyn_end */
fixloop:
    ldmia    r2!, {r0-r1}        /* (r0,r1) <- (SRC location,fixup) */
    and    r1, r1, #0xff
    cmp    r1, #23            /* relative fixup? */
    bne    fixnext

    /* relative fix: increase location by offset */
    add    r0, r0, r4
    ldr    r1, [r0]
    add    r1, r1, r4
    str    r1, [r0]
fixnext:
    cmp    r2, r3
    blo    fixloop

relocate_done:

ENDPROC(relocate_code)

relocate_code函数的分析主要分两个部分,第一部分是从__image_copy_start到__image_copy_end的重定位,该部分的代码如下:

    ldr    r1, =__image_copy_start/* r1 <- SRC &__image_copy_start */ //r1保存源image开始地址
    //r0 = gd->relocaddr,r4 = r0 - r1 = 0x8ff3b000 - 0x87800000 = 0x873b000
    subs    r4, r0, r1    /* r4 <- relocation offset */ //r4 = gd->reloc_off,保存偏移地址
    beq    relocate_done    /* skip relocation */ //如果r0和r1相等,则不需要uboot重定位
    ldr    r2, =__image_copy_end    /* r2 <- SRC &__image_copy_end */  //r2保存源image结束地址

copy_loop:
    ldmia    r1!, {r10-r11}    /* copy from source address [r1] */ //拷贝uboot代码到r10和r11
    stmia    r0!, {r10-r11}    /* copy to  target address [r0] */ //将uboot代码写到目的地址
    cmp    r1, r2     /* until source end address [r2] */ //判断uboot是否拷贝完成
    blo    copy_loop  //循环拷贝

函数传入的参数为r0 = 0x8ff3b000,uboot重定位的目的地址,函数进来后,将__image_copy_start的数值赋值给r1,也就是uboot复制开始地址,从表格可以知道r1 = 0x87800000,接下来进行一个减法操作,此时r4将保存着uboot的偏移量,也就是r4 = gd->reloc_off。

接下来会进行一个判断,判断uboot重定位的目的地址是否和uboot源地址是否相等,如果相等的话,表示不需要重定位,跳到relocate_done处,继续运行,如果不相等的话,则是需要进行重定位。

将__image_copy_end的值赋值给r2,也就是uboot复制结束的地址,从表格中可以知道,此时r2 = 0x87868960,接下来,开始进行uboot代码的拷贝,进入到copy_loop循环,从r1,也就是__image_copy_start开始,读取uboot代码到r10和r11寄存器,一次就拷贝两个32位的数据,r1自动更新,保存下一个需要拷贝的地址,然后将r10和r11里面的数据,写到r0保存的地址,也就是uboot重定位的目的地址,r0自动更新。

接下来,比较r1和r2是否相等,也就是判断uboot代码是否拷贝完成,如果没完成的话,跳转到copy_loop处,继续循环,直到uboot拷贝完成。

函数relocate_code()的第二部分是修复.rel.dyn的定位问题,.rel.dyn段存放了.text段中需要重定位地址的集合,uboot重定位就是uboot将自身拷贝到DRAM的另外一个地址继续运行(DRAM的高地址),一个可执行的bin文件,它的链接地址和运行地址一定要相等,也就是链接到哪个地址,运行之前就需要拷贝到哪个地址中进行运行,在前面的重定位后,运行地址和链接地址就不一样了,这样在进行寻址就会出问题,对.rel.dyn的定位问题进行修复,就是为了解决该问题。

下面,使用一个简单的例子进行分析:

 先在新适配的板级文件中添加测试代码,文件如下:

uboot/board/freescale/mx6ul_comp6ul_nand/mx6ul_comp6ul_nand.c

添加的内容如下:

static int rel_a = 0;

void rel_test(void)
{
    rel_a = 100;
    printf("rel_test\n");
}

/* 在board_init()函数中调用rel_test()函数 */
int board_init(void)
{
        ...
        ...
        rel_test();
        
        return 0;
}

接下来对uboot源码重新编译,并将u-boot文件进行反汇编分析,使用命令如下:

$ cd uboot
$ make
$ arm-linux-gnueabihf-objdump -D -m arm u-boot > u-boot.dis

然后,打开u-boot.dis文件,并在文件中,找到rel_a变量、rel_test函数和board_init函数相关的汇编代码,如下所示:

/* rel_test寻址分析 */
8780424c :
8780424c:    e59f300c     ldr    r3, [pc, #12]    ; 87804260 0x14>
87804250:    e3a02064     mov    r2, #100    ; 0x64
87804254:    e59f0008     ldr    r0, [pc, #8]    ; 87804264 0x18>
87804258:    e5832000     str    r2, [r3]
8780425c:    ea00f1a9     b    87840908 
87804260:    87868994             ;  instruction: 0x87868994
87804264:    878494ce     strhi    r9, [r4, lr, asr #9]
...
...
878043e4:    e59f2038     ldr    r2, [pc, #56]    ; 87804424 0x1bc>
878043e8:    e5923068     ldr    r3, [r2, #104]    ; 0x68
878043ec:    e3833030     orr    r3, r3, #48    ; 0x30
878043f0:    e5823068     str    r3, [r2, #104]    ; 0x68
878043f4:    ebffff94     bl    8780424c 
...
...
87868994 :
87868994:    00000000     andeq    r0, r0, r0

在0x878043f4处,也就是board_init()函数中调用了rel_test()函数,在汇编代码中,可以看到是使用了bl指令,而bl指令是相对寻址的(pc + offset),为位置无关指令,因此,可以知道,uboot中的函数调用时与绝对位置无关的。

接下来,分析rel_test()函数对全局变量rel_a的调用过程,在0x8780424c处开始,先设置r3的值为pc + 12地址处的值,由于ARM流水线的原因,当前pc的值为当前的地址加8,所以pc = 0x8780424c + 8 = 0x87804254,因此r3 = pc + 12 = ?0x87804254 + 12 = 0x87804260,从反汇编的代码中可以看到0x87804260处的值为0x87868994,所以,最终r3的值为0x87868994,而且从反汇编代码中可以看到0x87868994就是变量rel_a的地址,rel_test()函数继续执行,将100赋值到r2寄存器,然后通过str指令,将r2的值写到r3的地址处,也就是将0x87868994地址处的值赋值100,就是函数中调用的rel_a = 100;语句。

从上面的分析,可以知道rel_a = 100;的执行过程为:

  • 在函数rel_test()的末尾处有一个地址为0x87804260的内存空间,此内存空间中保存着变量rel_a的地址;
  • 函数rel_test()想要访问变量rel_a,首先需要访问0x87804260内存空间获取变量rel_a的地址,而访问0x87804260是通过偏移来访问的,与位置无关的操作;
  • 通过访问0x87804260获取变量rel_a的地址,然后对变量rel_a进行赋值操作;
  • 函数rel_test()对变量rel_a的访问并没有直接进行,而是通过一个偏移地址0x87804260,找到变量rel_a真正的地址,然后才对其操作,偏移地址的专业术语为Label。

从分析中,可以知道,该偏移地址就是uboot重定位运行是否会出错的原因,在上面可以知道,uboot重定位的偏移量为0x8ff3b000,将上面给出的反汇编代码进行重定位后,也就是加上地址偏移量0x873b000后,如下:

/* rel_test寻址分析(重定位后) */
8ff3f24c? :
8ff3f24c:    e59f300c     ldr    r3, [pc, #12]
8ff3f250:    e3a02064     mov    r2, #100    ; 0x64
8ff3f254:    e59f0008     ldr    r0, [pc, #8]    
8ff3f258:    e5832000     str    r2, [r3]
8ff3f25c:    ea00f1a9     b    87840908 
8ff3f260:    87868994             ;  instruction: 0x87868994
8ff3f264:    878494ce     strhi    r9, [r4, lr, asr #9]
...
...
8ff3f3e4:    e59f2038     ldr    r2, [pc, #56]    
8ff3f3e8:    e5923068     ldr    r3, [r2, #104]    ; 0x68
8ff3f3ec:    e3833030     orr    r3, r3, #48    ; 0x30
8ff3f3f0:    e5823068     str    r3, [r2, #104]    ; 0x68
8ff3f3f4:    ebffff94     bl    8780424c 
...
...
8ffa3994 :
8ffa3994:    00000000     andeq    r0, r0, r0

函数rel_test()假设调用后对rel_a变量进行访问,分析和上面一样,先通过pc和偏移量找到0x8ff3f260,该地址是重定位后的地址,可此时该地址的值为0x87868994,也就是没有重定位后的rel_a变量的地址,但是从上面可以知道,uboot重定位后,rel_a变量的新地址为0x8ffa3994,因此,我们可以知道,如果uboot重定位后,要想正常访问rel_a变量,必须要将0x8ff3f260地址中的值0x87868994加上偏移量0x873b000,才能保证uboot重定位后,能正常访问到rel_a变量,将.rel.dyn段进行重定位修复,就是为了解决链接地址和运行地址不一致的问题。

在uboot中,对于重定位后链接地址与运行地址不一致的解决办法就是使用位置无关码,在uboot编译使用ld链接的时候使用参数"-pie"可生成与位置无关的可执行程序,使用该参数后,会生成一个.rel.dyn段,uboot则是靠该段去修复重定位后产生的问题的,在uboot的反汇编文件中,有.rel.dyn段代码,如下:

Disassembly of section .rel.dyn:

87868988 <__rel_dyn_end-0x91a0>:
87868988:    87800020     strhi    r0, [r0, r0, lsr #32]
8786898c:    00000017     andeq    r0, r0, r7, lsl r0
87868990:    87800024     strhi    r0, [r0, r4, lsr #32]
87868994:    00000017     andeq    r0, r0, r7, lsl r0
...
...
87868f08:    87804260     strhi    r4, [r0, r0, ror #4]
87868f0c:    00000017     andeq    r0, r0, r7, lsl r0
87868f10:    87804264     strhi    r4, [r0, r4, ror #4]
87868f14:    00000017     andeq    r0, r0, r7, lsl r0
...
...

.rel.dyn段的格式如下,类似下面的两行就是一组:

87868f08:    87804260     strhi    r4, [r0, r0, ror #4]
87868f0c:    00000017     andeq    r0, r0, r7, lsl r0

也就是两个4字节数据为一组,其中高4字节是Label地址的标识0x17,低4字节就是Label的地址,首先会判断Label地址标识是否正确,也就是判断高4字节是否为0x17,如果是的话,低4字节就是Label地址,在上面给出的两行代码中就是存放rel_a变量地址的Label,0x87868f0c的值为0x17,说明低4字节是Label地址,也就是0x87804260,需要将0x87804260 + offset处的值改为重定位后的变量rel_a地址。

在对.rel.dyn段以及Label的相关概念有一定的了解后,接下来,我们分析函数relocate_code()的第二部分代码,修复.rel.dyn段重定位问题,代码如下:

    /*
     * fix .rel.dyn relocations //修复.rel.dyn段
     */
    ldr    r2, =__rel_dyn_start    /* r2 <- SRC &__rel_dyn_start */
    ldr    r3, =__rel_dyn_end    /* r3 <- SRC &__rel_dyn_end */
fixloop:
    ldmia    r2!, {r0-r1}        /* (r0,r1) <- (SRC location,fixup) */
    and    r1, r1, #0xff
    cmp    r1, #23            /* relative fixup? */
    bne    fixnext

    /* relative fix: increase location by offset */
    add    r0, r0, r4
    ldr    r1, [r0]
    add    r1, r1, r4
    str    r1, [r0]
fixnext:
    cmp    r2, r3
    blo    fixloop

relocate_code()函数第二部分代码调用后,将__rel_dyn_start的值赋给r2,也就是r2中保存着.rel.dyn段的起始地址,然后将__rel_dyn_end的值赋给r3,也就是r3中保存着.rel.dyn段的结束地址。

接下来,进入到fixloop处执行,使用ldmia指令,从.rel.dyn段起始地址开始,每次读取两个4字节数据存放到r0和r1寄存器中,其中r0存放低4字节的数据,也就是Label的地址,r1存放高4字节的数据,也就是Label的标识,然后将r1的值与0xff相与,取r1值的低8位,并将结果保存到r1中,接下来,判断r1中的值是否等于23(0x17),如果r1不等于23的话,也就说明不是描述Label,跳到fixnext处循环执行,直到r2和r3相等,也就是遍历完.rel.dyn段。

如果r1的值等于23(0x17)的话,继续执行,r0保存着Label地址,r4保存着uboot重定位的偏移,因此,r0 + r4就得到了重定位后的Label地址,也就是上面分析的0x87804260 + 0x873b000 = 0x8ff3f260 = r0,此时r0已经保存着重定位后的Label地址,然后使用ldr指令,读取r0中保存的值到r1中,也就是Label地址所保存的rel_a变量的地址,但是此时,该rel_a变量的地址仍然是重定位之前的地址0x87868994,所以,此时r1 = 0x87868994,接下来,使用add指令,将r1中的值加上r4,也就是加上uboot重定位偏移量,所以,此时r1 = 0x87868994 + 0x873b000 = 0x8ffa3994,这不就是前面分析的uboot重定位后的rel_a变量的地址吗?接下来使用str指令,将r1中的值写入到r0保存的地址中,也就是将Label地址中的值设置为0x8ffa3994,就是重定位后rel_a变量的新的地址。

比较r2和r3的值,查看.rel.dyn段重定位修复是否完成,循环直到完成,才能执行完relocate_code()函数。

第二部分执行完成后,就解决了.rel.dyn段的重定位问题,从而解决了uboot重定位后,链接地址和运行地址不一致的问题。

4、relocate_vectors函数

执行完relocate_code函数后,接下来就是执行relocate_vectors函数,该函数用于重定位中断向量表,该函数的定义同样在汇编文件:

uboot/arch/arm/lib/relocate.S

函数的定义如下所示:

ENTRY(relocate_vectors)

#ifdef CONFIG_CPU_V7M
    /*
     * On ARMv7-M we only have to write the new vector address
     * to VTOR register.
     */
    ldr    r0, [r9, #GD_RELOCADDR]    /* r0 = gd->relocaddr */
    ldr    r1, =V7M_SCB_BASE
    str    r0, [r1, V7M_SCB_VTOR]
#else
#ifdef CONFIG_HAS_VBAR
    /*
     * If the ARM processor has the security extensions,
     * use VBAR to relocate the exception vectors.
     */
    ldr    r0, [r9, #GD_RELOCADDR]    /* r0 = gd->relocaddr */
    mcr     p15, 0, r0, c12, c0, 0  /* Set VBAR */
#else
    /*
     * Copy the relocated exception vectors to the
     * correct address
     * CP15 c1 V bit gives us the location of the vectors:
     * 0x00000000 or 0xFFFF0000.
     */
    ldr    r0, [r9, #GD_RELOCADDR]    /* r0 = gd->relocaddr */
    mrc    p15, 0, r2, c1, c0, 0    /* V bit (bit[13]) in CP15 c1 */
    ands    r2, r2, #(1 << 13)
    ldreq    r1, =0x00000000        /* If V=0 */
    ldrne    r1, =0xFFFF0000        /* If V=1 */
    ldmia    r0!, {r2-r8,r10}
    stmia    r1!, {r2-r8,r10}
    ldmia    r0!, {r2-r8,r10}
    stmia    r1!, {r2-r8,r10}
#endif
#endif
    bx    lr

ENDPROC(relocate_vectors)

在该函数中,对于i.mx6ul芯片来说CONFIG_CPU_V7M没有定义,接下来,判断是否定义了CONFIG_HAS_VBAR,表示向量表偏移,在.config文件有该配置,因此,会执行该定义相关的代码,首先是将gd->relocaddr的值赋给r0,也就是r0里面保存重定位后uboot的首地址,中断向量表也是从这个首地址开始存储的,接下来,使用mcr指令,将r0的值写到CP15的VBAR寄存器中,其实就是将新的中断向量表首地址写到VBAR寄存器中,设置中断向量表偏移。

5、小结

本篇文章主要是对Uboot启动流程中,uboot重定位的流程进行简单分析,核心是对relocate_code函数和relocate_vectors函数的分析。