Mach-O的动态链接相关知识
0x00 摘要
通過分析Mach-O的動態鏈接過程,加深對Mach-O文件結構的理解。對Mach-O文件格式的簡單的分析看這里這里。
0x01 Mach-O Lazy Bind
Mach-O文件的通過dyld加載的時候并沒有確定每一個函數的具體地址在哪里,而是在真正調用該函數的時候通過過程連接表(procedure linkage table),后面簡稱PLT,來進行一次lazybind。
結合Mach-O文件的分析與代碼的調試簡單的分析一下,只能算是管中窺豹了。
源碼很簡單。
| 1 2 3 4 5 6 7 8 | #include <stdio.h> int main(int argc, const char * argv[]) { // insert code here... printf("Hello, World!\n"); printf("2Hello, World!\n"); return 0; } |
分別在兩個printf函數處下斷點,啟動程序。
1.1 第一次調用prinf
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | lazy_bind`main: 0x100000f10 <+0>: pushq %rbp 0x100000f11 <+1>: movq %rsp, %rbp 0x100000f14 <+4>: subq $0x20, %rsp 0x100000f18 <+8>: leaq 0x57(%rip), %rax ; "Hello, World!\n" 0x100000f1f <+15>: movl $0x0, -0x4(%rbp) 0x100000f26 <+22>: movl %edi, -0x8(%rbp) 0x100000f29 <+25>: movq %rsi, -0x10(%rbp) 0x100000f2d <+29>: movq %rax, %rdi 0x100000f30 <+32>: movb $0x0, %al -> 0x100000f32 <+34>: callq 0x100000f56 ; symbol stub for: printf 0x100000f37 <+39>: leaq 0x47(%rip), %rdi ; "2Hello, World!\n" 0x100000f3e <+46>: movl %eax, -0x14(%rbp) 0x100000f41 <+49>: movb $0x0, %al 0x100000f43 <+51>: callq 0x100000f56 ; symbol stub for: printf 0x100000f48 <+56>: xorl %ecx, %ecx 0x100000f4a <+58>: movl %eax, -0x18(%rbp) 0x100000f4d <+61>: movl %ecx, %eax 0x100000f4f <+63>: addq $0x20, %rsp 0x100000f53 <+67>: popq %rbp 0x100000f54 <+68>: retq |
在0x100000f52 <+34>行處通過callq 0x100000f64來調用printf。
執行callq指令之后代碼跳轉到這里:
| 1 2 | lazy_bind`printf: -> 0x100000f56 <+0>: jmpq *0xb4(%rip) ; (void *)0x0000000100000f6c |
1.2 __Data,__la_symbol_ptr 獲取函數地址
這里的jmpq要跳轉到0x0000000100000f6c這個地址是從__Data,__la_symbol_ptr中的Lazy Symbol Pointers中獲取到的。(怎么來的不是很清楚?)希望通過stub來調用printf函數。
通過命令行查看0x100001010處的地址獲得了同樣的值。
| 1 2 3 | (lldb) x 0x100001010 0x100001010: 6c 0f 00 00 01 00 00 00 00 00 00 00 00 00 00 00 l............... 0x100001020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ |
1.3 通過__stub__helper進行lazybind
在Mach-O中每一個Symbol Stub可能有以下兩種行為其中之一:
- 跳轉到函數的指令,執行函數體
- 通過動態鏈接器查找函數的Symbol(符號),然后執行函數。
通過工具查看__stubs的Section數據,發現只有一個函數就是_printf。
這里的Data其實就是上面看到的jmpq代碼。執行之后代碼跳轉到了這樣的代碼片段。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | -> 0x100000f6c: pushq $0x0 0x100000f71: jmp 0x100000f5c 0x100000f76: gs ;這里往下都沒有!!!! 0x100000f78: insb %dx, %es:(%rdi) 0x100000f79: insb %dx, %es:(%rdi) 0x100000f7a: outsl (%rsi), %dx 0x100000f7b: subb $0x20, %al 0x100000f7d: pushq %rdi 0x100000f7e: outsl (%rsi), %dx 0x100000f7f: jb 0x100000fed 0x100000f81: andl %ecx, %fs:(%rdx) 0x100000f84: addb %dh, (%rdx) 0x100000f86: gs 0x100000f88: insb %dx, %es:(%rdi) 0x100000f89: insb %dx, %es:(%rdi) 0x100000f8a: outsl (%rsi), %dx |
這里就是通過_stub_helper來調用dyld_stubbinder函數來計算printf函數的真實地址。通過下面的\_TEXT,__stub_helper具體信息可以看出,jmpq 0x100000f5c,就是在壓入參數0x0(函數的link的時候給的編號)之后跳轉到Section的起始處,調用了binder。
binder是一塊匯編代碼。這里就不做解釋了。作用就是計算具體的函數地址,并調用printf。
1.4 第二次調用printf函數
這個釋放斷點,程序將在調用第二個printf函數的地方停下來。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | lazy_bind`main: 0x100000f10 <+0>: pushq %rbp 0x100000f11 <+1>: movq %rsp, %rbp 0x100000f14 <+4>: subq $0x20, %rsp 0x100000f18 <+8>: leaq 0x57(%rip), %rax ; "Hello, World!\n" 0x100000f1f <+15>: movl $0x0, -0x4(%rbp) 0x100000f26 <+22>: movl %edi, -0x8(%rbp) 0x100000f29 <+25>: movq %rsi, -0x10(%rbp) 0x100000f2d <+29>: movq %rax, %rdi 0x100000f30 <+32>: movb $0x0, %al 0x100000f32 <+34>: callq 0x100000f56 ; symbol stub for: printf 0x100000f37 <+39>: leaq 0x47(%rip), %rdi ; "2Hello, World!\n" 0x100000f3e <+46>: movl %eax, -0x14(%rbp) 0x100000f41 <+49>: movb $0x0, %al -> 0x100000f43 <+51>: callq 0x100000f56 ; symbol stub for: printf 0x100000f48 <+56>: xorl %ecx, %ecx 0x100000f4a <+58>: movl %eax, -0x18(%rbp) 0x100000f4d <+61>: movl %ecx, %eax 0x100000f4f <+63>: addq $0x20, %rsp 0x100000f53 <+67>: popq %rbp 0x100000f54 <+68>: retq |
執行指令之后發現和第一次調用printf已經不一樣了。
| 1 2 | lazy_bind`printf: -> 0x100000f56 <+0>: jmpq *0xb4(%rip) ; (void *)0x00007fff96b1815c: printf |
通過指令再一次查看0x100001010處的內存值。
| 1 2 3 | (lldb) x 0x100001010 0x100001010: 5c 81 b1 96 ff 7f 00 00 00 00 00 00 00 00 00 00 \............... 0x100001020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ |
也就是說 __Data,__la_symbol_ptr中指向printf地址的值已經發生了變化,指向了printf的指令。
1.5 小結
證明了,延遲綁定只會在第一次調用的時候發生。整個流程與linux中的PLT與GOT在實現邏輯上基本是相同的,只是具體的代碼實現不一樣。
0x02 相關LoadCommand
上面只是通過調試簡單的了解了動態鏈接的表現,要理解動態鏈接還需對幾個數據結構有所了解。
2.1 LC_SYMTAB
LC_SYMTAB這個LoadCommand主要提供了兩個信息
- Symbol Table的偏移量與Symbol Table中元素的個數
- String Table的偏移量與String Table的長度
2.1.1 Symbol Table
在計算機科學中,符號表是一種用于語言翻譯器(例如編譯器和解釋器)中的數據結構。在符號表中,程序源代碼中的每個標識符都和它的聲明或使用信息綁定在一起,比如其數據類型、作用域以及內存地址。
–wiki
簡單的理解就是Symbol Table里面包含了所有會被調用的函數的信息,無論是已經bind的還是沒有bind的函數。
2.2.2 String Table
這個很好理解,在符號處理時所有會用到的字符串放在了這里。和__TEXT,__cstring不同。
2.2 LC_DYSYMTAB
LC_DYSYMTAB的數據結構,如圖所示。這一個LoadCommand與動態鏈接相關的就是紅框標出的兩個字段,標示了需要動態符號表的偏移量與符號個數。
動態符號表的數據結構非常的簡單,是一個32bit的索引的數組。通過索引可以在Symbol Table中尋找到對應的函數信息。
0x03 小結
通過分析兩次printf的調用流程,加深對Mach-O結構以及動態鏈接的流程理解,為進一步理解dyld的工作原理,源碼閱讀提供了知識的儲備。
通過和Linux的PTL與GOT比較可以更容易理解邏輯。
整個流程是如何通過代碼實現的還需要進一步的分析與研究。
0x04 參考
1.Dynamic Linking: ELF vs. Mach-O
http://timetobleed.com/dynamic-linking-elf-vs-mach-o/
2.Dynamic symbol table duel: ELF vs Mach-O, round 2
http://timetobleed.com/dynamic-symbol-table-duel-elf-vs-mach-o-round-2/
原文地址:http://turingh.github.io/2016/03/10/Mach-O%E7%9A%84%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5/
總結
以上是生活随笔為你收集整理的Mach-O的动态链接相关知识的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mach-o格式分析
- 下一篇: Android安全教程(3)---Fid