本篇大幅參考「程式設計師的自我修養:連結、載入、程式庫」一書。不同的是這篇不會用 x86 作例子,而是改用 ARM,除了不想被當成文抄公外,現在學 x86 組合語言也沒太大用處(大概只剩寫 BIOS 的人需要學吧?)。
首先來看兩個連初學者都會的 C 語言範例:
/* a.c */ extern int shared; int main() { int a = 100; swap(&a, &shared); }
/* b.c */ int shared = 1; void swap(int *a, int *b) { *a ^= *b ^= *a ^= *b; }swap() 為什麼可以這樣寫不是本文重點。先將這兩個 .c 編譯成 .o:
~# arm-none-linux-gnueabi-gcc -c a.c ~# arm-none-linux-gnueabi-gcc -c b.c觀察 a.o section header table,輸入 "arm-none-linux-gnueabi-readelf -S a.o":
因為 int shared, swap() 不在 a.c 裡面,所以 compiler 除了輸出 .text,還會輸出 .rel.text 紀錄需要 relocation 的地方。像 b.c 就沒有 .rel.text:
Relocation section '.rel.text' at offset 0x3bc contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000020 00000b1c R_ARM_CALL 00000000 swap 0000002c 00000c02 R_ARM_ABS32 00000000 shared輸入 "arm-none-linux-gnueabi-objdump -d a.o" 反組譯 a.o:
a.o: file format elf32-littlearm Disassembly of section .text: 00000000 <main>: 0: e92d4800 push {fp, lr} 4: e28db004 add fp, sp, #4 ; 0x4 8: e24dd010 sub sp, sp, #16 ; 0x10 c: e3a03064 mov r3, #100 ; 0x64 10: e50b3008 str r3, [fp, #-8] 14: e24b3008 sub r3, fp, #8 ; 0x8 18: e1a00003 mov r0, r3 1c: e59f1008 ldr r1, [pc, #8] ; 2c <main+0x2c> 20: ebfffffe bl 0 <swap> 24: e24bd004 sub sp, fp, #4 ; 0x4 28: e8bd8800 pop {fp, pc} 2c: 00000000 .word 0x0000000020: BL 是 "Branch with Link" 的縮寫,就算您不懂 ARM 組合語言,從上面的反組譯的結果也能看出是呼叫 swap()。而 2c: 則是用於儲存 shared 的位址(可以當成指標,這與 ARM 指令的定址方式有關)。20: 與 2c: 的值都是 0,有待 relocation 成正確的值。
輸入 "arm-none-linux-gnueabi-ld a.o b.o -e main -o ab" 把 a.o b.o 連結成 ab,這邊進入點改為 main,因為預設進入點為 _start,這是 startup code 的進入點,不過與本文無關,所以改成 main。
輸入 "arm-none-linux-gnueabi-objdump -d ab" 反組譯 ab:
"arm-none-linux-gnueabi-readelf -h ab" 觀察 ELF header:
除此之外,我們也看到需要 relocate 的 20: (變成 80b4 = 8094 + 20),2c: (變成 80c0 = 8094 + 2c) 都有了正確的位址。現在來解釋如何 relocate 成正確的位址。
輸入 "arm-none-linux-gnueabi-readelf -r a.o" 檢視 a.o .rel.text:
Relocation section '.rel.text' at offset 0x3bc contains 2 entries: Offset Info Type Sym.Value Sym. Name 00000020 00000b1c R_ARM_CALL 00000000 swap 0000002c 00000c02 R_ARM_ABS32 00000000 shared00000020, 0000002c 前面已經看過不用解釋,Info 由兩部份組成:
- 高 24bits 表示需要 relocation 的入口,它 symbol 在 symbol table 的索引。
- 低 8bits 代表 relocation type (上圖 Type)
先看 1.,輸入 "arm-none-linux-gnueabi-readelf -s a.o" 觀察 symbol table:
這下知道為何反組譯 a.o or ab 可以看到 "swap", "shared" 了吧? Relocation Type 我們看到 R_ARM_CALL, R_ARM_ABS32。先解釋 R_ARM_ABS32 ,因為他比較簡單,要搞懂它首先你得去 ARM 官網下載 ELF for the ARM Architecture。
下載回來後搜尋 R_ARM_ABS32:
下載回來後搜尋 R_ARM_ABS32:
(S+A)|T 就是 relocation 公式,T 指的是 ARM thumb mode,沒用到不用理會,S, A 分別代表是:
- S (when used on its own) is the address of the symbol.
- A is the addend for the relocation.
A 已經有了,就是 2c: 00000000 .word 0x00000000,所以修正後的位址等於:
= 0x00000000 + S
S 這個未知數要去哪邊找? 輸入 "arm-none-linux-gnueabi-readelf -s ab":
0x10138(S) + 0x00000000(A) = 0x10138,這就是答案。
R_ARM_CALL 就有點難度:
公式可以化簡為: (S+A) - P,P 在文件中的解釋是 "P is the address of the place being relocated (derived from r_offset).",每個英文單字都看得懂,看完還是不知道在寫什麼。筆者當初在這裡也卡了一陣子,然後想到這也許跟 BL 指令的細節有關(沒有重複,沒有遺漏的學習精神)。
文件上寫 BL 定址範圍為 +/- 32MB,可是 2^24 = 16MB/2 = +/- 8MB?這是因為 BL 定址的位址必須是 4 的倍數,所以改成 2^26 = 64MB/2 = +/- 32MB 就說得通了(反正最低 2bits 永遠為 0 沒必要浪費空間)。
BL 跳躍的位址計算公式為 (offset*4+PC),PC = BL 指令位址 + 8,回頭看 ab 反組譯的結果:
(0x80c4 - (0x80b4 + 0x8))/4 = 2。現在我們弄懂了這個指令,但應用官方給的公式還是有些地方對不上,回頭反組譯 a.o:
0x003FFFFE 以2補數表示 = -2,把S(0x80c4), A(-2), P(0x80b4) 代入公式:
(S + A) - P = (0x80c4 + (-2)) - 0x80b4 = 8
放進去要/4,所以就是 2。
如果有讀者真的照表操課,你會發現有個地方怪怪的,輸入 "arm-none-linux-gnueabi-readelf -S ab":
沒有 .rel.text!這是因為以 arm-none-linux-gnueabi-gcc 編譯出的執行檔,如不特別指定,.text 的起始位址都是 0x8000,這是因為可以跑 Linux 的 ARM SoC 普遍都有 MMU(這裡用"普遍"一詞,是因為還有一隻目前比較少人使用的 uCLinux 可以跑在沒有 MMU 的 SoC 上)。
要達成筆者前一篇的目的,請改用以下指令連結:
arm-none-linux-gnueabi-ld -r a.o b.o -e main -o abr
再用 arm-none-linux-gnueabi-readelf -S abr 檢查一次:
這次有了,再輸入 "arm-none-linux-gnueabi-readelf -r abr",看看有沒有之前 "arm-none-linux-gnueabi-readelf -r a.o" 也有的東西:
照理說,R_ARM_CALL 的定址是相對定址,a.o, b.o 也連結在一起了,不需要 loader 幫忙重算,筆者 n 年前使用 ARM Developer Suite 時是不需要做這部份的,也許加個旗標什麼的可以解決,不過弄懂原理後要 DIY 也不難就是了。
剩下的就請各位自行查閱筆者提供的參考資料。
To Be Continued...
沒有留言:
張貼留言