1. 程式人生 > >Objective-C runtime機制(前傳)——Mach-O格式

Objective-C runtime機制(前傳)——Mach-O格式

Mach-O

Mach-O是Mach Object檔案格式的縮寫。它是用於可執行檔案,動態庫,目的碼的檔案格式。作為a.out格式的替代,Mach-O格式提供了更強的擴充套件性,以及更快的符號表資訊訪問速度。

Mach-O格式為大部分基於Mach核心的作業系統所使用的,包括NeXTSTEP, Mac OS X和iOS,它們都以Mach-O格式作為其可執行檔案,動態庫,目的碼的檔案格式。

具體到我們的iOS程式,當用XCode打包後,會生成一個.app為副檔名的檔案(位於工程目錄/Products資料夾下),其實.app是一個資料夾,我們用滑鼠右鍵選擇‘Show Package contents’,就可以檢視資料夾的內容,其中會發現有一個和我們工程同名的unix 可執行檔案,這個就是iOS可執行檔案,它是符合Mach-O格式的。
這裡寫圖片描述

Mach-O檔案結構

關於Mach-O的檔案格式,在蘋果官網已經找不到相關說明了,但是你可以通過下面連結獲取PDF版說明:
Mach-O File Format Reference

Mach-O格式如下圖所示,它被分為headerload commandsdata三大部分:

這裡寫圖片描述

header:對Mach-O檔案的一個概要說明,包括Magic Number, 支援的CUP型別等。

load commands: 當系統載入Mach-O檔案時,load command會指導蘋果的動態載入器(dyld)h或核心,該如何載入檔案的Data資料。

data: Mach-O檔案的資料區,包含程式碼和資料。其中包含若干Segment塊(注意,除了Segment塊之外,還有別的內容,包括code signature,符號表之類,不要被蘋果的圖所誤導!

),每個Segment塊中包含0個或多個seciton。Segment根據對應的load command被dyld載入入記憶體中。

我們可以使用MachOView(一個檢視MachO 格式檔案資訊的開源工具)工具來檢視一個具體的檔案的Mach-O格式。

我們以一個普通的iOS APP為例,看看Mach-O檔案header部分的具體內容。通過MachOView開啟可執行檔案,可以看到header的結構:

這裡寫圖片描述

是不是有些懵?下面我們就結合Darwin核心原始碼,來了解下Mach header的定義。

Mach header的定義位於Darwin原始碼中的 EXTERNAL_HEADERS/mach-o/loader.h

中:

32位:

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位:

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位的Mach header基本相同,只不過64位header中多了一個保留引數reserved。

  • magic:魔數,用來標識這是一個Mach-O檔案,有32位和64位兩個版本:
#define MH_MAGIC    0xfeedface  /* the mach magic number */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
  • cputype:支援的CUP架構型別,如arm。
  • cpusubtype:在支援的CUP架構型別下,所支援的具體機器型號。在我們的例子中,APP是支援所有arm64的機型的:CUP_SUBTYPE_ARM64_ALL
  • filetype: Mach-O的檔案型別。包括:
#define MH_OBJECT   0x1     /* Target 檔案:編譯器對原始碼編譯後得到的中間結果 */
#define MH_EXECUTE  0x2     /* 可執行二進位制檔案 */
#define MH_FVMLIB   0x3     /* VM 共享庫檔案(還不清楚是什麼東西) */
#define MH_CORE     0x4     /* Core 檔案,一般在 App Crash 產生 */
#define MH_PRELOAD  0x5     /* preloaded executable file */
#define MH_DYLIB    0x6     /* 動態庫 */
#define MH_DYLINKER 0x7     /* 動態聯結器 /usr/lib/dyld */
#define MH_BUNDLE   0x8     /* 非獨立的二進位制檔案,往往通過 gcc-bundle 生成 */
#define MH_DYLIB_STUB   0x9     /* 靜態連結檔案(還不清楚是什麼東西) */
#define MH_DSYM     0xa     /* 符號檔案以及除錯資訊,在解析堆疊符號中常用 */
#define MH_KEXT_BUNDLE  0xb     /* x86_64 核心擴充套件 */
  • ncmds:load command的數量
  • sizeofcmds: 所有load command的大小
  • flags: Mach-O檔案的標誌位。主要作用是告訴系統該如何載入這個Mach-O檔案以及該檔案的一些特性。有很多值,我們取常見的幾種:
#define MH_NOUNDEFS 0x1     /* Target 檔案中沒有帶未定義的符號,常為靜態二進位制檔案 */
#define MH_SPLIT_SEGS   0x20  /* Target 檔案中的只讀 Segment 和可讀寫 Segment 分開  */
#define MH_TWOLEVEL 0x80        /* 該 Image 使用二級名稱空間(two name space binding)繫結方案 */
#define MH_FORCE_FLAT   0x100 /* 使用扁平名稱空間(flat name space binding)繫結(與 MH_TWOLEVEL 互斥) */
#define MH_WEAK_DEFINES 0x8000 /* 二進位制檔案使用了弱符號 */
#define MH_BINDS_TO_WEAK 0x10000 /* 二進位制檔案連結了弱符號 */
#define MH_ALLOW_STACK_EXECUTION 0x20000/* 允許 Stack 可執行 */
#define MH_PIE 0x200000  /* 載入程式在隨機的地址空間,只在 MH_EXECUTE中使用 */
#define MH_NO_HEAP_EXECUTION 0x1000000 /* 將 Heap 標記為不可執行,可防止 heap spray 攻擊 */

MH_PIE 隨機地址空間

每次系統載入程序後,都會為其隨機分配一個虛擬記憶體空間。
在傳統系統中,程序每次載入的虛擬記憶體是相同的。這就讓黑客有可能篡改記憶體來破解軟體。

dyld

dyld是蘋果公司的動態連結庫,用來把Mach-O檔案載入入記憶體。

二級名稱空間

表示其符號空間中還會包含所在庫的資訊。這樣可以使得不同的庫匯出通用的符號。與其相對的是扁平名稱空間。

Load commands

load commands 緊跟在header之後,用來告訴核心和dyld,如何將各個Segment載入入記憶體中。
load command被源碼錶示為struct,有若干種load command,但是共同的特點是,在其結構的開頭處,必須是如下兩個屬性:

struct load_command {
    uint32_t cmd;       /* type of load command */
    uint32_t cmdsize;   /* total size of command in bytes */
};

蘋果為cmd定義了若干的巨集,用來表示cmd的型別,下面列舉出幾種:

// 描述該如何將32或64位的segment 載入入記憶體,對應segment command型別
#define LC_SEGMENT  0x1
#define LC_SEGMENT_64   0x19    
// UUID, 2進位制檔案的唯一識別符號
#define LC_UUID     0x1b
// 啟動動態載入器dyld
#define LC_LOAD_DYLINKER 0xe

Segment load command

在這麼多的load command中,需要我們重點關注的是segment load command。segment command解釋了該如何將Data中的各個Segment載入入記憶體中,而和我們APP相關的邏輯及資料,則大部分位於各個Segment中。

而和我們的Run time相關的Segment,則位於__DATA型別Segment下。

Segment load command分為32位和64位:

struct segment_command { /* for 32-bit architectures */
    uint32_t    cmd;        /* LC_SEGMENT */
    uint32_t    cmdsize;    /* includes sizeof section structs */
    char        segname[16];    /* segment name */
    uint32_t    vmaddr;     /* memory address of this segment */
    uint32_t    vmsize;     /* memory size of this segment */
    uint32_t    fileoff;    /* file offset of this segment */
    uint32_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 */
};

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 */
};

32位和64位的Segment load command基本類似,只不過在64位的結構中,把和定址相關的資料型別,由32位的uint32_t改為了64位的uint64_t型別。

結構體的定義,看註釋基本能夠看懂,就是maxprot, initprot不太明白啥意思。

這裡介紹一個特殊的‘Segment’,叫做__PAGEZERO Segment。 這裡說它特殊,是因為這個Segment其實是蘋果虛擬出來的,只是一個邏輯上的段,而在Data中,根本沒有對應的內容,也沒有佔用任何硬碟空間。

__PAGEZERO Segment在VM中被置為Read only,邏輯上佔用APP最開始的4GB空間,用來處理空指標。

我們用MachOV點開__PAGEZERO Segment所對應的Segment load command,LC_SEGMENT_64(__PAGEZERO):

這裡寫圖片描述

可以看到其vm size是4GB,但其真正的實體地址File size和offset都是0。

Section header

在Data中,程式的邏輯和資料是按照Segment(段)儲存,在Segment中,又分為0或多個section,每個section中在儲存實際的內容。而之所以這麼做的原因在於,在section中,可以不用記憶體對齊達到節約記憶體的作用,而所有的section作為整體的Segment,又可以整體的記憶體對齊。

在Mach-O檔案中,每一個Segment load command下面,都會包含對應Segment 下所有section的header。
section header的定義如下:

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) */
};

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 */
};

這樣,關於load commonds部分,其真正的結構其實和蘋果提供的圖片有些許的差異:

這裡寫圖片描述

Data

Mach-O的Data部分,其實是真正儲存APP 二進位制資料的地方,前面的header和load command,僅是提供檔案的說明以及載入資訊的功能。

Data部分也被分為若干的部分,除了我們前面提到的Segment外,還包括符號表,程式碼簽名,動態載入器資訊等。

而程式的邏輯和資料,則是放在以Segment分割的Data部分中的。我們在這裡,僅關心Data中的Segment的部分。

Segment根據內容的不同,分為若干型別,型別名稱均是以“雙下劃線+大寫英文”表示,有的Segment下面還會包含若干的section,section的命名是以”雙下劃線+小寫英文”表示。

先來看Segment,Mach-O中有如下幾種Segment:

#define SEG_PAGEZERO    "__PAGEZERO" /* 當時 MH_EXECUTE 檔案時,表示空指標區域 */
#define SEG_TEXT    "__TEXT" /* 程式碼/只讀資料段 */
#define SEG_DATA    "__DATA" /* 資料段 */
#define SEG_OBJC    "__OBJC" /* Objective-C runtime 段 */
#define SEG_LINKEDIT    "__LINKEDIT" /* 包含需要被動態連結器使用的符號和其他表,包括符號表、字串表等 */

這裡面注意到到SEG_OBJC,是和OC的runtime相關的。但是根據這篇文章中說所,在OC 2.0中已經廢棄掉__OBJC段,而是將其放入到了__DATA段中以__objc開頭的section中。這些和runtime相關的sections是本文的終點,我們稍後再分析。我們先看看其他的段。

__TEXT段

__TEXT是程式的只讀段,用於儲存我們所寫的程式碼和字串常量,const修飾常量等。
下面是__TEXT段下常見的section:

Section 用途
__TEXT.__text 主程式程式碼
__TEXT.__cstring C 語言字串
__TEXT.__const const 關鍵字修飾的常量
__TEXT.__stubs 用於 Stub 的佔位程式碼,很多地方稱之為樁程式碼。
__TEXT.__stubs_helper 當 Stub 無法找到真正的符號地址後的最終指向
__TEXT.__objc_methname Objective-C 方法名稱
__TEXT.__objc_methtype Objective-C 方法型別
__TEXT.__objc_classname Objective-C 類名稱

例如,我們點選__TEXT.__objc_classname, 會看到我們程式中所使用到的類的名稱:

這裡寫圖片描述

而在__TEXT.__cstring section中,則看到我們定義的字串常量(如@”I’m a cat!! miao miao”):

這裡寫圖片描述

值得注意的是,這些都是以明文形式展現的。如果我們將加密key用字串常量或巨集定義的形式儲存在程式中,可以想象其安全性是得不到保障的。

__DATA段

__DATA段用於儲存程式中所定義的資料,可讀寫。__DATA段下常見的sectin有:

Section 用途
__DATA.__data 初始化過的可變資料
__DATA.__la_symbol_ptr lazy binding 的指標表,表中的指標一開始都指向 __stub_helper
__DATA.nl_symbol_ptr 非 lazy binding 的指標表,每個表項中的指標都指向一個在裝載過程中,被動態鏈機器搜尋完成的符號
__DATA.__const 沒有初始化過的常量
__DATA.__cfstring 程式中使用的 Core Foundation 字串(CFStringRefs)
__DATA.__bss BSS,存放為初始化的全域性變數,即常說的靜態記憶體分配
__DATA.__common 沒有初始化過的符號宣告
__DATA.__objc_classlist Objective-C 類列表
__DATA.__objc_protolist Objective-C 原型
__DATA.__objc_imginfo Objective-C 映象資訊
__DATA.__objc_selfrefs Objective-C self 引用
__DATA.__objc_protorefs Objective-C 原型引用
__DATA.__objc_superrefs Objective-C 超類引用

可見,在__DATA段下,有許多以__objc開頭的section,而這些section,均是和runtime的載入有關的。
這裡寫圖片描述
我們將在後續的文章中,繼續探討這些section和runtime的關係。

總結

這次我們一起了解了XNU核心下的二進位制檔案格式Mach-O。它由header,load command以及data三部分組成:
這裡寫圖片描述
我們重點應該瞭解的應該是data部分,因為這裡儲存著我們程式真正的資料和程式碼。
在data部分中,又區分為以Segment劃分的部分以及程式碼簽名等其他部分。
在Segment下,有區分有若干的section。
常用的Segment有__PAGE_ZERO, __TEXT, __DATA(注意區分Mach-O的data和這裡的__DATA段名稱)。

參考資料

趣探 Mach-O:檔案格式分析
深入理解Macho檔案(二)- 消失的__OBJC段與新生的__DATA段
mach-o格式分析
Mach-O 檔案格式探索
Mach-O 維基百科