U-Boot relocate

在上文中已经介绍了u-boot启动的整体流程,在u-boot启动流程中有一个非常关键的步骤就是重定向,本文将对其进行较为详细的分析梳理,在阅读本文之前,请思考

  • 为什么要进行重定向?

  • 重定向是从哪里到哪里的重定向?

  • 重定向的内容是什么?重定向过程中内存布局是什么样子的?

  • 什么是PIC位置无关代码?ARM位置无关代码原理是什么?

  • 重定向具体是怎么实现的?

本文仍然是基于ARM vexpress-a9虚拟板卡和u-boot-2016-09源码

为什么要进行重定向?

关于这个问题,在u-boot的README和官方文档中并没有找到比较好的答案,而是在网上搜索到了一些回答,总结如下

  • 某些情况下,u-boot一开始是运行在ROM或NorFlash上的,而考虑到运行空间大小或者运行效率的问题,需要将其拷贝到DDR中运行,才能运行u-boot的完整功能

  • 由于Linux kernel一般是运行在DDR的低端内存,拷贝u-boot到一个与kernel不冲突的位置运行

由于重定向一定是拷贝到DDR中,因此在重定向之前,必须已经初始化了DDR

重定向地址计算

以vexpress-a9重定向操作为例,在board_init_f中调用init_sequence_f初始化数组,与重定向地址计算的函数主要是以下几个

  • setup_mon_len

  • dram_init

  • setup_dest_addr

  • reserve_uboot

  • reserve_malloc

  • reserve_board

  • reserve_global_data

  • reserve_fdt

  • reserve_stacks

  • reloc_fdt

  • setup_reloc

以上函数各自计算了重定向不同内容在重定向之后所在DDR的位置,存放于全局结构体GD中

计算u-boot大小

函数setup_mon_len主要是用于获取u-boot从_start__bss_end的大小

这个大小从编译后的System.map文件中可以看出,就是一个完整的u-boot大小了,得到的大小信息存放在GD->mon_len中,因为u-boot把u-boot本体叫做monitor

计算SDRAM大小

函数dram_init中获取了SDRAM大小

该大小由具体板级文件得到,在vexpress-a9中,就是SDRAM Bank#1的大小512MB,存放在GD->ram_size

setup_dest_addr

函数setup_dest_addr中计算了u-boot的重定向地址,重点在以下这段代码

前文已经介绍了,GD->ram_size中已经得到了SDRAM的空间大小,这里将GD->ram_top指向了SDRAM基地址CONFIG_SYS_SDRAM_BASE,然后调用get_effective_memsize,将GD->ram_top偏移了有效SDRAM大小的长度,也就是从SDRAM的基地址移动到了尾部,然后将GD->relocaddr指向了GD->ram_top

预留u-boot位置

函数reserve_uboot中计算重定向后u-boot本身的位置,将重定向后u-boot的起始地址放置在GD->relocaddr

预留malloc区间

函数reserve_malloc中通过将重定向后栈空间指针GD->start_addr_sp 跳过malloc区域,为malloc内存分配预留了空间,malloc空间大小由宏定义TOTAL_MALLOC_LEN决定,该大小最终由include/configs/下的板级文件定义

预留板级信息

函数reserve_board在重定向空间中预留了板级信息位置,与malloc区间相邻,起始地址记录在GD->bd

预留GD空间

函数reserve_global_data在重定向空间中预留了GD的空间,与板级信息相邻,记录在GD->new_gd

预留设备树信息

函数reserve_fdt在重定向空间中预留了设备树信息空间,记录在GD->new_fdt

预留栈空间

函数reserve_stacks为重定向空间中预留了栈空间位置,记录在GD->start_addr_sp中,其实就是临近设备树信息的位置,board_f.c中只是为栈地址进行了对齐操作,而真正的栈位置在架构相关代码arch_reserve_stacks中,arm架构代码在arch/arm/lib/stack.c中

arm栈预留处理中,首先预留了中断栈空间,大小为CONFIG_STACKSIZE_IRQ + CONFIG_STACKSIZE_FIQ,即中断和快速中断栈空间,中断栈位置记录在GD->irq_sp中,再减去了16字节,将程序栈记录在GD->start_addr_sp

设备树重定向

函数reloc_fdt中进行了设备树信息的拷贝,代码中直接使用memcpy进行拷贝

GD重定向

函数setup_reloc中,计算了重定向偏移,记录在GD->reloc_off中,然后用memcpy将GD拷贝到了重定向位置

地址计算总结

经过上述分析,在函数board_init_f中,主要是在重定向空间也就是SDRAM中,为一些需要重定向的内容预留出空间位置,并将不同区域的起始地址记录在了GD中,并且GD本身也需要重定向,需要重定向的内容如下

  • U-Boot镜像本身

  • malloc动态内存分配区域

  • 板级信息bd

  • GD

  • 设备树信息

  • IRQ+FIQ栈

  • 程序栈

经过以上程序执行后,SDRAM中重定向空间划分如下图

其中GD和设备树都已经重定向过了

重定向过程分析

分析完了要重定向的内容,与重定向地址以及内存布局后,接下来就要重点分析一下u-boot程序是如何完成自身的重定向的,u-boot程序要在运行的过程中将自身重定向到SDRAM的顶部中,并跳转过去执行,到底是如何做到的呢?要想搞清楚这其中的原理,首先需要介绍一下位置无关代码PIC以及ARM ELF重定向相关知识

PIC与ARM ELF重定向

正常情况下,一个可运行的程序是经过编译器、汇编器和链接器经过编译链接生成的可执行文件,在ARM平台下其格式为ARM ELF。ARM ELF文件中包含头部Header信息、各种segment段,例如代码段、bss段、data段等,程序中的各种函数、标签、预定义的已初始化和未初始化的全局变量等内容的所在地址都是固定的,当执行函数跳转、取全局变量数据时,可直接通过指令获取跳转或变量的绝对地址,因此正常的函数跳转、全局变量访问都是正确的

试想一下,如果将程序中的某部分或者全部拷贝到一个新的地址,那么当在新的地址取指运行时,不管是函数跳转还是全局变量访问,地址还是在原始程序位置,又会返回到原地址执行,重定向的意义何在?

编译工具和ELF格式在设计的时候就考虑到了这种重定向的情况,不光是ARM,其实很多编译器和可执行文件格式都有位置无关PIC和重定向的设计。u-boot源码doc/README.arm-relocation文档中,对ARM架构的重定向进行了说明,在架构层面,需要添加-pie选项使链接器生成fixup table.rel.dyn.dynsym段,这几种段在ARM ELF文档中定义为类型2和类型23

在arch/arm/config.mk文件中,确实定义了-pie编译选项

以函数board_init_f中寻址init_sequence_f为例,来看看编译过后u-boot中这两个符号的相互关系,首先使用objdump反汇编u-boot

1
arm-linux-gnueabi-objdump -d u-boot > u-boot.dump

在u-boot.dump中查找符号board_init_finit_sequence_f,它们之间的关系如下

注意看上图中两行用红色框出的代码,第一处使用了相对地址,将r0赋值为pc + 16的位置,注意ARM中由于流水线设计,当前程序运行位置与PC实际指向位置差8,也就是说PC现在位于地址0x60809088的地方,指令为bl 60819790,那么pc + 16的位置就是0x60809094,也就是第二个红色框代码行,这一行内容是0x6082b244,这指向的内容就是init_sequence_f的位置,而当链接选项开启了-pie时,会将代码中与函数、标签、全局变量寻址与跳转相关内容的偏移地址,在”.rel.dyn”段中记录一个备份,而函数board_init_f中引用init_sequence_f的地址偏移,在.rel.dyn段中也有一个备份

而重定向的关键在于,将代码段拷贝到新的位置后,在将rel.dyn段进行拷贝后,由程序本身修改rel.dyn中所有符号的偏移为新的偏移,这样新位置执行的程序就可以使用新的rel.dyn来正确寻址了

这段解释较为抽象,下面将会以实际代码来分析真实的重定向过程到底是如何实现的

重定向过程

首先,经过上面的分析,已经明确了要将运行于片上静态RAM中的u-boot重定向到SDRAM的高地址,转而在SDRAM中运行u-boot,结合链接脚本u-boot.lds和System.map,可以得到如下图所示的地址空间示意图

u-boot重定向核心处理位于crt0.S和relocate.S两个汇编文件中,在调用board_init_f完成了重定向地址计算和SDRAM初始化后,就开始了重定向处理流程,代码如下

这段代码首先要理解红色框出的3行对链接寄存器lr的处理,首先是将lr指向当前代码段的here标签位置,然后将lr增加了重定向偏移的大小,即重定向后的新u-boot的here位置,因此,在执行了b relocate_code这个跳转指令后,当relocate_code标签中最后一句bx lr返回时,返回的不是当前u-boot代码段中的here,而已经是重定向后新的u-boot的here了,这就从老的u-boot跳转到了新的u-boot继续往下执行

当然,此时虽然设置了lr,只是提前预置了relocate_code返回后的跳转操作,而u-boot的拷贝工作是在relocate_code中完成的

relocate_code代码的处理可分为两部分,第一部分是对u-boot镜像的拷贝,第二部分就是对.rel.dyn段的拷贝和重置

第一部分较好理解,首先是将r1r2分别记录__image_copy_start__image_copy_end也就是u-boot镜像的边界,然后r4在调用relocate_code前就已经设置为了重定向偏移,然后就在copy_loop中进行拷贝操作,拷贝使用的是多寄存器指令ldmiastmia,先使用ldmiar1的连续8个字节内容拷贝到r10r11中,然后使用stmiar10r11写入到r0位置,一个循环完成8字节的拷贝,后缀ia表示操作后地址自增,然后在循环末尾判断是否到达__image_copy_end

第二部分是理解重定向原理的关键,首先是用r2r3记录.rel.dyn段的边界,然后依然是使用ldmia一次性拷贝8字节到新的位置上,但是这里有一行代码cmp r1, #23,这是什么意思呢?

回过头再来看看之前分析init_sequence_f偏移位置时的反汇编代码

ldmia每次加载两个字到r0r1,当加载到.rel.dyn段的init_sequence_f偏移时,r0 存放的就是偏移地址6082b244 也就是init_sequence_f的偏移,而r1的值就是0x17也就是23,和重定向代码的处理对应上了,这个23是什么呢?这个在ARM ELF Specification中有介绍

这是ARM ELF重定向Entry类型的一种,即重定向的地址由程序来决定。如果第二个字内容是23的话,就执行下一段代码,假设现在就是运行到.rel.dyninit_sequence_f 这个前位置,首先是把r0加上重定向偏移,指向了重定向位置,也就是r0指向了重定向后的init_sequence_f,然后将r0的内容加载到r1r1指向了重定向后init_sequence_f数组的第一个成员地址,然后再将r1加上重定向偏移,再使用str将新的成员偏移写入。经过多轮循环后,所有段中用于存储相对偏移的数值就都修改为了重定向后新的位置

总结

  1. 由于片上SRAM空间较小、需要为kernel留出空间等原因,需要进行重定向操作

  2. 重定向源地址为Flash或者SRAM,目的地址是SDRAM

  3. 在函数board_init_f中,会初始化SDRAM,然后计算GD、设备树信息、板级信息、u-boot本身的重定向地址,基本位于SDRAM顶端

  4. u-boot本身的重定向利用了ARM ELF重定向23类型和链接生成的.rel.dyn段信息,使用-pie选项链接时,可执行文件会将跳转等操作都变为相对PC的位置无关代码,然后在每个标签的末尾添加本标签要跳转的所有符号相对偏移位置,这些相对位置在.rel.dyn段中有一个备份记录,重定向函数以及变量链接关系时,通过.rel.dyn段的所有偏移位置加上重定向偏移,再修改到每个标签末尾的偏移位置,由此重定向后的代码就可以正常访问重定向后相对位置的标签了