代码重定位
链接地址、运行地址、加载地址、存储地址关系
1.链接地址
在程序编译的时候,每个目标文件都是由源代码 编译得到,最终多个目标文件链接生成一个最终的可执行文件 ,而链接地址就是指示链接器,各个目标文件的在可执行程序中的位置。比如,一个可执行程序a.out由a.o、b.o、c.o组成,那么最终的a.out中谁在前、谁在中间、谁在结尾,都可以通过制定链接地址来决定。
链接地址是静态的,在进行程序编译的时候指定的。
注意,对于CPU来说是不管你这个链接地址是物理地址还是虚拟地址。
总结:
1、链接地址是给编译器用的,用来计算代码中相关地址偏移的
2、只要和PC值相关的就是位置无关代码(相对偏移),和PC无关的就是位置相关代码(绝对值)
2、运行地址
程序实际在内存中运行时候的地址,比如CPU要执行一条指令,那么必然要通过给PC赋值,从对应的地址空间中去取出来,那么这个地址就是实际的运行地址。
运行地址是动态的,如果你将程序加载到内存中时,改变存放在内存的地址,那么运行地址也就随之改变了。
注意,CPU同样不关心运行地址是虚拟地址还是物理地址。
3、加载地址
每一个程序一开始都是存放在flash中的,而运行是在内存中,这个时候就需要从flash中将指令读取到内存中(运行地址),flash的地址就是加载地址。
所以,加载地址指的是将代码从一个地址A搬移到地址B,这时候地址A就是加载地址
4、存储地址
指令在flash中存放的存储地址,就是存储地址。
5、加载地址和存储地址区别
本质上区别不大,都指的是程序存放的位置。
uboot代码重定位
我们C代码编译成汇编后,我们需要思考以下几个问题:
- 我们的函数跳转转为汇编代码后是怎样的;
- 全局变量又是如何寻址的;
实际上函数用b/bl相对跳转,因此代码进行重定位之后函数跳转是没有影响的。但是全局变量使用的是绝对地址,是有影响的。
那uboot是如何来处理这些情况的呢?更准确的说应该是compiler和uboot如何一起来处理这些情况的呢?
这里利用了PIC位置无关代码,通过为编译器指定编译选项-fpic或-fpie产生,生成rel.dyn/rela.dyn段, 这样编译产生的目标文件包含了PIC所需要的信息,-fpic,-fpie是gcc的PIC编译选项。ld也有PIC连接选项-pie,要获得一个完整的PIC可运行文件,连接目标文件时必须为ld指定-pie选项,
//全局变量相对地址 pc + 0x80
300017fc: e59f3050 ldr r3, [pc, #80] ; 30001854 <lcd_test+0x64>
//函数尾端的label
30001854: 3000564c .word 0x3000564c
//通过-pie编译的.rel.dyn段
30006660: 30001854 .word 0x30001854
==================================
通过.rel.dyn段找到地址0x30001854,通过地址找到里面值的地址0x3000564c,将其分别+offset,重定位后:
30001854+relocaddr: 3000564c+relocaddr .word 0x3000564c+relocaddr
根据重定位汇编代码可知:
ldr r2, =__rel_dyn_start /* r2设置为__rel_dyn_start */
ldr r3, =__rel_dyn_end /* r3设置为__rel_dyn_end */
fixloop:
ldmia r2!, {r0-r1} /* 加载[r2],[r2+4]到r0,r1中,那么 r0得到的就是0x30001854 */
and r1, r1, #0xff /* 取出r1中数据的低8位 */
cmp r1, #23 /* relative fixup? */
bne fixnext
/* relative fix: increase location by offset */
add r0, r0, r4 /* r4是重定位偏移,设置r0=r0+r4,也就是新的u-boot里面sunflower_320x240全局变量的地址 */
ldr r1, [r0] /* 将r0地址内的数据存到r1中,即0x3000564c,这个值就是sunflower_320x240全局变量数据的存放地址 */
add r1, r1, r4 /* 给r1加上偏移r4,将加了偏移后的值(变量的地址)写回,这样新的u-boot就能正确访问了;*/
str r1, [r0]
fixnext:
cmp r2, r3
blo fixloop
r4寄存器保存的是重定位偏移量,何时保存和计算?
在board_init_f阶段,U-Boot会执行board_init_f_alloc_reserve等函数。这些函数会精细地规划整个DDR内存的布局,包括为新的堆栈(Stack)、全局数据(gd)、日志缓冲区(Log Buffer)、以及最重要的——U-Boot自身代码,都预留好位置。最终计算出的U-Boot代码要搬移到的目标地址,会保存在gd->relocaddr中。
为什么选择 DDR 的高地址区域?
- 为内核腾出连续的低端内存:Linux 内核启动时需要从物理内存的低地址开始加载,且通常希望使用大块的连续空间。将 U-Boot 自身移动到高地址,可以避免自身占用低地址内存,为内核“让路”,防止内核解压时覆盖掉 U-Boot。
- 提高内存利用率:当 U-Boot 从 Flash 搬运到 DDR 的高地址后,其原先在低地址占用的空间就可以被释放,供内核或其他程序使用,提高了宝贵的内存资源利用率。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 351134995@qq.com