關注倉庫,及時得到更新:iOS-Source-Code-Analyzegit
Follow: Draveness · Githubgithub
Objective-C 做爲基於 Runtime 的語言,它有很是強大的動態特性,能夠在運行期間自省、進行方法調劑、爲類增長屬性、修改消息轉發鏈路,在代碼運行期間經過 Runtime 幾乎能夠修改 Objecitve-C 層的一切類、方法以及屬性。shell
真正絕對意義上的動態語言或者靜態語言是不存在的。數組
C 語言每每會給咱們留下不可修改的這一印象;在以前的幾年時間裏,筆者確實也是這麼認爲的,然而最近接觸到的 fishhook 使我對 C 語言的不可修改有了更加深入的理解。數據結構
在文章中涉及到一個比較重要的概念,就是鏡像(image);在 Mach-O 文件系統中,全部的可執行文件、dylib 以及 Bundle 都是鏡像。app
到這裏,咱們該簡單介紹一下今天分享的 fishhook;fishhook 是一個由 facebook 開源的第三方框架,其主要做用就是動態修改 C 語言函數實現。框架
這個框架的代碼其實很是的簡單,只包含兩個文件:fishhook.c
以及 fishhook.h
;兩個文件全部的代碼加起來也不超過 300 行。ide
不過它的實現原理是很是有意思而且精妙的,咱們能夠從 fishhook
提供的接口中入手。函數
fishhook 提供很是簡單的兩個接口以及一個結構體:oop
struct rebinding { const char *name; void *replacement; void **replaced; }; int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel); int rebind_symbols_image(void *header, intptr_t slide, struct rebinding rebindings[], size_t rebindings_nel);
其中 rebind_symbols
接收一個 rebindings
數組,也就是從新綁定信息,還有就是 rebindings_nel
,也就是 rebindings
的個數。
使用 fishhook 修改 C 函數很容易,咱們使用它提供的幾個範例來介紹它的使用方法。
這裏要修改的是底層的 open
函數的實現,首先在工程中引入 fishhook.h
頭文件,而後聲明一個與原函數簽名相同的函數指針:
#import "fishhook.h" static int (*origianl_open)(const char *, int, ...);
而後從新實現 new_open
函數:
int new_open(const char *path, int oflag, ...) { va_list ap = {0}; mode_t mode = 0; if ((oflag & O_CREAT) != 0) { // mode only applies to O_CREAT va_start(ap, oflag); mode = va_arg(ap, int); va_end(ap); printf("Calling real open('%s', %d, %d)\n", path, oflag, mode); return orig_open(path, oflag, mode); } else { printf("Calling real open('%s', %d)\n", path, oflag); return orig_open(path, oflag, mode); } }
這裏調用的 original_open
其實至關於執行原 open
;最後,在 main 函數中使用 rebind_symbols
對符號進行重綁定:
// 初始化一個 rebinding 結構體 struct rebinding open_rebinding = { "open", new_open, (void *)&original_open }; // 將結構體包裝成數組,並傳入數組的大小,對原符號 open 進行重綁定 rebind_symbols((struct rebinding[1]){open_rebinding}, 1); // 調用 open 函數 __unused int fd = open(argv[0], O_RDONLY);
在對符號進行重綁定以後,全部調用 open
函數的地方實際上都會執行 new_open
的實現,也就完成了對 open
的修改。
程序運行以後打印了 Calling real open('/Users/apple/Library/Developer/Xcode/DerivedData/Demo-cdnoozusghmqtubdnbzedzdwaagp/Build/Products/Debug/Demo', 0)
說明咱們的對 open
函數的修改達到了預期的效果。
整個 main.m 文件中的代碼在文章的最後面 main.m
在介紹 fishhook 具體實現原理以前,有幾個很是重要的知識須要咱們瞭解,那就是 dyld、動態連接以及 Mach-O 文件系統。
dyld 是 the dynamic link editor 的縮寫(筆者並不知道爲何要這麼縮寫)。至於它的做用,簡單一點說,就是負責將各類各樣程序須要的鏡像加載到程序運行的內存空間中,這個過程發生的時間很是早 --- 在 objc 運行時初始化以前。
在 dyld 加載鏡像時,會執行註冊過的回調函數;固然,咱們也可使用下面的方法註冊自定義的回調函數,同時也會爲全部已經加載的鏡像執行回調:
extern void _dyld_register_func_for_add_image( void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide) );
對於每個已經存在的鏡像,當它被動態連接時,都會執行回調 void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)
,傳入文件的 mach_header
以及一個虛擬內存地址 intptr_t
。
須要注意的是,dyld 只負責將一些須要動態連接的庫加載進來,好比說 C 語言標準庫,或者 Foundation 這些 ObjC 提供的庫;而咱們本身在程序中寫的代碼是不會經過 dyld 進行加載的。
以一個最簡單的 Hello World 程序爲例:
#include <stdio.h> int main(int argc, const char * argv[]) { printf("Hello, World!\n"); return 0; }
代碼中只引用了一個 stdio
庫中的函數 printf
;咱們若是 Build 這段代碼,生成可執行文件以後,使用下面的命令 nm
:
$ nm -nm HelloWorld
nm
命令能夠查看可執行文件中的符號(對 nm
不熟悉的讀者能夠在終端中使用 man nm
查看手冊):
(undefined) external _printf (from libSystem) (undefined) external dyld_stub_binder (from libSystem) 0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header 0000000100000f50 (__TEXT,__text) external _main
在可執行文件中的符號列表中,_printf
這個符號是未定義(undefined)的,換句話說,編譯器還不知道這個符號對應什麼東西。
可是,若是在文件中加入一個 C 函數 hello_world
:
#include <stdio.h> void hello_world() { printf("Hello, World!\n"); } int main(int argc, const char * argv[]) { printf("Hello, World!\n"); return 0; }
在構建以後,一樣使用 nm
查看其中的符號:
(undefined) external _printf (from libSystem) (undefined) external dyld_stub_binder (from libSystem) 0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header 0000000100000f30 (__TEXT,__text) external _hello_world 0000000100000f50 (__TEXT,__text) external _main
咱們的符號 _hello_world
並非未定義的(undefined),它包含一個內存地址以及 __TEXT
段。也就是說手寫的一些函數,在編譯以後,其地址並非未定義的,這一點對於以後分析 fishhook 有所幫助。
使用 nm
打印出的另外一個符號 dyld_stub_binder
對應另外一個同名函數。dyld_stub_binder
會在目標符號(例如 printf
)被調用時,將其連接到指定的動態連接庫 libSystem
,再執行 printf
的實現(printf
符號位於 __DATA
端中的 lazy 符號表中):
每個鏡像中的 __DATA
端都包含兩個與動態連接有關的表,其中一個是 __nl_symbol_ptr
,另外一個是 __la_symbol_ptr
:
__nl_symbol_ptr
中的 non-lazy 符號是在動態連接庫綁定的時候進行加載的
__la_symbol_ptr
中的符號會在該符號被第一次調用時,經過 dyld 中的 dyld_stub_binder
過程來進行加載
0000000100001010 dq 0x0000000100000f9c ; XREF=0x1000002f8, imp___stubs__printf
地址 0x0000000100000f9c
就是 printf
函數打印字符串實現的位置:
在上述代碼調用 printf
時,因爲符號是沒有被加載的,就會經過 dyld_stub_binder
動態綁定符號。
因爲文章中會涉及一些關於 Mach-O 文件格式的知識,因此在這裏會簡單介紹一下 Mach-O 文件格式的結構。
每個 Mach-O 文件都會被分爲不一樣的 Segments,好比 __TEXT
, __DATA
, __LINKEDIT
:
這也就是 Mach-O 中的 segment_command
(32 位與 64 位不一樣):
struct segment_command_64 { /* for 64-bit architectures */ uint32_t cmd; /* LC_SEGMENT_64 */ uint32_t cmdsize; /* includes sizeof section_64 structs */ char segname[16]; /* segment name */ uint64_t vmaddr; /* memory address of this segment */ uint64_t vmsize; /* memory size of this segment */ uint64_t fileoff; /* file offset of this segment */ uint64_t filesize; /* amount to map from the file */ vm_prot_t maxprot; /* maximum VM protection */ vm_prot_t initprot; /* initial VM protection */ uint32_t nsects; /* number of sections in segment */ uint32_t flags; /* flags */ };
而每個 segment_command
中又包含了不一樣的 section
:
struct section_64 { /* for 64-bit architectures */ char sectname[16]; /* name of this section */ char segname[16]; /* segment this section goes in */ uint64_t addr; /* memory address of this section */ uint64_t size; /* size in bytes of this section */ uint32_t offset; /* file offset of this section */ uint32_t align; /* section alignment (power of 2) */ uint32_t reloff; /* file offset of relocation entries */ uint32_t nreloc; /* number of relocation entries */ uint32_t flags; /* flags (section type and attributes)*/ uint32_t reserved1; /* reserved (for offset or index) */ uint32_t reserved2; /* reserved (for count or sizeof) */ uint32_t reserved3; /* reserved */ };
你只須要對這幾個概念有一個簡單的瞭解,知道它們有怎樣的包含關係,當文章中挑出這個名字時,對它不是一無所知就足夠了,這裏並不會涉及太多相關的知識。
到目前爲止,咱們對 dyld 以及 Mach-O 有了一個初步的瞭解,而 fishhook 使用了前面章節提到的 _dyld_register_func_for_add_image
註冊了一個回調,在每次加載鏡像到程序中執行回調,動態修改 C 函數實現。
在具體分析其源代碼以前,先爲各位讀者詳細地介紹它的實現原理:
dyld 經過更新 Mach-O 二進制文件 __DATA
段中的一些指針來綁定 lazy 和 non-lazy 的符號;而 fishhook 先肯定某一個符號在 __DATA
段中的位置,而後保存原符號對應的函數指針,並使用新的函數指針覆蓋原有符號的函數指針,實現重綁定。
整個過程能夠用這麼一張圖來表示:
原理看起來仍是很簡單的,其中最複雜的部分就是從二進制文件中尋找某個符號的位置,在 fishhook 的 README 中,有這樣一張圖:
這張圖初看很複雜,不過它演示的是尋找符號的過程,咱們根據這張圖來分析一下這個過程:
從 __DATA
段中的 lazy 符號指針表中查找某個符號,得到這個符號的偏移量 1061
,而後在每個 section_64
中查找 reserved1
,經過這兩個值找到 Indirect Symbol Table 中符號對應的條目
在 Indirect Symbol Table 找到符號表指針以及對應的索引 16343
以後,就須要訪問符號表
而後經過符號表中的偏移量,獲取字符串表中的符號 _close
上面梳理了尋找符號的過程,如今,咱們終於要開始分析 fishhook 的源代碼,看它是如何一步一步替換原有函數實現的。
對實現的分析會 rebind_symbols
函數爲入口,首先看一下函數的調用棧:
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel); └── extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)); static void _rebind_symbols_for_image(const struct mach_header *header, intptr_t slide) └── static void rebind_symbols_for_image(struct rebindings_entry *rebindings, const struct mach_header *header, intptr_t slide) └── static void perform_rebinding_with_section(struct rebindings_entry *rebindings, section_t *section, intptr_t slide, nlist_t *symtab, char *strtab, uint32_t *indirect_symtab)
其實函數調用棧很是簡單,由於整個庫中也沒有幾個函數,rebind_symbols
做爲接口,其主要做用就是註冊一個函數並在鏡像加載時回調:
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) { int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel); if (retval < 0) return retval; if (!_rebindings_head->next) { _dyld_register_func_for_add_image(_rebind_symbols_for_image); } else { uint32_t c = _dyld_image_count(); for (uint32_t i = 0; i < c; i++) { _rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i)); } } return retval; }
在 rebind_symbols
最開始執行時,會先調用一個 prepend_rebindings
的函數,將整個 rebindings
數組添加到 _rebindings_head
這個私有數據結構的頭部:
static int prepend_rebindings(struct rebindings_entry **rebindings_head, struct rebinding rebindings[], size_t nel) { struct rebindings_entry *new_entry = malloc(sizeof(struct rebindings_entry)); if (!new_entry) { return -1; } new_entry->rebindings = malloc(sizeof(struct rebinding) * nel); if (!new_entry->rebindings) { free(new_entry); return -1; } memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel); new_entry->rebindings_nel = nel; new_entry->next = *rebindings_head; *rebindings_head = new_entry; return 0; }
也就是說每次調用的 rebind_symbols
方法傳入的 rebindings
數組以及數組的長度都會以 rebindings_entry
的形式添加到 _rebindings_head
這個私有鏈表的首部:
struct rebindings_entry { struct rebinding *rebindings; size_t rebindings_nel; struct rebindings_entry *next; }; static struct rebindings_entry *_rebindings_head;
這樣能夠經過判斷 _rebindings_head->next
的值來判斷是否爲第一次調用,而後使用 _dyld_register_func_for_add_image
將 _rebind_symbols_for_image
註冊爲回調或者爲全部存在的鏡像單獨調用 _rebind_symbols_for_image
:
static void _rebind_symbols_for_image(const struct mach_header *header, intptr_t slide) { rebind_symbols_for_image(_rebindings_head, header, slide); }
_rebind_symbols_for_image
只是對另外一個名字很是類似的函數 rebind_symbols_for_image
的封裝,從這個函數開始,就到了重綁定符號的過程;不過因爲這個方法的實現比較長,具體分析會分紅三個部分並省略一些不影響理解的代碼:
static void rebind_symbols_for_image(struct rebindings_entry *rebindings, const struct mach_header *header, intptr_t slide) { segment_command_t *cur_seg_cmd; segment_command_t *linkedit_segment = NULL; struct symtab_command* symtab_cmd = NULL; struct dysymtab_command* dysymtab_cmd = NULL; uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t); for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) { cur_seg_cmd = (segment_command_t *)cur; if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) { if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) { linkedit_segment = cur_seg_cmd; } } else if (cur_seg_cmd->cmd == LC_SYMTAB) { symtab_cmd = (struct symtab_command*)cur_seg_cmd; } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) { dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd; } } ... }
這部分的代碼主要功能是從鏡像中查找 linkedit_segment
symtab_command
和 dysymtab_command
;在開始查找以前,要先跳過 mach_header_t
長度的位置,而後將當前指針強轉成 segment_command_t
,經過對比 cmd
的值,來找到須要的 segment_command_t
。
在查找了幾個關鍵的 segment 以後,咱們能夠根據幾個 segment 獲取對應表的內存地址:
static void rebind_symbols_for_image(struct rebindings_entry *rebindings, const struct mach_header *header, intptr_t slide) { ... uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff; nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff); char *strtab = (char *)(linkedit_base + symtab_cmd->stroff); uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff); ... }
在 linkedit_segment
結構體中得到其虛擬地址以及文件偏移量,而後經過一下公式來計算當前 __LINKEDIT
段的位置:
slide + vmaffr - fileoff
相似地,在 symtab_command
中獲取符號表偏移量和字符串表偏移量,從 dysymtab_command
中獲取間接符號表(indirect symbol table)偏移量,就可以得到_符號表_、_字符串表_以及_間接符號表_的引用了。
間接符號表中的元素都是 uint32_t *
,指針的值是對應條目 n_list
在符號表中的位置
符號表中的元素都是 nlist_t
結構體,其中包含了當前符號在字符串表中的下標
struct nlist_64 { union { uint32_t n_strx; /* index into the string table */ } n_un; uint8_t n_type; /* type flag, see below */ uint8_t n_sect; /* section number or NO_SECT */ uint16_t n_desc; /* see <mach-o/stab.h> */ uint64_t n_value; /* value of this symbol (or stab offset) */ };
字符串表中的元素是 char
字符
該函數的最後一部分就開啓了遍歷模式,查找整個鏡像中的 SECTION_TYPE
爲 S_LAZY_SYMBOL_POINTERS
或者 S_NON_LAZY_SYMBOL_POINTERS
的 section,而後調用下一個函數 perform_rebinding_with_section
來對 section 中的符號進行處理:
static void perform_rebinding_with_section(struct rebindings_entry *rebindings, section_t *section, intptr_t slide, nlist_t *symtab, char *strtab, uint32_t *indirect_symtab) { uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1; void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr); for (uint i = 0; i < section->size / sizeof(void *); i++) { uint32_t symtab_index = indirect_symbol_indices[i]; uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx; char *symbol_name = strtab + strtab_offset; struct rebindings_entry *cur = rebindings; while (cur) { for (uint j = 0; j < cur->rebindings_nel; j++) { if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) { if (cur->rebindings[j].replaced != NULL && indirect_symbol_bindings[i] != cur->rebindings[j].replacement) { *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]; } indirect_symbol_bindings[i] = cur->rebindings[j].replacement; goto symbol_loop; } } cur = cur->next; } symbol_loop:; } }
該函數的實現的核心內容就是將符號表中的 symbol_name
與 rebinding
中的名字 name
進行比較,若是出現了匹配,就會將原函數的實現傳入 origian_open
函數指針的地址,並使用新的函數實現 new_open
代替原實現:
if (cur->rebindings[j].replaced != NULL && indirect_symbol_bindings[i] != cur->rebindings[j].replacement) { *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]; // 將原函數的實現傳入 original_open 函數指針的地址 } indirect_symbol_bindings[i] = cur->rebindings[j].replacement; // 使用新的函數實現 new_open 替換原實現
若是你理解了上面的實現代碼,該函數的其它代碼就很好理解了:
經過 indirect_symtab + section->reserved1
獲取 indirect_symbol_indices *
,也就是符號表的數組
經過 (void **)((uintptr_t)slide + section->addr)
獲取函數指針列表 indirect_symbol_bindings
遍歷符號表數組 indirect_symbol_indices *
中的全部符號表中,獲取其中的符號表索引 symtab_index
經過符號表索引 symtab_index
獲取符號表中某一個 n_list
結構體,獲得字符串表中的索引 symtab[symtab_index].n_un.n_strx
最後在字符串表中得到符號的名字 char *symbol_name
到這裏比較前的準備工做就完成了,剩下的代碼會遍歷整個 rebindings_entry
數組,在其中查找匹配的符號,完成函數實現的替換:
while (cur) { for (uint j = 0; j < cur->rebindings_nel; j++) { if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) { if (cur->rebindings[j].replaced != NULL && indirect_symbol_bindings[i] != cur->rebindings[j].replacement) { *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]; } indirect_symbol_bindings[i] = cur->rebindings[j].replacement; goto symbol_loop; } } cur = cur->next; }
在以後對某一函數的調用(例如 open
),當查找其函數實現時,都會查找到 new_open
的函數指針;在 new_open
調用 origianl_open
時,一樣也會執行原有的函數實現,由於咱們經過 *(cur->rebindings[j].replaced) = indirect_symbol_bindings[i]
將原函數實現綁定到了新的函數指針上。
fishhook 在 dyld
加載鏡像時,插入了一個回調函數,交換了原有函數的實現;可是 fishhook 可否修改非動態連接庫,好比開發人員本身手寫的函數呢?咱們能夠作一個很是簡單的小實驗,下面是咱們的 main.m
文件:
#import <Foundation/Foundation.h> #import "fishhook.h" void hello() { printf("hello\n"); } static void (*original_hello)(); void new_hello() { printf("New_hello\n"); original_hello(); } int main(int argc, const char * argv[]) { @autoreleasepool { struct rebinding open_rebinding = { "hello", new_hello, (void *)&original_hello }; rebind_symbols((struct rebinding[1]){open_rebinding}, 1); hello(); } return 0; }
這裏的函數實現很是的簡單,相信也不須要筆者過多解釋了,咱們直接運行這份代碼:
代碼中只打印了 hello
,說明 fishhook 對這種手寫的函數是沒有做用的,若是在下面這裏打一個斷點:
代碼並不會進這裏,由於 hello 這個函數並不在任何的鏡像中存在,這也符合在最開始咱們研究 dyld 時得出的結論:
dyld 只負責將一些須要動態連接的庫加載進來,好比說 C 語言標準庫,或者 Foundation 這些 ObjC 提供的庫;而咱們本身在程序中寫的代碼是不會經過 dyld 進行加載的,也就沒法修改其實現。
fishhook 的實現很是的巧妙,可是它的使用也有必定的侷限性,在接觸到 fishhook 以前,從沒有想到過能夠經過一種方式修改 C 函數的實現,在筆者的印象中,C 語言做爲靜態語言。
main.m
#import <Foundation/Foundation.h> #import "fishhook.h" static int (*original_open)(const char *, int, ...); int new_open(const char *path, int oflag, ...) { va_list ap = {0}; mode_t mode = 0; if ((oflag & O_CREAT) != 0) { // mode only applies to O_CREAT va_start(ap, oflag); mode = va_arg(ap, int); va_end(ap); printf("Calling real open('%s', %d, %d)\n", path, oflag, mode); return original_open(path, oflag, mode); } else { printf("Calling real open('%s', %d)\n", path, oflag); return original_open(path, oflag, mode); } } int main(int argc, const char * argv[]) { @autoreleasepool { struct rebinding open_rebinding = { "open", new_open, (void *)&original_open }; rebind_symbols((struct rebinding[1]){open_rebinding}, 1); __unused int fd = open(argv[0], O_RDONLY); } return 0; }
關注倉庫,及時得到更新:iOS-Source-Code-Analyze
Follow: Draveness · Github