代碼重定位的思考(1)----PC基址跳轉(zhuǎn)
所謂代碼的重定位(relocate),就是把可執(zhí)行代碼移動(dòng)到內(nèi)存中的另外一個(gè)地址去。OS一般會(huì)把內(nèi)核從硬盤COPY到內(nèi)存中去執(zhí)行,就是用到了重定位這個(gè)技術(shù)??蓤?zhí)行代碼經(jīng)過編譯,連接和定位之后,代碼段和數(shù)據(jù)段都已經(jīng)被定位器固定了。那么移動(dòng)這段代碼之后,程序若碰到branch指令,會(huì)不會(huì)跳到錯(cuò)誤的地址去執(zhí)行呢?
為了驗(yàn)證這個(gè)問題,筆者以Renesas SH2A體系的CPU為例,來做了相關(guān)的測試。
硬件平臺(tái):
CPU和外部SDRAM
其中CPU內(nèi)部包含一小塊SRAM
程序被下載到外部SDRAM里面執(zhí)行。筆者將外部SDRAM地址08010000到08010120區(qū)間的代碼復(fù)制到CPU內(nèi)部SRAM地址FFF80000處,并跳轉(zhuǎn)到FFF80000去執(zhí)行。
08010000到08010120區(qū)間包含了flush_cache()和它調(diào)用的init_node()的代碼。
下面是代碼拷貝的匯編函數(shù)
_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函數(shù),里面調(diào)用另一個(gè)函數(shù)init_node(),以便讓CPU產(chǎn)生branch指令
// 入口地址為0x08010000
void flush_cache(void)
{
CCNT.CCR1.BIT.ICF = 1;
CCNT.CCR1.BIT.OCF = 1;
// 入口地址為0x08010028
init_node(0);
}
經(jīng)過調(diào)試,發(fā)現(xiàn)程序能正常跳轉(zhuǎn)到地址FFF80000去執(zhí)行flush_cache(),接下來也能正常跳轉(zhuǎn)到init_node()去執(zhí)行,CPU完成了代碼的重定位。
下面我們來看看編譯器對(duì)flush_cache()生成的地址,機(jī)器碼和反匯編。
移動(dòng)之前:
地址 機(jī)器碼 指令
08010014 D703 MOV.L @(H'000C:8,PC),R7 ;注意這里的R7就是init_node()的入口地址
08010016 472B JMP @R7 ;JMP是一條branch指令,用于子函數(shù)的跳轉(zhuǎn)
移動(dòng)之后:
地址 機(jī)器碼 指令
FFF80014 D703 MOV.L @(H'000C:8,PC),R7
FFF80016 472B JMP @R7
我們發(fā)現(xiàn)了一個(gè)很奇怪的問題,就是移動(dòng)之前和移動(dòng)之后的代碼是完全不變的,但是程序在執(zhí)行JMP @R7之后,確都能準(zhǔn)確地跳轉(zhuǎn)到0x08010028去執(zhí)行init_node()。這是為什么呢?
很明顯,我們發(fā)現(xiàn)了MOV.L @(H'000C:8,PC),R7這條指令的與眾不同。該指令是把PC的內(nèi)容加上H'000C:8計(jì)算產(chǎn)生的偏移量之和送入R7,于是真相明白了。編譯器在遇到branch指令時(shí),是以PC為基址來跳轉(zhuǎn)的。
我們來查看該指令。
16-bit/32-bit displacement
PC indirect with displacement(帶轉(zhuǎn)移的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代碼的時(shí)候,是將flush_cache()和init_node()作為一個(gè)整體來COPY的,所以CPU還是可以跳轉(zhuǎn)到init_node()去。
最后思考一個(gè)問題,如果不把init_node()的代碼COPY到SRAM里去,flush_cache()還能不能調(diào)用它呢?
實(shí)驗(yàn)證明,如果把 MOV.L #H'8010120,R6換成MOV.L #H'8010020,R6,即不拷貝init_node(),程序會(huì)跳轉(zhuǎn)到一個(gè)錯(cuò)誤的地址。
疑問:
以PC為基址的偏移量是有范圍的(0xff * 4 = 1020Bytes),以Rn為基址的最大范圍是16380Bytes(16KB)。如果程序超過了16KB,要調(diào)用的函數(shù)的代碼不在這個(gè)范圍內(nèi),程序還會(huì)正常跳轉(zhuǎn)嗎?
代碼重定位的思考(2)----Literal Pool
根據(jù)(1)的測試,我們已經(jīng)知道,編譯器會(huì)把每個(gè)函數(shù)的入口地址以及常量的地址都放到一個(gè)稱為Literal Pool(文字池)的區(qū)域。
請(qǐng)注意:Literal Pool里面只存放地址!
假設(shè)要操作一個(gè)常數(shù),使用如下指令:
MOV.L H'FFFF0000,R0
編譯器將這條指令編譯成如下:
MOV.L @(H'0010:8,PC),R5
(H'0010:8,PC)就是H'FFFF0000在Literal Pool里面的地址,CPU根據(jù)這個(gè)地址,取出H'FFFF0000這個(gè)值,然后送入R5。
同理,調(diào)用函數(shù)時(shí),如(1)里面經(jīng)過編譯后的代碼:
MOV.L @(H'000C:8,PC),R7
JMP @R7
(H'000C:8,PC)就是被調(diào)函數(shù)的入口地址在Literal Pool里面的地址,CPU根據(jù)這個(gè)地址,在Literal Pool里取出被調(diào)函數(shù)的入口地址,然后去執(zhí)行被調(diào)函數(shù)。
另外,一些指令被翻譯成機(jī)器碼之后,只占16或者32位。這樣的話,操作數(shù)只有4位或者8位,無法直接處理32位的立即數(shù)。CPU借助Literal Pool,只需要在操作數(shù)里放置偏移量就可以使用一個(gè)32位的操作數(shù)。
SH2A的指令長度為16位。
試想一下,假設(shè)CPU的指令有64位或者更多,那么就可以直接使用一個(gè)32位的操作數(shù)了。實(shí)時(shí)上,在CISC體系的CPU中,例如Intel,指令長度可以達(dá)到15個(gè)Bytes,一條指令往往被編譯成占用5個(gè)以上BYTE的機(jī)器碼,一條指令就可以附帶一個(gè)32位的操作數(shù)。但在RISC體系里,顯然無法實(shí)現(xiàn)該功能。
總結(jié):
我們現(xiàn)在可以回答(1)里面最后的疑問了。
程序在移動(dòng)時(shí),通常要把代碼段和數(shù)據(jù)段一起移動(dòng)。因?yàn)長iteral Pool一般位于代碼段的后面,當(dāng)我們一起COPY這個(gè)Literal Pool的時(shí)候,程序會(huì)正確的找到被調(diào)函數(shù)的入口地址。見附錄。
注意Literal Pool與代碼的距離是有個(gè)范圍的,如果PC的偏移量超過了這個(gè)范圍,程序一樣會(huì)跳轉(zhuǎn)錯(cuò)誤。這時(shí),我們可以告知編譯器,把Literal Pool放在這個(gè)范圍內(nèi)。SH2A的編譯器一般都會(huì)自動(dòng)完成這一功能(Automatic Literal Pool Gereration Function),或者用.POOL關(guān)鍵字來優(yōu)化Literal Pool。一般的編譯器都會(huì)有類似的功能。
附錄:
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匯編器手冊
代碼重定位的思考(3)----位置無關(guān)代碼PIC
譯器在編譯一段程序時(shí),要經(jīng)過三個(gè)步驟:編譯,鏈接和加載。在鏈接時(shí),要對(duì)所有目標(biāo)文件進(jìn)行重定位,建立符號(hào)引用規(guī)則,同時(shí)為變量,函數(shù)等分配地址。程序執(zhí)行時(shí),把代碼加載到鏈接時(shí)指定的地址空間,以保證程序在執(zhí)行過程中對(duì)變量,函數(shù)等符號(hào)的正確引用,是程序正常運(yùn)行。
但是,在操作系統(tǒng)中,一個(gè)進(jìn)程通常從硬盤等二級(jí)存儲(chǔ)設(shè)備拷貝到內(nèi)存中去執(zhí)行,這兩者的地址是不同的,因此操作系統(tǒng)要對(duì)這個(gè)進(jìn)程進(jìn)行重定位,才能正確運(yùn)行該進(jìn)程。
在設(shè)計(jì)系統(tǒng)引導(dǎo)程序如bootloader時(shí),也要對(duì)代碼進(jìn)行重定位。因?yàn)槲覀優(yōu)榱颂岣咚俣龋枰獙ootloader從ROM拷貝到RAM中去執(zhí)行,這兩者的地址也不同。
拷貝bootloader的這一小段代碼是上電后就開始執(zhí)行的,這些代碼即使不在鏈接時(shí)指定的地址空間也能正確運(yùn)行,這就是位置無關(guān)代碼(position independent code)。
PIC的特點(diǎn)是,它被加載到任意地址空間都可以正確的執(zhí)行。其原理是PIC對(duì)常量和函數(shù)入口地址的操作都是基于PC+偏移量的尋址方式。即使程序被移動(dòng),但是PC也變化了,而偏移量是不變的,所以程序仍然可以找到正確的入口地址或者常量。
例如:SH里面的BRA指令就可以用來設(shè)計(jì)PIC
BRA _main
編譯后:
_main * 2 + PC -> PC
在SH體系中,MOV指令操作一個(gè)常數(shù)或者函數(shù)入口地址,比如:
MOV.L #_main, R0
MOV.L #H'FF010010,R0
編譯器通常將這個(gè)常數(shù)或者地址存放在(2)里面的literal pool中。
請(qǐng)注意:literal pool里面實(shí)際上只存放絕對(duì)地址或者常量??!
因此,我們可以得出一個(gè)結(jié)論:
使用literal pool并不能產(chǎn)生位置無關(guān)代碼,因?yàn)樵趌iteral pool里面存放的是函數(shù)入口的絕對(duì)地址或者常數(shù),移動(dòng)程序后,這些內(nèi)容并沒有改變。
在設(shè)計(jì)bootloader時(shí),一個(gè)可行的方法是,將bootloader鏈接到RAM里面的指定位置(0x1000000),然后將bootloader加載到ROM里面的地址0x0處,CPU上電從ROM地址0x0執(zhí)行。此時(shí)的bootloader的運(yùn)行地址為0x0,而鏈接地址為0x1000000。由于bootloader頭部的一小段代碼是位置無關(guān)代碼,它仍然可以在地址0x0處運(yùn)行,并將整個(gè)bootloader拷貝到RAM地址的0x1000000處,然后清除bss段并設(shè)置堆棧,最后將main()的絕對(duì)地址(鏈接時(shí)的地址),比如0x1000100裝入PC,并跳轉(zhuǎn)到RAM里去。
MOV.L #_main, R0
JSR @R0
NOP
執(zhí)行到這里后,程序就從ROM跳轉(zhuǎn)到RAM里面了。
指令MOV.L _main, R0之前的所有指令都是位置無關(guān)代碼。由于MOV指令會(huì)把main的絕對(duì)地址0x1000100放入literal pool,因此執(zhí)行JSR @R0之后,程序就跳轉(zhuǎn)到RAM里面的0x1000100處了。
至此,bootloader就在鏈接時(shí)指定的地址處運(yùn)行了,這時(shí)它就可以在RAM里使用literal pool里面的絕對(duì)地址來進(jìn)行函數(shù)的跳轉(zhuǎn)或者操作常數(shù)了。這與重定位之前的情況是一樣的。
值得一提的是,在有操作系統(tǒng)的系統(tǒng)中,不可能把進(jìn)程都用PIC來寫(編譯器不能將C程序完全編譯成PIC),由于進(jìn)程可以隨意的從硬盤加載到內(nèi)存中,因此必須從硬件上來實(shí)現(xiàn)重定位,比如重定位寄存器。x86提供代碼段,數(shù)據(jù)段,堆棧段重定位寄存器,操作系統(tǒng)通過修改這些寄存器,來重定位內(nèi)存中的代碼,數(shù)據(jù)和棧。