重定位与链接脚本
Posted Lewin~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重定位与链接脚本相关的知识,希望对你有一定的参考价值。
一.位置无关码和位置有关码
位置无关码(pic):由汇编源文件(start.S)编译生成的可执行程序(.elf或.bin文件),这个程序放在“内存任何地方”都能运行成功。
位置有关码(pdc):由汇编源文件(start.S)编译生成的可执行程序(.elf或.bin文件),这个程序只能放在“指定的内存上”才能运行成功。
由此可以看出位置无关代码适应性更强,放在哪里都能运行,但位置无关码存在一些限制,不能完成所有功能,有时候不得不使用位置有关代码。
二.链接地址和运行地址
设计程序时,我们会给程序指定一个链接地址(我们认为程序将来在此运行,假如指定的是0x0这个地址),但实际上程序将来是否真的会放在0x0运行,与你指定的链接地址(0x0)没有关系,由实际运行时被加载到内存的哪个位置说了算,倘若这个程序必须放在0x0处才能运行成功,说明这个程序是位置有关代码,否则就是位置无关代码;由此可知,
位置有关代码的特点:链接地址=运行地址,
位置无关代码的特点:链接地址 不一定等于 运行地址。
对于位置有关代码来说:最终执行时的运行地址和编译链接时给定的链接地址必须相同,否则一定出错。以S5PV210的裸机程序为例,在裸机程序中的Makefile用-Ttext 0x0指定了链接地址为0x0,意味着我们认为这个程序将来会放在0x0这个内存地址上去运行,但是实际上我们运行的地址是0xd002_0010(用DNW烧写时指定的烧写地址),这两块地址看似不同实则相同,原因是S5PV210内部做了映射,把SRAM(从0xd002_0000开始)映射到0x0去,因此这个程序无论是烧写到0xd002_0010还是0x0000_0010都能正常执行,都会烧写到SRAM(USB启动跳过前16KB校验头),相当于一个房间有两个门,理论上这里-Ttext应该指定为0x0000_0010或0xd002_0010,但实际上指定任意一个地址都行,这个程序仍然能运行,原因是因为使用了位置无关码。
这里提到了链接地址和运行地址:
链接地址:链接时指定的地址(指定方式为:Makefile中用-Ttext,或者链接脚本,因此由程序员决定)
运行地址:程序实际运行时地址(指定方式为:由实际运行时被加载到内存的哪个位置说了算,因此由程序运行时决定)
三.分析S5PV210的启动过程
S5PV210的启动过程:三星推荐和uboot的实现是不同的
三星推荐的启动方式中:bootloader必须小于96KB并大于16KB,假定bootloader为80KB,启动过程是这样子:先开机上电后BL0运行,BL0做一系列初始化(关看门狗、开icache等),然后根据外部OMpin引脚判断启动方式,然后根据启动方式初始化对应的启动设备(比如Nandflash或iNand等等),然后再加载外部启动设备中的bootloader的前16KB(BL1)到SRAM中去运行,BL1运行时会加载BL2(bootloader中80-16=64KB)到SRAM中(从SRAM的16KB处开始)去运行;BL2运行时会初始化DDR并且将OS搬运到DDR去执行OS,启动完成。
uboot实际使用的方式:uboot大小随意,假定为200KB。启动过程是这样子:先开机上电后BL0运行,中间过程同上在此省略,然后BL0会加载外部启动设备中的uboot的前16KB(BL1)到SRAM中去运行,BL1运行时会初始化DDR,然后将整个uboot搬运到DDR中,然后用一句长跳转(从SRAM跳转到DDR)指令从SRAM中直接跳转到DDR中继续执行uboot直到uboot完全启动。uboot启动后在uboot命令行中去启动OS。
现在知道为什么要使用重定位了吧???
uboot大小为100~200KB,远大于96KB,96KB的SRAM完全不够放,所以不得不把uboot放在其它地方(DDR)运行,在SRAM把uboot从INand搬到DDR,需要一段位置无关码完成搬运,搬运完成后,又需要一条长跳转指令(位置有关码)跳到DDR中继续执行uboot。
四.编译流程:预处理、编译、汇编、链接、strip 、objcopy
预处理:(gcc编译器默认替我们执行)
- 预处理器执行。
- 头文件插入、宏定义展开、条件编译选择使用的代码、注释删除等 均在此步。
- hello.c文件-》hello.i文件
- gcc -E:只预处理
编译:
- 编译器来执行。
- 把.i文件翻译成.S汇编代码。
- hello.i文件-》hello.S文件
- gcc -S:只编译
汇编:
- 汇编器来执行。
- 把汇编代码翻译成符合ELF格式的机器码。
- hello.S文件-》hello.o文件 (即OBJ文件)
- gcc -c:编译和汇编
链接:
- 链接器来执行。
- 把上步生成的OBJ文件和系统库的OBJ文件、库文件中的各函数(段)按照一定规则(链接脚本来指定)连接起来生成可执行文件。
- %.o文件-》hello.elf
- gcc -o:只链接、给可执行程序命名
strip:
- strip是把可执行程序中的符号信息给拿掉,以节省空间。(Debug版本和Release版本)
objcopy:
- 去除一些杂冗的信息,包括符号信息,注释信息
- 把ELF格式的可执行文件生成bin格式的可执行文件(可烧录的镜像)。
- hello.elf-》hello.bin
具体strip和objcopy的区别可参考博文:https://blog.csdn.net/weixin_30827565/article/details/97330409
五.程序段的概念:代码段、数据段、bss段(ZI段)、自定义段
段就是程序的一部分,程序被细分成各种段,每个段的名字不同,然后在链接时就可以用这些名字来指示这些段,通过段名便于我们在链接脚本中给各个段安排合适的位置。
段名分为2种:一种是编译器链接器内部定好的,先天性的名字;一种是程序员自己指定的、自定义的段名。
先天性段名:
代码段:(.text),又叫文本段,代码段其实就是函数编译后生成的东西
数据段:(.data),数据段就是C语言中有显式初始化为非0的全局变量
bss段:(.bss),又叫ZI(zero initial)段,就是零初始化段,对应C语言中初始化为0的或未被初始化的全局变量。
后天性段名:
自定义段,段名、段的属性和特征都由程序员自己定义。
分析一些问题,跟这里结合,然后试图明白一些本质:
1、C语言中全局变量如果未显式初始化,值是0。本质就是C语言把这类全局变量放在了.bss段,而.bss段会在main执行之前清空,从而保证了为0。
2、C运行时环境如何保证显式初始化为非0的全局变量在main之前就被赋值了?就是因为它把这类变量放在了.data段中,而.data段会在main执行之前被处理(初始化)。
六.链接脚本究竟要做什么?
链接脚本其实是个规则文件,程序员通过链接脚本指挥链接器工作。链接器会参考链接脚本,按照程序员指定的规则以一定顺序处理.o文件中那些段,将其链接成一个可执行程序。
链接脚本的关键内容有2部分:段名 + 地址(作为链接地址的内存地址)
链接脚本的理解:
SECTIONS 这个是整个链接脚本
. 点号在链接脚本中代表当前位置。
= 等号代表赋值
七.代码重定位实战
任务:在SRAM中将代码从0xd0020010重定位到0xd0024000
执行步骤:
第一步:Makefile中用-T链接脚本名 指定要使用的链接脚本,这里使用-Tlink.lds
led.bin: start.o led.o
arm-linux-ld -Tlink.lds -o led.elf $^
arm-linux-objcopy -O binary led.elf led.bin
arm-linux-objdump -D led.elf > led_elf.dis
gcc mkv210_image.c -o mkx210
./mkx210 led.bin 210.bin
%.o : %.S
arm-linux-gcc -o $@ $< -c -nostdlib
%.o : %.c
arm-linux-gcc -o $@ $< -c -nostdlib
clean:
rm *.o *.elf *.bin *.dis mkx210 -f
第二步:创建一个名为link.lds的链接脚本,编写链接脚本,在链接脚本中设置链接地址为0xd0024000
SECTIONS
. = 0xd0024000;
.text :
start.o
* (.text)
.data :
* (.data)
bss_start = .;
.bss :
* (.bss)
bss_end = .;
第三步:DNW下载程序到0xd0020010
第四步:程序运行时通过前面一小段位置无关码PIC将整个程序复制一份到0xd0024000,实现重定位功能(重定位一个程序,一个程序包含text、data、bss段,由于bss段的内容都是0,我们只需重定位text、data段,然后在后面将bss段清空即可)
copy_text_data:
adr r0, _start
ldr r1, =_start
/*
* adr和ldr的区别是:adr加载的是运行时地址,而ldr加载的是链接地址
* 此处r0 = 0xd0020010,r1 = 0xd0024000
* 原因是adr和ldr虽然都是用于加载的伪指令,但是他们最终替换生成的汇编代码大有不同
* 通过反汇编查看:
* adr r0, _start 替换成 d002401c: e24f0024 sub r0, pc, #36 ; 0x24
* ldr r1, =_start 替换成 d0024020: e59f1048 ldr r1, [pc, #72] ; d0024070
* 前者是直接寻址,后者是寄存器间接寻址,两者寻址方式不同,
* 1. adr转化成的汇编指令sub受当前pc的值的影响,
* 当前pc = d002401c + 8 = d0024024,因此这条指令执行后,r0 = d0024024 - 36 = d0024000,
* 但实际上当前pc不可能是d002401c,因为我们把程序下载到0xd0020010而非0xd0024000,
* 故执行到这条指令时,当前pc = d0020010 + d002401c - d0024000 + 8 = d0020034,
* 执行完这条指令后,r0 = d0020034 - 36 = d0020034 - 0x24 = d0020010,这才是r0的真值;
*
* 2. ldr伪指令转化成ldr指令,因为是寄存器寻址,所以不受当前pc的影响
* 当前pc = d0024020 + 8 = d0024028,因此这条指令执行后,r1 = [d0024028 + 72] = [d0024070],
* r1等于以d00024070为地址的内容,通过反汇编查看d00024070处的内容是:d0024000,因此r1 = d0024000,
* 同理,当前pc不可能是d0024028,因为我们把程序下载带0xd0020010而非0xd0024000,
* 故执行到这条指令时,当前pc = d0020010 + d0024020 - d0024000 + 8 = d0020038,
* 执行完这条指令后,r1 = [d0020038 +72] = [d0020080],而程序下载到d0020080这个地址内容也还是d0024000,
* 链接地址为0xd0024000的地方下载到0xd0020010处,那么链接地址为0xd0024070的地方就被下载到0xd0020080处,
* 因此这说明r1并不受当前值pc的影响。
*/
ldr r2, =bss_start //bss_start作为重定位结束的标志,重定位只需重定位text和data段
cmp r0, r1 //比较r0和r1是否相等
beq run_led /*相等说明链接地址 = 运行时地址,
*则无需重定位和清空bss,编译器早已帮我们清空bss了
*/
copy_loop:
ldr r3, [r0], #4 //源地址
str r3, [r1], #4 //目的地址 这两句等价于c语言中 int r0,r1; *r1++ = *r0++;
cmp r1,r2 //判断重定位是否完成
bne copy_loop //否则继续循环
/* 第五步:清bss段 */
clean_bss:
ldr r0, =bss_start //bss段起始地址
ldr r1, =bss_end //bss段结束地址
cmp r0, r1 //判断是否没有bss段
beq run_led //是则直接执行第六步
mov r2, #0 //否则就要清空bss段
clear_loop:
str r2, [r0], #4 //以4字节的方式清空bss段,此句等于c语言中 int r0; *r0++ = 0;
cmp r0, r1 //判断bss段是否清空
bne clear_loop //否则继续循环
第五步:使用一条长跳转指令跳转到0xd0024000继续执行,重定位结束(涉及绝对跳转和相对跳转,参考博文:https://blog.csdn.net/xshenpan/article/details/49337845?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165442692516781685380352%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=165442692516781685380352&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allbaidu_landing_v2~default-2-49337845-null-null.142v11pc_search_result_control_group,157v13control&utm_term=bl%E5%92%8Cldr&spm=1018.2226.3001.4187)
run_led:
ldr pc, =led_blink //绝对跳转,是位置有关码
//bl led_blink //相对跳转,是位置无关码,与pc的值有关
整个代码示例:
#define WTCON 0xE2700000
#define SVN_STACK 0xD0037D80
.global _start
_start:
/* 第一步:关看门狗 */
ldr r0, =0
ldr r1, =WTCON
str r0, [r1]
/* 第二步:设置SVN栈 */
ldr sp, =SVN_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;
/* 第四步:重定位 */
copy_text_data:
adr r0, _start
ldr r1, =_start
/*
* 此处r0 = 0xd0020010,r1 = 0xd0024000
* 原因是adr和ldr虽然都是用于加载的伪指令,但是他们最终替换生成的汇编代码大有不同
* 通过反汇编查看:
* adr r0, _start 替换成 d002401c: e24f0024 sub r0, pc, #36 ; 0x24
* ldr r1, =_start 替换成 d0024020: e59f1048 ldr r1, [pc, #72] ; d0024070
* 前者是直接寻址,后者是寄存器间接寻址,两者寻址方式不同,
* 1. adr转化成的汇编指令sub受当前pc的值的影响,
* 当前pc = d002401c + 8 = d0024024,因此这条指令执行后,r0 = d0024024 - 36 = d0024000,
* 但实际上当前pc不可能是d002401c,因为我们把程序下载到0xd0020010而非0xd0024000,
* 故执行到这条指令时,当前pc = d0020010 + d002401c - d0024000 + 8 = d0020034,
* 执行完这条指令后,r0 = d0020034 - 36 = d0020034 - 0x24 = d0020010,这才是r0的真值;
*
* 2. ldr伪指令转化成ldr指令,因为是寄存器寻址,所以不受当前pc的影响
* 当前pc = d0024020 + 8 = d0024028,因此这条指令执行后,r1 = [d0024028 + 72] = [d0024070],
* r1等于以d00024070为地址的内容,通过反汇编查看d00024070处的内容是:d0024000,因此r1 = d0024000,
* 同理,当前pc不可能是d0024028,因为我们把程序下载带0xd0020010而非0xd0024000,
* 故执行到这条指令时,当前pc = d0020010 + d0024020 - d0024000 + 8 = d0020038,
* 执行完这条指令后,r1 = [d0020038 +72] = [d0020080],而程序下载到d0020080这个地址内容也还是d0024000,
* 链接地址为0xd0024000的地方下载到0xd0020010处,那么链接地址为0xd0024070的地方就被下载到0xd0020080处,
* 因此这说明r1并不受当前值pc的影响。
*/
ldr r2, =bss_start //bss_start作为重定位结束的标志,重定位只需重定位text和data段
cmp r0, r1 //比较r0和r1是否相等
beq run_led /*相等说明链接地址 = 运行时地址,
*则无需重定位和清空bss,编译器早已帮我们清空bss了
*/
copy_loop:
ldr r3, [r0], #4 //源地址
str r3, [r1], #4 //目的地址 这两句等价于c语言中 int r0,r1; *r1++ = *r0++;
cmp r1,r2 //判断重定位是否完成
bne copy_loop //否则继续循环
/* 第五步:清bss段 */
clean_bss:
ldr r0, =bss_start //bss段起始地址
ldr r1, =bss_end //bss段结束地址
cmp r0, r1 //判断是否没有bss段
beq run_led //是则直接执行第六步
mov r2, #0 //否则就要清空bss段
clear_loop:
str r2, [r0], #4 //以4字节的方式清空bss段,此句等于c语言中 int r0; *r0++ = 0;
cmp r0, r1 //判断bss段是否清空
bne clear_loop //否则继续循环
/* 第六步:流水灯 */
run_led:
ldr pc, =led_blink //绝对跳转,是位置有关码
//bl led_blink //相对跳转,是位置无关码,与当前pc的值有关
/* 死循环 */
b .
以上是关于重定位与链接脚本的主要内容,如果未能解决你的问题,请参考以下文章