較爲詳細的解析了Mach-O文件格式,並着重闡述了動態連接相關的知識點,開始吧~saonian~html
進程是可執行文件在內存中加載獲得的結果,這種文件必須是操做系統理解的格式,這樣操做系統才能解析文件,簡歷所須要的依賴(如庫),初始化運行環境並執行。ios
Mach-O(Mach Object File Format)是macOS上的可執行文件,Linux和大部分Unix系統採用的是原生格式 ELF(Extensible Firmware Interface)
,windows支持的格式爲PE32/PE32+
,macOS
支持三種可執行文件格式:解釋器腳本文件、通用二進制格式和Mach-O
格式,以下圖所示:git
可執行格式 | magic | 用途 |
---|---|---|
腳本 | \x7FELF |
主要用於 shell 腳本,可是也經常使用語其餘解釋器,如 Perl, AWK 等。也就是咱們常見的腳本文件中在 #! 標記後的字符串,即爲執行命令的指令方式,以文件的 stdin 來傳遞命令 |
通用二進制格式 | 0xcafebabe 0xbebafeca |
包含多種架構支持的二進制格式,只在 macOS 上支持 |
Mach-O |
0xfeedface (32 位) 0xfeedfacf (64 位) |
macOS 的原生二進制格式 |
通用二進制格式(
Universal Binary
)也稱爲「胖二進制格式(Fat Binary)」,主要是解決歷史問題,以支持Power PC(PPC)
架構以及Inter
架構,是一種對多架構的二進制文件的打包集合。github
其中常見的包括:可執行文件、動態庫文件、動態連接器等都是Mach-O
格式,具體可經過file
命令查看具體的可執行文件格式,以下圖:shell
其結構以下圖所示,主要包括四部分組成:windows
Header
頭部緩存
描述了該文件的CPU
類型、文件類型、加載命令等信息;數據結構
Load commands
加載命令架構
描述了文件中數據的具體組織結構,不一樣數據類型如何使用不一樣的加載命令表示;app
Data
數據段
存放了包括代碼、字符常量、類、方法等代碼和數據,而且擁有多個Segment
段,每一個Segment
段都包含零到多個Section
節;
Loader info
連接信息及其餘
文件末端包含了一系列連接信息,如動態連接器用來連接可執行文件或者依賴所需使用的符號表、字符串表等,以及簽名信息等;
爲什麼
Segment
段中存在Section
節?分段的目的主要:不一樣段可被映射到不一樣虛擬存儲區域,便於讀寫權限管理;利用現代CPU緩存體系及程序的局部性原理,將指令和數據緩存分離有利用提高緩存命中率;指令或數據共享,有利於提高內存空間利用率。而分節主要是能夠不徹底按照
page
的大小進行內存對齊,提高內存空間利用率。
Mach-O
文件頭部具體的數據結構以下(區分32位和64位架構):
//32bit
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 */
};
//64bit
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
:魔數,用於確認該文件是32位仍是64位
cputype
,CPU類型,如arm
、x86_64
cpusubtype
,CPU具體類型,如arm64
、armv7
filetype
,文件類型,如可執行文件、庫文件、動態連接器、符號文件和調試信息等,其中MH_EXECUTE
表明可執行文件,具體的文件類型定義以下:
/* 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 */
複製代碼
ncmd
,加載命令條數
sizeofcmds
,全部加載命令在文件中佔用地址空間大小
reserved
,保留字段
flags
,標誌位,具體的定義以下:
#define MH_NOUNDEFS 0x1 // 目前沒有未定義的符號,不存在連接依賴
#define MH_DYLDLINK 0x4 // 該文件是dyld的輸入文件,沒法被再次靜態連接
#define MH_PIE 0x200000 // 加載程序在隨機的地址空間,只在 MH_EXECUTE中使用
#define MH_TWOLEVEL 0x80 // 兩級名稱空間
複製代碼
除了用MachOView能查看MachO文件信息,還能夠經過otool命令查看,咱們先來分析Header中的內容:otool -h xxx
來查看。
Load commands
緊跟在頭部以後(以下圖),這些加載指令清晰地告訴加載器如何處理二進制數據,有些命令是由內核處理的,有些是由動態連接器處理的,常見的加載命令以下:
LC_SEGMENT/LC_SEGMENT_64
: 將該段(32/64位)映射到進程地址空間中,包含了Segment
中全部Section
加載信息;
其中_PAGEZERO
段不具備訪問權限,用來處理空指針,其值爲0;TEXT
爲代碼段,_DATA/_DATA_CONST
爲可讀寫的數據段;_LINKEDIT
連接段包含了一些符號表、間接符號表、rebase
操做碼、綁定操做碼、導出符號、函數啓動信息、數據表、代碼簽名、字符串表等數據,該加載命令下沒有Section
,須要配合LC_SYMTAB
來解析symbol table
和string table
。
_LINKEDIT
加載命令信息中的文件偏移爲0x4000(十進制16384
)正好對應Dynamic Loader Info
起始地址,文件大小爲0x5840(十進制22592)=0x9840(0x9830+10)-0x4000
,正好對應從Dynamic Loader Info
到文件末尾的數據部分;
LC_DYLD_INFO_ONLY
:加載動態連接庫信息(重定向地址、弱引用綁定、懶加載綁定、開放函數等的偏移值等信息)
LC_SYMTAB
:載入符號表地址
LC_DYSYMTAB
:載入動態符號表地址
LC_LOAD_DYLINKER
:加載動態加載庫
LC_UUID
:肯定文件的惟一標識,crash解析中也會有這個,去檢測dysm文件和crash文件是否匹配
LC_VERSION_MIN_MACOSX/LC_VERSION_MIN_IPHONEOS
:肯定二進制文件要求的最低操做系統版本
LC_SOURCE_VERSION
:構建該二進制文件使用的源代碼版本
LC_MAIN
:設置程序主線程的入口地址和棧大小
LC_ENCRYPTION_INFO_64
:獲取加密信息
LC_LOAD_DYLIB
:加載額外的動態庫
LC_FUNCTION_STARTS
:定義一個函數起始地址表,使調試器和其餘程序易於看到一個地址是否在函數內
LC_DATA_IN_CODE
:定義在代碼段內的非指令的表
LC_CODE_SIGNATURE
:獲取應用簽名信息
具體的加載命令的數據結構以下(64位格式,與32位格式差異不大):
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 */
};
複製代碼
cmd:
就是Load commands
的類型,這裏LC_SEGMENT_64
表明將文件中64位的段映射到進程的地址空間;cmdsize:
表明load command
的大小segment name:
段的名稱VM Address :
段的虛擬內存地址VM Size :
段的虛擬內存大小file offset:
段在文件中偏移量file size:
段在文件中的大小nsects:
標示了Segment
中有多少secetion
除了使用MachOView
查看,還能夠經過otool -l xxx
查看,以下圖所示:
對於段的地址大小可經過size -l -m xxx
查看,以下圖:
下面重點闡述幾個重要的加載命令,便於後續理解整個程序啓動、動態加載、逆向等知識點。
該加載命令的內容以下圖所示:
其中虛擬地址範圍爲0x0~0x100000000
正好對應4GB
空間,該文件的起始虛擬地址空間也是從0x100000000
開始,即全部代碼和數據都是被加載到4GB
以後的地址。對應的文件內容大小爲0,即在該文件中不佔用實際空間,且具備不可讀寫不可執行權限,這樣內核就能夠識別到空指針或指針截斷的錯誤的範圍該地址空間的調用而拋出段異常,如EXC_BAD_ACCESS
異常。
__LINKEDIT
包含了動態連接相關的信息,如虛擬地址空間地址及文件偏移、文件權限等,而LC_DYLD_INFO_ONLY
加載命令,包含了重定位、綁定及導出等偏移/大小信息。
對於LC_SYMTAB
加載命令,其數據結構定義以下:
struct symtab_command {
uint32_t cmd; /* LC_SYMTAB */
uint32_t cmdsize; /* sizeof(struct symtab_command) */
uint32_t symoff; /* symbol table offset */
uint32_t nsyms; /* number of symbol table entries */
uint32_t stroff; /* string table offset */
uint32_t strsize; /* string table size in bytes */
};
複製代碼
這個命令告訴了連接器(包括靜態連接器或動態連接器)Symbol Table
和String Table
的位置及大小信息。
其中符號的結構由內核定義,以下:
struct nlist_64 {
union {
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};
複製代碼
n_un
,符號的名字在字符串表中的序號(在一個 Mach-O 文件裏,具備惟一性)n_sect
,符號所在的 section index(內部符號有效值從 1 開始,最大爲 255;外部符號爲0)n_value
,符號的地址值(在連接過程當中,會隨着其 section 發生變化)n_type
是一個 8 bit 的複合字段:
bit[5:8]
: 若是不爲 0,表示這是一個與調試有關的符號,值意義類型詳見mach-o/stab.hbit[4:5]
: 若爲 1,則表示該符號是私有的(外部符號)bit[1:4]
: 符號類型
N_UNDF
(0x0): 未定義N_ABS
(0x2): 符號地址指向到絕對地址,連接器後期不會再修改N_SECT
(0xe): 本地符號,即符號定義於當前 Mach-ON_PBUD
(0xc): 預綁定符號N_INDR
(0xa): 表示該符號和另外一個符號是同一個,n_value
指向到 string table,即該同名符號的名字bit[0:1]
: 表示這是外部符號,即該符號要麼定義在外部,要麼定義在本地可是能夠被外部使用;對於LC_DYSYMTAB
加載命令,其數據結構以下:
struct dysymtab_command {
uint32_t cmd; /* LC_DYSYMTAB */
uint32_t cmdsize; /* sizeof(struct dysymtab_command) */
uint32_t ilocalsym; /* index to local symbols */
uint32_t nlocalsym; /* number of local symbols */
uint32_t iextdefsym;/* index to externally defined symbols */
uint32_t nextdefsym;/* number of externally defined symbols */
uint32_t iundefsym; /* index to undefined symbols */
uint32_t nundefsym; /* number of undefined symbols */
uint32_t tocoff; /* file offset to table of contents */
uint32_t ntoc; /* number of entries in table of contents */
uint32_t modtaboff; /* file offset to module table */
uint32_t nmodtab; /* number of module table entries */
uint32_t extrefsymoff; /* offset to referenced symbol table */
uint32_t nextrefsyms; /* number of referenced symbol table entries */
uint32_t indirectsymoff; /* file offset to the indirect symbol table */
uint32_t nindirectsyms; /* number of indirect symbol table entries */
uint32_t extreloff; /* offset to external relocation entries */
uint32_t nextrel; /* number of external relocation entries */
uint32_t locreloff; /* offset to local relocation entries */
uint32_t nlocrel; /* number of local relocation entries */
};
複製代碼
主要包含了本地、外部符號、未定義外部符號、間接符號表的位置及數目,其中indriectsymoff
指定了Dynamic Symbol Table
的文件偏移位置及數目;
可以使用
otool -I xxx
來獲取間接符號表內容;
其中間接符號包含了符號名、符號所處的節及符號間接地址,其所處的Section
處在__stubs
、__got
、及__la_symbol_ptr
等節;
對於後續須要動態連接定位的符號頭部,如LC_SEGMENT_64
中的_TEXT.__stubs
、_DATA_CONST.__got
、_DATA.__la_symbol_ptr
,其頭部字段中包含了Indirect Sym Index(Reserverd1)
字段,該字段指明在Indirect Symbol Table
間接符號表中的條目序號,以下圖:
_la_symbol_ptr
中的符號在間接符號表中的起始條目序號爲26。
該加載命令包含了重要的程序啓動動態連接器的路徑,以下圖x86_64
的爲/usr/lib/dyld
。
Section
的數據結構
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
常見的 Section
以下表所示:
Section | 用途 |
---|---|
_TEXT.__text |
主程序代碼 |
_TEXT.__cstring |
C 語言字符串 |
_TEXT.__const |
const 關鍵字修飾的常量 |
_TEXT.__stubs |
用於 Stub 的佔位代碼,不少地方稱之爲樁代碼,用於重定向到 lazy 和 non-lazy 符號的 section ,被標記爲 S_SYMBOL_STUBS 。TEXT Segment 裏代碼和 dylib 外部符號的引用地址對函數符號的引用都指向了 stubs。其中每項都是 jmp 代碼間接尋址,可跳到la_symbol_ptr Section 中。 |
_TEXT.__stubs_helper |
當 Stub 沒法找到真正的符號地址後的最終指向 |
_TEXT.__objc_methname |
Objective-C 方法名稱 |
_TEXT.__objc_methtype |
Objective-C 方法類型 |
_TEXT.__objc_classname |
Objective-C 類名稱 |
_TEXT.__eh_frame |
調試輔助信息 |
_TEXT.__unwind_info |
用於存儲處理異常狀況信息 |
_DATA.__data |
初始化過的可變數據 |
_DATA.__la_symbol_ptr |
lazy binding 的指針表,表中的指針一開始都指向 __stub_helper |
_DATA.nl_symbol_ptr |
非 lazy binding 的指針表,每一個表項中的指針都指向一個在裝載過程當中,被動態鏈機器搜索完成的符號 |
_DATA.__got |
全局偏移表 |
_DATA.__const |
沒有初始化過的常量 |
_DATA.__cfstring |
程序中使用的 Core Foundation 字符串(CFStringRefs ) |
_DATA.__bss |
BSS ,存放爲初始化的全局變量,即常說的靜態內存分配 |
_DATA.__common |
沒有初始化過的符號聲明 |
_DATA.__mod_init_func |
初始化函數,在main 以前調用 |
_DATA.__mod_term_func |
終止函數,在main 返回以後調用 |
_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.__got
節,其內容以下圖所示:
其相似一個表,每一個條目是一個地址值,定義的是Non-Lazy Symbol Pointers
即非懶加載符號地址,全部條目的內容都是0。其引入的目的是解決程序在連接階段存放不能肯定目的地址的符號,當鏡像被加載時,動態連接器dyld
會對每一個條目對應的符號進行重定位,將其真正的地址寫入,做爲條目的內容。對於dyld
如何肯定符號信息的,能夠經過上面的Indirect Symbol Table
中的符號看出,包含了符號名稱、間接符號地址。
與之對應的是_DATA.__la_symbol_ptr
節,其內容以下圖所示:
其實際內容都指向了_TEXT.__stub_helper
節,最終經過jumpq
指令跳轉到了dyld_stub_binder
符號,即__got
節中的Non_Lazy Symbol Pointer
中的條目,該符號爲一個函數,定義於dyld_stub_binder.S,由 dyld
提供。
dyld_stub_binder
函數其大體邏輯是:內部會尋找鎖調用符號的真實地址,並寫入_la_symbol_ptr
條目中,而後跳轉到真實地址執行;
對於_TEXT.__stubs
節,其內容以下:
該內容也是一個表,每一個條目都是一段數據,稱爲「符號樁」。經過otool -v xx -s _TEXT __stubs
命令查看內容以下:
其內容都是jmpq
跳轉指令,跳轉的地址以第一條地址爲例計算:
0x100003000 = 0x100001dbc(rip) + 0x1244
複製代碼
該地址指向的是__la_symbol_ptr
節,而該節最終都指向了dyld_stub_binder
。
連接加載信息包含了動態加載信息Dynamic Loader Info
(包含了重定向地址、弱引用綁定、懶加載綁定、開放函數等的偏移值等信息,其加載命令爲LC_DYLD_INFO_ONLY
),函數起始地址表Function Starts
(其加載命令爲LC_FUNCTION_STARTS
),符號表Symbol Table
,動態符號表Dynamic Symbol Table
,代碼段非指令表Data in Code Table
,字符串表String Table
(以空值爲終止符)及代碼簽名Code Signature
,以下圖所示:
因爲地址空間隨機化技術(ddress space layout randomization, ASLR
)和地址無關可執行技術(position-indendent excutable, PIE
),使得程序在內存的加載地址是隨機的,所以須要程序在動態連接階段將內部地址進行修正。Rebase
數據描述了哪些是對指向 MachO
內部的引用並將其修正,而 Bind
數據描述哪些是指向外部的引用並進行修正。Lazy Bind
數據描述了哪些符號須要延遲綁定,即僅在第一次使用時纔會綁定,不會在啓動時進行,提升啓動效率;Export
數據描述了對外可見的符號。其內容都是以操做數(Opcodes
)、當即數(immediate)
以及採用uleb128/sleb128
編碼的偏移值組成。
PIE(position-independent executable)
是一種生成地址無關可執行程序的技術。若是編譯器在生成可執行程序的過程當中使用了PIE,那麼當可執行程序被加載到內存中時其加載地址存在不可預知性。PIE還有個孿生兄弟PIC(position-independent code)
。其做用和PIE相同,都是使被編譯後的程序可以隨機的加載到某個內存地址。區別在於PIC是在生成動態連接庫時使用(Linux中的so),PIE是在生成可執行文件時使用。
以Rebase
舉例,其協議和操做就是找到地址後將其值加上偏移便可,具體的獲取操做數和當即數是經過REBASE_OPCODE_MASK(0xF0)
和REBASE_IMMEDIATE_MASK(0x0F)
對數據進行與&
操做,如0x100004000
的數據字節0x11
,其操做數爲0x10=0x11&0xF0
對應的是REBASE_OPCODE_SET_TYPE_IMM
,當即數0x01=0x11&0x0F
爲type=1(REBASE_TYPE_POINTER)
,具體的操做數及當即數對應的邏輯可查閱dyld
源碼。
注意:MachOView中標註的
Actions
存在誤導性,重定位、綁定等操做都是按照字節數據順序讀取並操做直至完整的讀取完全部的數據,其標註具體緣由未知,待確認補充!
對於Dynamic Symbol Table
中的Indirect Symbols
其內容爲一個表,每一個條目的內容爲其在Symbol Table
中的序號,以下圖:
其內容爲0x3c=60
,對應的就是符號表第60個符號,經過符號表中的起始地址0x4380
,每一個符號佔用0x10
,則0x4740=0x4380+0x10*0x3c
,對應的就是_CFRunLoopAddSource
符號地址。
對於字符串表String Table
中內容爲全部的符號名稱,每一個名稱中間經過空字符串間隔,以下圖所示:
Symbol Table
中的String Table Index
字段就是字符串表中對應的第index
個字符串。