作者:evilpan
原文鏈接:https://evilpan.com/2020/09/06/macho-inside-out/
之前寫了一篇深入淺出ELF,作為姊妹篇這次就來聊聊MacOS的可執行文件格式MachO。
Mach-O 101
在之前的文章中我們說過,可執行文件的使命有兩個,一是方便開發者在編譯、鏈接時提供可擴展的封裝結構;二是在執行時能給操作系統(內核)提供內存映射信息。MachO也不例外。
MachO本身沒有什麼特別的含義,就是Mach object
的簡寫,而Mach是早期的一個微內核。和ELF一樣,MachO也極具拓展性,從全局視角來看,MachO文件可以分為三個部分,分別是:
- Mach Header: 文件頭信息
- 可變長度的LOAD_COMMAND信息
- 上述LOAD_COMMAND中所用到的具體信息(segments)
這裡的segment可以理解為一段連續的內存空間,擁有對應的讀/寫/執行權限,並且在內存中總是頁對齊的。每個segment由一個或者多個section組成,section表示特定含義數據或者代碼的集合(不需要頁對齊)。在macOS中,通常約定segment的名稱為雙下劃線加全大寫字母(如__TEXT
),section的名稱為雙下劃線加小寫字母(如__text
)。
下面對這三個部分進行分別介紹。
注: MachO文件結構的表示通常分為32位和64位兩種,本文以64位為例,畢竟這是歷史的進程。
Header
文件頭信息參考mach-o/loader.h中的定義如下:
/* * The 64-bit mach header appears at the very beginning of object files for * 64-bit architectures. */ struct mach_header_64 { uint32_t magic; /* mach magic number identifier */ cpu_type_t cputype; /* cpu specifier */ cpu_subtype_t cpusubtype; /* machine specifier */ uint32_t filetype; /* type of file */ uint32_t ncmds; /* number of load commands */ uint32_t sizeofcmds; /* the size of all the load commands */ uint32_t flags; /* flags */ uint32_t reserved; /* reserved */ }; /* Constant for the magic field of the mach_header_64 (64-bit architectures) */ #define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */ #define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */
filetype
表示類型,常見的有:
- MH_OBJECT: 可重定向的目標文件
- MH_EXECUTE: 可執行文件
- MH_DYLIB: 動態綁定的共享庫文件
- …
flags
為不同的文件標籤的組合,每個標籤佔一個位,可以用位或來進行組合,常見的標籤有:
- MH_NOUNDEFS: 該文件沒有未定義的引用
- MH_DYLDLINK: 該文件將要作為動態鏈接器的輸入,不能再被靜態鏈接器修改
- MH_TWOLEVEL: 該文件使用兩級名字空間綁定
- MH_PIE: 可執行文件會被加載到隨機地址,只對
MH_EXECUTE
有效 - …
另外一個值得關注的就是ncmds
和sizeofcmds
,分別指定了 LOAD_COMMAND 的個數以及總大小,從這裡也大概能猜到,每個 command 的大小是可變的。
Command
LOAD_COMMAND
是體現MachO文件拓展性的地方,每個 command 的頭兩個word分別表示類型和大小,如下:
struct load_command { uint32_t cmd; /* type of load command */ uint32_t cmdsize; /* total size of command in bytes */ };
不同的cmd
類型都會有其對應的結構體來描述其內容,cmdsize
表示的是整個cmd的大小,即包括頭部和內容。也就是說在處理的時候當前cmd的位置加上cmdsize就是下一個cmd的位置。注意每個command的大小(即cmdsize)需要word對齊,對於32位CPU來說是4字節,64位則是8字節;同時對齊末尾的填充部分必須是0。
loader.h
中絕大部分的篇幅就是用來定義各種不同command類型的結構體了,這裡挑選一些比較常見的來進行介紹。
LC_SEGMENT
LC_SEGMENT
/LC_SEGMENT64
可以說是最重要的一個command。表示當前文件的一部分將會映射到目標進程(task)的地址空間中,包括程序運行所需要的所有代碼和數據。假設當前MachO文件的起始地址為begin,則映射的內容為:
- 原始地址(文件地址): begin + fileoff,大小為filesize
- 目的地址(進程虛址): vmaddr,大小為vmsize
其中vmsize >= filesize
,如果有多出來的部分需要填充為零。segment_command的結構體表示如下:
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 */ };
maxprot/initprot表示對應segment虛擬地址空間的RWX權限。如果segment包含一個或者多個section,那麼在該segment結構體之後就緊跟着對應各個section頭,總大小也包括在cmdsize之中,其結構如下:
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 */ };
每個section頭對應一個section,位置在相對文件起始地址的offset處,大小為size字節,對應的虛擬地址為addr。這裡的align對齊指的是在虛擬地址空間中的對齊,實際上在文件中是連續存放的,因為有size指定大小。reloff和nreloc與符號的重定向有關,在下面的加載過程一節中再進行介紹。
從這裡可以看出,section的內容和segment是不連續存放的,只是section header在對應segment之後。而segment的vmsize實際上會大於segment+section_header的大小(即cmdsize),猜測多出來的空間是內核加載MachO時將對應section內容填充進去,後面將會對這一猜測進行驗證。
TEXT
__TEXT
段包含__text
、__stubs
、__stub_helper
、__cstring
等section,一般用來存放不可修改的數據,比如代碼和const字符串,可以用otool
查看對應的section內容:
$ otool -V main -s __TEXT __stubs main: Contents of (__TEXT,__stubs) section 0000000100000f6a jmpq *0xa8(%rip) ## literal pool symbol address: _printf 0000000100000f70 jmpq *0xaa(%rip) ## literal pool symbol address: _set_foo
在實際的MachO可執行文件中觀察發現TEXT的fileoff為0,也就是說TEXT段映射的時候會將當前文件頭部分也映射到進程空間中。
(lldbinit) image dump sections main Sections for '/Users/evilpan/temp/macho-test/main' (x86_64): SectID Type Load Address Perm File Off. File Size Flags Section Name ---------- ---------------- --------------------------------------- ---- ---------- ---------- ---------- ---------------------------- 0x00000100 container [0x0000000000000000-0x0000000100000000)* --- 0x00000000 0x00000000 0x00000000 main.__PAGEZERO 0x00000200 container [0x0000000100000000-0x0000000100001000) r-x 0x00000000 0x00001000 0x00000000 main.__TEXT 0x00000001 code [0x0000000100000ee0-0x0000000100000f6a) r-x 0x00000ee0 0x0000008a 0x80000400 main.__TEXT.__text 0x00000002 code [0x0000000100000f6a-0x0000000100000f76) r-x 0x00000f6a 0x0000000c 0x80000408 main.__TEXT.__stubs 0x00000003 code [0x0000000100000f78-0x0000000100000f9c) r-x 0x00000f78 0x00000024 0x80000400 main.__TEXT.__stub_helper 0x00000004 data-cstr [0x0000000100000f9c-0x0000000100000fb0) r-x 0x00000f9c 0x00000014 0x00000002 main.__TEXT.__cstring 0x00000005 compact-unwind [0x0000000100000fb0-0x0000000100000ff8) r-x 0x00000fb0 0x00000048 0x00000000 main.__TEXT.__unwind_info 0x00000300 container [0x0000000100001000-0x0000000100002000) rw- 0x00001000 0x00001000 0x00000000 main.__DATA 0x00000006 data-ptrs [0x0000000100001000-0x0000000100001008) rw- 0x00001000 0x00000008 0x00000006 main.__DATA.__nl_symbol_ptr 0x00000007 data-ptrs [0x0000000100001008-0x0000000100001018) rw- 0x00001008 0x00000010 0x00000006 main.__DATA.__got 0x00000008 data-ptrs [0x0000000100001018-0x0000000100001028) rw- 0x00001018 0x00000010 0x00000007 main.__DATA.__la_symbol_ptr 0x00000009 zero-fill [0x0000000100001028-0x000000010000102c) rw- 0x00000000 0x00000000 0x00000001 main.__DATA.__common 0x00000400 container [0x0000000100002000-0x0000000100007000) r-- 0x00002000 0x00004a90 0x00000000 main.__LINKEDIT
上面例子中__TEXT
段的的vm_size和file_size都是0x1000
,這個大小在文件中正好是第一個__DATA
section的起始地址:
後記
本文通過對MachO文件的文件格式研究,介紹了MacOS和iOS中可執行文件的加載過程,從內核中的處理一直到動態連接器dyld的代碼分析。可以看出MachO與ELF相比實現方式各有千秋,但是在內核中原生增加了對代碼的簽名和加密,其實ELF也很容易實現類似的功能,但開放系統需要更多考慮兼容性的問題,不像蘋果可以大刀闊斧的隨便改。
對於MachO的深入理解其實也有助於日常的相關研究,比如Apple Store的加密實現以及代碼簽名的大致原理,還有針對dyld_cache的處理等,其中每一項都值得去深入挖掘。而且本文也沒有介紹到全部的MachO特性,比如Objective-C相關的段,具體的實戰部分後面有時間會再去整理一下。
參考資料
- Overview of the Mach-O Executable Format
- Mach Object Files
- apple/darwin-xnu
- opensource-apple/dyld
- RTFSC
转载请注明:IAMCOOL » 深入淺出 MachO