runtime源碼解析(前傳1)--Mach-O格式

Mach-O

Mach-O是Mach Object文件格式的縮寫罐盔。它是用于可執(zhí)行文件担平,動態(tài)庫,目標代碼的文件格式芒率。作為a.out格式的替代囤耳,Mach-O格式提供了更強的擴展性,以及更快的符號表信息訪問速度偶芍。

Mach-O格式為大部分基于Mach內(nèi)核的操作系統(tǒng)所使用的充择,包括NeXTSTEP, Mac OS X和iOS,它們都以Mach-O格式作為其可執(zhí)行文件匪蟀,動態(tài)庫椎麦,目標代碼的文件格式。

具體到我們的iOS程序材彪,當用XCode打包后观挎,會生成一個.app為擴展名的文件(位于工程目錄/Products文件夾下),其實.app是一個文件夾段化,我們用鼠標右鍵選擇‘Show Package contents’嘁捷,就可以查看文件夾的內(nèi)容,其中會發(fā)現(xiàn)有一個和我們工程同名的unix 可執(zhí)行文件显熏,這個就是iOS可執(zhí)行文件雄嚣,它是符合Mach-O格式的。

Mach-O文件結構

關于Mach-O的文件格式佃延,在蘋果官網(wǎng)已經(jīng)找不到相關說明了现诀,但是你可以通過下面鏈接獲取PDF版說明:
Mach-O File Format Reference

Mach-O格式如下圖所示,它被分為header履肃, load commands仔沿, data三大部分:


header:對Mach-O文件的一個概要說明,包括Magic Number, 支持的CPU類型等尺棋。
load commands: 當系統(tǒng)加載Mach-O文件時封锉,load command會指導蘋果的動態(tài)加載器(dyld)h或內(nèi)核,該如何加載文件的Data數(shù)據(jù)膘螟。
data: Mach-O文件的數(shù)據(jù)區(qū)成福,包含代碼和數(shù)據(jù)。其中包含若干Segment塊(注意荆残,除了Segment塊之外奴艾,還有別的內(nèi)容,包括code signature内斯,符號表之類蕴潦,不要被蘋果的圖所誤導O裉洹),每個Segment塊中包含0個或多個seciton潭苞。Segment根據(jù)對應的load command被dyld加載入內(nèi)存中忽冻。

我們可以使用MachOView(一個查看MachO 格式文件信息的開源工具)工具來查看一個具體的文件的Mach-O格式。

header

我們以一個普通的iOS APP為例此疹,看看Mach-O文件header部分的具體內(nèi)容僧诚。通過MachOView打開可執(zhí)行文件,可以看到header的結構:



是不是有些懵蝗碎?下面我們就結合Darwin內(nèi)核源碼湖笨,來了解下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中多了一個保留參數(shù)reserved肩豁。

  • magic:魔數(shù)脊串,用來標識這是一個Mach-O文件,有32位和64位兩個版本:
#define MH_MAGIC    0xfeedface  /* the mach magic number */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
  • cputype:支持的CPU架構類型清钥,如arm琼锋。
  • cpusubtype:在支持的CPU架構類型下,所支持的具體機器型號祟昭。在我們的例子中缕坎,APP是支持所有arm64的機型的:CUP_SUBTYPE_ARM64_ALL
  • filetype: Mach-O的文件類型。包括:
#define MH_OBJECT   0x1     /* Target 文件:編譯器對源碼編譯后得到的中間結果 */
#define MH_EXECUTE  0x2     /* 可執(zhí)行二進制文件 */
#define MH_FVMLIB   0x3     /* VM 共享庫文件(還不清楚是什么東西) */
#define MH_CORE     0x4     /* Core 文件篡悟,一般在 App Crash 產(chǎn)生 */
#define MH_PRELOAD  0x5     /* preloaded executable file */
#define MH_DYLIB    0x6     /* 動態(tài)庫 */
#define MH_DYLINKER 0x7     /* 動態(tài)連接器 /usr/lib/dyld */
#define MH_BUNDLE   0x8     /* 非獨立的二進制文件谜叹,往往通過 gcc-bundle 生成 */
#define MH_DYLIB_STUB   0x9     /* 靜態(tài)鏈接文件(還不清楚是什么東西) */
#define MH_DSYM     0xa     /* 符號文件以及調(diào)試信息,在解析堆棧符號中常用 */
#define MH_KEXT_BUNDLE  0xb     /* x86_64 內(nèi)核擴展 */
  • ncmds:load command的數(shù)量
  • sizeofcmds: 所有l(wèi)oad command的大小
  • flags: Mach-O文件的標志位搬葬。主要作用是告訴系統(tǒng)該如何加載這個Mach-O文件以及該文件的一些特性荷腊。有很多值,我們?nèi)〕R姷膸追N:
#define MH_NOUNDEFS 0x1     /* Target 文件中沒有帶未定義的符號急凰,常為靜態(tài)二進制文件 */
#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 可執(zhí)行 */
#define MH_PIE 0x200000  /* 加載程序在隨機的地址空間女仰,只在 MH_EXECUTE中使用 */
#define MH_NO_HEAP_EXECUTION 0x1000000 /* 將 Heap 標記為不可執(zhí)行,可防止 heap spray 攻擊 */
MH_PIE 隨機地址空間

每次系統(tǒng)加載進程后抡锈,都會為其隨機分配一個虛擬內(nèi)存空間疾忍。
在傳統(tǒng)系統(tǒng)中,進程每次加載的虛擬內(nèi)存是相同的床三。這就讓黑客有可能篡改內(nèi)存來破解軟件一罩。

dyld

dyld是蘋果公司的動態(tài)鏈接庫,用來把Mach-O文件加載入內(nèi)存撇簿。

二級命名空間

表示其符號空間中還會包含所在庫的信息聂渊。這樣可以使得不同的庫導出通用的符號推汽。與其相對的是扁平命名空間。

Load commands

load commands 緊跟在header之后歧沪,用來告訴內(nèi)核和dyld歹撒,如何將各個Segment加載入內(nèi)存中。
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 加載入內(nèi)存邪码,對應segment command類型
#define LC_SEGMENT  0x1
#define LC_SEGMENT_64   0x19    
// UUID, 2進制文件的唯一標識符
#define LC_UUID     0x1b
// 啟動動態(tài)加載器dyld
#define LC_LOAD_DYLINKER 0xe
Segment load command

在這么多的load command中裕菠,需要我們重點關注的是segment load command。segment command解釋了該如何將Data中的各個Segment加載入內(nèi)存中闭专,而和我們APP相關的邏輯及數(shù)據(jù)奴潘,則大部分位于各個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位的結構中平委,把和尋址相關的數(shù)據(jù)類型奈虾,由32位的uint32_t改為了64位的uint64_t類型。

結構體的定義廉赔,看注釋基本能夠看懂肉微,就是maxprot, initprot不太明白啥意思蜡塌。

這里介紹一個特殊的‘Segment’碉纳,叫做__PAGEZERO Segment。 這里說它特殊岗照,是因為這個Segment其實是蘋果虛擬出來的村象,只是一個邏輯上的段,而在Data中攒至,根本沒有對應的內(nèi)容厚者,也沒有占用任何硬盤空間。

__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中,程序的邏輯和數(shù)據(jù)是按照Segment(段)存儲烫止,在Segment中蒋荚,又分為0或多個section,每個section中在存儲實際的內(nèi)容馆蠕。而之所以這么做的原因在于期升,在section中,可以不用內(nèi)存對齊達到節(jié)約內(nèi)存的作用互躬,而所有的section作為整體的Segment播赁,又可以整體的內(nèi)存對齊。

在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 二進制數(shù)據(jù)的地方房维,前面的header和load command沼瘫,僅是提供文件的說明以及加載信息的功能。

Data部分也被分為若干的部分咙俩,除了我們前面提到的Segment外,還包括符號表湿故,代碼簽名阿趁,動態(tài)加載器信息等。

而程序的邏輯和數(shù)據(jù)坛猪,則是放在以Segment分割的Data部分中的脖阵。我們在這里,僅關心Data中的Segment的部分墅茉。

Segment根據(jù)內(nèi)容的不同命黔,分為若干類型,類型名稱均是以“雙下劃線+大寫英文”表示就斤,有的Segment下面還會包含若干的section悍募,section的命名是以”雙下劃線+小寫英文”表示。

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

#define SEG_PAGEZERO    "__PAGEZERO" /* 當時 MH_EXECUTE 文件時坠宴,表示空指針區(qū)域 */
#define SEG_TEXT    "__TEXT" /* 代碼/只讀數(shù)據(jù)段 */
#define SEG_DATA    "__DATA" /* 數(shù)據(jù)段 */
#define SEG_OBJC    "__OBJC" /* Objective-C runtime 段 */
#define SEG_LINKEDIT    "__LINKEDIT" /* 包含需要被動態(tài)鏈接器使用的符號和其他表,包括符號表绷旗、字符串表等 */

這里面注意到到SEG_OBJC喜鼓,是和OC的runtime相關的副砍。但是根據(jù)這篇文章
中說所,在OC 2.0中已經(jīng)廢棄掉__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”):



值得注意的是熏版,這些都是以明文形式展現(xiàn)的纷责。如果我們將加密key用字符串常量或宏定義的形式存儲在程序中,可以想象其安全性是得不到保障的撼短。
__DATA段

__DATA段用于存儲程序中所定義的數(shù)據(jù)再膳,可讀寫。__DATA段下常見的sectin有:

Section 用途
__DATA.__data 初始化過的可變數(shù)據(jù)
__DATA.__la_symbol_ptr lazy binding 的指針表曲横,表中的指針一開始都指向 __stub_helper
__DATA.nl_symbol_ptr 非 lazy binding 的指針表喂柒,每個表項中的指針都指向一個在裝載過程中,被動態(tài)鏈機器搜索完成的符號
__DATA.__const 沒有初始化過的常量
__DATA.__cfstring 程序中使用的 Core Foundation 字符串(CFStringRefs)
__DATA.__bss BSS禾嫉,存放為初始化的全局變量灾杰,即常說的靜態(tài)內(nèi)存分配
__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的加載有關的孽椰。

我們將在后續(xù)的文章中昭娩,繼續(xù)探討這些section和runtime的關系。

總結

這次我們一起了解了XNU內(nèi)核下的二進制文件格式Mach-O黍匾。它由header栏渺,load command以及data三部分組成:



我們重點應該了解的應該是data部分,因為這里存儲著我們程序真正的數(shù)據(jù)和代碼膀捷。
在data部分中迈嘹,又區(qū)分為以Segment劃分的部分以及代碼簽名等其他部分。
在Segment下,有區(qū)分有若干的section秀仲。
常用的Segment有__PAGE_ZERO, __TEXT, __DATA(注意區(qū)分Mach-O的data和這里的__DATA段名稱)融痛。

參考資料

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

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市神僵,隨后出現(xiàn)的幾起案子雁刷,更是在濱河造成了極大的恐慌,老刑警劉巖保礼,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件沛励,死亡現(xiàn)場離奇詭異,居然都是意外死亡炮障,警方通過查閱死者的電腦和手機目派,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胁赢,“玉大人企蹭,你說我怎么就攤上這事≈悄” “怎么了谅摄?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長系馆。 經(jīng)常有香客問我送漠,道長,這世上最難降的妖魔是什么由蘑? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任闽寡,我火速辦了婚禮,結果婚禮上纵穿,老公的妹妹穿的比我還像新娘下隧。我一直安慰自己,他們只是感情好谓媒,可當我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著何乎,像睡著了一般句惯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上支救,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天抢野,我揣著相機與錄音,去河邊找鬼各墨。 笑死指孤,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播恃轩,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼结洼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了叉跛?” 一聲冷哼從身側響起松忍,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎筷厘,沒想到半個月后鸣峭,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡酥艳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年摊溶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片充石。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡莫换,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出赫冬,到底是詐尸還是另有隱情浓镜,我是刑警寧澤,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布劲厌,位于F島的核電站膛薛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏补鼻。R本人自食惡果不足惜哄啄,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望风范。 院中可真熱鬧咨跌,春花似錦、人聲如沸硼婿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽寇漫。三九已至刊殉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間州胳,已是汗流浹背记焊。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留栓撞,地道東北人遍膜。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓碗硬,卻偏偏與公主長得像,于是被迫代替她去往敵國和親瓢颅。 傳聞我的和親對象是個殘疾皇子恩尾,可洞房花燭夜當晚...
    茶點故事閱讀 43,728評論 2 351

推薦閱讀更多精彩內(nèi)容