1. 程式人生 > IOS開發 >iOS彙編教程(八)靜態連結中的 Relocation - 靜態庫連結時是如何保證對變數的相對定址依然正確的?

iOS彙編教程(八)靜態連結中的 Relocation - 靜態庫連結時是如何保證對變數的相對定址依然正確的?

系列文章

  1. iOS彙編入門教程(一)ARM64彙編基礎
  2. iOS彙編入門教程(二)在Xcode工程中嵌入彙編程式碼
  3. iOS彙編入門教程(三)彙編中的 Section 與資料存取
  4. iOS彙編教程(四)基於 LLDB 動態除錯快速分析系統函式的實現
  5. iOS彙編教程(五)Objc Block 的記憶體佈局和彙編表示
  6. iOS彙編教程(六)CPU 指令重排與記憶體屏障
  7. iOS彙編教程(七)ARM Exclusive - 互斥鎖與讀寫一致性的底層實現原理

簡介

在 iOS 應用開發過程中,我們常常通過靜態庫方式引用一些閉源三方 SDK,在編譯連結時靜態庫的程式碼段、資料段和符號表等會被拼接到 App 的主二進位制中,在拼接過程中靜態庫內程式碼段與資料段的相對位置會發生改變,導致原來程式碼中的相對定址不能正確指向連結後產物中的資料,這就需要在連結時根據靜態庫插入主二進位制的情況對程式碼段進行修正,這一過程被稱為 Relocation Fixup,它是保證靜態連結後邏輯正確性的關鍵。

本文將介紹靜態庫的 Relocation 段,以及靜態連結時基於 Relocation Info 對 __TEXT,__text 段進行 Fixup 的過程和原理。

一個例子

下面我們來看一個簡單例子,我們先新建一個 iOS 工程命名為 SimpleApp,再新建一個 Static Library 的 Target 命名為 SimpleLib,靜態庫裡只包含一個靜態全域性變數和一個讀取全域性變數的函式:

// SimpleLib.m
static int simple_val = 100;
int getSimpleVal() {
    return simple_val;
}
複製程式碼

App 的主二進位制連結了 SimpleLib,並通過 getSimpleVal 函式讀取 SimpleLib 中的靜態全域性變數:

// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    extern int getSimpleVal(void);
    printf("the value of simple val is %d\n",getSimpleVal());
}
複製程式碼

如果僅僅在 SimpleLib 內部考慮,不考慮編譯成靜態庫供外部使用,simple_val 變數的存取可以通過下面的方式實現:

.section    __TEXT,__text,regular,pure_instructions
.globl	_getSimpleVal
.p2align
2 _getSimpleVal: adrp x8,_simple_val@PAGE add x8,x8,_simple_val@PAGEOFF ldr w0,[x8] ret .section __DATA,__data .p2align 2 _simple_val: .long 100 複製程式碼

連結時的段重排

由於 simple_val 被定義在 __DATA,__data 段,在程式碼段通過 adrp & add 進行 PC-relative Addressing 來讀取變數的值,這是毫無疑問的。

但問題是當 SimpleLib 被連結到 SimpleApp 時,程式碼段與資料段將會分別進行合併,也就是說 SimpleLib 原來的段之間發生了重排,這會導致 __TEXT,__text 段中的語句與 __DATA,__data 段中的資料之間的相對距離已經發生變化,原來編譯出的 adrp & add 指令對已經失效,必須進行修正。

靜態庫的程式碼段

上文中的彙編程式碼是在不考慮編譯成靜態庫的情況下生成的,下面我們將實際的 SimpleLib.a 反彙編,看一下 _getSimpleVal 的程式碼段內容。

最簡單的辦法是使用 LLVM 工具鏈中的 otool,在 Shell 中輸入 otool -tv SimpleLib.a,下面是反彙編的結果:

(__TEXT,__text) section
_getSimpleVal:
0000000000000000	adrp	x8,0
0000000000000004	add	x8,#0x0
0000000000000008	ldr	w0,[x8]
000000000000000c	ret
複製程式碼

可以看到 adrp & add 指令對操作的地址都被寫成了 0,顯然是無法正常取址的,這應該是系統考慮到靜態庫無法單獨工作,因此不必再計算無謂的內部段之間的相對地址,將地址計算這一過程滯後到了連結時,具體的 Fixup 方式我們稍後會詳細介紹。

連結後的變化

最後我們來觀察一下靜態庫連結後主二進位制中程式碼段的內容:

(__TEXT,__text) section
_getSimpleVal:
0000000100006a3c	adrp	x8,#0x10000c000
0000000100006a40	add	x8,#0xd88
0000000100006a44	ldr	w0,[x8]
0000000100006a48	ret
複製程式碼

可以看到這時的 adrp & add 已經被 Fixup 成了真實地址 0x10000cd88,為了驗證地址的正確性,我們可以再通過 otool -dv 獲取到 __DATA,__data 段的內容:

000000010000cd80	00000000 00000000 00000064
複製程式碼

可以看到 cd80 中儲存的 0x64 正是 simple_val 的初值 100,除了這種方式,我們還可以通過 MachOView 檢視二進位制的符號表來直觀驗證變數地址。

由此可見,靜態庫在連線時會對其程式碼段中涉及到 PC-relative Addressing 的部分進行 Fixup,那麼連結器是如何知道哪些程式碼需要修正,以及如何完成修正呢?

靜態連結中的 Fixup

Relocation 段

我們通過 MachOView 開啟 SimpleLib.a 可以發現在靜態庫的二進位制中包含了一個名為 Relocation 的 Section,開啟它的內容如下:

根據 mach-o/reloc.h 中的結構體定義:

struct relocation_info {
   int32_t	r_address;	/* offset in the section to what is being
				   relocated */
   uint32_t     r_symbolnum:24,/* symbol index if r_extern == 1 or section
				   ordinal if r_extern == 0 */
		r_pcrel:1,/* was relocated pc relative already */
		r_length:2,/* 0=byte,1=word,2=long,3=quad */
		r_extern:1,/* does not include value of sym referenced */
		r_type:4;	/* if not 0,machine specific relocation type */
};
複製程式碼

我們可以知道每個 Relocation Info 的長度為 8 位元組,前 4 位元組指明瞭要 Fixup 的段偏移,後 4 個位元組指明瞭 Fixup 方式,例如上圖中的例子,指明瞭要對 __TEXT,__text 中偏移量為 +0 和 +4 的語句進行 Fixup,且 Fixup 對應的目標符號在符號表中的索引號為 #1,我們開啟符號表可以看到,#1 正式 simple_val 這個變數的符號。

修復MachOView 的顯示問題

有可能你在自己電腦上用 MachOView 檢視 Relocation 段時發現內容缺失,這似乎是 MachOView 的一個 bug,這裡有一個簡單的修復辦法,但只能保證你正常檢視 Symbol Index 和 Fixup Address,其他功能不一定正常,因此只是個臨時方案

開啟 MachOView 工程,搜尋方法 createReloc64Node,可以看到在方法的開頭對 relocation_info->r_address 進行了解析,隨後的複雜解析可能由於系統升級等原因導致了相容異常,我們可以在 if (relocation_info->r_extern) 語句前插入對 Symbol Index 的解析並 continue 來臨時解決 Relocation 段的檢視問題:

[node.details appendRow:[NSString stringWithFormat:@"%.8lX",range.location]
                           :lastReadHex
                           :@"Address"
                           :[NSString stringWithFormat:@"0x%qX",relocation_info->r_address + baseAddress]];

// read the second half of the entry
[dataController read_uint32:range lastReadHex:&lastReadHex];
  
/** INSERT CODE BEGAN */
[node.details appendRow:[NSString stringWithFormat:@"%.8lX",range.location]
                                                  :lastReadHex
                                                  :@"Symbol Index"
                                                  :[NSString stringWithFormat:@"%d",relocation_info->r_symbolnum]];
  
continue;
/** INSERT CODE END */

//========================================================================
if (relocation_info->r_extern) {
// omit codes ...
複製程式碼

基於 Relocation 段的 Fixup

Relocation 中的關鍵資訊主要有三部分:段、段偏移和對應的符號。

定位段的 Relocation Info

Load Commands 中每個段都有其對應的 Relocation Offset & Number of Relocations 來宣告段是否有對應的 Relocation Fixup,例如 __TEXT,__text 段的 Section Header 內容如下:

它指明瞭程式碼段的 Fixup 位於二進位制偏移量 2980 處,轉化為十六進位制為 0xBA4,需要注意這個值需要加上 SimpleLib.o 的基礎偏移量 0xC8,最後結果為 0xBA4 + 0xC8 = 0xC6C,與上文中給出的程式碼段 Relocation 偏移量一致。

執行 Relocation Fixup

每一個 Relocation Entry 對應的是相應段中的一個單元,以程式碼段為例,每一個 Relocation Entry 對應的是一條指令,我們上面的取值包含兩條指令:

0000000000000000	adrp	x8,#0x0
複製程式碼

因此需要對程式碼段 +0 和 +4 兩處進行 Fixup,因此包含了 2 個 Relocation Entry 如下圖所示:

這兩條 Relocation Info 指明瞭程式碼段 +0 和 +4 兩條指令是對 #1 符號的定址操作,需要在靜態連結時對他們進行 Fixup,Fixup 過程大致為重新計算連結後資料段中 #1 符號的新地址,並將其的高 21 和低 12 位拆開,分別對 adrp 和 add 指令進行 Fixup,如果你對 Fixup 的具體過程感興趣,可以去 LLVM 原始碼中查詢連結的相關邏輯閱讀。

總結

由於靜態連結時需要將靜態庫的各段拆分合併到主二進位制中,導致了需要對靜態庫內 PC-relative Addressing 進行 Relocation,Relocation Info 被包含在靜態庫內,在連結時連結器會根據 Relocation Info 來 Fixup 拼接到主二進位制的靜態庫程式碼。同樣的在動態庫連結時也有執行時的 Rebase & Bind 過程,將在後面的文章中介紹。