從0學ARM-什么是位置無關碼?
一、為什么需要位置無關碼?
首先我們需要了解一下ARM板子的啟動流程。
1. exynos 4412啟動流程
首先看一下 exynos 4412 memory map :
可知:iROM基地址是0x00000000 iRAM基地址是0x02020000
這兩塊內存都在 SOC中。
查看exynos 4412 Booting Sequence:
位于第五章。
上圖是exynos4412上電復位時的啟動流程,大致如下:
<1>執行內部只讀存儲器iROM中的一段代碼(廠家固化在里面的),這段代碼主要是初始化一些系統的基本配置,比如初步時鐘配置、堆棧、啟動模式(對應圖中的標志①)。
<2>iROM中的代碼根據階段一獲取的啟動模式(OM_STAT寄存器),從相應的存儲介質中拷貝BL1鏡像到內部靜態隨機存儲器SRAM,BL1主要是完善系統時鐘的初始化工作、內存控制器一些時序的配置。做完這些工作后把OS鏡像拷貝到內存中(對應圖中標志②③)。
<3>跳轉到OS中執行。
SRAM只有256KB,而uboot鏡像一般是超過這個大小的,也就是說它不能把完整的uboot鏡像拷貝到SRAM中,因此,推測這里的拷貝方式應該還是:「BL1拷貝的僅僅是uboot的一部分」,這一部分除了能設置好基本的硬件運行環境外,「還能把其自身(uboot鏡像)完整的拷貝到內存中」,然后uboot在內存中運行,完成OS鏡像的拷貝和引導
一般情況下兩者的地址并不相同,程序在DRAM中的地址重定位過程必須由程序員來完成。
這樣就有了「位置無關代碼」的概念,指代碼不在連接時指定的運行地址空間,也可以執行,它一段加載到任意地址空間都能執行的特殊代碼。
uboot搬移到DRAM中,然后跳轉到DRAM繼續運行uboot剩下的代碼,那么在搬移之前的這段代碼必須是位置無關,而且不能使用絕對尋址指令,否則尋址就會出錯。
二、怎么實現位置無關碼?
1. 什么是《編譯地址》?什么是《運行地址》?「編譯地址:」
32位的處理器,它的每一條指令是4個字節,以4個字節存儲順序,進行順序執行,CPU是順序執行的,只要沒發生什么跳轉,它會順序進行執行, 編譯器會對每一條指令分配一個編譯地址,這是編譯器分配的,在編譯過程中分配的地址,我們稱之為編譯地址。
「運行地址:」
是指程序指令真正運行的地址,是由用戶指定的,用戶將運行地址燒錄到哪里,哪里就是運行的地址。比如有一個指令的編譯地址是0x40008000,實際運行的地址是0x40008000,如果用戶將指令燒到0x60000000上,那么這條指令的運行地址就是0x60000000。
當編譯地址和運行地址不同的時候會出現什么結果?結果是不能跳轉,編譯后會產生跳轉地址,如果實際地址和編譯后產生的地址不相等,那么就不能跳轉。
「C語言編譯地址:」
都希望把編譯地址和實際運行地址放在一起的,但是匯編代碼因為不需要做C語言到匯編的轉換,可以直接的去寫地址,所以直接寫的就是他的運行地址,這就是為什么任何bootloader剛開始會有一段匯編代碼,因為起始代碼編譯地址和實際地址不相等,這段代碼和匯編無關,跳轉用的運行地址。
2. 舉例
實現位置無關碼主要考慮以下兩個方面:
- 1. 位置無關的函數跳轉
- 2. 位置無關的常量訪問
代碼
編譯代碼使用的連接文件「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對應的鏈接地址是0x40008000
- 9行 bl func對應的指令
- 10行 ldr pc,=pc對應的指令
- func的鏈接地址0x40008010
- 全局變量aaaa對應的內存位于bss段0x4000802c
- 19行 aaaa = 0x11 賦值語句對應的機器碼
如果我們將生成的bin文件拷貝到內存0x40008000位置運行必然沒有問題,
bl func 和 ldr pc,=func 都能跳轉到func函數,而19行代碼,也能訪問到全局變量aaaa。
如果我們將該程序拷貝到其他地址是否能正常運行呢?
假定我們拷貝到0地址運行,那么程序的執行地址需要從0開始重新編排,即_start對應0地址,main對應0x18。
拷貝到0地址后內存布局:
拷貝到0地址運行后,**內存中指令(機器碼)**的內容還和以前一樣, pc的值會根據實際運行地址重新修正。
1.首先看bl func
對應的匯編代碼是 第9行;該指令的機器碼是0xeb000001, 我們在《4. 從0開始學ARM-ARM指令,移位、數據處理、BL、機器碼》講過該機器碼格式 是從pc的位置向前偏移1條指令 因為三級流水線,所以應該往下偏移3條指令,即func的位置, 所以bl仍然可以正確找到func這個函數。
bl func
2.ldr pc,=func 對應的匯編代碼是 第10行;
我們可以看到是從pc值+4位置取出對應的內存的值,pc值+4是14,該位置對應15行, 即將40008010寫入到pc,
而我們的bin文件只有44個字節大小,所以此時內存40008010并沒有我們編寫的任何代碼。所以ldr pc,=func 無法跳轉到func。
3.c訪問全局變量aaaa
對應的匯編代碼是 第19行;
c訪問全局變量aaaa
我們可以看到是從pc值+4位置取出對應的內存的值,pc值+4是28,該位置對應22行, 即將4000802c寫入到r3,然后20行會將r2中值寫入到0x4000802c這個地址, 而此時該地址并不是全局變量aaaa, 所以此指令是無法找到bss段的aaaa變量的內存。
四、總結
1. 位置無關碼:CPU取指時用相對地址取指令(比如pc +4),只要其相對地址沒有變,都能夠取指并運行。即該段代碼無論放在內存的哪個地址,都能正確運行。究其原因,是因為代碼里沒有使用絕對地址,都是相對地址。
2. 位置相關碼:利用絕對地址取指并運行,這就需要你存放程序(鏈接過程中)需要按照連接腳本的要求那樣執行(Makefile里面有 -Ttext xxx指定或連接腳本)。即它的地址與代碼處于的位置相關,是絕對地址,如:mov PC ,#0xff;ldr pc,=0xffff等。
3. 位置無關碼的應用:1). 程序在運行期間動態加載到內存;
2). 程序在不同場合與不同程序組合后加載到內存(共享的動態鏈接庫);
3). 在運行期間不同地址相互之間的映射(如bootloader)
4. 結論
- 使用「mov pc ,xxx ; ldr pc ,xxx」等就是位置相關碼。這些使用絕對指令尋址。
- 而使用「bl ,b ,adr,ldr」一般為位置無關碼。
- 在使用「b, bl」調用C語言中的函數里「不要使用全局變量」,因為C中全局變量的地址「也是根據鏈接地址生成」的。
- 使用=和不使用=號是有很大區別的。「無=號:取該標號處的值,位置無關 有=號:取該標號的地址,位置相關」
【考一考】 考一考大家為什么uboot的異常向量表的reset異常,指令是b reset,而其他異常卻是我們本文所說的位置相關碼,ldr pc,XXXXXX?
arm對應的uboot異常向量表如下:
- arch/arm/cpu/armv7/start.S
本文轉載自微信公眾號「 一口Linux」,可以通過以下二維碼關注。轉載本文請聯系 一口Linux公眾號。