Mach-O 探究
Mach-O
為Mach Object
檔案格式的縮寫,它是一種用於可執行檔案,目的碼,動態庫,核心轉儲的檔案格式。作為a.out格式的替代,Mach-O提供了更強的擴充套件性,並提升了符號表中資訊的訪問速度。
Mach-O
格式為大部分基於Mach
核心的作業系統所使用的,包括NeXTSTEP
, Mac OS X
和iOS
,它們都以Mach-O
格式作為其可執行檔案,動態庫,目的碼的檔案格式。
Mach-O簡介
在iOS開發中,我們的程式碼在編譯後會生成一個.app的檔案(Product資料夾下),而.app檔案我們可以把它看做是一個資料夾,內部存放了APP正常執行所需要的檔案,通常比較容易識別的是一些資原始檔。如圖:
我們通過顯示包內容看下.app資料夾內都有什麼:
當然我們這片文章的主角也在這個資料夾內:
系統識別Mach-O(這個名字是專案的名字)為可執行檔案(Mach-O是一種可執行檔案格式),我們來看下這個檔案,Mach-O檔案是無法直接開啟或者檢視包內容,這裡我們需要藉助MachOView工具來檢視,工具是開源的如果你想看具體的實現,你可以看工具的原始碼,當然你可以直接下載使用。
開啟後的頁面是這樣的:
Mach-O結構
實際上我們從使用MachOView開啟後的檔案目錄也可以看出,Mach-O的檔案結構分為三大部分:Header
,Load Commands
,Data
。
下面是官方提供的一張結構圖:
根據上圖,我們將我們看到的目錄大致劃分為:
- Mach-O 頭(Mach Header):這裡描述了 Mach-O 的 CPU 架構、檔案型別以及載入命令等資訊;
- 載入命令(Load Command):當系統載入Mach-O檔案時,load command會指導蘋果的動態載入器(dyld)h或核心,該如何載入檔案的Data資料。
- 資料區(Data):Mach-O檔案的資料區,包含程式碼和資料。其中包含若干Segment塊,每個Segment塊中包含0個或多個seciton。Segment根據對應的load command被dyld載入入記憶體中。
注意
:通過對比我們發現實際上官網給出的結構並不準確,在實際結果中還包含了Dynamic Loader Info
,Function Starts
,Symbol Table
,Data In Code Entries
,Dynamic Symbol Table
,String Table
,Code Signature
等。
下面我們來詳細看下每部分的內容
Mach64 Header
這裡我們可以和蘋果開源的Darwin原始碼一起看方便理解,原始碼在這裡。
32位
/*
* The 32-bit mach header appears at the very beginning of the object file for
* 32-bit architectures.
*/
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位:
/*
* 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 */
};
我們看到32位和64位的mach_header
基本是一致的,只是在64位中新增了reserved
欄位,下面我們來看下其中每個欄位所表示的意義。
Magic Number
offset | data | description | value |
---|---|---|---|
00000000 | FEEDFACF | Magic Number | MH_MAGIC_64 |
我們可以將其直譯為魔數
,他的值(Value)有兩個:
#define MH_MAGIC 0xfeedface /* the mach magic number */ 32位
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */ 64位
用於這個Mach-O檔案的標識,有32位和64位兩個值。由此可以看出我們的示例是一個64位的Mach-O檔案。
CPU Type ,CPU SubType
offset | data | description | value |
---|---|---|---|
00000004 | 0100000C | CPU Type | CPU_TYPE_64 |
00000008 | 00000000 | CPU SubType | |
00000000 | CPU_SubType_ARM64_ALL |
CPU Type和CPU SubType 表示支援的CUP架構型別和子型別,如ARM。而具體的型別有哪些我們可以通過查詢/mach/machine.h.
中的定義檢視這裡不做過多的擴充套件,具體可以看這裡
我們的示例中,APP是支援所有arm64的機型的:CUP_SUBTYPE_ARM64_ALL。
File Type
offset | data | description | value |
---|---|---|---|
0000000C | 00000002 | File Type | MH_EXECUTE |
File Type 表示 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 核心擴充套件 */
這裡型別均是在loader.h
檔案中定義的
對於我們示例中的我們的File Type為 MH_EXECUTE
表示 可執行的二進位制檔案。
ncmds(Number of Load Commands)
offset | data | description | value |
---|---|---|---|
00000010 | 00000017 | Number of Load Commands | 23 |
ncmds表示load command的數量。在我們的示例中表示數量為23個。
sizeofcmds(Size of Load Commands)
offset | data | description | value |
---|---|---|---|
00000014 | 00000AF8 | Size of Load Commands | 2808 |
sizeofcmds表示所有load command的總大小。示例中總大小為2808。
Flags
offset | data | description | value |
---|---|---|---|
00000018 | 00200085 | Flags | |
00000001 | MH_NOUNDEFS | ||
00000004 | MH_DYLDLINK | ||
00000080 | MH_TWOLEVEL | ||
00200000 | MH_PIE |
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 攻擊 */
結合我們的示例,我們共有4個Flags:
- MH_NOUNDEFS
- MH_DYLDLINK dyld是蘋果公司的動態連結庫,用來把Mach-O檔案載入入記憶體
- MH_TWOLEVEL 表示其符號空間中還會包含所在庫的資訊。這樣可以使得不同的庫匯出通用的符號。與其相對的是扁平名稱空間。
- MH_PIE 每次系統載入程式後,都會為其隨機分配一個虛擬記憶體空間(在傳統系統中,程式每次載入的虛擬記憶體是相同的。這就讓黑客有可能篡改記憶體來破解軟體)
注意
:flags的值也定義在loader.h檔案中 都可以通過原始碼檢視。
Load Commands
Load Commands 緊跟在Header之後,用來告訴核心和dyld,如何將各個Segment載入入記憶體中。load command被原始碼錶示為struct,有若干種load command,但是共同的特點是,在其結構的開頭處,必須是如下兩個屬性:
/*
* The load commands directly follow the mach_header. The total size of all
* of the commands is given by the sizeofcmds field in the mach_header. All
* load commands must have as their first two fields cmd and cmdsize.
* The cmd
* field is filled in with a constant for that command type.
* Each command type
* has a structure specifically for it.
* The cmdsize field is the size in bytes
* of the particular load command structure plus anything that follows it that
* is a part of the load command (i.e. section structures, strings, etc.).
* To
* advance to the next load command the cmdsize can be added to the offset or
* pointer of the current load command.
* The cmdsize for 32-bit architectures
* MUST be a multiple of 4 bytes and for 64-bit architectures MUST be a multiple
* of 8 bytes (these are forever the maximum alignment of any load commands).
* The padded bytes must be zero. All tables in the object file must also
* follow these rules so the file can be memory mapped. Otherwise the pointers
* to these tables will not work well or at all on some machines. With all
* padding zeroed like objects will compare byte for byte.
*/
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
對應我們示例中的Load Commands
我們在嘗試去理解load_command
的註釋:
load commands緊跟著mach_header,load commands的總大小由mach_header彙總的sizeofcmds欄位給出,所有的load commands都必須以cmd和cmdsize兩個欄位作為前兩個欄位(結合我們的示例也得到驗證),cmd欄位的值為commandtype常量,每一個commandtype都有一種特定的結構。cmdsize欄位以位元組為單位包含loadcommand結構和額外的其他欄位(例如 section structures,strings等)。要前進到下一個載入命令,可以將cmdsize新增到當前載入命令的偏移量或指標。對於32位體系結構的cmdsize
必須是4位元組的倍數,並且對於64位架構,必須是8位元組的倍數(這些永遠是所有裝入命令的最大對齊),填充位元組必須為零。
Segment
在這麼多的load command中,需要我們重點關注的是segment load command,segment command解釋了該如何將Data中的各個Segment載入入記憶體中,而和我們APP相關的邏輯及資料,則大部分位於各個Segment中。
而和我們的Run time相關的Segment,則位於__DATA型別Segment下。
Segment load command也分為32位和64位:
32位
/*
* The segment load command indicates that a part of this file is to be
* mapped into the task's address space. The size of this segment in memory,
* vmsize, maybe equal to or larger than the amount to map from this file,
* filesize. The file is mapped starting at fileoff to the beginning of
* the segment in memory, vmaddr. The rest of the memory of the segment,
* if any, is allocated zero fill on demand. The segment's maximum virtual
* memory protection and initial virtual memory protection are specified
* by the maxprot and initprot fields. If the segment has sections then the
* section structures directly follow the segment command and their size is
* reflected in cmdsize.
*/
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 */
};
64位
/*
* The 64-bit segment load command indicates that a part of this file is to be
* mapped into a 64-bit task's address space. If the 64-bit segment has
* sections then section_64 structures directly follow the 64-bit segment
* command and their size is reflected in cmdsize.
*/
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_command基本一致,只是在64位的結構中把和定址相關的資料型別由uint32_t
改為uint64_t
我們先看下示例中,和Segment相關的Command:
結合原始碼我們可以看到:
#define SEG_PAGEZERO "__PAGEZERO" /* 當時 MH_EXECUTE 檔案時,捕獲到空指標 */
#define SEG_TEXT "__TEXT" /* 程式碼/只讀資料段 */
#define SEG_DATA "__DATA" /* 資料段 */
#define SEG_LINKEDIT "__LINKEDIT" /* 包含需要被動態連結器使用的符號和其他表,包括符號表、字串表等 */
根據前面結構圖我們知道Load Commands實際上是一個二級結構:Segment->Section,正如示例中所示
因此,下面我們在看下section的結構
Section
/*
* A segment is made up of zero or more sections. Non-MH_OBJECT files have
* all of their segments with the proper sections in each, and padded to the
* specified segment alignment when produced by the link editor. The first
* segment of a MH_EXECUTE and MH_FVMLIB format file contains the mach_header
* and load commands of the object file before its first section. The zero
* fill sections are always last in their segment (in all formats). This
* allows the zeroed segment padding to be mapped into memory where zero fill
* sections might be. The gigabyte zero fill sections, those with the section
* type S_GB_ZEROFILL, can only be in a segment with sections of this type.
* These segments are then placed after all other segments.
*
* The MH_OBJECT format has all of its sections in one segment for
* compactness. There is no padding to a specified segment boundary and the
* mach_header and load commands are not part of the segment.
*
* Sections with the same section name, sectname, going into the same segment,
* segname, are combined by the link editor. The resulting section is aligned
* to the maximum alignment of the combined sections and is the new section's
* alignment. The combined sections are aligned to their original alignment in
* the combined section. Any padded bytes to get the specified alignment are
* zeroed.
*
* The format of the relocation entries referenced by the reloff and nreloc
* fields of the section structure for mach object files is described in the
* header file <reloc.h>.
*/
struct section { /* for 32-bit architectures */
char sectname[16]; /* name of this section 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]; /* Section 名字 */
char segname[16]; /* 所在的Segment名稱*/
uint64_t addr; /* Section 所在的記憶體地址 */
uint64_t size; /* Section 的大小 */
uint32_t offset; /* Section 所在的檔案偏移 */
uint32_t align; /* Section 的記憶體對齊邊界 (2 的次冪) */
uint32_t reloff; /* 重定位資訊的檔案偏移 */
uint32_t nreloc; /* 重定位條目的數目 */
uint32_t flags; /* 標誌屬性 (section type and attributes)*/
uint32_t reserved1; /* 保留欄位1 (for offset or index) */
uint32_t reserved2; /* 保留欄位2 (for count or sizeof) */
uint32_t reserved3; /* 保留欄位3 */
};
在64位和32位的section定義中,64位新增了一個reserved3保留欄位,以及將section的addr和size欄位由原來的uint32_t型別升級為uint64_t。
在Data中,程式的邏輯和資料是按照Segment(段)儲存,在Segment中,又分為0或多個section,每個section中在儲存實際的內容。而之所以這麼做的原因在於,在section中,可以不用記憶體對齊達到節約記憶體的作用,而所有的section作為整體的Segment,又可以整體的記憶體對齊。
結合我們示例中的一個section結構如下圖:
DATA(資料)
Mach-O的Data部分,其實是真正儲存APP二進位制資料的位置,前面的header和load command,僅是提供檔案的說明以及載入資訊的功能。
前面我們介紹過,我們通過Load Commands從DATA中讀取資料,而Load Commands被劃分成了多個Segment,也就是說 我們通過不同的Load Commands從DATA中讀取不同的資料。
在介紹Segment的時候我們說過Segment被劃分成__PAGEZERO
,__TEXT
,__DATA
,__LINKEDIT
這幾段。
結合我們的示例,我們發現DATA被劃分為:__TEXT
,__DATA
下面我們來看下這幾個資料段(section):
__TEXT段
__TEXT是程式的只讀段,用於儲存我們所寫的程式碼和字串常量,const修飾常量等。
下面是幾個我們常見的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 類名稱 |
我們來結合示例看下這幾個section的內容:
cstring
我們可以從中看到lw_property
,lw_publicproperty
這兩個屬性名。以及我們列印的NSLog中的內容,同時我們發現,我們可能定義的某些三方key或者appid在這裡都暴露在外部。
static const NSString *lw_constsecretKey = @"11234455556";
objc_methname
我們可以看到我們自定義的方法名lw_publicMethod
,lw_privateMethod
以及lw_property
,lw_publicproperty
重寫的setter和getter方法。
classname
我們可以看到我們自定義的類的類:LWCustomClass
_DATA
__DATA段用於儲存程式中所定義的資料,可讀寫。__DATA段下常見的sectin有:
下面我們看下常見的__DATA下的section:
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 超類引用 |
這些以objc開頭的DATA欄位都是跟runtime有關的,後面我們會詳細分析。
__objc_imageinfo
typedef struct objc_image_info {
uint32_t version; // currently 0
uint32_t flags;
#if __cplusplus >= 201103L
private:
// 位移列舉
enum : uint32_t {
IsReplacement = 1<<0, // used for Fix&Continue, now ignored
SupportsGC = 1<<1, // 是否支援垃圾回收
RequiresGC = 1<<2, // 映象是否需要回收
OptimizedByDyld = 1<<3, // image is from an optimized shared cache
CorrectedSynthesize = 1<<4, // used for an old workaround, now ignored
IsSimulated = 1<<5, // image compiled for a simulator platform
HasCategoryClassProperties = 1<<6, // class properties in category_t
SwiftVersionMaskShift = 8,
SwiftVersionMask = 0xff << SwiftVersionMaskShift // Swift ABI version
};
public:
enum : uint32_t {
SwiftVersion1 = 1,
SwiftVersion1_2 = 2,
SwiftVersion2 = 3,
SwiftVersion3 = 4
};
public:
bool isReplacement() const { return flags & IsReplacement; }
bool supportsGC() const { return flags & SupportsGC; }
bool requiresGC() const { return flags & RequiresGC; }
bool optimizedByDyld() const { return flags & OptimizedByDyld; }
bool hasCategoryClassProperties() const { return flags & HasCategoryClassProperties; }
bool containsSwift() const { return (flags & SwiftVersionMask) != 0; }
uint32_t swiftVersion() const { return (flags & SwiftVersionMask) >> SwiftVersionMaskShift; }
#endif
} objc_image_info;
我們發現objc_image_info
中主要是有version欄位和flag欄位,
__objc_classlist
這個section列出了所有的class,包括meta class。
圖中的value值是就是這個類結構體的地址(包括元類),類結構體的結構為objc中的objc_class結構體,結構如下:
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
__objc_catlist
這裡可以檢視程式碼中的所有分類,其value的值為指向分類結構體的指標
對應oc中的結構為category_t,具體結構如下:
struct category_t {
// 是指 class_name 而不是 category_name
const char *name;
// 要擴充套件的類物件,編譯期間是不會定義的,而是在執行時通過 * name 對應到對應的類物件。
classref_t cls;
// 物件方法列表
struct method_list_t *instanceMethods;
// 類方法列表
struct method_list_t *classMethods;
// 協議列表
struct protocol_list_t *protocols;
// 例項屬性
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
// 類屬性(這個結構體以_開頭命名???)
struct property_list_t *_classProperties;
// methodsForMeta 返回類方法列表或者物件方法列表
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
// 屬性列表返回方法
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
__objc_protolist
該Section中記錄了專案中所有的協議。 其value值為指向協議的指標
協議的結構體為protocol_t,具體如下:
struct protocol_t : objc_object {
const char *mangledName;
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;
uint32_t size; // sizeof(protocol_t)
uint32_t flags;
// Fields below this point are not always present on disk.
const char **_extendedMethodTypes;
const char *_demangledName;
property_list_t *_classProperties;
}
__objc_classrefs
該section記錄了哪些class被引用了,這裡記錄了所有被例項化的class,有些類雖然在包裡,但是我們並未使用,因此這裡不會有。
__objc_selrefs
這section記錄哪些SEL對應的字串被引用了,有系統方法,也有自定義方法:
__objc_superrefs
該section記錄了呼叫super方法的類。
比如,在子類方法中,我們呼叫了父類的方法,就會將子類記錄在這裡。
__objc_const
該section用來記錄在OC記憶體初始化過程中的不可變內容。這裡所謂的不可變內容並不是我們在程式中所寫的const NSInteger k = 5這種常量資料(它存在__TEXT的const section中),而是在OC記憶體佈局中不可變得部分。
應用啟動
根據上面介紹的在應用啟動期間,dyld和kern會讀取Mach-O檔案中的Load Command去讀取和載入_DATA資料段下的內容,而這一切都發生在main函式之前。所以我們看下main函式之前都發生了什麼?
啟動呼叫堆疊
新增一個符號斷點(Symbolic BreakPoint)讓應用在執行到_objc_init
方法是斷點執行。
這樣我們就能看到下面的這個呼叫棧:
因為_objc_init
方法是runtime的入口,因此在這之前呼叫的方法都是dyld和ImageLoader的操作
dyld
dyld(the dynamic link editor)動態連結器,系統 kernel 做好啟動程式的初始準備後,交給 dyld 負責,dyld的主要工作內容為(參考 dyld: Dynamic Linking On OS X ):
- 從 kernel 留下的原始呼叫棧引導和啟動自己
- 將程式依賴的動態連結庫遞迴載入進記憶體,當然這裡有快取機制
- non-lazy 符號立即 link 到可執行檔案,lazy 的存表裡
- Runs static initializers for the executable
- 找到可執行檔案的 main 函式,準備引數並呼叫
- 程式執行中負責繫結 lazy 符號、提供 runtime dynamic loading services、提供偵錯程式介面
- 程式main函式 return 後執行 static terminator
- 某些場景下 main 函式結束後調 libSystem 的 _exit 函式
ImageLoader
這裡的image不是圖片的意思,它是一個二進位制檔案,你可以把他理解為一個映象檔案。內部是被編譯過的符號、程式碼等,因此ImageLoader
作用是將這些檔案載入進記憶體,且每一個檔案對應一個ImageLoader
例項來負責載入。
他的主要工作為:
- 在程式執行時它先將動態連結的 image 遞迴載入 (也就是上面測試棧中一串的遞迴呼叫的時刻)
- 再從可執行檔案 image 遞迴載入所有符號
ImageLoaderMachO
顧名思義這裡應該是去載入MachO檔案,從堆疊中我們可以看到主要跟doInitialization
方法和doModInitFunctions
方法。
doInitialization
這個方法的主要作用是:獲取Mach-O
的init方法的地址並呼叫
bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
CRSetCrashLogMessage2(this->getPath());
// mach-o has -init and static initializers
doImageInit(context);
doModInitFunctions(context);
CRSetCrashLogMessage2(NULL);
return (fHasDashInit || fHasInitializers);
}
void ImageLoaderMachO::doImageInit(const LinkContext& context)
{
if ( fHasDashInit ) {
// mach-o檔案中指令的個數
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
// 遍歷指令
for (uint32_t i = 0; i < cmd_count; ++i) {
switch (cmd->cmd) {
case LC_ROUTINES_COMMAND:
// 獲取macho_routines_command的init_address
Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);
// 執行-init方法
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
break;
}
// 計算下一個指令((char*)cmd)+cmd->cmdsize
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}
doModInitFunctions
這個方法的主要作用是:獲取Mach-O
的static initializer的地址並呼叫
void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
if ( fHasInitializers ) {
// mach-o檔案中指令的個數
const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
// 遍歷所有的指令
for (uint32_t i = 0; i < cmd_count; ++i) {
// 如果指令是Mach-o中的LC_SEGMENT_COMMAND
if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
// 從sectionsStart到sectionsEnd
for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
const uint8_t type = sect->flags & SECTION_TYPE;
if ( type == S_MOD_INIT_FUNC_POINTERS ) {
for (size_t i=0; i < count; ++i) {
if ( context.verboseInit )
dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());
// 執行initializer方法
func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
}
}
}
}
// 根據指令的地址+指令大小獲取到下一個指令
cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
}
}
}
總結
上述我們介紹了Mach-O檔案的主要結構,以及每個segment和section的功能和欄位的作用,結尾處我們通過檢視應用啟動呼叫堆疊來確認Mach-O檔案何時被ImageLoader解析並載入到記憶體中,提供給後續的runtime使用。鑑於main函式之前系統核心,dyld,ImageLoader,rumtime做了很多準備,我們決定新開一篇文章來講述這個過程發生了什麼,敬請期待!
參考文章
XNU原始碼
探祕 Mach-O 檔案
Mach-O檔案結構理解
Mach-O 與動態連結
iOS 程式 main 函式之前發生了什麼
相關文章
- Mach-OMac
- Mach-O、dyldMac
- 分析Mach-O檔案Mac
- 解析Mach-o檔案Mac
- dyld 載入 Mach-OMac
- iOS 逆向 - Mach-O檔案iOSMac
- 探祕 Mach-O 檔案Mac
- 趣探 Mach-O:FishHook 解析MacHook
- Mach-O檔案周邊二三事Mac
- Mach-O Inside: BSS SectionMacIDE
- Mach-O 可執行檔案Mac
- 趣探 Mach-O:符號解析Mac符號
- mach-o 檔案分析(解析類)Mac
- 趣探 Mach-O:檔案格式分析Mac
- iOS 如何獲取 Mach-O 的 UUIDiOSMacUI
- 五種 Mach-O 型別的淺要分析Mac型別
- synchronized探究synchronized
- webAR 探究Web
- Mach-O 的動態連結(Lazy Bind 機制)Mac
- JAVA 探究NIOJava
- iOS Block探究iOSBloC
- 探究Java集合Java
- iOS RunLoop 探究iOSOOP
- RAC原理探究
- objc系列譯文(6.3):Mach-O 可執行檔案OBJMac
- Flutter BuildContext 探究FlutterUIContext
- iOS 深入探究 AutoreleasePooliOS
- 探究Spring原理Spring
- context包探究Context
- cherrypy應用探究
- exchange partition原理探究
- RecyclerViewPrefetch功能探究View
- Oracle深入Undo探究Oracle
- Mach-O Inside: 命令列工具集 otool objdump od 與 dwarfdumpMacIDE命令列OBJ
- iOS系統分析(二)Mach-O二進位制檔案解析iOSMac
- 物件導向再探究物件
- 探究Flutter Engine除錯Flutter除錯
- RunLoop底層原理探究OOP