uboot启动分析

uboot启动分析

uboot是一个操作系统引导程序,多用于嵌入式设备上linux操作系统的引导。它是一个裸机程序,启动流程对其他裸机系统的启动都具有参考意义。下面,我们一起学习一下uboot的启动流程。

uboot的启动流程如下图所示:

我们对启动流程进行分析时,首先需要分析其链接脚本uboot.lds,从中了解大概的镜像布局,来知道程序的入口,以及一些特殊的地址变量定义,段等。

uboot程序的最开始为_start,从_start开始,是中断向量表的定义,这是大多数嵌入式裸机程序的共识,它们总是以复位中断为入口,进入整个系统。在uboot_start标号开始处,我们可以看到复位中断的中断处理程序入口b reset,这样就来到了reset复位程序处。

reset程序是系统启动或复位后执行的程序,因此,其第一步就是设置处理器为SVC,关闭FRQIRQ,以确定处理器的工作模式(SVC:处于特权模式,且有自己的影子寄存器;关中断:中断尚没有配置,防止程序中断引起的程序异常);第二步,立马就进行中断向量表的设置,使中断能够执行,这对用户来说是一个高优先级的配置任务;第三步,也是做一些需要尽快确定的系统状态,比如关闭MMUTLBsD-cache,打开I-cache等相关的操作,以及初始化栈指针使得C语言函数能够调用;这样,复位后基本的极简环境就设置好了,我们可以进一步执行uboot相关程序,进入_main

_main就是uboot的进一步初始化了,其有了前面搭建的极简环境。第一步初始化早期mallocgd全局数组(gd全局数组用于保存uboot的一些重要信息,例如镜像的开始地址,镜像的重装载地址,malloc地址等等,这在后面再详细展开说明),调用board_init_f来执行一系列的函数来初始化硬件和gd结构体。其中,硬件初始化主要是初始化RAM(SRAM,DDR),因为后面重定位uboot需要对RAM进行读写操作,是必要的初始化;其主要的就是进行gd结构体的初始化,完成了完整的内存布局,和其中各种变量的赋值。第二步就是对自己进行重定位,这一部分挺重要的,主要就是需要先将自己拷贝到重定位地址,然后修改.rel.dyn段的位置无关码,最后对环境进行重新配置。因为运行地址进行了变化,所以程序必须是位置无关的,在编译的时候加上-pie选项就可以将它编译成位置无关的,其会将程序中的绝对地址的地址都收集到一个特殊的段rel.dyn中,那么,在进行重定位的时候,只需将该段中记录的地址的数进行一个修改即可,详细过程在后文讲述。第三步,调用board_init_r函数来执行一系列函数,进行最终的uboot初始化,包括malloc,各种板级初始化等,最后调用run_main_loop,进入uboot命令行处理程序。

run_main_loop在开始时会进入倒计时,倒计时结束则会执行bootcmd环境变量中的命令,启动linux之类的系统;在倒计时被按键中断的话会进入cli_loop命令行处理程序,接收用户的指令执行。这部分就是uboot的用户应用功能了,启动流程就到此结束。

以上就是uboot启动流程的全部内容了,关于linux启动方面不属于启动流程的内容,是uboot实现的功能,在以后的文章中说明。下面,就上面提到的几个中间进行展开介绍。

gd全局变量

gd全局变量贯穿整个uboot启动,在前面的启动流程中我们看到了其中存储了重要的地址信息,暗含了内存布局,下面是它的结构体原型中的某些项,我们一起来体会一下它究竟是哪些内容:

typedef struct global_data {
        bd_t *bd;
        unsigned long flags;
        unsigned int baudrate;
        unsigned long cpu_clk;  /* CPU clock in Hz!             */
        unsigned long bus_clk;
        /* We cannot bracket this with CONFIG_PCI due to mpc5xxx */
        unsigned long pci_clk;
        unsigned long mem_clk;
...
        unsigned long have_console;     /* serial_init() was called */
...
        unsigned long env_addr; /* Address  of Environment struct */
        unsigned long env_valid;        /* Checksum of Environment valid? */
​
        unsigned long ram_top;  /* Top address of RAM used by U-Boot */
​
        unsigned long relocaddr;        /* Start address of U-Boot in RAM */
...
        unsigned long mon_len;  /* monitor len */
        unsigned long irq_sp;           /* irq stack pointer */
        unsigned long start_addr_sp;    /* start_addr_stackpointer */
        unsigned long reloc_off;
        struct global_data *new_gd;     /* relocated global data */
...
#ifdef CONFIG_TIMER
        struct udevice  *timer; /* Timer instance for Driver Model */
#endif
const void *fdt_blob;   /* Our device tree, NULL if none */
        void *new_fdt;          /* Relocated FDT */
        unsigned long fdt_size; /* Space reserved for relocated FDT */
        struct jt_funcs *jt;            /* jump table */
        char env_buf[32];       /* buffer for getenv() before reloc. */
...
#ifdef CONFIG_SYS_MALLOC_F_LEN
        unsigned long malloc_base;      /* base address of early malloc() */
        unsigned long malloc_limit;     /* limit address */
        unsigned long malloc_ptr;       /* current address */
#endif
...
} gd_t;

从原型中我们可以看到,这个global_data结构体就如它的名字一样,它应该是记录了uboot所有用到的全局变量,其内容也比我们前面看到的要丰富,涵盖栈,mallocram_top等程序运行的环境信息,以及用于重定位的信息reloc_off等,还有用于指导uboot运行的各种变量例如时钟,事件,标志位等等。

uboot启动中,和gd密切相关的就是环境信息和重定位信息了,依据环境信息搭建了内存布局,为程序运行提供了环境;重定位信息使得uboot可以根据它来将自己重定位到RAM高地址的地方。

uboot重定位

uboot的启动流程中,我们可以看到其中有一步是将自己重定位到RAM的高地址处,为什么呢?uboot它是一个引导程序,它的核心功能就是引导像linux这样的系统启动,那么它就需要加载引导的镜像文件到内存中。为了给镜像文件留出足够的RAM,同时进行保证内存布局的连续性,那么最好就是uboot处于RAM的最高地址处,下面的空间留给镜像。而uboot的运行地址原本是uboot程序的编译者在编译的时候指定的,其具有不确定性。即uboot无法决定其运行地址,因此它选择在自己运行时自己将自己重定位到RAM高地址处,既然无法决定自己的出身,那么就逆天改命。

但是,重定位要注意什么呢?重定位的话,就意味着这个程序最终会在不同的地址上运行,也即其运行地址是可变的,而由于只有链接地址和运行地址相同时,程序才能正确运行,那么其链接地址就必须有一定的办法可以改变(其在编译的时候确定),这就要求其指令必须是位置无关的。举个例子,如果有某条指令取x8729 0862地址上的数据,那么如果当将程序重定位到偏移为0x1000 0000处的位置,再去取0x8729 0862上的数据,就出错了,因为原来的数据现在的地址变成了0x9729 0862,这就是位置有关代码,即存在绝对地址。可以发现,当访问变量时就会出现位置有关代码,还是很常见的。在解决这类问题时,现在应该是有多种机制,uboot中是在编译时指定pie选项,它会将代码中出现的绝对地址所在的地址收集到一个特殊的段.rel.dyn中,这样,在进行重定位的时候,除了将代码拷贝到重定位地址以外,还需取出.rel.dyn段中的项,将其作为地址(需要加上偏移,对应到重定位代码的对应地址),修改该地址上的值,进行链接地址的修改。

代码重定位后,有一个重要的一点,怎么从当前的位置转到重定位之后的位置呢?其实现在在系统中已经有两个uboot镜像了,且两个的运行地址和链接地址都是相对应的,都可以正常运行,但是我们处于重定位之前的镜像中。uboot的做法是定义一个标号,将该标号地址加上偏移,那么就对应于重定位之后的镜像的标号处,在进行一个跳转即可转到重定位之后的代码。

另外,在重定位之后,因为uboot的运行地址发生了变化,相应的一系列的环境像栈,malloc,中断向量表也需要重新配置。不过,这里就确定了uboot的最终环境。

下面是重定位部分相关的源码,可以看看实际的代码是怎么实现的,就会更加理解上面将的概念:

uboot.lds

链接脚本定义了.rel*段位.rel.dyn段,并定义了段的开始和结束的地址变量。

SECTIONS
{
...
    .rel_dyn_start :
     {
      *(.__rel_dyn_start)
     }
     .rel.dyn : {
      *(.rel*)
     }
     .rel_dyn_end :
     {
      *(.__rel_dyn_end)
     }
...
}

-pie的作用

我们从.rel.dyn段反推回去,看看位置无关代码做了什么。

  • 首先,我们将uboot反汇编:

    arm-linux-gnueabihf-objdump -D -m arm u-boot > u-boot.dis
    vim u-boot.dis
  • 查找.rel.dyn段:

  • 我们可以看到它以8个字节为一组,高四字节0x00000017标识这是一个lable(即需要重定位的对象),低四个字节是这个lable的地址。

  • 查看lable地址上的内容,即0x87800020

  • 可以看到,该地址上是一个地址,且有标号_undefined_instruction标识。并且在代码中有ldr pc, [pc, #20],相对寻址的方式,将该地址上的数送到pc指针上。

  • 查看0x87800060内容:

    可以看到该处是一个函数,且函数名和上面的标号查一个_

综上,我们就可以推理出-pie的作用,它对指令中所有的绝对地址(位置有关代码),都采用相对寻址的方式,将绝对地址存在别处,用相对寻址从别处加载进来。且在别处定义的时候同时定义一个标号,标号即为实际变量或函数名加上一个_前缀。且在编译的时候,会抽出所有的别处定义的地址放到.rel*段中,以供访问。关于如何抽取的,可能是抽取所有的_前缀的标签,再判断是否是吧。

relocate_code调用的上下文

这里需要注意它设置好了lr为重定位之后的here,跳转使用的是b

   /* 【设置新栈】 */
        ldr     sp, [r9, #GD_START_ADDR_SP]     /* sp = gd->start_addr_sp */
        bic     sp, sp, #7      /* 8-byte alignment for ABI compliance */
​
        /* 【确定here地址,用于从当前镜像转到重定位后的镜像中】 */
        ldr     r9, [r9, #GD_BD]                /* r9 = gd->bd */
        sub     r9, r9, #GD_SIZE                /* new GD is below bd */
​
        adr     lr, here
        ldr     r0, [r9, #GD_RELOC_OFF]         /* r0 = gd->reloc_off */
        add     lr, lr, r0
#if defined(CONFIG_CPU_V7M)
        orr     lr, #1                          /* As required by Thumb-only */
#endif
        /* 【重定位relocate_code】 */
        ldr     r0, [r9, #GD_RELOCADDR]         /* r0 = gd->relocaddr */
        b       relocate_code
here:   /* 【here,relocate_code结束会跳到重定位后的这里】 */
        /* 【重定位中断向量表】 */
        bl      relocate_vectors
        
        bl      c_runtime_cpu_setup     /* we still call old routine here,没啥 */
#endif
        /* 【清除bss段,略】 */

relocate_code

ENTRY(relocate_code)
        /* r1拷贝的开始  r2拷贝的结束 r4偏移 */
        ldr     r1, =__image_copy_start /* r1 <- SRC &__image_copy_start */
        subs    r4, r0, r1              /* r4 <- relocation offset */
        beq     relocate_done           /* skip relocation */
        ldr     r2, =__image_copy_end   /* r2 <- SRC &__image_copy_end */
        
        /* 【拷贝到重定位地址】 */
copy_loop:
        ldmia   r1!, {r10-r11}          /* copy from source address [r1]    */
        stmia   r0!, {r10-r11}          /* copy to   target address [r0]    */
        cmp     r1, r2                  /* until source end address [r2]    */
        blo     copy_loop
​
        /* 【根据.rel*段修改其中的链接地址】 */
        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                 /* 判断是否是0x17 */
        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:
...
/* 【跳转到lr中,即重定位后的here】 */
#ifdef __ARM_ARCH_4__
        mov     pc, lr
#else
        bx      lr
#endif
​
ENDPROC(relocate_code)

小结

uboot的启动分析就到此结束,从中我们也有一些程序编写上的收获。

我们可以看到,程序启动的过程不是一蹴而就的,它其中有很多次的初始化,因为有的环境必须依赖于某些环境。u_boot最开始最首先就是处理器模式的设置,关中断,设置中断向量表,栈指针,这样程序基本可以运行了,且可以调用C函数。后来进行了board_init_r初始化,这里它也分的很清楚,因为它需要进行代码的重定位,因此不急着做uboot所有硬件的初始化,只是做了必要的RAM初始化,然后对gd进行设置。最后重定位之后,才最终确定了环境,重新设置spmalloc等等,并进行uboot运行环境的全部初始化,包含各种硬件等等,在board_init_r里。在理解这些时一定要知道程序在这里是要干什么,需要依赖什么环境,需要什么才配置什么,而不是一次做完。

  • 19
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值