2018年10月21日 星期日

HMI 回憶錄 (7)

前篇的副產品,就算是講古,筆者還是有做功課的。

本篇大幅參考「程式設計師的自我修養:連結、載入、程式庫」一書。不同的是這篇不會用 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:


輸入 "arm-none-linux-gnueabi-readelf -r a.o" 觀察 a.o 哪些地方需要 relocation:
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 0x00000000

20: 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:


main 位址為 0x8094,為了確認這點,我們輸入
"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   shared
00000020, 0000002c 前面已經看過不用解釋,Info 由兩部份組成:
  1. 高 24bits 表示需要 relocation 的入口,它 symbol 在 symbol table 的索引。
  2. 低 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:


(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...

沒有留言:

張貼留言