從0學(xué)ARM-什么是位置無(wú)關(guān)碼?
一、為什么需要位置無(wú)關(guān)碼?
首先我們需要了解一下ARM板子的啟動(dòng)流程。
1. exynos 4412啟動(dòng)流程
首先看一下 exynos 4412 memory map :
可知:iROM基地址是0x00000000 iRAM基地址是0x02020000
這兩塊內(nèi)存都在 SOC中。
查看exynos 4412 Booting Sequence:
位于第五章。
上圖是exynos4412上電復(fù)位時(shí)的啟動(dòng)流程,大致如下:
<1>執(zhí)行內(nèi)部只讀存儲(chǔ)器iROM中的一段代碼(廠家固化在里面的),這段代碼主要是初始化一些系統(tǒng)的基本配置,比如初步時(shí)鐘配置、堆棧、啟動(dòng)模式(對(duì)應(yīng)圖中的標(biāo)志①)。
<2>iROM中的代碼根據(jù)階段一獲取的啟動(dòng)模式(OM_STAT寄存器),從相應(yīng)的存儲(chǔ)介質(zhì)中拷貝BL1鏡像到內(nèi)部靜態(tài)隨機(jī)存儲(chǔ)器SRAM,BL1主要是完善系統(tǒng)時(shí)鐘的初始化工作、內(nèi)存控制器一些時(shí)序的配置。做完這些工作后把OS鏡像拷貝到內(nèi)存中(對(duì)應(yīng)圖中標(biāo)志②③)。
<3>跳轉(zhuǎn)到OS中執(zhí)行。
SRAM只有256KB,而uboot鏡像一般是超過(guò)這個(gè)大小的,也就是說(shuō)它不能把完整的uboot鏡像拷貝到SRAM中,因此,推測(cè)這里的拷貝方式應(yīng)該還是:「BL1拷貝的僅僅是uboot的一部分」,這一部分除了能設(shè)置好基本的硬件運(yùn)行環(huán)境外,「還能把其自身(uboot鏡像)完整的拷貝到內(nèi)存中」,然后uboot在內(nèi)存中運(yùn)行,完成OS鏡像的拷貝和引導(dǎo)
一般情況下兩者的地址并不相同,程序在DRAM中的地址重定位過(guò)程必須由程序員來(lái)完成。
這樣就有了「位置無(wú)關(guān)代碼」的概念,指代碼不在連接時(shí)指定的運(yùn)行地址空間,也可以執(zhí)行,它一段加載到任意地址空間都能執(zhí)行的特殊代碼。
uboot搬移到DRAM中,然后跳轉(zhuǎn)到DRAM繼續(xù)運(yùn)行uboot剩下的代碼,那么在搬移之前的這段代碼必須是位置無(wú)關(guān),而且不能使用絕對(duì)尋址指令,否則尋址就會(huì)出錯(cuò)。
二、怎么實(shí)現(xiàn)位置無(wú)關(guān)碼?
1. 什么是《編譯地址》?什么是《運(yùn)行地址》?「編譯地址:」
32位的處理器,它的每一條指令是4個(gè)字節(jié),以4個(gè)字節(jié)存儲(chǔ)順序,進(jìn)行順序執(zhí)行,CPU是順序執(zhí)行的,只要沒(méi)發(fā)生什么跳轉(zhuǎn),它會(huì)順序進(jìn)行執(zhí)行, 編譯器會(huì)對(duì)每一條指令分配一個(gè)編譯地址,這是編譯器分配的,在編譯過(guò)程中分配的地址,我們稱(chēng)之為編譯地址。
「運(yùn)行地址:」
是指程序指令真正運(yùn)行的地址,是由用戶(hù)指定的,用戶(hù)將運(yùn)行地址燒錄到哪里,哪里就是運(yùn)行的地址。比如有一個(gè)指令的編譯地址是0x40008000,實(shí)際運(yùn)行的地址是0x40008000,如果用戶(hù)將指令燒到0x60000000上,那么這條指令的運(yùn)行地址就是0x60000000。
當(dāng)編譯地址和運(yùn)行地址不同的時(shí)候會(huì)出現(xiàn)什么結(jié)果?結(jié)果是不能跳轉(zhuǎn),編譯后會(huì)產(chǎn)生跳轉(zhuǎn)地址,如果實(shí)際地址和編譯后產(chǎn)生的地址不相等,那么就不能跳轉(zhuǎn)。
「C語(yǔ)言編譯地址:」
都希望把編譯地址和實(shí)際運(yùn)行地址放在一起的,但是匯編代碼因?yàn)椴恍枰鯟語(yǔ)言到匯編的轉(zhuǎn)換,可以直接的去寫(xiě)地址,所以直接寫(xiě)的就是他的運(yùn)行地址,這就是為什么任何bootloader剛開(kāi)始會(huì)有一段匯編代碼,因?yàn)槠鹗即a編譯地址和實(shí)際地址不相等,這段代碼和匯編無(wú)關(guān),跳轉(zhuǎn)用的運(yùn)行地址。
2. 舉例
實(shí)現(xiàn)位置無(wú)關(guān)碼主要考慮以下兩個(gè)方面:
- 1. 位置無(wú)關(guān)的函數(shù)跳轉(zhuǎn)
- 2. 位置無(wú)關(guān)的常量訪(fǎng)問(wèn)
代碼
編譯代碼使用的連接文件「map.lds」如下:
- OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
- /*OUTPUT_FORMAT("elf32-arm", "elf32-arm", "elf32-arm")*/
- OUTPUT_ARCH(arm)
- ENTRY(_start)
- SECTIONS
- {
- . = 0x40008000;
- . = ALIGN(4);
- .text :
- {
- gcd.o(.text)
- *(.text)
- }
- . = ALIGN(4);
- .rodata :
- { *(.rodata) }
- . = ALIGN(4);
- .data :
- { *(.data) }
- . = ALIGN(4);
- .bss :
- { *(.bss) }
- }
如文件map.lds所示:「0x40008000」就是鏈接地址,
其他源文件如下:「gcd.s」
- .text
- .global _start
- _start:
- ldr sp,=0x70000000 /*get stack top pointer*/
- bl func
- ldr pc,=func
- b main
- func:
- mv pc,lr
「main.c」
- /*
- * main.c
- *
- * Created on: 2020-12-12
- * Author: 一口Linux
- */
- int aaaa=0;
- int main(void)
- {
- aaaa = 0x11;
- while(1);
- return 0;
- }
「Makefile」
- TARGET=gcd
- TARGETC=main
- all:
- arm-none-linux-gnueabi-gcc -O1 -g -c -o $(TARGETC).o $(TARGETC).c
- arm-none-linux-gnueabi-gcc -O1 -g -c -o $(TARGET).o $(TARGET).s
- arm-none-linux-gnueabi-gcc -O1 -g -S -o $(TARGETC).s $(TARGETC).c
- arm-none-linux-gnueabi-ld $(TARGETC).o $(TARGET).o -Tmap.lds -o $(TARGET).elf
- arm-none-linux-gnueabi-objcopy -O binary -S $(TARGET).elf $(TARGET).bin
- arm-none-linux-gnueabi-objdump -D $(TARGET).elf > $(TARGET).dis
- clean:
- rm -rf *.o *.elf *.dis *.bin
反匯編文件「gcd.dis」
如上圖所示:
- _start對(duì)應(yīng)的鏈接地址是0x40008000
- 9行 bl func對(duì)應(yīng)的指令
- 10行 ldr pc,=pc對(duì)應(yīng)的指令
- func的鏈接地址0x40008010
- 全局變量aaaa對(duì)應(yīng)的內(nèi)存位于bss段0x4000802c
- 19行 aaaa = 0x11 賦值語(yǔ)句對(duì)應(yīng)的機(jī)器碼
如果我們將生成的bin文件拷貝到內(nèi)存0x40008000位置運(yùn)行必然沒(méi)有問(wèn)題,
bl func 和 ldr pc,=func 都能跳轉(zhuǎn)到func函數(shù),而19行代碼,也能訪(fǎng)問(wèn)到全局變量aaaa。
如果我們將該程序拷貝到其他地址是否能正常運(yùn)行呢?
假定我們拷貝到0地址運(yùn)行,那么程序的執(zhí)行地址需要從0開(kāi)始重新編排,即_start對(duì)應(yīng)0地址,main對(duì)應(yīng)0x18。
拷貝到0地址后內(nèi)存布局:
拷貝到0地址運(yùn)行后,**內(nèi)存中指令(機(jī)器碼)**的內(nèi)容還和以前一樣, pc的值會(huì)根據(jù)實(shí)際運(yùn)行地址重新修正。
1.首先看bl func
對(duì)應(yīng)的匯編代碼是 第9行;該指令的機(jī)器碼是0xeb000001, 我們?cè)凇?. 從0開(kāi)始學(xué)ARM-ARM指令,移位、數(shù)據(jù)處理、BL、機(jī)器碼》講過(guò)該機(jī)器碼格式 是從pc的位置向前偏移1條指令 因?yàn)槿?jí)流水線(xiàn),所以應(yīng)該往下偏移3條指令,即func的位置, 所以bl仍然可以正確找到func這個(gè)函數(shù)。
bl func
2.ldr pc,=func 對(duì)應(yīng)的匯編代碼是 第10行;
我們可以看到是從pc值+4位置取出對(duì)應(yīng)的內(nèi)存的值,pc值+4是14,該位置對(duì)應(yīng)15行, 即將40008010寫(xiě)入到pc,
而我們的bin文件只有44個(gè)字節(jié)大小,所以此時(shí)內(nèi)存40008010并沒(méi)有我們編寫(xiě)的任何代碼。所以ldr pc,=func 無(wú)法跳轉(zhuǎn)到func。
3.c訪(fǎng)問(wèn)全局變量aaaa
對(duì)應(yīng)的匯編代碼是 第19行;
c訪(fǎng)問(wèn)全局變量aaaa
我們可以看到是從pc值+4位置取出對(duì)應(yīng)的內(nèi)存的值,pc值+4是28,該位置對(duì)應(yīng)22行, 即將4000802c寫(xiě)入到r3,然后20行會(huì)將r2中值寫(xiě)入到0x4000802c這個(gè)地址, 而此時(shí)該地址并不是全局變量aaaa, 所以此指令是無(wú)法找到bss段的aaaa變量的內(nèi)存。
四、總結(jié)
1. 位置無(wú)關(guān)碼:CPU取指時(shí)用相對(duì)地址取指令(比如pc +4),只要其相對(duì)地址沒(méi)有變,都能夠取指并運(yùn)行。即該段代碼無(wú)論放在內(nèi)存的哪個(gè)地址,都能正確運(yùn)行。究其原因,是因?yàn)榇a里沒(méi)有使用絕對(duì)地址,都是相對(duì)地址。
2. 位置相關(guān)碼:利用絕對(duì)地址取指并運(yùn)行,這就需要你存放程序(鏈接過(guò)程中)需要按照連接腳本的要求那樣執(zhí)行(Makefile里面有 -Ttext xxx指定或連接腳本)。即它的地址與代碼處于的位置相關(guān),是絕對(duì)地址,如:mov PC ,#0xff;ldr pc,=0xffff等。
3. 位置無(wú)關(guān)碼的應(yīng)用:1). 程序在運(yùn)行期間動(dòng)態(tài)加載到內(nèi)存;
2). 程序在不同場(chǎng)合與不同程序組合后加載到內(nèi)存(共享的動(dòng)態(tài)鏈接庫(kù));
3). 在運(yùn)行期間不同地址相互之間的映射(如bootloader)
4. 結(jié)論
- 使用「mov pc ,xxx ; ldr pc ,xxx」等就是位置相關(guān)碼。這些使用絕對(duì)指令尋址。
- 而使用「bl ,b ,adr,ldr」一般為位置無(wú)關(guān)碼。
- 在使用「b, bl」調(diào)用C語(yǔ)言中的函數(shù)里「不要使用全局變量」,因?yàn)镃中全局變量的地址「也是根據(jù)鏈接地址生成」的。
- 使用=和不使用=號(hào)是有很大區(qū)別的。「無(wú)=號(hào):取該標(biāo)號(hào)處的值,位置無(wú)關(guān) 有=號(hào):取該標(biāo)號(hào)的地址,位置相關(guān)」
【考一考】 考一考大家為什么uboot的異常向量表的reset異常,指令是b reset,而其他異常卻是我們本文所說(shuō)的位置相關(guān)碼,ldr pc,XXXXXX?
arm對(duì)應(yīng)的uboot異常向量表如下:
- arch/arm/cpu/armv7/start.S
本文轉(zhuǎn)載自微信公眾號(hào)「 一口Linux」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系 一口Linux公眾號(hào)。