前言
任务
在 SRAM 中将代码从 0xd0020010 重定位到 0xd0024000。
任务解释:本来代码是运行在0xd0020010的,但是因为一些原因我们又希望代码实际是在0xd0024000位置运行
的。这时候就需要重定位了。
注解:本练习对代码本身运行无实际意义,我们做这个重定位纯粹是为了练习重定位技能。但是某些情况重定位就是必须的,譬如在 uboot 中。
一、思路
第一点:通过链接脚本将代码链接到 0xd0024000;
第二点:dnw 下载时将 bin 文件下载到 0xd0020010;
第一点加上第二点,就保证了:代码实际下载运行在 0xd0020010,但是却被链接 0xd0024000
。从而为重定位奠定了基础。
当我们把代码链接地址设置为 0xd0024000
时,实际隐含意思就是: 我这个代码将来必须放在 0xd0024000
位置才能正确执行。如果实际运行地址不是这个地址就要出事(除非代码是 PIC 位置无关码),当以上都明白了后,就知道重定位代码的作用就是:在 PIC 执行完之前(在代码中第一句位置有关码执行之前)必须将整个代码搬移到 0xd0024000
位置去执行,这就是重定位。
第三点:代码执行时, 通过代码前段的少量位置无关码将整个代码搬移到 0xd0024000;
第四点:使用一个长跳转跳转到 0xd0024000 处的代码继续执行,重定位完成;
长跳转:首先这句代码是一句跳转指令(ARM 中的跳转指令就是类似于分支指令 B、BL 等作用的指令),跳转指令通过给 PC(r15)赋一个新值来完成代码段的跳转执行。长跳转指的是跳转到的地址和当前地址差异比较大,跳转的范围比较宽广。
当我们执行完代码重定位后,实际上在 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,让这段 PIC(也就是重定位代码)从运行地址处, 把整个程序镜像拷贝一份到链接地址处,完了之后使用一句长跳转指令, 从运行地址处直接跳转到链接地址处去执行同一个函数(led_blink),这样就实现了重定位之后的无缝连接。
二、链接脚本分析讲解
1、adr 与 ldr 伪指令的区别
ldr
和 adr
都是伪指令,区别是 ldr
是长加载、adr
是短加载。
重点:adr
指令加载符号地址,加载的是运行时地址;ldr
指令在加载符号地址时,加载的是链接地址。
深入分析:只要知道 adr
和 ldr
分别用于加载运行地址和链接地址,从而可以判断是否需要重定位即可;根本不需知道为什么 adr
和 ldr
是这样子,但是我们还是给大家扩展讲下为什么 adr
和 ldr
可以加载不同的地址。
可以看到,
adr
指令对应转换为 sub r0, pc, #40 ; 0x28
, 从当前 pc 指针的值,减去 40;
ldr
指令对应转换为 ldr r1, [pc, #72]
,当前 pc 指针的值,加上 72;
下面我们再具体看一下:
注意:有个前提必须知道,下方代码块的最左侧一栏,0xd0024000
是链接地址,即我们期望程序运行时的地址,当时这个链接地址不一定等于运行时地址;在这个例子中,我们用 dwn 将程序下载到 0xd0020010
。
也就是说,程序的链接地址是 0xd0024000
, 但是刚开始的运行地址是 0xd0020010
. 需要我们完成代码重定向,实现运行地址等于链接地址的工作。
pc 寄存器的值,表示 CPU 下一条需要指令的地址;因此 pc 寄存器的内容是运行地址。
于是我们往下走:
sub r0, pc, #40 ; 0x28
,首先当前 pc 寄存器的值并不是左侧栏的 d0024020,但是 pc 寄存器的值肯定指向某个地址,该地址处存放 sub r0, pc, #40 ; 0x28
指令的内容,这个对应关系是不会变的;然后,考虑到 ARM 流水线的结构,实际的 pc 指向的位置,应该是 sub r0, pc, #40 ; 0x28
指令开始,继续偏移两条指令的地址处(对应指令 ldr r2, [pc, #72]
)。
现在,将 pc 寄存器的值减去 40(等于16进制减去 0x28),也就是说,pc 寄存器向上偏移 10条 ARM 指令(10 * 4 = 40),于是 pc 指向了 _start 标号处的第一条指令的地址处。所以说, adr
指令得到的地址,依然是运行时地址,是从当前 pc 指针向前偏移,属于相对寻址。
d0024000 <_start>:
d0024000: e59f0064 ldr r0, [pc, #100] ; d002406c <run_on_dram+0x8>
d0024004: e3a01000 mov r1, #0
d0024008: e5801000 str r1, [r0]
d002400c: e59fd05c ldr sp, [pc, #92] ; d0024070 <run_on_dram+0xc>
d0024010: ee110f10 mrc 15, 0, r0, cr1, cr0, {0}
d0024014: e3c00a01 bic r0, r0, #4096 ; 0x1000
d0024018: e3800a01 orr r0, r0, #4096 ; 0x1000
d002401c: ee010f10 mcr 15, 0, r0, cr1, cr0, {0}
d0024020: e24f0028 sub r0, pc, #40 ; 0x28
d0024024: e59f1048 ldr r1, [pc, #72] ; d0024074 <run_on_dram+0x10>
d0024028: e59f2048 ldr r2, [pc, #72] ; d0024078 <run_on_dram+0x14>
d002402c: e1500001 cmp r0, r1
d0024030: 0a000003 beq d0024044 <clean_bss>
同理:
ldr
指令对应转换为 ldr r1, [pc, #72]
,从 pc 寄存器的值加上 72,于是 pc 寄存器指向的位置,应该就是来到了 0xd0024074 对应的地方。
从下面代码块看到,将来 pc 寄存器指向的位置,取得的指令是一个数字:d0024000
,这个值就是我们期望的链接地址。
所以说, ldr r1, [pc, #72]
指令是绝对寻址,因为它的作用是:将 pc 寄存器偏移到一个位置,这个位置专门存放数据。
d0024064 <run_on_dram>:
d0024064: e59ff014 ldr pc, [pc, #20] ; d0024080 <run_on_dram+0x1c>
d0024068: eafffffe b d0024068 <run_on_dram+0x4>
d002406c: e2700000 rsbs r0, r0, #0
d0024070: d0037d80 andle r7, r3, r0, lsl #27
d0024074: d0024000 andle r4, r2, r0
d0024078: d002412c andle r4, r2, ip, lsr #2
d002407c: d002412c andle r4, r2, ip, lsr #2
d0024080: d00240a0 andle r4, r2, r0, lsr #1
d0024084: 00001a41 andeq r1, r0, r1, asr #20
d0024088: 61656100 cmnvs r5, r0, lsl #2
d002408c: 01006962 tsteq r0, r2, ror #18
d0024090: 00000010 andeq r0, r0, r0, lsl r0
d0024094: 45543505 ldrbmi r3, [r4, #-1285] ; 0x505
d0024098: 08040600 stmdaeq r4, {r9, sl}
d002409c: 00010901 andeq r0, r1, r1, lsl #18
adr r0, _start
ldr r1, =_start
因此,r0 寄存器中的值是程序运行时地址, r1 寄存器中的值是链接地址。
2、重定位(代码拷贝)
从上节的分析我们知道,r0 寄存器中的值是程序运行时地址, r1 寄存器中的值是链接地址。
重定位就是汇编代码中的 copy_loop 函数,代码的作用是:使用循环结构来逐句复制代码到链接地址。
复制的源地址是 SRAM 的 0xd0020010(运行时地址),复制目标地址是 SRAM 的 0xd0024000 (链接地
址),复制长度是 r1 到 r2,即 bss_start 减去 _start。
可以看到,链接脚本中定义 bss_start 到 bss_end 的地址范围,是 bss 段的内容; bss_start 地址前面的内容,是整个程序中的 .text 代码段 和 .data 数据段。
我们还可以看到, r1 和 r2 寄存器的值,分别对应 d0024000 和 d002412c,都是链接地址;
所以复制的长度,就是整个重定位需要重定位的长度,也就是整个程序中代码段+数据段的长度;
bss 段(bss 段中就是0初始化的全局变量)不需要重定位。
3、清 bss 段
清除 bss 段是为了满足 C 语言的运行时要求(C 语言要求显式初始化为 0 的全局变量,或者未显式初始化的全
局变量的值为 0,实际上 C 语言编译器就是通过清 bss 段来实现 C 语言的这个特性的)。一般情况下我们的程
序是不需要负责清零 bss 段的(C 语言编译器和链接器会帮我们的程序自动添加一段头程序,这段程序会在我们
的 main 函数之前运行,这段代码就负责清除 bss)。但是在我们代码重定位了之后,因为编译器帮我们附加的
代码只是帮我们清除了运行地址那一份代码中的 bss,而未清除重定位地址处开头的那一份代码的 bss,所以重
定位之后需要自己去清除 bss。
4、长跳转
清理完 bss 段后重定位就结束了。然后当前的状况是:
1、当前运行地址还在 0xd0020010 开头的(重定位前的)那一份代码中运行着。
2、此时 SRAM 中已经有了 2 份代码,1份在 d0020010 开头,另一份在 d0024000 开头的位置。
然后就要长跳转了。
可以看到,最终 pc 寄存器指向的地址的内容是:0xd002,40a0,即链接地址范围内的 led_blink 的入口地址。
源自朱有鹏老师.