声明及感谢, 本文为参照朱有鹏的linux课程总结的, 在此对朱有鹏老师表示感谢.
关于重定位的一些概念:
位置无关码(PIC position independent code ): 汇编源文件被编码成二进制可执行程序时编码方式与位置(内存地址)无关
位置有关码: 汇编源码编码成二进制可执行程序后和内存地址是无关的.
一般情况下, 我们在编译和链接时会给定一个地址,这个地址叫链接地址, 然后希望我们的程序运行在我们所指定的链接地址那里(运行地址).如果链接地址和运行地址不一致, 很可能程序将不能被运行,. 像这样基本就是位置有关码.
当然程序也有特殊的, 运行的地址和链接的地址不一致, 他都能正常运行, 那, 就是位置无关码.
相对比说: 位置无关码要比位置有关码好一些, 适应性强, 放在那里都能正常运行, ,
但是位置无关码有一些限制, 不能完成所有的功能, 有时候就必须使用位置有关码.
在逻辑程序中使用 makefile 用 -Ttext 0x0 来指定链接地址是0x0, 这意味着我们认为这个程序将来会放在0x0这个内存地址中去运行, 但是,实际我们运行时的地址0xd0020010(这里说的是s5pv210芯片) 这个地址在我们下载程序时, 由dnw 下载工具就已经设置好, 下载进去,了, .
这么看的话我们所链接的地址(0x0) 和运行地址 0xd0020010 看似好像不相同, 但是却能运行, 这是为什么,? 其实就是S5pv210内存做了映射, 把SRAM地址(0xd0020010)映射到0x0 地址中去了, 因此裸机程序能够运行.
有两个概念需要分清:
链接地址: 链接时指定的地址(指定方式为: Makefile 中用-Ttext, 或者链接脚本)
运行地址: 程序实际运行时地址(指定方式: 由实际运行时被加载到内存的哪个位置说了算).
关于210的启动过程, 请看
linux学习(十) 210的启动过程 博文
为什么要重定位?
原因: 链接地址和运行地址有时候必须不同, 而且还不能全部用位置无关码, 这时候只能重定位
扩展: 分散加载: 把uboot分成2部分(BL1和整个uboot), 两部分分别指定不同的连接地址, 启动时将BL1加载到SRAM中, 整个uboot加载到DDR中, 这儿时候不用重定位也能使用.
其实分散加载就相当于手工重定位, 重定位是用代码来进行重定位, 分散加载时手工操作重定位的.
链接地址什么时候决定
链接地址是由程序员在编译链接时的过程中,通过makefile 中的-Ttext xxx 或者在链接脚本中指定.程序员事先预支自己的程序期望在哪个地址来执行. 因此会用这个地址来做链接地址.
再说下: 重定位实际就是在运行地址处执行一段位置无关码PIC, 让这段PIC(也就是重定位代码,)从运行地址处把整个程序镜像拷贝一份到链接地址处, 完了之后使用依据长跳转指令从运行地址处直接跳转到链接地址处执行同一个函数, 这样就实现了重定位之后的无缝链接.
adr与ldr 的区别: ldr 是长加载, adr 是短加载
但是主要的是, ldr 加载符号时, 加载的时链接地址, adr 加载符号时, 加载的时运行地址.
这样咱们就可以依靠adr 和 ldr 来判断, 当前运行的地址是否就是链接地址, 就能判断出是否需要重定位.
做一个实验, 将一个代码运行在0xd0020010的位置, 重定位到0xd0024000,
在makefile 中 我们在编译链接的时候, 给整个程序的链接地址是0xd0024000位置,
然后给程序下载的位置是0xd0020010位置, 所以一开始程序在0xd0020010位置处运行,
程序运行一段代码无关码PIC, 将程序拷贝到重定位的地址0xd0024000位置,然后跳转到重定位地址处运行, 所以后面的位置有关码就会运行在 链接地址和运行地址一样的位置,
程序得到正常的运行.
makefile 中给程序链接地址, 其实就是链接哪个脚本, 按照脚本规则安排了程序的链接
就是:
al-ld -Tlink.lds -o led.elf $^
ld链接器 给-Txxx 后面的xxx(link.lds) 就是链接脚本的文件名字, ld链接器就会按照链接脚本规则给程序进行链接.
这个实验纯粹是为了练习.
观看代码:
首先是一段链接脚本的代码,
https://blog.csdn.net/longjingcha110/article/details/88679477
接着汇编开始的代码.
#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加载时就叫短加载
// 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:
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指令实现长跳转
从代码上面来看, 先判断链接地址和运行地址是否一致, 如果不一致,就需要重定位,
重定位就需要将.text 段的数据及.data段的数据拷贝的指定重定位的地址处.
对于bss(初始化为0的全局变量)段, 就是把其段的内存地址空间全部清零.
为什么一开始,放在0xd0020010位置的bss段不需要字节清bss段呢?
因此一开始的.bss段有可能是编译器已经帮你清除了.或者有可能本来那段内存就没有使用到,所以本来就是零, 反正咱不管一开始的那段.bss段, 但是对于重定位后的.bss段,
你就必须手工清零. 否则程序运行可能会出错.
在处理代码段(.text)和数据段(.data),及清bss段之后, 就要进行长跳转到重定位之后的代码位置处执行.
就是
ldr pc, =led_blink