重定位与链接脚本

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
	/*
	 *	此处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 .

以上是关于重定位与链接脚本的主要内容,如果未能解决你的问题,请参考以下文章

重定位与链接脚本

S5PV210裸机程序之重定位与链接脚本

重定位与链接脚本

重定位与链接脚本

重定位与链接脚本

重定位和链接脚本