8219|16

87

帖子

0

TA的资源

一粒金砂(初级)

楼主
 

代码重定位的思考 [复制链接]

代码重定位的思考(1)----PC基址跳转



所谓代码的重定位(relocate),就是把可执行代码移动到内存中的另外一个地址去。OS一般会把内核从硬盘COPY到内存中去执行,就是用到了重定位这个技术。可执行代码经过编译,连接和定位之后,代码段和数据段都已经被定位器固定了。那么移动这段代码之后,程序若碰到branch指令,会不会跳到错误的地址去执行呢?
为了验证这个问题,笔者以Renesas SH2A体系的CPU为例,来做了相关的测试。

硬件平台:
CPU和外部SDRAM
其中CPU内部包含一小块SRAM

程序被下载到外部SDRAM里面执行。笔者将外部SDRAM地址08010000到08010120区间的代码复制到CPU内部SRAM地址FFF80000处,并跳转到FFF80000去执行。
08010000到08010120区间包含了flush_cache()和它调用的init_node()的代码。

下面是代码拷贝的汇编函数
_relocate:
MOVML.L R6,@-R15
STS.L PR,@-R15

MOV.L #H'FFF80000,R0        ; start address of the internal RAM.
MOV.L #_flush_cache,R1      ; start address of flush_cache()
MOV.L #H'8010120,R6         ; copy stop address
  
Copy_Loop:
MOV.L @R1,R2
MOV.L R2,@R0                ; 开始拷贝代码到SRAM
ADD #H'4,R0
ADD #H'4,R1

CMP/EQ R6,R1
BT Continue                 ; if T = 1, copy finished

MOV.L #Copy_Loop,R3
JMP @R3
NOP
  
Continue:
CLRT
MOV.L #H'FFF80000,R5
JSR @R5                     ; execute flush_cache() in the internal RAM.
NOP

LDS.L @R15+,PR
MOVML.L @R15+,R6

RTS/N

.END  

下面是flush_cache()的C函数,里面调用另一个函数init_node(),以便让CPU产生branch指令
// 入口地址为0x08010000
void flush_cache(void)
{
  CCNT.CCR1.BIT.ICF = 1;
  CCNT.CCR1.BIT.OCF = 1;

// 入口地址为0x08010028
  init_node(0);
}

经过调试,发现程序能正常跳转到地址FFF80000去执行flush_cache(),接下来也能正常跳转到init_node()去执行,CPU完成了代码的重定位。

下面我们来看看编译器对flush_cache()生成的地址,机器码和反汇编。

移动之前:

地址      机器码   指令
08010014  D703    MOV.L @(H'000C:8,PC),R7    ;注意这里的R7就是init_node()的入口地址
08010016  472B    JMP @R7                    ;JMP是一条branch指令,用于子函数的跳转

移动之后:

地址      机器码   指令
FFF80014  D703    MOV.L @(H'000C:8,PC),R7
FFF80016  472B    JMP @R7


我们发现了一个很奇怪的问题,就是移动之前和移动之后的代码是完全不变的,但是程序在执行JMP @R7之后,确都能准确地跳转到0x08010028去执行init_node()。这是为什么呢?
很明显,我们发现了MOV.L @(H'000C:8,PC),R7这条指令的与众不同。该指令是把PC的内容加上H'000C:8计算产生的偏移量之和送入R7,于是真相明白了。编译器在遇到branch指令时,是以PC为基址来跳转的。

我们来查看该指令。
16-bit/32-bit displacement
PC indirect with displacement(带转移的PC间接寻址)
MOV.L @(disp:8,PC),R7

The effective address is the sum of PC value and an 8-bit displacement(disp). The value of disp is zero-extended, and is doubled for a word operation, and quadrupled for a longword operation. For a longword operation, the lowest two bits of the PC value are masked.

Word:
PC + disp * 2
Longword:
PC & H'FFFFFFFC + disp * 4

由于copy代码的时候,是将flush_cache()和init_node()作为一个整体来COPY的,所以CPU还是可以跳转到init_node()去。

最后思考一个问题,如果不把init_node()的代码COPY到SRAM里去,flush_cache()还能不能调用它呢?
实验证明,如果把 MOV.L #H'8010120,R6换成MOV.L #H'8010020,R6,即不拷贝init_node(),程序会跳转到一个错误的地址。


疑问:
以PC为基址的偏移量是有范围的(0xff * 4 = 1020Bytes),以Rn为基址的最大范围是16380Bytes(16KB)。如果程序超过了16KB,要调用的函数的代码不在这个范围内,程序还会正常跳转吗?

最新回复

好东西,参考一下  详情 回复 发表于 2008-12-29 08:42
点赞 关注

回复
举报

61

帖子

0

TA的资源

一粒金砂(初级)

沙发
 
代码重定位的思考(2)----Literal Pool


根据(1)的测试,我们已经知道,编译器会把每个函数的入口地址以及常量的地址都放到一个称为Literal Pool(文字池)的区域。
请注意:Literal Pool里面只存放地址!

假设要操作一个常数,使用如下指令:
MOV.L H'FFFF0000,R0
编译器将这条指令编译成如下:
MOV.L @(H'0010:8,PC),R5

(H'0010:8,PC)就是H'FFFF0000在Literal Pool里面的地址,CPU根据这个地址,取出H'FFFF0000这个值,然后送入R5。

同理,调用函数时,如(1)里面经过编译后的代码:
MOV.L @(H'000C:8,PC),R7
JMP @R7

(H'000C:8,PC)就是被调函数的入口地址在Literal Pool里面的地址,CPU根据这个地址,在Literal Pool里取出被调函数的入口地址,然后去执行被调函数。


另外,一些指令被翻译成机器码之后,只占16或者32位。这样的话,操作数只有4位或者8位,无法直接处理32位的立即数。CPU借助Literal Pool,只需要在操作数里放置偏移量就可以使用一个32位的操作数。
SH2A的指令长度为16位。

试想一下,假设CPU的指令有64位或者更多,那么就可以直接使用一个32位的操作数了。实时上,在CISC体系的CPU中,例如Intel,指令长度可以达到15个Bytes,一条指令往往被编译成占用5个以上BYTE的机器码,一条指令就可以附带一个32位的操作数。但在RISC体系里,显然无法实现该功能。


总结:
我们现在可以回答(1)里面最后的疑问了。
程序在移动时,通常要把代码段和数据段一起移动。因为Literal Pool一般位于代码段的后面,当我们一起COPY这个Literal Pool的时候,程序会正确的找到被调函数的入口地址。见附录。
注意Literal Pool与代码的距离是有个范围的,如果PC的偏移量超过了这个范围,程序一样会跳转错误。这时,我们可以告知编译器,把Literal Pool放在这个范围内。SH2A的编译器一般都会自动完成这一功能(Automatic Literal Pool Gereration Function),或者用.POOL关键字来优化Literal Pool。一般的编译器都会有类似的功能。





附录:

Source program
-------------------------------------------  
.SECTION CD1, CODE, LOCATE=H'0000F000
CD1_START:
MOV.L #H'FFFF0000,R0
MOV.W #H'FF00,R1
MOV.L #CD1_START,R2
MOV   #H'FF,R3
RTS
MOV   R0,R10

.END
--------------------------------------------


Automatic literal pool generation result(source list)
-----------------------------------------------------
1  0000F000               1       .SECTION CD1, CODE, LOCATE=H'0000F000
2  0000F000               2     CD1_START
3  0000F000     D003  3       MOV.L #H'FFFF0000,R0
4  0000F002     9103  4       MOV.W #H'FF00,R1
5  0000F004     D203  5       MOV.L #CD1_START,R2
6  0000F006     E3FF  6       MOV   #H'FF,R3
7  0000F008     000B  7       RTS
8  0000F00A     6A03  8       MOV   R0,R10

9            **** begin pool ****
10 0000F00C     FF00             data for source-line 4
11 0000F00E     0000              alignment code
12 0000F010     FFFF0000      data for source-line 3
13 0000F014     0000F000      data for source-line 5
14           **** end pool   ****
15                              9       .END
-----------------------------------------------------



------摘自SH2A汇编器手册
 
 

回复

63

帖子

0

TA的资源

一粒金砂(初级)

板凳
 
代码重定位的思考(3)----位置无关代码PIC


编译器在编译一段程序时,要经过三个步骤:编译,链接和加载。在链接时,要对所有目标文件进行重定位,建立符号引用规则,同时为变量,函数等分配地址。程序执行时,把代码加载到链接时指定的地址空间,以保证程序在执行过程中对变量,函数等符号的正确引用,是程序正常运行。

但是,在操作系统中,一个进程通常从硬盘等二级存储设备拷贝到内存中去执行,这两者的地址是不同的,因此操作系统要对这个进程进行重定位,才能正确运行该进程。

在设计系统引导程序如bootloader时,也要对代码进行重定位。因为我们为了提高速度,需要将bootloader从ROM拷贝到RAM中去执行,这两者的地址也不同。
拷贝bootloader的这一小段代码是上电后就开始执行的,这些代码即使不在链接时指定的地址空间也能正确运行,这就是位置无关代码(position independent code)。


PIC的特点是,它被加载到任意地址空间都可以正确的执行。其原理是PIC对常量和函数入口地址的操作都是基于PC+偏移量的寻址方式。即使程序被移动,但是PC也变化了,而偏移量是不变的,所以程序仍然可以找到正确的入口地址或者常量。
例如:SH里面的BRA指令就可以用来设计PIC
BRA _main
编译后:
_main * 2 + PC -> PC


在SH体系中,MOV指令操作一个常数或者函数入口地址,比如:
MOV.L #_main, R0
MOV.L #H'FF010010,R0
编译器通常将这个常数或者地址存放在(2)里面的literal pool中。
请注意:literal pool里面实际上只存放绝对地址或者常量!!
因此,我们可以得出一个结论:
使用literal pool并不能产生位置无关代码,因为在literal pool里面存放的是函数入口的绝对地址或者常数,移动程序后,这些内容并没有改变。

在设计bootloader时,一个可行的方法是,将bootloader链接到RAM里面的指定位置(0x1000000),然后将bootloader加载到ROM里面的地址0x0处,CPU上电从ROM地址0x0执行。此时的bootloader的运行地址为0x0,而链接地址为0x1000000。由于bootloader头部的一小段代码是位置无关代码,它仍然可以在地址0x0处运行,并将整个bootloader拷贝到RAM地址的0x1000000处,然后清除bss段并设置堆栈,最后将main()的绝对地址(链接时的地址),比如0x1000100装入PC,并跳转到RAM里去。

MOV.L #_main, R0
JSR @R0
NOP

执行到这里后,程序就从ROM跳转到RAM里面了。
指令MOV.L _main, R0之前的所有指令都是位置无关代码。由于MOV指令会把main的绝对地址0x1000100放入literal pool,因此执行JSR @R0之后,程序就跳转到RAM里面的0x1000100处了。

至此,bootloader就在链接时指定的地址处运行了,这时它就可以在RAM里使用literal pool里面的绝对地址来进行函数的跳转或者操作常数了。这与重定位之前的情况是一样的。


值得一提的是,在有操作系统的系统中,不可能把进程都用PIC来写(编译器不能将C程序完全编译成PIC),由于进程可以随意的从硬盘加载到内存中,因此必须从硬件上来实现重定位,比如重定位寄存器。x86提供代码段,数据段,堆栈段重定位寄存器,操作系统通过修改这些寄存器,来重定位内存中的代码,数据和栈。

 
 
 

回复

64

帖子

0

TA的资源

一粒金砂(初级)

4
 
全文完。
喜欢的朋友帮顶!
thanks!
 
 
 

回复

79

帖子

0

TA的资源

一粒金砂(初级)

5
 
嗯,crak也需要代码重定位。
 
 
 

回复

54

帖子

0

TA的资源

一粒金砂(初级)

6
 
不懂,帮顶
 
 
 

回复

80

帖子

0

TA的资源

一粒金砂(初级)

7
 
bootloader 启动NK就是代码重定位的最好例子。
 
 
 

回复

68

帖子

0

TA的资源

一粒金砂(初级)

8
 
引用 6 楼 gooogleman 的回复:
bootloader 启动NK就是代码重定位的最好例子。


那个不算,因为地址已经在编译器里指定了,生成的影像文件拷备到内存的时候也是按那个地址开始。
 
 
 

回复

75

帖子

0

TA的资源

一粒金砂(初级)

9
 
呵呵
顶上去!
 
 
 

回复

69

帖子

0

TA的资源

一粒金砂(初级)

10
 
学习中……
 
 
 

回复

84

帖子

0

TA的资源

一粒金砂(初级)

11
 
顶啊,,没人??
 
 
 

回复

54

帖子

0

TA的资源

一粒金砂(初级)

12
 
顶!
 
 
 

回复

68

帖子

0

TA的资源

一粒金砂(初级)

13
 
好像:相对跳转指令。。。

 
 
 

回复

80

帖子

0

TA的资源

一粒金砂(初级)

14
 
不懂,帮顶
 
 
 

回复

70

帖子

0

TA的资源

一粒金砂(初级)

15
 
引用 12 楼 lbing7 的回复:
好像:相对跳转指令。。。



呵呵
这个帖子看不懂的话不要紧,大家可以多看看汇编和编译原理。回头来看的话就比较容易理解了。

本人正在酝酿下一个帖子,是uC/OS-II平台下的多线程TCP/IP设计。使用的CPU是瑞萨M16C, 可能比较长。
名字都想好了,《瑞萨M16C下的基于uC/OS-II平台的多线程TCP/IP设计》
呵呵

多多捧场啊。。
 
 
 

回复

69

帖子

0

TA的资源

一粒金砂(初级)

16
 
期待ing
 
 
 

回复

71

帖子

0

TA的资源

一粒金砂(初级)

17
 
好东西,参考一下
 
 
 

回复
您需要登录后才可以回帖 登录 | 注册

随便看看
查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/9 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表