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_f
和init_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
段的拷贝和重置

第一部分较好理解,首先是将r1
和r2
分别记录__image_copy_start
和__image_copy_end
也就是u-boot镜像的边界,然后r4
在调用relocate_code
前就已经设置为了重定向偏移,然后就在copy_loop
中进行拷贝操作,拷贝使用的是多寄存器指令ldmia
和stmia
,先使用ldmia
将r1
的连续8个字节内容拷贝到r10
和r11
中,然后使用stmia
将r10
和r11
写入到r0
位置,一个循环完成8字节的拷贝,后缀ia表示操作后地址自增,然后在循环末尾判断是否到达__image_copy_end
处
第二部分是理解重定向原理的关键,首先是用r2
和r3
记录.rel.dyn
段的边界,然后依然是使用ldmia
一次性拷贝8字节到新的位置上,但是这里有一行代码cmp r1, #23
,这是什么意思呢?
回过头再来看看之前分析init_sequence_f
偏移位置时的反汇编代码

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

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

总结
由于片上SRAM空间较小、需要为kernel留出空间等原因,需要进行重定向操作
重定向源地址为Flash或者SRAM,目的地址是SDRAM
在函数
board_init_f
中,会初始化SDRAM,然后计算GD、设备树信息、板级信息、u-boot本身的重定向地址,基本位于SDRAM顶端u-boot本身的重定向利用了ARM ELF重定向23类型和链接生成的
.rel.dyn
段信息,使用-pie
选项链接时,可执行文件会将跳转等操作都变为相对PC的位置无关代码,然后在每个标签的末尾添加本标签要跳转的所有符号相对偏移位置,这些相对位置在.rel.dyn
段中有一个备份记录,重定向函数以及变量链接关系时,通过.rel.dyn
段的所有偏移位置加上重定向偏移,再修改到每个标签末尾的偏移位置,由此重定向后的代码就可以正常访问重定向后相对位置的标签了