以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。
参考博客
一、链接地址与运行地址
1、链接地址
链接地址,是指程序员通过Makefile中“ -Ttext xxx ”或者在链接脚本中指定的地址。程序员预先知道程序的执行要求,比如放在哪里才能顺利执行,于是就把这个地址当做链接地址。
(1)Linux中应用程序默认的链接地址是0x0000_0000
Linux中应用程序默认链接地址是0x0000_0000,比如使用命令“gcc hello.c -o hello”进行编译与链接时,使用的就是默认的链接地址0x0000_0000。
每个应用程序运行在一个独立进程中,可以独享4G的虚拟地址空间,因此每个应用程序都可以链接到0x0000_0000地址处,因为每个进程都是从0地址开始的。
(2)X210中裸机程序的链接地址应该为0xd002_0010
X210开发板的链接地址,理论上应该是0xd002_0010。这个地址不是随意定的,而是iROM中的BL0加载BL1时指定好的地址,由CPU设计时决定的。
2、运行地址
运行地址,是指代码运行时的地址。
运行时地址无法在编译链接时绝对地确定,它是在下载时确定的。
二、重定位与位置无关码
1、重定位的概念
链接地址和运行地址可能不同,比如我们指定链接地址为0xd0024000,但后来通过dnw软件将镜像文件下载到0xd002_0010,则此时的运行地址是0xd002 0010。
这种情况下,如果镜像文件里有位置相关码,则位置相关码会运行不起来,因为位置已经发生改变。因此在位置相关码运行之前,需要先执行一段位置无关码,把整个镜像文件拷贝到以链接地址为起始地址的空间里,然后通过长跳转指令,跳转到(以链接地址为起始地址的那份代码的)相应位置继续运行。
当执行完代码重定位后,在SRAM中有2份镜像文件,一份是下载到0xd0020010处开头的镜像文件,另一份是复制到0xd0024000处开头的镜像文件。
接下来如果使用短跳转 “bl led_blink”,则执行的是运行地址0xd0020010开头的那一份代码。如果使用长跳转“ldr pc, =led_blink”,则执行的是链接地址 0xd0024000 开头的那一份代码,即直接从 0xd0020010开头的代码跳转到0xd0024000开头的那一份代码的 led_blink函数处。
当链接地址和运行地址相同时,短跳转和长跳转效果是一样的。
注意
(1)adr指令加载符号地址,加载的是运行时地址;ldr指令在加载符号地址时(第1个操作数是pc则叫长跳转,若是其他寄存器则叫长加载),加载的是链接地址。
(2)长跳转指跳转到的地址和当前地址差异比较大,跳转的范围比较宽广。它通过给PC(r15)赋一个新值来完成代码段的跳转执行。
2、位置无关码的概念
位置无关码与位置有关码的实质区别,在于操作时,位置有关码使用的是绝对地址,而位置无关码使用的是相对地址(PC + offset),主要体现在取址、跳转这两个操作上。判断方法就是看某指令是否受到链接脚本中链接地址的影响。
常见的情形分析
简单总结:b、bl、adr、ldr指令是位置无关码,ldr伪指令去加载标号地址时是位置有关码。
(1)b、BL、adr指令用的都是相对地址,所以是位置无关码。
(2)ldr pc,=main;因为main标志在编译的时候,会受到链接脚本的链接位置的影响,因此main是链接后的绝对地址,所以是位置有关码。
(3)ldr r0, =bss_start,这句代码的作用是把bss_start(在链接脚本中)的地址放入r0中,因为bss_start的值会受到链接脚本中链接位置的影响,因此bss_start是链接后的绝对地址,所以是位置有关码。
(4)ldr r0, 0xe0002700;这个操作取0xe0002700内存地址中的值赋值给r0,因为这个内存地址不会受到链接脚本中链接位置的影响,所以是位置无关码。下面的这几句代码也不会受到链接脚本中链接地址的影响,所以也是位置无关码。
#define WTCON 0xE2700000
#define PS_HOLD_CONTROL 0xE010E81C
#define SVC_STACK 0xD0037D80
ldr r0, =WTCON
ldr r0, =PS_HOLD_CONTROL
三、例子说明
一开始代码运行于SRAM内部的 0xd002_0010 这个位置,也就是BL1该呆的位置。后续进行代码重定位测试,包括在SRAM内部重定位,重定位到DDR SDRAM(内存)中。代码如下:
1、在SRAM内部重定位
链接脚本如下(其中0xd002_4000这个地址位于SRAM中)。
SECTIONS
{
. = 0xd0024000;
.text : {
start.o
* (.text)
}
.data : {
* (.data)
}
bss_start = .;
.bss : {
* (.bss)
}
bss_end = .;
}
led.S文件如下:
/*
* 文件名: led.s
* 描述: 演示重定位(在SRAM内部重定位,不涉及内存的初始化)
*/
#define WTCON 0xE2700000
#define SVC_STACK 0xd0037d80
.global _start // 把_start链接属性改为外部,这样其他文件就可以看见_start了
_start:
// 第1步:关看门狗(向WTCON的bit5写入0即可)
ldr r0, =WTCON
ldr r1, =0x0
str r1, [r0]
// 第2步:设置SVC栈
ldr sp, =SVC_STACK
// 第3步:开/关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;
// 第4步:重定位
// adr指令用于加载_start当前运行地址
adr r0, _start // adr加载时就叫短加载
// ldr指令用于加载_start的链接地址:0xd0024000
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 // 源
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
clear_loop:
str r2, [r0], #4 // 先将r2中的值放入r0所指向的内存地址(r0中的值作为内存地址),
cmp r0, r1 // 然后r0 = r0 + 4 即地址加4
bne clear_loop
run_on_dram:
// 长跳转到led_blink开始第二阶段
ldr pc, =led_blink // ldr指令实现长跳转
// 从这里之后就可以开始调用C程序了
//bl led_blink // bl指令实现短跳转
// 汇编最后的这个死循环不能丢
b .
清除bss段是为了满足C语言的运行时要求(C语言要求显式初始化为0的全局变量,或者未显式初始化的全局变量的值为0,实际上C语言编译器就是通过清bss段来实现C语言的这个特性的)。一般情况下我们的程序是不需要负责清零bss段的(C语言编译器和链接器会帮我们的程序自动添加一段头程序,这段程序会在我们的main函数之前运行,这段代码就负责清除bss)。但是在我们代码重定位了之后,因为编译器帮我们附加的代码只是帮我们清除了运行地址那一份代码中的bss,而未清除重定位地址处开头的那一份代码的bss,所以重定位之后需要自己去清除bss。
2、重定位到内存中
链接脚本如下(其中0x2000_0000这个地址位于内存中)。
SECTIONS
{
. = 0x20000000;
.text : {
start.o
sdram_init.o
* (.text)
}
.data : {
* (.data)
}
bss_start = .;
.bss : {
* (.bss)
}
bss_end = .;
}
led.S文件如下:
/*
* 文件名: led.s
* 描述: 演示重定位至DDR中,,需要先初始化内存
*/
#define WTCON 0xE2700000
#define SVC_STACK 0xd0037d80
.global _start // 把_start链接属性改为外部,这样其他文件就可以看见_start了
_start:
// 第1步:关看门狗(向WTCON的bit5写入0即可)
ldr r0, =WTCON
ldr r1, =0x0
str r1, [r0]
// 第2步:设置SVC栈
ldr sp, =SVC_STACK
// 第3步:开/关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;
// 第4步:初始化ddr
bl sdram_asm_init
// 第5步:重定位
// adr指令用于加载_start当前运行地址
adr r0, _start // adr加载时就叫短加载
// ldr指令用于加载_start的链接地址:0xd0024000
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 // 源
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
clear_loop:
str r2, [r0], #4 // 先将r2中的值放入r0所指向的内存地址(r0中的值作为内存地址),
cmp r0, r1 // 然后r0 = r0 + 4
bne clear_loop
run_on_dram:
// 长跳转到led_blink开始第二阶段
ldr pc, =led_blink // ldr指令实现长跳转
// 从这里之后就可以开始调用C程序了
//bl led_blink // bl指令实现短跳转
// 汇编最后的这个死循环不能丢
b .