現在做iOS開發的挺多,瞭解一下在蘋果平臺上程式執行的原理
解析 MACH_O 檔案
這篇文章描述瞭如何解析 Mach-O 檔案並稍微解釋了一下它的格式。這不是一份權威指南,不過當你不知從何開始時,它可能有些幫助。想了解更多資訊,請考慮閱讀官方文件和作業系統提供的標頭檔案。
Macho-O 是什麼
維基百科 的簡單描述:
Mach-O 是 Mach object 檔案格式的縮寫,它是一種用於記錄可執行檔案、物件程式碼、共享庫、動態載入程式碼和記憶體轉儲的檔案格式。作為 a.out 格式的替代品,Mach-O 提供了更好的擴充套件性,並提升了符號表中資訊的訪問速度。
大多數基於 Mach 核心的作業系統都使用 Mach-O。NeXTSTEP、OS X 和 iOS 是使用這種格式作為本地可執行檔案、庫和物件程式碼的例子。
Mach-O 格式
Mach-O 沒有類似於 XML、YAML、JSON 等諸如此類的特殊格式,它只是一個二進位制位元組流,被劃分為了有意義的資料塊。這些塊包含元資訊,比如,位元組順序、cpu 型別、塊的大小,等等。
典型的 Mach-O 檔案(對應的 官方文件 )包含三個區域:
- 頭-包含該二進位制檔案的一般資訊:位元組順序、(魔數)、cpu 型別、載入指令的數量等等。
- 載入指令-它是一張包含很多內容的表,內容包括區域的位置、符號表、動態符號表等。每個載入指令都包含一個元資訊,比如指令型別、名稱、在二進位制檔案中的位置等等。
- 資料-通常是物件檔案中最大的部分。主要包含程式碼、資料,例如符號表,動態符號表等等。
這裡是一個簡化的圖形表示︰
OS X 有兩種型別的目標檔案:Mach-O 檔案和通用二進位制檔案,也叫作胖檔案。它們之間的區別是:Mach-O 檔案包含一種架構(i386、x86_64、arm64 等等)的物件程式碼,而胖檔案可能包含若干包含不同架構(i386、x86_64、arm、arm64 等等)物件程式碼的物件檔案。
胖檔案的結構相當簡單︰ 胖檔案頭以及後面的 Mach-O 檔案:
解析 Mach-O 檔案
OS X 沒有提供 libmacho
或任何類似的工具,我們唯一擁有的是一組定義在 /usr/include/mach-o/*
中的 C 結構體,因此我們需要自己實現解析。它可能非常棘手,但也並不是非常困難。
記憶體描述
在我們開始解析前,讓我們看看一個 Mach-O 檔案的詳細描述。簡單起見,下面的物件檔案是單個 i386 Mach-O 檔案(而不是胖檔案),它只包含兩個段型別的資料條目。
僅需下面的結構體我們就可以描述該檔案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
struct mach_header { uint32_t magic; cpu_type_t cputype; cpu_subtype_t cpusubtype; uint32_t filetype; uint32_t ncmds; uint32_t sizeofcmds; uint32_t flags; }; struct segment_command { uint32_t cmd; uint32_t cmdsize; char segname[16]; uint32_t vmaddr; uint32_t vmsize; uint32_t fileoff; uint32_t filesize; vm_prot_t maxprot; vm_prot_t initprot; uint32_t nsects; uint32_t flags; }; |
下面是記憶體對映的情況:
如果你想要從檔案中讀取特定的資訊,你只需要一個正確的資料結構和偏移量。
解析
讓我們來編寫一個程式,它能讀取 Mach-O 或 胖檔案 並列印每個段的名稱以及它編譯的目標架構。
結束時,我們可能會有類似這樣的東西︰
1 2 3 4 5 6 |
$ ./segname_dumper some_binary i386 segname __PAGEZERO segname __TEXT segname __LINKEDIT |
驅動
讓我們從一個簡單的“驅動”開始。
至少有兩種可用的方式來解析此類檔案︰ 載入檔案內容到記憶體中並直接處理緩衝區 或開啟一個檔案在其中來回跳轉。兩種方法都有自己的優點和缺點,但這裡我會選用第二種。此外,我假定沒有人會用錯誤的方式使用該程式,因此我沒有新增錯誤處理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <stdio.h> #include <stdlib.h> #include <mach-o/loader.h> #include <mach-o/swap.h> void dump_segments(FILE *obj_file); int main(int argc, char *argv[]) { const char *filename = argv[1]; FILE *obj_file = fopen(filename, "rb"); dump_segments(obj_file); fclose(obj_file); return 0; } void dump_segments(FILE *obj_file) { // Driver } |
魔數、CPU、位元組序
為了至少閱讀物件檔案的頭,我們需要得到我們需要的所有資訊︰ CPU 架構(32 位或 64 位) 和位元組順序。但首先我們需要取出一個魔數︰
1 2 3 4 5 6 7 8 9 10 |
uint32_t read_magic(FILE *obj_file, int offset) { uint32_t magic; fseek(obj_file, offset, SEEK_SET); fread(&magic, sizeof(uint32_t), 1, obj_file); return magic; } void dump_segments(FILE *obj_file) { uint32_t magic = read_magic(obj_file, 0); } |
函式 read_magic 是非常直截了當的,但有一件事可能看起來很怪︰ fseek。問題是,每當有人讀取檔案,檔案內部的偏移量都會發生改變。最好顯式指定偏移量,以確保我們閱讀到我們實際上想要讀取的內容。此外,這個小技巧稍後也會有用。
描述 32 位和 64 位物件檔案的結構體是不同的(例如︰ mach_header 和 mach_header_64),我們需要檢查檔案的體系結構來選擇需要的結構體:
1 2 3 4 5 6 7 8 9 |
int is_magic_64(uint32_t magic) { return magic == MH_MAGIC_64 || magic == MH_CIGAM_64; } void dump_segments(FILE *obj_file) { uint32_t magic = read_magic(obj_file, 0); int is_64 = is_magic_64(magic); } |
MH_MAGIC_64 和 MH_CIGAM_64 是系統提供的魔數。第二個看起來比第一個更多 magicly(譯者注:原文如此,magic 對應的形容詞是 magical,作者使用了magicly。)。解釋如下。
由於歷史的原因,不同的計算機可能使用不同的 位元組順序︰ 大端位元組序 (從左到右) 和小端位元組序 (從右至左)。魔數同樣儲存了這一資訊︰ MH_CIGAM
和 MH_CIGAM_64
表示位元組順序不同於主機作業系統,因此所有位元組都應顛倒︰
1 2 3 4 5 6 7 8 9 10 |
int should_swap_bytes(uint32_t magic) { return magic == MH_CIGAM || magic == MH_CIGAM_64; } void dump_segments(FILE *obj_file) { uint32_t magic = read_magic(obj_file, 0); int is_64 = is_magic_64(magic); int is_swap = should_swap_bytes(magic); } |
Mach-O 頭
終於我們能夠讀取 mach_header 了
。我們先來介紹幾個用於從檔案中讀取資料的常用函式。
1 2 3 4 5 6 7 |
void *load_bytes(FILE *obj_file, int offset, int size) { void *buf = calloc(1, size); fseek(obj_file, offset, SEEK_SET); fread(buf, size, 1, obj_file); return buf; } |
注意︰ 資料應當在使用後釋放 !
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
void dump_mach_header(FILE *obj_file, int offset, int is_64, int is_swap) { if (is_64) { int header_size = sizeof(struct mach_header_64); struct mach_header_64 *header = load_bytes(obj_file, offset, header_size); if (is_swap) { swap_mach_header_64(header, 0); } free(header); } else { int header_size = sizeof(struct mach_header); struct mach_header *header = load_bytes(obj_file, offset, header_size); if (is_swap) { swap_mach_header(header, 0); } free(header); } free(buffer); } void dump_segments(FILE *obj_file) { uint32_t magic = read_magic(obj_file, 0); int is_64 = is_magic_64(magic); int is_swap = should_swap_bytes(magic); dump_mach_header(obj_file, 0, is_64, is_swap); } |
為了不搞砸驅動函式,我們在這裡引入另一個函式 dump_mach_header
。下一步是讀取所有段指令並列印它們的名字。問題是,mach-o 檔案通常也包含其他指令。如果你還記得 segment_command
結構的第一個欄位的是 uint32_t cmd;
,此欄位表示指令的型別。下面是我們將使用的由系統提供的另一種結構體︰
1 2 3 4 5 |
struct load_command { uint32_t cmd; uint32_t cmdsize; }; |
除了以上的所有資訊 mach_header
還有許許多多載入命令,所以我們可以只是遍歷並跳過我們不感興趣的指令。此外,我們需要計算標頭結束位置的偏移量。這裡是 dump_mach_header
的最終版本︰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
void dump_mach_header(FILE *obj_file, int offset, int is_64, int is_swap) { uint32_t ncmds; int load_commands_offset = offset; if (is_64) { int header_size = sizeof(struct mach_header_64); struct mach_header_64 *header = load_bytes(obj_file, offset, header_size); if (is_swap) { swap_mach_header_64(header, 0); } ncmds = header->ncmds; load_commands_offset += header_size; free(header); } else { int header_size = sizeof(struct mach_header); struct mach_header *header = load_bytes(obj_file, offset, header_size); if (is_swap) { swap_mach_header(header, 0); } ncmds = header->ncmds; load_commands_offset += header_size; free(header); } dump_segment_commands(obj_file, load_commands_offset, is_swap, ncmds); } |
段指令
是時候去列印所有的段名了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
void dump_segment_commands(FILE *obj_file, int offset, int is_swap, uint32_t ncmds) { int actual_offset = offset; for (int i = 0; i < ncmds; i++) { struct load_command *cmd = load_bytes(obj_file, actual_offset, sizeof(struct load_command)); if (is_swap) { swap_load_command(cmd, 0); } if (cmd->cmd == LC_SEGMENT_64) { struct segment_command_64 *segment = load_bytes(obj_file, actual_offset, sizeof(struct segment_command_64)); if (is_swap) { swap_segment_command_64(segment, 0); } printf("segname: %sn", segment->segname); free(segment); } else if (cmd->cmd == LC_SEGMENT) { struct segment_command *segment = load_bytes(obj_file, actual_offset, sizeof(struct segment_command)); if (is_swap) { swap_segment_command(segment, 0); } printf("segname: %sn", segment->segname); free(segment); } actual_offset += cmd->cmdsize; free(cmd); } } |
這個函式不需要 is_64
引數,因為我們可以從 cmd
型別本身 (LC_SEGMENT
/LC_SEGMENT_64
)推斷它。如果它不是段,我們只需跳過該命令並向前移動到下一個。
CPU 名
我想要展示的最後一件事是如何基於 mach_header
的 cputype
獲得處理器的名稱。我相信這不是最好的選擇,但對於本文的示例來說,它是可以接受的︰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
struct _cpu_type_names { cpu_type_t cputype; const char *cpu_name; }; static struct _cpu_type_names cpu_type_names[] = { { CPU_TYPE_I386, "i386" }, { CPU_TYPE_X86_64, "x86_64" }, { CPU_TYPE_ARM, "arm" }, { CPU_TYPE_ARM64, "arm64" } }; static const char *cpu_type_name(cpu_type_t cpu_type) { static int cpu_type_names_size = sizeof(cpu_type_names) / sizeof(struct _cpu_type_names); for (int i = 0; i < cpu_type_names_size; i++ ) { if (cpu_type == cpu_type_names[i].cputype) { return cpu_type_names[i].cpu_name; } } return "unknown"; } |
OS X 為大量的 CPU 提供了 CPU_TYPE_ *
,所以我們可以 “容易”地為特定的魔數關聯一個字串。為了列印 CPU 的名稱,我們需要稍微修改 dump_mach_header
:
1 2 3 4 5 6 7 8 9 10 11 12 |
int header_size = sizeof(struct mach_header_64); struct mach_header_64 *header = load_bytes(obj_file, offset, header_size); if (is_swap) { swap_mach_header_64(header, 0); } ncmds = header->ncmds; load_commands_offset += header_size; printf("%sn", cpu_type_name(header->cputype)); // <- free(header); |
胖物件
這篇文章已經包含大量的內容,所以我不會描述如何處理胖物件,但你可以在這裡找到如何實現它︰ segment_dumper
接下來是什麼
大概就是以上這些。
這裡是一組可能有用的連結,如果你想要更深入地挖掘和了解更多關於 mach-o 的內容:
- OS X ABI Mach-O 檔案格式參考 – 蘋果官方文件
- MachOView – 是一個可視的 Mach-O 檔案瀏覽器。它提供完整的瀏覽和就地編輯 Intel 和 ARM 二進位制檔案的解決方案。
- Mach-O 可執行行檔案-來自 objc.io 的優秀文章。
- bitcode_retriever – 簡單的 C 程式,從Mach-O 二進位制檔案檢索 Bitcode。
- segment_dumper – 來自本文的原始碼
打賞支援我翻譯更多好文章,謝謝!
打賞譯者
打賞支援我翻譯更多好文章,謝謝!
任選一種支付方式