嵌入式知识-ARM裸机-学习笔记(4):重定位与链接脚本的使用

嵌入式知识-ARM裸机-学习笔记(4):重定位与链接脚本的使用

一、重定位

1. 一些基本概念的引入

位置有关编码: 汇编源码编码成二进制可执行程序后和内存地址是有关的。(大部分代码都是位置有关的,即受地址影响)
位置无关编码(PIC,position independent code): 汇编源文件被编码成二进制可执行程序时编码方式与位置(内存地址)无关。

链接地址: 链接时指定的地址(指定方式为:Makefile中用-Ttext,或者链接脚本)
运行地址: 程序实际运行时地址(指定方式:由实际运行时被加载到内存的哪个位置说了算)
我们在设计一个程序时,会给这个程序指定一个运行地址(链接地址)。我们在编译程序时其实心里是知道我们程序将来被运行时的地址(运行地址)的,而且必须给编译器链接器指定这个地址(链接地址)才行。最后得到的二进制程序理论上是和你指定的运行地址有关,将来这个程序被执行时必须放在当时编译链接时给定的那个地址(链接地址)下才行,否则不能运行(位置有关代码)。但是有个别特别的指令他可以跟指定的地址(链接地址)没有关系,这些代码实际运行时不管放在哪里都能正常运行(位置无关代码)。
对于位置有关代码来说:最终执行时的运行地址和编译链接时给定的链接地址必须相同,否则一定出错。

2. 重定位的引入

为什么需要重定位?
因为对于有些时候,链接地址和运行地址必须是不同的地址,而且还不能全部使用位置无关代码,所以需要重定位来实现分散加载。(这里的分散加载其实相当一种手工的重定位)

什么是重定位?
重定位实际就是在运行地址处执行一段位置无关码PIC,让这段PIC(也就是重定位代码)从运行地址处把整个程序镜像拷贝一份到链接地址处,完了之后使用一句长跳转指令从运行地址处直接跳转到链接地址处去执行同一个函数(led_blink),这样就实现了重定位之后的无缝连接。

3. 重定位的使用

目标:在SRAM中将代码从0xd0020010重定位到0xd0024000。
本来代码是运行在0xd0020010的,但是因为一些原因我们又希望代码实际是在0xd0024000位置运行的。这时候就需要重定位了。

思路:(1)通过链接脚本将代码链接到0xd0024000(. = 0xd0024000);(2)dnw下载时将bin文件下载到0xd0020010;(3)代码执行时通过代码前段的少量位置无关码将整个代码搬移到0xd0024000;(4)使用一个长跳转跳转到0xd0024000处的代码继续执行,重定位完成。
思路解析:
(1)加上(2),就保证了:代码实际下载运行在0xd0020010,但是却被链接在0xd0024000
当我们把代码链接地址设置为0xd0024000时,实际隐含意思就是我这个代码将来必须放在0xd0024000位置才能正确执行。如果实际运行地址不是这个地址就要出事(除非代码是PIC位置无关码)。
长跳转: 首先这句代码是一句跳转指令(ARM中的跳转指令就是类似于分支指令B、BL等作用的指令),跳转指令通过给PC(r15)赋一个新值来完成代码段的跳转执行。长跳转指的是跳转到的地址和当前地址差异比较大,跳转的范围比较宽广。在重定位后,长跳转通常是跳转到重定位后的地址下的某个函数。

长跳转和短跳转的区别
当我们执行完代码重定位后,实际上在SRAM中有2份代码的镜像(一份是我们下载到0xd0020010处开头的(1),另一份是重定位代码复制到0xd0024000处开头的(2)),这两份内容完全相同,仅仅地址不同。重定位之后使用ldr pc, =led_blink这句长跳转直接从0xd0020010处代码跳转到0xd0024000开头的那一份代码的led_blink函数处去执行。(实际上此时在SRAM中有2个led_blink函数镜像,两个都能执行,如果短跳转bl led_blink则执行的就是0xd0020010开头的这一份,如果长跳转ldr pc, =led_blink则执行的是0xd0024000开头处的这一份)。

当链接地址和运行地址相同时,短跳转和长跳转实际效果是一样的;但是当链接地址不等于运行地址时,短跳转和长跳转就有差异了。这时候短跳转实际执行的是运行地址处的那一份,而长跳转执行的是链接地址处那一份。

下面是一个重定位的例子:

//start.s文件
#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加载时就叫短加载(整个汇编程序的开始为_start)		
	// 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:	//从运行地址[r0]读取内容写到r3中,再把r3中的内容写到链接地址[r1]中
	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 .

代码解读:
对于汇编文件来说,关看门狗、设置SVC栈、开icache都属于位置无关代码,不受内存地址的影响,因此在他们之后进行重定位。

adr和ldr伪指令的区别:ldr和adr都是伪指令,区别是ldr是长加载、adr是短加载。adr指令加载符号地址,加载的是运行时地址;ldr指令在加载符号地址时,加载的是链接地址,通过对比两者的地址从而可以判断是否需要重定位。

(1)重定位:重定位就是汇编代码中的copy_loop函数,代码的作用是使用循环结构来逐句复制代码到链接地址。复制的源地址是SRAM的0xd0020010,复制目标地址是SRAM的0xd0024000复制长度bss_start减去_start复制的长度就是整个重定位需要重定位的长度,也就是整个程序中代码段+数据段的长度。bss段(bss段中就是0初始化的全局变量)不需要重定位。
(2)清bss段:清除bss段是为了满足C语言的运行时要求(C语言要求显式初始化为0的全局变量,或者未显式初始化的全局变量的值为0,实际上C语言编译器就是通过清bss段来实现C语言的这个特性的)。一般情况下我们的程序是不需要负责清零bss段的(C语言编译器和链接器会帮我们的程序自动添加一段头程序,这段程序会在我们的main函数之前运行,这段代码就负责清除bss)。但是在我们代码重定位了之后,因为编译器帮我们附加的代码只是帮我们清除了运行地址那一份代码中的bss,而未清除重定位地址处开头的那一份代码的bss,所以重定位之后需要自己去清除bss。
(3)长跳转:清理完bss段后重定位就结束了。然后当前的状况是:1、当前运行地址还在0xd0020010开头的(重定位前的)那一份代码中运行着。2、此时SRAM中已经有了2份代码,1份在d0020010开头,另一份在d0024000开头的位置。最后通过长跳转ldr pc, =led_blink 跳转到重定位后的led_blink函数(也就是以d0024000地址开头的代码中的led_blink函数)。

//Makefile文件
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

可以注意到:在链接这一步,由原来的arm-linux-ld -Ttext 0x0 -o led.elf $^ 变为了现在的arm-linux-ld -Tlink.lds -o led.elf $^,而其中的.lds即为下面的链接脚本,从而实现的重定位链接地址的改变。

二、链接脚本

1. 链接脚本的引入

运行时的地址由什么决定?
运行时的地址是由运行时决定的,在编译链接时是无法绝对确定运行时的地址。
链接地址由什么决定?
链接地址是由程序员在编译链接的过程中,通过Makefile中 -Ttext xxx 或者在链接脚本 中指定的。程序员事先会预知自己的程序的执行要求,并且有一个期望的执行地址,并且会用这个地址来做链接地址。

举例:
1、linux中的应用程序。例如 gcc hello.c -o hello,这时使用默认的链接地址就是0x0,所以应用程序都是链接在0地址的。因为应用程序运行在操作系统的一个进程中,在这个进程中这个应用程序独享4G的虚拟地址空间。所以应用程序都可以链接到0地址,即每个进程都是从0地址开始的。(编译时可以不给定链接地址而都使用0)
2、210中的裸机程序。运行地址由我们下载时确定,下载时下载到0xd0020010,所以就从这里开始运行。(这个下载地址也不是我们随意定的,是iROM中的BL0加载BL1时事先指定好的地址,这是由CPU的设计决定)。所以理论上我们编译链接时应该将地址指定到0xd0020010,但是实际上我们在之前裸机程序中都是使用位置无关码PIC,所以链接地址可以是0

2. 从源码到可执行程序的步骤

  1. 预编译: 预编译器执行。譬如C中的宏定义就是由预编译器处理,注释等也是由预编译器处理的,它的作用是在正式编译之前把有效代码之外的都消去
  2. 编译: 编译器来执行。把源码.c .S编程成机器码.o文件。在编译时,各文件编译各自的,各函数生成各自的二进制代码,是以函数为单位的(函数段)
  3. 链接: 链接器来执行。把.o文件中的各函数(段)按照一定规则(链接脚本来指定)累积在一起,形成可执行文件。编译时生成的是一个个无关的函数段,而在链接时则将他们联系起来形成一个整体。
  4. strip: strip是把可执行程序中的符号信息给拿掉,以节省空间。
  5. objcopy: 由可执行程序生成可烧录的镜像bin文件。

在第2和第3步中涉及到了一个叫程序段的概念:
这里的段就是程序的一部分,我们把整个程序的所有东西分成了一个一个的段,给每个段起个名字(例如delay是延时函数),然后在链接时就可以用这个名字来指示这些段。也就是说给段命名就是为了在链接脚本中用段名来让段站在合适的位置。

段名分为2种:
(1)先天性段名:

  • 代码段:(.text),又叫文本段,代码段其实就是函数编译后生成的东西(就是放函数的)

  • 数据段:(.data),数据段就是C语言中有显式初始化为非0的全局变量(int a = 10)

  • bss段:(.bss),又叫ZI(zero initial)段,就是零初始化段,对应C语言中初始化为0的全局变量。

(2)后天性段名:段名由程序员自己定义,段的属性和特征也由程序员自己定义。

举例:
1、C语言中全局变量如果未显式初始化,值是0。本质就是C语言把这类全局变量放在了bss段,从而保证了为0
2、C运行时环境如何保证显式初始化为非0的全局变量的值在main之前就被赋值了?就是因为它把这类变量放在了 .data段 中,而.data段会在main执行之前被处理(初始化)。

3. 链接脚本的作用

链接脚本其实是个规则文件,他是程序员用来指挥链接器工作的。链接器会参考链接脚本,并且使用其中规定的规则来处理.o文件中那些段,将其链接成一个可执行程序。

在链接脚本中有2部分的关键内容:段名(根据段名去寻找对应的内容) + 地址(作为链接地址的内存地址)
链接的过程其实就是把指定的内容(段名中的内容)放到指定位置(地址)去的一个过程。

下面是一个链接脚本的例子:

//link.lds文件
SECTIONS
{
	. = 0xd0024000;	//这句话决定了长加载时加载到的地址,相当于指定了一个开始地址(从反汇编可以看出)
	//这一段是代码段
	.text : {
		start.o		//start.o中的函数必须排在前(由start.s生成的)
		* (.text)	//剩下所有的属性是.text的都排在后(例如led.o)
	}
    //这一段是数据段		
	.data : {
		* (.data)
	}
	//到这里的地址X为dx0024000+代码段的大小+数据段的大小
	bss_start = .; 	//汇编中的标号,这里的bss_start可以在汇编中使用,表示bss段开始的地址X
	//这一段是bss段
	.bss : {
		* (.bss)
	}
	
	bss_end  = .;	
}

解释一下代码:
SECTIONS {} : 这个是整个链接脚本。
.: 点号在链接脚本中代表当前位置。
=: 等号代表赋值。
.text表示代码段,由于启动程序需要在前,因此需要将start.o放在前面。

发布了58 篇原创文章 · 获赞 76 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_42826337/article/details/104478319