Mach-O 文件結構
- 理解
Mach-O
文件 Mach-O
文件結構Mach Header
Load Commands
Data
- 理解大小端模式
- 理解通用二進制文件
一、理解可執行文件
1. 可執行文件
進程
,其實就是可執行文件
在內存中加載得到的結果;可執行文件
必須是操作系統可理解的格式,而且不同系統的可執行文件
的格式也是不同的;
2. 不同平臺的可執行文件
Linux:ELF
文件Windows
:PE32/PE32+
文件OS和iOS
:Mach-O(Mach Object)
文件
二、理解 Mach-O 文件
作爲iOS
,iPadOS
、macOS
平臺的可執行文件格式,Mach-O
文件涉及 App 啓動運行、bitcode
分析、 crash
符號化等諸多多個功能:
1. Mach-O 文件
Mach-O
文件是iOS
,iPadOS
、macOS
平臺的可執行文件格式。對應系統通過應用二進制接口 (application binary interface
,縮寫爲ABI
) 來運行該格式的文件;Mach-O
格式用來替代BSD
系統中的a.out
格式,保存了在編譯和鏈接過程中產生的機器代碼和數據
,從而爲靜態鏈接和動態鏈接的代碼提供單一文件格式。Mach-O
提供了更強的擴展性,以及更快的符號表信息訪問速度;
2.Mach-O 格式的常見文件類型
Executable
:可執行文件 (.out
.o
);Dylib
:動態鏈接庫;Bundle
:不能被鏈接,只能在運行時使用dlopen()
加載;Image
:包含Executable
、Dylib
和Bundle
;Framework
:包含Dylib
、資源文件和頭文件的文件夾;
三、Mach-O 文件結構
1. 查看 Mach-O 的兩種方法
- 使用
MachOView
軟件,可直接查看MachO
文件的結構; - 使用終端命令
objdump
;
2. 查看 Mach-O 文件結構
使用MachOView
查看Mach-O
,效果如下:
Mach-O
文件中包含三個主要的部分:
Header
:頭部,描述CPU
類型、文件類型、加載命令的條數大小等信息;Load Commands
:加載命令,其條數和大小已經在header
中被提供;Data
:數據段;
其他的信息還有:
Dynamic Loader Info
:動態庫加載信息Function Starts
:入口函數Symbol Table
:符號表Dynamic Symbol Table
: 動態庫符號表String Table
:字符串表
四、Mach Header(可執行文件頭)
1. 功能總結
Header
是鏈接器加載時最先讀取的內容,因爲它決定了一些基礎架構
、系統類型
等信息;Header
包含整個Mach-O
文件的關鍵信息,如CPU類型
、文件類型
、加載命令的條數大小
等信息,使得系統能夠迅速定位Mach-O
文件的運行環境;Header
針對32
位和64
位架構的CPU
,分別對應mach_header
和mach_header_64
的結構體;
2. 源碼分析
Header
被定義在loader.h
文件中,具體代碼如下:
struct mach_header_64 {
uint32_t magic; // 32位或者64位,系統內核用來判斷是否是mach-o格式
cpu_type_t cputype; // CPU架構類型,比如ARM
cpu_subtype_t cpusubtype; // CPU的具體類型,例如arm64、armv7
uint32_t filetype; // mach-o文件類型, 可執行文件、目標文件或者靜態庫和動態庫
uint32_t ncmds; // LoadCommands加載命令的條數(加載命令緊跟header之後)
uint32_t sizeofcmds; // 全部LoadCommands加載命令的大小
uint32_t flags; // 標誌位標識二進制文件支持的功能,主要是和系統加載、鏈接有關
uint32_t reserved; // 保留字段(相比於32位多出的字段)
};
複製代碼
由於可執行文件
、目標文件
或者靜態庫
和動態庫
等都是Mach-O
格式,所以才需要filetype
來說明。常用的文件類型有以下幾種:
#define MH_OBJECT 0x1 /* 目標文件*/
#define MH_EXECUTE 0x2 /* 可執行文件*/
#define MH_DYLIB 0x6 /* 動態庫*/
#define MH_DYLINKER 0x7 /* 動態鏈接器*/
#define MH_DSYM 0xa /* 存儲二進制文件符號信息,用於debug分析*/
複製代碼
3.MachOView 演示
五、Load Commands
1. 功能總結
Load Commands
是加載命令的列表,用於描述Data
在二進制文件和虛擬內存中的佈局信息;Load Commands
記錄了很多信息,例如動態鏈接器的位置、程序的入口、依賴庫的信息、代碼的位置、符號表的位置等;Load commands
由內核定義,不同版本的command
數量不同,其條數和大小記錄在header
中;Load commands
的type
是以LC_
爲前綴常量,譬如LC_SEGMENT
、LC_SYMTAB
等;
2.. 代碼分析
Load Command
被定義在loader.h
文件中,具體代碼如下:
struct load_command {
uint32_t cmd; /* 加載命令的類型 */
uint32_t cmdsize; /* 加載命令的大小 */
};
複製代碼
每個Load Command
都有獨立的結構,但是所有結構的前兩個字段是固定的。比如LC_SEGMENT_64
,這是一個讀取segment
、section
有關命令,具體代碼如下:
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; // 表示加載命令類型
uint32_t cmdsize; // 表示加載命令大小(還包括了緊跟其後的nsects個section的大小)
char segname[16]; // 16個字節的段名字
uint64_t vmaddr; // 段的虛擬內存起始地址
uint64_t vmsize; // 段的虛擬內存大小
uint64_t fileoff; // 段在文件中的偏移量
uint64_t filesize; // 段在文件中的大小
vm_prot_t maxprot; // 段頁面所需要的最高內存保護(4 = r,2 = w,1 = x)
vm_prot_t initprot; // 段頁面初始的內存保護
uint32_t nsects; // 段中section數量
uint32_t flags; // 標誌位
};
複製代碼
六、Data
1. 功能總結
Data
中存儲了實際的數據與代碼,主要包含方法、符號表、動態符號表、動態庫加載信息 (重定向、符號綁定等) 等;Data
中的排布完全按照Load Command
中的描述;Data
由Segment
(段)和Section
(節)的方式來組成,通常,Data
擁有多個segment
,每個segment
可以有零到多個section
節;- 不同的
segment
都有一段虛擬地址
映射到進程的地址空間;
幾乎所有的Mach-O
文件都包含3
個segment
- __TEXT:代碼段,只讀可執行,存儲
函數的二進制代碼(__text)
,常量字符串(__cstring)
,OC的類/方法名
等信息 - __DATA:數據段, 可讀可寫,存儲
OC的字符串(__cfstring)
,以及運行時的元數據:class/protocol/method
,以及全局變量,靜態變量等; - __LINKEDIT:只讀,存儲啓動
App
需要的信息,如bind & rebase 的地址
、函數的名稱和地址等信息;
2. 源碼分析
在Data
區中,Section
佔了很大的比例,而且在Mach-O
中集中體現在__TEXT
和__DATA
兩段裏。
Section
被定義在loader.h
文件中,具體代碼如下:
struct section_64 { /* for 64-bit architectures */
char sectname[16]; // 當前section的名稱
char segname[16]; // section所在的segment名稱
uint64_t addr; // 內存中起始位置
uint64_t size; // section大小
uint32_t offset; // section的文件偏移
uint32_t align; // 字節大小對齊
uint32_t reloff; // 重定位入口的文件偏移
uint32_t nreloc; // 重定位入口數量
uint32_t flags; // 標誌,section的類型和屬性
uint32_t reserved1; // 保留(用於偏移量或索引)
uint32_t reserved2; // 保留(用於count或sizeof)
uint32_t reserved3; // 保留
};
複製代碼
七、理解大小端模式
分析Mach-O文
件時,經常會看到內存地址相關的內容,這裏就涉及到了大小端模式的概念;
- 小端模式:數據的低字節,保存在內存的低地址;
- 大端模式:數據的低字節,保存在內存的高地址;
iOS
設備的處理器是基於ARM
架構的,默認是採用小端模式 (低字節放低位)讀取數據的,而網絡和藍牙傳輸數據通常是用的大端模式 (低字節放高位):
下面以unsigned int value = 0x12345678
爲例,分別看看在兩種字節序下其存儲情況,我們可以用unsigned char buf[4]
來表示value
Little-Endian: 低地址存放低位,如下:
低地址 ------------------> 高地址
0x78 | 0x56 | 0x34 | 0x12
Big-Endian: 低地址存放高位,如下:
低地址 -----------------> 高地址
0x12 | 0x34 | 0x56 | 0x78
複製代碼
八、理解通用二進制文件
1. 基本概念
- 通用二進制文件的存儲結構,是將多種架構的
Mach-O
文件打包在一起,CPU
在讀取該二進制文件時可以自動檢測並選用合適的架構; - 通用二進制文件會同時存儲多種架構,所以比單一架構的二進制文件大很多,會佔用大量的磁盤空間。但由於系統運行時會自動選擇最合適的,不相關的架構代碼,不會佔用內存空間,所以執行效率提高了;
- 通用二進制格式也被稱爲胖二進制格式;
2. 通用二進制格式分析
通用二進制格式的定義在<mach-o/fat.h>
中:
- 下載 xnu 後,依次在
xnu -> EXTERNAL_HEADERS ->mach-o
中找到該文件。 - 通用二進制文件有兩個重要結構體:
fat_header
、fat_arch
;
兩個結構體的定義如下:
/*
- magic:可以讓系統內核讀取該文件時知道是通用二進制文件
- nfat_arch:表明下面有多個fat_arch結構體,即通用二進制文件包含多少個Mach-O
*/
struct fat_header {
uint32_t magic; /* FAT_MAGIC */
uint32_t nfat_arch; /* number of structs that follow */
};
/*
fat_arch是描述Mach-O
- cputype 和 cpusubtype:說明Mach-O適用的平臺
- offset(偏移)、size(大小)、align(頁對齊)描述了Mach-O二進制位於通用二進制文件的位置
*/
struct fat_arch {
cpu_type_t cputype; /* cpu specifier (int) */
cpu_subtype_t cpusubtype; /* machine specifier (int) */
uint32_t offset; /* file offset to this object file */
uint32_t size; /* size of this object file */
uint32_t align; /* alignment as a power of 2 */
};
複製代碼
參考鏈接
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://juejin.cn/post/7022810233105809439