参考朱老师教程视频讲解,视频链接地址:http://edu.51cto.com/lecturer/user_id-9584512.html
1、重定位相关概念
位置无关码(PIC,position independent code):汇编源文件被编译成二进制可执行文件时编码方式与位置(内存地址)无关。
位置有关码:汇编源文件被编译成二进制可执行程序后和位置(内存地址)有关。
链接地址:链接时指定的地址(指定方式:Makefile中用-Ttext,或者链接脚本)
运行地址:程序实际运行的地址(指定方式:由实际运行时被加载到内存的哪个位置说了算)
链接地址是由程序员在编译链接的过程中,通过Makefile中-Ttext xxx或者在链接脚本中指定的。程序员事先会预知自己的程序的执行要求,病情有一个期望的执行地址,并且会用这个地址来做链接地址。
运行地址是由运行时决定的,编译链接时是无法绝对确定运行时地址的。
2、S5PV210的启动方式和uboot的启动方式的区别
三星推荐的启动方式:Bootloader必须小于96KB并大于16KB(BL1),假定Bootloader为80KB,启动过程为:
a、先开机上电后BL0运行,BL0会加载外部启动设备中的Bootloader的前16KB(BL1)到SRAM中去运行,BL1运行时会加载BL2(80-16=64KB)到SRAM中去运行
b、BL2运行时会初始化DDR并且将整个OS搬运到DDR去执行
uboot实际使用方式:uboot大小随意,假定200KB。启动过程为:
a、先开机上电后BL0运行,BL0会加载外部启动设备中的uboot的前16KB(BL1)到SRAM中去运行,BL1运行时会初始化DDR,然后将整个uboot搬运到DDR中(重定位),然后用一句长跳转(从SRAM跳转到DDR)指令从SRAM中直接跳转到DDR中继续执行uboot,直到uboot完全启动。
b、uboot启动后在uboot命令行中去启动OS。
分散加载:把uboot分成2部分(BL1和uboot),两部分分别指定不同的链接地址。启动时将两部分加载到不同的地址(BL1加载到SRAM中,整个uboot加载到DDR中),这时候不用重定位也能启动。
3、从源码到可执行程序的步骤:预编译、编译、链接、strip
预编译: 预编译器执行。譬如C中的宏定义就是由预编译器处理,注释等也是由预编译器处理的。
编译: 编译器来执行。把源码.c .s编程机器码.o文件。
链接: 链接器来执行。把.0文件中的各函数(段)按照一定规则(链接脚本来指定)累积在一块。形成可执行文件。
strip: 把可执行程序中的符号信息给拿掉,以节省空间。(Debug版本和Release版本)
objcopy: 由可执行程序生产科烧录的镜像.bin文件。
4、程序段的概念:代码段、数据段、bss段(ZI段)、自定义段
段就是程序的一部分,我们把整个程序的所有东西分成一个一个的段,给每个段起个名字,然后在链接时就可以用整个名字来指示这些段或者代表这些段。其本质就是段命名是为了在链接脚本中用段名来让段站在核实的位置。
段名分两种:一种是编译器链接器内部定好的,先天性名字;一种是程序员自己指定的、自定义的名字。
先天性名字:
代码段(.text): 又叫文本段,代码段其实就是函数编译后的文件
数据段(.data):C语音中有显示初始化为非0的全局变量。如 int a = 0; 函数外声明定义
bss段(.bss): 又叫ZI段(zero initial),就是零初始化段,对应C语言中全为0的全局变量。
如 int a; 函数外声明定义
后天性名字:段名由程序员自定义,段的属性和特征也是程序员自定义
5、链接脚本
链接脚本其实是个规则文件,它是由程序员用来指挥链接器工作的。链接器会参考链接脚本,兵器使用其中规定的规则来处理.o文件中的那些段,将其连接处一个可执行程序。
链接脚本的关键内容有两部分组成:段名+地址(作为链接地址的内存)
链接脚本的理解:
SECTIONS { } 这个是真个链接脚本
. 点好在链接脚本中代表当前位置。
= 代表赋值
如下脚本分析:
SECTIONS
{
. = 0xd0024000;
.text : {
start.o
* (.text)
}
.data : {
* (.data)
}
bss_start = .;
.bss : {
* (.bss)
}
bss_end = .;
}
上面的脚本中 . = 0xd0024000; 代表当前的内存地址是0xd0024000
.text
和 .data
和 .bss
代表段名
start.o 在段的内容中排在了第一个位置,第一执行
*(.text)
中的 * 代表万能匹配符 , (.text)
代表是属性是文本段或代码段,所以这句话的意思就是所有的文本段或代码段,一下类同。
bss_start = .;
这句话中说明 bss_start 的地址是当前的内存地址,注意此时的当前地址并不是0xd0024000,而是 0xd0024000 + .text的内存长度 + .data的内存长度。长度是按照从上到下顺序递增来算的。
bss_end = .;
意思和bss_start = .;
相同,只不过此时 bss_end的内存地址应该等于 bss_start的内存地址 + .bss段的内存长度。
这个链接脚本中就规定了一些程序段的分配原则。
注意,在使用链接脚本时需要修改Makefile中的编译规则。
需要把
arm-linux-ld -Ttext 0x0 -o led.elf $^
改为
arm-linux-ld -Tlink.lds -o led.elf $^
其中的link.lds就是链接脚本。
6、重定位相关指令基本概念
长跳转:这句代码是一句跳转指令(ARM中的跳转指令类似于分支指令B、BL等作用的指令),跳转指令通过给pc(r15) 赋一个新值来完成代码段的跳转执行。
比如:ldr pc, =led_blink // ldr指令实现长跳转
这句长跳转直接从当前地址0xd0020010处所对应的运行时地址跳转到链接地址0xd0024000开头的那一份代码中的led_blink函数去执行。
其实在重定位过程中,在SRAM中有两份一模一样的代码,分别在0xd0020010地址和0xd0024000地址,两份代码一模一样,区别就在放在不同的内存地址中都可以执行。
短跳转:这句跳转指令还是停留在当前运行时地址内跳转,而长跳转则是跳转到链接地址内运行。
总结:重定位实际就是在运行地址处执行一段位置无关码(PIC) ,让这段PIC(重定位码)从运行时地址处把整个程序拷贝一份到链接地址处,完了之后使用一句长跳转指令从运行时地址直接跳转到链接地址处执行同一个函数,这样就实现了重定位的无缝对接。
adr与ldr伪指令的区别
adr和ldr都是伪指令,区别是adr是短加载,ldr是长加载。
重点:adr指令加载符号地址,加载的是运行时地址;ldr指令加载符号地址,加载的是连接地址
7、重定位代码分析
a、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-gcc -o $@ $< -c -nostdlib
用来编译源文件为目标文件
arm-linux-ld -Tlink.lds -o led.elf $^
用来编译链接文件到二进制文件led.elf
arm-linux-objcopy -O binary led.elf led.bin
用来编译二进制文件为可烧录的镜像文件led.bin
arm-linux-objdump -D led.elf > led_elf.dis
用来编译二进制文件led.elf为反编译文件led_elf.dis
gcc mkv210_image.c -o mkx210
用来编译源文件为可执行的文件
./mkx210 led.bin 210.bin
用来把led.bin添加16Bytes的校验和,放在第三个字节中。
b、链接脚本文件代码
SECTIONS
{
. = 0xd0024000;
.text : {
start.o
* (.text)
}
.data : {
* (.data)
}
bss_start = .;
.bss : {
* (.bss)
}
bss_end = .;
}
解释如上:5、链接脚本
c、启动代码
/*
* 文件名: led.s
* 作者: 朱老师
* 描述: 演示重定位(在SRAM内部重定位)
*/
#define WTCON 0xE2700000
#define SVC_STACK 0xd0037d80
// 把_start链接属性改为外部,这样其他文件就可以看见_start了
.global _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加载时就叫短加载
adr r0, _start
// ldr指令用于加载_start的链接地址:0xd0024000
// ldr加载时如果目标寄存器是pc就叫长跳转,如果目标寄存器是r1等就叫长加载
ldr r1, =_start
// bss段的起始地址
// 就是我们重定位代码的结束地址,重定位只需重定位代码段和数据段即可
ldr r2, =bss_start
// 比较_start的运行时地址和链接地址是否相等,
// 如果相等说明不需要重定位,所以跳过copy_loop,直接到clean_bss
// 如果不相等说明需要重定位,那么直接执行下面的copy_loop进行重定位
// 重定位完成后继续执行clean_bss。
cmp r0, r1
beq 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 .
说明:清楚bss段是为了满足C语音的运行时要求(C语音要求显示初始化为0的全局变量,或者未显示初始化的全局变量的值为0,实际上C语音编辑器就是通过请bss段来实现C语音的这个特性的)。一般情况下我们的程序是不需要负责清零bss段的(C语音编译器和链接器会帮我们自动添加一段头程序,这段程序会在我们的main函数之前运行,负责清除bss段)。但是我们重定位之后,因为编译器帮我们添加的那段清零的bss段是在运行时地址,而并不是在链接地址的bss段,所以重定位之后需要我们手动自己去清除链接地址的那段bss段。