(1)重定位:分为静态重定位和动态重定位
静态重定位:静态重定位是在程序执行之前进行重定位,它根据装配模块将要装入的内存起始位置,直接修改装配模块中的有关使用地址的指令
我们下面要分析就是静态重定位的情况。
动态重定位:动态重定位是指,不是在程序执行之前而是在程序执行过程中进行地址重定位。更确切地说,是在每次访问内存单元前才进行地址变换。动态重定位可使装配模块不加任何修改而装入内存,但是它需要硬件——定位寄存器的支持。
图片的参考来源:http://c.biancheng.net/cpp/html/2608.html
上面重定位的定义的参考来源:http://bbs.pediy.com/showthread.php?t=76876
(2)为什么需要重定位呢?
我们的程序中的代码可分为:位置无关码和位置有关码。顾名思义,位置无关码就是代码在哪个地址运行都行的。那么位置有关码呢?就必须在规定的地址处执行才可以,这个规定的地址就是链接地址,而我们代码执行时候的地址是运行地址,位置有关码就要求链接地址和运行地址必须一致,执行的时候才不会出错。
譬如:uboot实际使用的方式:uboot大小随意,假定为200KB。启动过程是这样子:先开机上电后BL0运行,BL0会加载外部启动设备中的uboot的前16KB(BL1)到SRAM中去运行,BL1运行时会初始化DDR,然后将整个uboot搬运到DDR中,然后用一句长跳转(从SRAM跳转到DDR)指令从SRAM中直接跳转到DDR中继续执行uboot直到uboot完全启动。uboot启动后在uboot命令行中去启动OS。
扩展:分散加载:把uboot分成2部分(BL1和整个uboot),两部分分别指定不同的链接地址。启动时将两部分加载到不同的地址(BL1加载到SRAM,整个uboot加载到DDR),这时候不用重定位也能启动。
注释:
在之前的210中的裸机程序中,运行地址由我们下载时确定,下载时下载到0xd0020010,所以就从这里开始运行。(这个下载地址也不是我们随意定的,是iROM中的BL0加载BL1时事先指定好的地址,这是由CPU的设计决定的)。所以理论上我们编译链接时应该将地址指定到0xd0020010,但是实际上我们在之前的裸机程序中都是使用位置无关码PIC,所以链接地址可以是0。
(3)在说重定位的具体实现之前,我们先来说一下链接脚本(文件.lds)
链接脚本的代码如下:看懂下面的代码我们还需要了解:bss段,数据段(.data),代码段(.text)等知识,可以参考我的这篇博客:
程序中的bss段,数据段(.data),代码段(.text):http://blog.csdn.net/czg13548930186/article/details/54882907
然后,我们通过Makefile,编译链接程序时可以指定按照链接脚本的顺序来链接代码:arm-linux-ld -Tlink.lds-o led.elf $^
link.lds为链接脚本的名字:
SECTIONS
{
. = 0xd0024000; /*指定链接地址为0xd0024000*/
.text : { /*代码段*/
start.o /*指定链接的顺序为:start.o->sdram_init.o->其他的一些文件*/
sdram_init.o
* (.text) /*这里表示其他的一些.o文件*/
}
.data : { /*数据段*/
* (.data) /*这里表示所有的数据段的文件*/
}
bss_start = .; /*把当前的地址赋值给bss_start*/
.bss : { /*bss段*/
* (.bss) /*所有bss段的文件*/
}
bss_end = .; /*把当前的地址赋值给bss_end*/
}
(4)下面,我们就来看看重定位代码的具体实现
#define WTCON 0xE2700000
#define SVC_STACK 0xD0037D80
.global _start // 把_start链接属性改为外部,这样其他文件就可以看见_start了
_start:
//关看门狗(向WTCON的bit5写入0即可)
ldr r0,= WTCON
ldr r1,= (0<<5)
str r1,[r0]
//设置SVC栈,实现汇编与C的相互调用
ldr sp,= SVC_STACK
//开关icache
mrc p15,0,r0,c1,c0,0 //读出cp15的c1到r0中
//bic r0, r0, #(1<<12) //bit12 清0 关icache
orr r0, r0, #(1<<12) //bit12 置1 开icache
mcr p15,0,r0,c1,c0,0 //将r0写入cp15中的c1中
//重定位
// adr指令用于加载_start当前运行地址(即dnw下载时选择的地址(0xd0020010))
adr r0, _start // adr加载时就叫短加载
// ldr指令用于加载_start的链接地址:0x20000000
ldr r1, =_start // ldr加载时如果目标寄存器是pc就叫长跳转,如果目标寄存器是r1等就叫长加载
// bss段的起始地址
ldr r2, =bss_start // 就是我们重定位代码的结束地址,重定位只需重定位代码段和数据段即可
cmp r0, r1 // 比较_start的运行时地址和链接地址是否相等
beq clean_bss // 如果相等说明不需要重定位,所以跳过copy_loop,直接到clean_bss
// 如果不相等说明需要重定位,那么直接执行下面的copy_loop进行重定位
// 重定位完成后继续执行clean_bss。
// 用汇编来实现的一个while循环
copy_loop:
ldr r3, [r0], #4 // 源 后面的#4就是:r0的四个字节赋值给r3,r0地址加4
str r3, [r1], #4 // 目的 这两句代码就完成了4个字节内容的拷贝
cmp r1, r2 // r1和r2都是用ldr加载的,都是链接地址,所以r1不断+4总能等于r2
bne copy_loop
// 清bss段,其实就是在链接地址处把bss段全部清零
clean_bss:
ldr r0, =bss_start
ldr r1, =bss_end
cmp r0, r1 // 如果r0等于r1,说明bss段为空,直接下去
beq run_on_dram // 清除bss完之后的地址
mov r2, #0 // r2 = 0
clear_loop:
str r2, [r0], #4 // 先将r2中的值放入r0所指向的内存地址(r0中的值作为内存地址),
cmp r0, r1 // 然后r0 = r0 + 4(即将bss清0)
bne clear_loop
run_on_dram:
// 长跳转到led_blink开始第二阶段
ldr pc, =czg_led // ldr指令实现长跳转,b指令是短跳转
//最后这里跳转,是跳转到重定位的代码所对应的czg_led这个函数那里去执行的
问题:
1.为什么最后我们要用ldr长跳转而不是b短跳转?
因为实际上此时在SRAM中有2个czg_led函数镜像,两个都能执行,如果短跳转b czg_led则执行的就是0xd0020010开头处的这一份,
如果长跳转ldr pc, =czg_led则执行的是0xd0024000开头处的这一份)。这就是短跳转和长跳转的区别。
2.为什么复制的长度是代码段 +数据段的长度而bss段不需要重定位?
bss段在程序中并不占用任何空间,只是一个占位符,不占用执行文件的空间,没必要复制过去增加难度。我们知道C语言要求的运行条件是要将栈设置好,并且bss段要初始化为0;
一般情况下我们的程序是不需要负责清零bss段的(C语言编译器和链接器会帮我们的程序自动添加一段头程序,这段程序会在我们的main函数之前运行,这段代码就负责清除bss).但是在我们代码重定位了之后,因为编译器帮我们附加的代码只是帮我们清除了运行地址那一份代码中的bss,而未清除重定位地址开头的那一份代码的bss,所以重定位之后需要自己去清除bss.
总结:由上面我们可以看到:重定位的时候,先使用一段位置无关码来对重定位的地址那里的内存进行一些操作:
(1)把整段代码搬运过重定位的内存那里(用copy_loop来实现)
(2)清bss段(clean_bss来实现)
(3)跳转到重定位的那段内存去执行(run_on_dram来实现)
注意:上面那些汇编指令所实现的功能我们可以通过反汇编文件(.dis文件)来验证:arm-linux-objdump -D led.elf > led_elf.dis
补充复习图: