本文所讀的原始碼,可以從這裡找到,這是 Mach-O 系列的第一篇
我們的程式想要跑起來,肯定它的可執行檔案格式要被作業系統所理解,比如 ELF
是 Linux
下可執行檔案的格式,PE32/PE32+
是windows
的可執行檔案的格式,那麼對於OS X
和iOS
來說 Mach-O
是其可執行檔案的格式。
我們平時瞭解到的可執行檔案、庫檔案、Dsym檔案、動態庫、動態聯結器都是這種格式的。Mach-O 的組成結構如下圖所示包括了Header
、Load commands
、Data
(包含Segement
的具體資料)
Header 的結構
Mach-O
的頭部,使得可以快速確認一些資訊,比如當前檔案用於32位還是64位,對應的處理器是什麼、檔案型別是什麼
可以拿下面的程式碼做一個例子
1 2 3 4 5 6 7 |
#include int main(int argc, const char * argv[]) { // insert code here... printf("Hello, World!\n"); return 0; } |
在終端執行以下命令,可以生成一個可執行檔案a.out
1 |
192:Test Joy$ gcc -g main.c |
我們可以使用MachOView
(是一個檢視MachO
格式檔案資訊的開源工具)來檢視
.out
檔案的具體格式如何
看到這裡肯定有點懵比,不知道這是什麼東西,下面看一下 header
的資料結構
32位結構
1 2 3 4 5 6 7 8 9 |
struct mach_header { 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 */ }; |
64位架構
1 2 3 4 5 6 7 8 9 10 |
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 */ }; |
32位和64位架構的標頭檔案,沒有太大的區別,只是64位多了一個保留欄位罷了
magic:
魔數,用於快速確認該檔案用於64位還是32位cputype:
CPU型別,比如 armcpusubtype:
對應的具體型別,比如arm64、armv7filetype:
檔案型別,比如可執行檔案、庫檔案、Dsym檔案,demo中是2MH_EXECUTE
,代表可執行檔案
1 2 3 4 5 6 7 8 9 10 11 12 13 |
* Constants for the filetype field of the mach_header */ #define MH_OBJECT 0x1 /* relocatable object file */ #define MH_EXECUTE 0x2 /* demand paged executable file */ #define MH_FVMLIB 0x3 /* fixed VM shared library file */ #define MH_CORE 0x4 /* core file */ #define MH_PRELOAD 0x5 /* preloaded executable file */ #define MH_DYLIB 0x6 /* dynamically bound shared library */ #define MH_DYLINKER 0x7 /* dynamic link editor */ #define MH_BUNDLE 0x8 /* dynamically bound bundle file */ #define MH_DYLIB_STUB 0x9 /* shared library stub for static */ #define MH_DSYM 0xa /* companion file with only debug */ #define MH_KEXT_BUNDLE 0xb /* x86_64 kexts */ |
ncmds :
載入命令條數sizeofcmds
:所有載入命令的大小reserved:
保留欄位flags:
標誌位,剛才demo
中顯示的都在這裡了,其餘的有興趣可以閱讀mach o
原始碼
1 2 3 4 |
#define MH_NOUNDEFS 0x1 // 目前沒有未定義的符號,不存在連結依賴 #define MH_DYLDLINK 0x4 // 該檔案是dyld的輸入檔案,無法被再次靜態連結 #define MH_PIE 0x200000 // 載入程式在隨機的地址空間,只在 MH_EXECUTE中使用 #define MH_TWOLEVEL 0x80 // 兩級名稱空間 |
隨機地址空間
程式每一次啟動,地址空間都會簡單地隨機化。
對於大多數應用程式來說,地址空間隨機化是一個和他們完全不相關的實現細節,但是對於黑客來說,它具有重大的意義。
如果採用傳統的方式,程式的每一次啟動的虛擬記憶體映象都是一致的,黑客很容易採取重寫記憶體的方式來破解程式。採用ASLR
可以有效的避免黑客攻擊。
dyld
動態連結器,他是蘋果開源的一個專案,可以在這裡下載,當核心執行LC_DYLINK
(後面會說到)時,聯結器會啟動,查詢程式所依賴的動態庫,並載入到記憶體中。
二級名稱空間
這是dyld
的一個獨有特性,說是符號空間中還包括所在庫的資訊,這樣子就可以讓兩個不同的庫匯出相同的符號,與其對應的是平坦名稱空間
Load commands 結構
Load commands
緊跟在頭部之後,這些載入指令清晰地告訴載入器如何處理二進位制資料,有些命令是由核心處理的,有些是由動態連結器處理的。在原始碼中有明顯的註釋來說明這些是動態聯結器處理的。
這裡列舉幾個看上去比較熟悉的….
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 將檔案的32位或64位的段對映到程式地址空間 #define LC_SEGMENT 0x1 #define LC_SEGMENT_64 0x19 // 唯一的 UUID,標示二進位制檔案 #define LC_UUID 0x1b /* the uuid */ // 剛才提到的,啟動動態載入聯結器 #define LC_LOAD_DYLINKER 0xe /* load a dynamic linker */ // 程式碼簽名和加密 #define LC_CODE_SIGNATURE 0x1d /* local of code signature */ #define LC_ENCRYPTION_INFO 0x21 /* encrypted segment information */ |
load command
的結構如下
1 2 3 4 |
struct load_command { uint32_t cmd; /* type of load command */ uint32_t cmdsize; /* total size of command in bytes */ }; |
通過 MachOView
來繼續檢視剛才Demo中的Load commands
的一些細節,LC_SEGMENT_64
和LC_SEGMENT
是載入的主要命令,它負責指導核心來設定程式的記憶體空間
cmd:
就是就是Load commands
的型別,這裡LC_SEGMENT_64
代表將檔案中64位的段對映到程式的地址空間。LC_SEGMENT_64
和LC_SEGMENT
的結構差別不大,下面只列舉一個,有興趣可以閱讀原始碼
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 */ }; |
cmdsize:
代表load command
的大小VM Address :
段的虛擬記憶體地址VM Size :
段的虛擬記憶體大小file offset:
段在檔案中偏移量file size:
段在檔案中的大小
將該段對應的檔案內容載入到記憶體中:從offset
處載入 file size
大小到虛擬記憶體 vmaddr
處,由於這裡在記憶體地址空間中是_PAGEZERO
段(這個段不具有訪問許可權,用來處理空指標)所以都是零
還有圖片中的其他段,比如_TEXT
對應的就是程式碼段,_DATA
對應的是可讀/可寫的資料,_LINKEDIT
是支援dyld
的,裡面包含一些符號表等資料
nsects:
標示了Segment
中有多少secetion
segment name:
段的名稱,當前是__PAGEZERO
Segment & Section
這裡有個命名的問題,如下圖所示,__TEXT
代表的是Segment
,小寫的__text
代表 Section
Section
的資料結構
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct section { /* for 32-bit architectures */ char sectname[16]; /* name of this section */ char segname[16]; /* segment this section goes in */ uint32_t addr; /* memory address of this section */ uint32_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) */ }; |
sectname:
比如_text
、stubs
segname :
該section
所屬的segment
,比如__TEXT
addr :
該section
在記憶體的起始位置size:
該section
的大小offset:
該section
的檔案偏移align :
位元組大小對齊reloff :
重定位入口的檔案偏移nreloc:
需要重定位的入口數量flags:
包含section
的type
和attributes
發現很多底層知識都是以 Mach-O
為基礎的,所以最近打算花時間結合Mach-O
做一些相對深入的總結,比如符號解析、bitcode
、逆向工程等,加油吧
參考連結
- 深入理解 MAC OS X & iOS 作業系統
- mach-o/loader.h
- Mach-O檔案格式和程式從載入到執行過程
- OS X ABI Mach-O File Format Reference