Mach-O

hibo發表於2019-10-02

一、什麼是Mach-O檔案?

Mach-OMach Object檔案格式的縮寫,是mac以及iOS上可執行檔案的格式。Mach-O檔案對應有多種格式:

  1. 目標檔案.o
  2. 庫檔案: .a靜態庫檔案 .dylib動態庫檔案 .framework系統級為動態庫檔案,自己建立的為靜態庫檔案
  3. 可執行檔案及MDW.app內部的MDW檔案(通用二進位制檔案)
  4. dyld動態連結器將依賴的動態庫載入到記憶體
  5. .dsym符號表

Xcode中我們可以直接建立.c檔案,通過終端clang命令來對.c檔案進行編譯或生成可執行檔案,下面看一下clang怎樣使用的。

1、建立一個main.c檔案如下:

#include <stdio.h>
int main(){
    printf("列印:yahibo\n");
    return 0;
}
複製程式碼

2、編譯檔案

clang -c main.c
複製程式碼

會生成main.o檔案,該檔案即為mach-o檔案,通過命令file main.o檢視檔案資訊如下:

main.o: Mach-O 64-bit object x86_64
複製程式碼

是一個object型別的檔案稱為目標檔案,並不是可執行檔案

3、生成可執行檔案

  • 命令clang main.o 會生成a.out檔案,即可執行檔案,通過ls檢視
  • 命令clang -o main main.o 也會生成可執行檔案main
  • 命令clang -o main main.c 直接根據原始檔生成可執行檔案main
  • 命令 size -x -l -m a.out 檢視檔案資訊,如下:
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0)
Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0)
	Section __text: 0x2a (addr 0x100000f50 offset 3920)
	Section __stubs: 0x6 (addr 0x100000f7a offset 3962)
	Section __stub_helper: 0x1a (addr 0x100000f80 offset 3968)
	Section __cstring: 0x11 (addr 0x100000f9a offset 3994)
	Section __unwind_info: 0x48 (addr 0x100000fac offset 4012)
	total 0xa3
Segment __DATA: 0x1000 (vmaddr 0x100001000 fileoff 4096)
	Section __nl_symbol_ptr: 0x10 (addr 0x100001000 offset 4096)
	Section __la_symbol_ptr: 0x8 (addr 0x100001010 offset 4112)
	total 0x18
Segment __LINKEDIT: 0x1000 (vmaddr 0x100002000 fileoff 8192)
total 0x100003000
複製程式碼

4、執行檔案

./a.out 或 ./main
複製程式碼

輸出:

列印:yahibo
複製程式碼

以上步驟可以用來寫c並編譯執行。

多個檔案是如何編譯的呢?

開發中根據不同功能模組我們會分很多檔案來實現,在clang中是可以對多個檔案進行一次性打包,生成一個可執行檔案。如下:

1、新建一個功能檔案

people.c

#include <stdio.h>
void sleep(){
    printf("正在睡覺\n");
}
複製程式碼

2、在main.c中宣告sleep方法並呼叫

void sleep();//宣告方法
int main(){
    printf("列印:yahibo\n");
    sleep();//呼叫方法
    return 0;
}
複製程式碼

3、編譯為可執行檔案

clang -o main main.c people.c
複製程式碼

4、執行可執行檔案

./main
複製程式碼

執行如下:

列印:yahibo
正在睡覺
複製程式碼

二、通用二進位制檔案(Universal binary)

iOS中不同手機對應著可能不同的架構,如arm64、armv7、armv7s。為了相容不同架構的手機,蘋果推出了通用二進位制檔案,包含了應用程式常用的這些架構,因此通用二進位制檔案,比單一架構二進位制檔案要大很多。

架構選擇

arm.png

  • 注意以上標記的兩處取交集,來確認最終架構
  • 1處預設架構為arm64、armv7
  • 如果需要新增armv7s還需要在1處新增armv7s字元

通過以上配置真實編譯出來的是包含arm64、armv7架構,因為工程中使用了第三方靜態庫不包含armv7s因此這裡配置為標準架構模式。

通用二進位制檔案在哪呢?

xxx.app中的xxx黑色檔案即是通用二進位制檔案,右鍵xxx.app顯示包內容即可獲得。

lipo命令

通過lipo命令可以檢視、拆分及合併以上提出的架構,在做靜態庫時也會使用,來合併真機下和模擬器下的靜態庫,以適應不同的除錯環境。

  • MDW.app中我獲取可執行檔案MDW

1、檢視架構資訊

lipo -info MDW
複製程式碼

列印如下:

Architectures in the fat file: MDW are: armv7 arm64
複製程式碼

2、拆分armv7、arm64架構

lipo MDW -thin armv7 -output MDW_armv7
lipo MDW -thin arm64 -output MDW_arm64
複製程式碼

檢視armv7資訊:

lipo -info MDW_armv7
複製程式碼

列印如下:

Non-fat file: MDW_armv7 is architecture: armv7
複製程式碼

檢視arm64資訊:

lipo -info MDW_arm64
複製程式碼

列印如下:

Non-fat file: MDW_arm64 is architecture: arm64
複製程式碼

3、合併架構

lipo -create MDW_armv7 MDW_arm64 -output MDW_ALL
複製程式碼

檢視合併後的資訊

lipo -info MDW_ALL
複製程式碼

列印如下:

Architectures in the fat file: MDW_ALL are: armv7 arm64
複製程式碼

產生的可執行檔案如圖:

mackos.png

三、Mach-O檔案結構

官方圖解:

structure.png

檔案分為三個部分:

  • Header:包含Mach-O檔案的基本資訊,位元組順序、架構型別、載入指令的數量等
  • Load commands:包含區域位置、符號表、動態符號表,載入Mach-O檔案時使用這裡的資料確定記憶體分佈
  • Data:資料段segement,包含具體程式碼、常量、類、方法等,有多個segment,每個segment有0到多個section,每個段有一個虛擬地址對映到程式的地址空間

直接使用MachOView開啟MDW可執行檔案,如下:

macho.png

  • 胖二進位制檔案中包含了armv7、arm64架構
  • 通過MachOView即可檢視可執行檔案的所有資訊

1、Header

除了以上直接檢視header,還可以通過otool命令檢視header資訊:

otool -f MDW
複製程式碼

列印如下:

Fat headers
fat_magic 0xcafebabe
nfat_arch 2
architecture 0
    cputype 12
    cpusubtype 9
    capabilities 0x0
    offset 16384
    size 7587424
    align 2^14 (16384)
architecture 1
    cputype 16777228
    cpusubtype 0
    capabilities 0x0
    offset 7618560
    size 8748384
    align 2^14 (16384)
複製程式碼

otool -h MDW
複製程式碼

列印:

Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
 0xfeedface      12          9  0x00           2    48       5080 0x00210085
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
 0xfeedfacf 16777228          0  0x00           2    48       5752 0x00210085
複製程式碼
  • 以上列印的兩段分別是armv7、arm64架構下的header資訊 在objc4原始碼loader.h檔案中有mach_header的結構體定義,如下:
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 */
};
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 */
};
複製程式碼
  • magic:魔數,確定是64位還是32位
  • cputype:cpu型別
  • cpusubtype:cpu子型別
  • filetype:Mach-O支援多種檔案型別,使用filetype來標註具體檔案型別
  • ncmds:載入命令的數量
  • sizeofcmds:命令區域(load commands)總的位元組大小
  • flags:標識二進位制檔案所支援的功能,主要與系統的載入、連結有關

2、Load commands

Header之後是load commands段為載入命令段,在header結構體中有對載入命令段相關資訊的描述,用於解析載入命令。在objc4原始碼loader.h中,有對loadcommand的定義:

struct load_command {
 	uint32_t cmd;		/* type of load command */
 	uint32_t cmdsize;	/* total size of command in bytes */
};
複製程式碼
  • cmd:命令型別,針對不同架構有不同的結構(32位、64位)
  • cmdsize:命令所佔位元組大小(32位size必須為4位元組的倍數,64位size必須為8位元組的倍數) 在檔案中有兩個結構體segment_commandsegment_command_64針對不同架構的結構體,內部設定欄位相同。以segment_command_64為例:
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:載入命令型別
  • LC_SEGMENT:表示這好似一個段載入命令,需要將它載入到對應的程式空間上
  • LC_LOAD_DYLIB:這是一個需要動態載入的連結庫,它使用dylib_command結構體表示
  • LC_MAIN:記錄了可執行檔案的主函式main()的位置,它使用entry_point_command結構體表示
  • LC_CODE_SIGNATURE:程式碼簽名的載入命令,描述了Mach-O的程式碼簽名資訊,它屬於連結資訊,使用linkedit_data_command結構體表示
  • cmdsize:載入命令所佔記憶體大小
  • segname:存放16位元組大小的段名字,當前是__PAGEZERO
  • vmaddr:段的虛擬記憶體起始地址
  • vmsize:段的虛擬記憶體大小
  • fileoff:段在檔案中偏移量
  • filesize:段在檔案大小
  • maxprot:段頁面所需要的最高記憶體保護(4=r,2=w,1=x)
  • initprot:段頁面初始的記憶體保護
  • nsects:段中包含section的數量
  • flags:其他雜項標誌位 在看MachOView中的loadcommands欄位:

loadcommands.png

以上為是應用程式所有載入命令,通過上面流程能夠看到對系統庫的載入順序。對比專案中引入的庫檔案,順序是一致的,如下圖:

xcode.png

以上載入命令含義如下:

  • LC_SEGMENT_64:將檔案中的段對映到程式地址空間中
  • LC_DYLD_INFO_ONLY:動態連結相關資訊
  • LC_SYMTAB:符號表資訊,位置、偏移、資料個數,供dyld使用
  • LC_DYSYMTAB:動態符號表資訊,供dyld使用
  • LC_LOAD_DYLINKER:連結器資訊,記錄使用那些連結器完成核心後序的載入工作
  • LC_UUID:Mach-O檔案的唯一標識
  • LC_VERSION_MIN_MACOSX:支援最低作業系統版本
  • LC_SOURCE_VERSION:原始碼的版本號
  • LC_MAIN:設定主執行緒的入口即棧大小
  • LC_LOAD_DYLIB:依賴庫資訊,dyld通過該命令去載入依賴庫
  • LC_FUNCTION_STARTS:函式的起始地址表
  • LC_CODE_SIGNATURE:程式碼簽名

3、Data

Data區域由Segment段和Section節組成:

segment.png

  • segment主要有__TEXT__DATA組成
  • __text:是主程式程式碼
  • __stubs、__stub_helper:是動態連結的樁
  • __cstring:程式中c語言字串
  • __const:常量

Section含義:

  • Section64(__TEXT,__objc_methname):OC類名
  • Section64(__DATA,__objc_classlist):OC類列表
  • Section64(__DATA,__objc_protollist):OC原型列表
  • Section64(__DATA,__objc_imageinfo):OC映象資訊
  • Section64(__DATA,__objc_selfrefs):OC類自引用
  • Section64(__DATA,__objc_superrefs):OC類超類的引用
  • Section64(__DATA,__ivar):OC類成員變數

等等,都是通過section來對OC中的具體類別做載入的。segment段分32位和64位,欄位相同,以64為例如下:

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 */
};
複製程式碼
  • sectname:__text ,就是主程式程式碼
  • segname:該section所屬的segment名,第一個是__TEXT
  • addr:當前section在記憶體中的起始位置
  • size:當前section所佔記憶體大小
  • offset:當前section的檔案偏移
  • align:位元組大小對齊
  • reloff:重定位入口的檔案偏移,0
  • nreloc:需要重定位的入口數量,0
  • flags:包含sectiontypeattributes
  • reserved1、reserved2預留欄位