六.ARM裸机学习之重定位和链接脚本

四.重定位引入和链接脚本
在了解重定位之前,必须先区分开链接地址和运行地址,位置有关代码和位置无关代码
===============================================
链接地址和运行地址:
链接地址:指在链接时指定的地址,是我们设想的将来程序要运行的地址。程序中所有标号的地址在链接后便确定了,不管程序在哪运行,都不会改变。使用arm-linux-objdump反汇编查看的就是链接地址
运行地址:指程序在板子中实际运行的地址
从上面的定义可以看出,链接地址和运行地址可以相同,也可以不同。
===============================================
位置有关代码和位置无关代码:
位置有关代码:从字面意思看就是该指令的执行是与内存地址有关的;如果运行地址和链接地址不相等,那么执行到此种指令的时候将会出现程序跑飞的情况。
位置无关代码(PIC):从字面意思看就是该指令的执行是与内存地址无关的;无论运行地址和链接地址相等或者不相等,此种指令都能正常运行
举几个例子:
b/bl label:属于位置无关代码,因为 b/bl 指令找到 label 的方法是根据当前 pc 的值结合偏移量来找到label
adr r0, label伪指令:属于位置无关代码,因为adr伪指令在汇编过程中会被替代成 add/sub pc, 偏移量的形式,可见也是基于当前pc和偏移量来找到 label
ldr r0,=label伪指令:属于位置有关代码,因为此伪指令将会被替代成 ldr r0, [pc, 偏移量]的形式,pc+偏移量中的内容将会是 label 的值,也就是链接时确定的地址。
===============================================
重定位:
  重定位就是将代码搬移到链接地址处去运行的动作。
 uboot之所以要进行代码重定位:
  是因为uboot的镜像太大了,不可能在iRAM中放得下,所以必须要放在 sdram 中运行,而BL1又必须在iRAM中运行,所以就需要进行代码重定位。

总结:
我认为重定位就是当程序的加载地址和运行地址不同时,运行地址通过在编译连接过程中的链接脚本完成将可执行文件链接到程序真实运行的地址位置(可以理解为将已经加载的程序复制一份到链接地址处,需要在位置有关码之前的一段位置无关码来完成)之后通过长跳转将PC指针从加载地址处重新定位到链接地址处.
思考:如何重定位?
1、链接脚本指定链接地址
2、判断运行地址和链接地址是否相同
3、复制代码到指定的链接地址处,使用长转移指令进行跳转
五.重定位实战领悟
要完成的功能是:在SRAM中将代码从0xd0020010重定位到0xd0024000(本来代码是运行在0xd0020010的,但我们又希望代码实际是在0xd0024000位置运行的,这时就需要重定位了)。
1、思路分析
 (1)通过链接脚本将代码链接到0xd0024000
(2)dnw下载时将bin文件下载到0xd0020010
(3)代码执行时,通过代码前段的少量PIC位置无关码将整个代码搬移到0xd0024000
(4)使用一个长跳转,跳转到0xd0024000处的代码继续执行,重定位完成
 通过(1)和(2)就保证了代码实际下载运行在0xd0020010,但是却被链接在0xd0024000,从而为重定位奠定了基础。当我们把代码链接地址设置为0xd0024000时,实际隐含意思就是这个代码将来必须放在0xd0024000位置才能正确执行。如果实际运行地址不是0xd0024000就要出事(除非代码是PIC位置无关码)。
 
当我们执行完代码重定位后,实际上在SRAM中有2份代码的镜像:一份是我们下载到0xd0020010处开头的,另一份是重定位代码复制到0xd0024000处开头的,这两份内容完全相同,仅仅地址不同。重定位之后使用ldr pc, =led_blink这句长跳转直接从0xd0020010处的代码跳转到0xd0024000开头的那份代码的led_blink函数去执行(实际上此时在SRAM中有2个led_blink函数的镜像,两个都能执行。如果短跳转bl led_blink则执行的就是0xd0020010开头的这一份,如果长跳转ldr pc, =led_blink则执行的就是0xd0024000开头的这一份)。
 当链接地址和运行地址相同时,短跳转和长跳转实际效果是一样的。当链接地址和运行地址不同时,短跳转实际执行的是运行地址处的那一份代码,而长跳转执行的是链接地址处的那一份代码。
 通过以上信息,就知道重定位代码的作用是:在PIC位置无关码执行完之前(代码中第一句位置有关码执行之前)必须将整个代码搬移到0xd0024000位置去执行。
2.代码实现(源自朱老师课程源码)
2.1 Makefile文件
六.ARM裸机学习之重定位和链接脚本
2.2链接脚本(link.lds)
六.ARM裸机学习之重定位和链接脚本
2.3start.S
六.ARM裸机学习之重定位和链接脚本
2.4led.c
六.ARM裸机学习之重定位和链接脚本

六.重定位代码分析
在分析重定位汇编源码之前,必须对adr与ldr伪指令有很深的理解。
例:
adr r0, _start
ldr r1, =_start
//这里的ldr不是命令ldr,而是伪指令ldr,区分的方法就是看第二个参数,如果有等号,就是伪指令。
ldr和adr都是伪指令,区别是ldr是长加载、adr是短加载。 
adr指令加载符号地址,加载的是运行时地址;ldr指令在加载符号地址时,加载的是链接地址。如下截自朱老师的源码
六.ARM裸机学习之重定位和链接脚本
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份在0xd0020010开头,另一份在0xd0024000开头
最后执行ldr pc, =led_blink这句长跳转直接从0xd0020010处的代码跳转到0xd0024000开头的那份代码的led_blink函数去执行。