mach-O文件結構分析

一、概述

運行時架構(runtime architecture)是針對軟件運行環(huán)境定義的一系列規(guī)則,包括但不限于:

  1. 如何為代碼和數據(code and data)排位;
  2. 在內存中怎樣去加載或者追蹤程序的部分代碼;
  3. 告訴編譯器應該如何組裝代碼;
  4. 如何調用系統(tǒng)服務,如加載插件;

Mac 系統(tǒng)支持多種運行時架構,但是內核可以直接讀取的可執(zhí)行文件只有一種:Mach-O。因此,mac 的運行時架構也被命名為:Mach-O Runtime Architecture;因此,Mach-O 是一種存儲標準,用于 Mach-O runtime architecture 架構中對程序的磁盤存儲;

Mach-O 是 mach object 的縮寫,在 -objc解決分類不加載的問題的官方文檔中,明確指出所有的源文件都會被轉化成一個 objcet,只不過最后經過鏈接操作,工程或被轉化成靜態(tài)庫、動態(tài)庫或者是可執(zhí)行文件(類型不同的 mach-O);

Mach-O 文件分為三大部分:

  1. mach-header;
  2. load commands;
  3. segment and section;

二、mach_header

header 位于 Mach-O 文件的頭部,其作用是:

  1. 識別 Mach-O 的格式;
  2. 文件類型;
  3. CPU 架構信息;

64 位 header 結構體如下:

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 */
};

1. magic

一個整數,用于標識該文件為 Mach-O 類型??梢岳斫獬啥喾N類型的文件會被加載,而該 Image 如果值為特定的值,則該 Image 為 Mach-O 類型。

另外,如果該 Mach-O 的架構和編譯該 Mach-O 文件的 CPU 字節(jié)序(大小端)一致,則使用 MH_MAGIC,相反則使用 MH_CIGAM;

32 和 64 位為固定的值:

/* Constant for the magic field of the mach_header (32-bit architectures) */
#define MH_MAGIC    0xfeedface  /* the mach magic number */
#define MH_CIGAM    0xcefaedfe  /* NXSwapInt(MH_MAGIC) */

/* Constant for the magic field of the mach_header_64 (64-bit architectures) */
#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
#define MH_CIGAM_64 0xcffaedfe /* NXSwapInt(MH_MAGIC_64) */

如 dyld 源碼中使用這個字段來判斷是否為 Mach-O 文件:

image的使用

2. cputype

一個整數,標志該文件將被使用在何種 CPU 架構上;

定義在如下文件中:

image.png

部分 type 如下:


image.png

3. subtype:

arm 架構下有 arm_v7、arm_all 之類的區(qū)別,而 subtype 就是表示這個,部分定義如下:

image.png

4. filetype

filetype 就是我們熟知的 Mach-O 文件的類型,比如動態(tài)庫、主工程生成可執(zhí)行文件、bundle 等等,部分 type 如下:

type

舉個例子??:

image.png

如上圖,主工程生成的可執(zhí)行文件就是 MH_EXECUTE、動態(tài)庫則為 MH_DYLIB、ViewController.o 則為 MH_OBJECT,而 dyld 鏈接器 則為 MH_DYLINKER;

需要注意的是,靜態(tài)庫只是一個 mach-o object 的集合:

image.png

關于 fat 的格式和靜態(tài)庫為什么沒有 header,暫時不深究???

5. ncmds && sizeofcmds

表示 header 之后的 Load Command 的段數和大??;

實例:

看看 CoreAutoLayout 動態(tài)庫的 Mach-O 文件:

image.png

ps:后文會有 ncmds 在 fishhook 中的使用;

三、Load Command

1. Load Command 作用概述

其作用有:

  1. Mach-O 文件的布局;

這一點和 Mach-O 本身的設計有關,Load Command 本身不包含數據,Load Command 中的 segment 和section 類似于一個指針的作用,其描述(指向)的 segment 或者 section 實體才是真正存儲數據或代碼的地方。

  1. 鏈接信息;

這一點主要是通過幾個段(LC_SYMTAB、LC_LOAD_DYNLINER __Linkedit 等) 來描述符號表相關的信息,鏈接器位置等。dyld 通過這些信息進行符號表的 rebase 和 bind 等操作;

  1. Mach-O 文件在虛擬內存中的初始化布局;

這一點應該跟 __PAGEZERO 有關,具體??待補充

  1. 符號表的位置;

是鏈接信息的一部分,主要由 LC_SYMTAB、LC_DYSYMTAB、__LINKEDIT 來描述符號表、動態(tài)符號表、字符串表的位置;

  1. 程序 main 線程的初始執(zhí)行狀態(tài);

這里指的應該是 LC_MAIN 段描述的程序的入口函數位置;

  1. 主工程所導入的共享庫信息;

這一點就不多說了,在 machOView 中可以直觀的看到,也可以通過 otool 指令來獲?。?/p>

2. Load Command 的理解

以上是官方文檔對 Load Command 的表述,這里加上自己的理解。

Load Commands 由多個 command 組成,其大小由 command 的數量和 command 的 size 決定。Load Commands 更多的是一個統(tǒng)稱的概念;

如果 Load Command 按照是否指向數據實體來分類,分為兩種:

  1. 指向具體數據段

該種 command 存儲了一些信息,且指向 Data 部分的具體數據。

如 LC_SEGMENT(segment_command) 指向存放函數代碼的 __TEXT 段,程序員打交道最多的 __DATA / __DATA_CONST 段;

再比如 LC_CODE_SIGNATURE 指向 Data 中的簽名數據:

image.png

再比如動態(tài)鏈接相關的 __LINKEDIT 對應的 command 指向 Data 區(qū)域的 __LINKEDIT 段;

  1. 不指向具體數據段

該種 command 一般不包含數據實體,只起到描述性作用。

不像 LC_SEGMENT 一般會指向一個 SEGMENT,比如 __TEXT。而 LC_MAIN、LC_RPATH 等這些 command 都只是告訴 dyld 一些信息,不指向具體的數據段。常見的 command 如下在會問會有列舉;

3. Load Command 源碼解讀

接著,再說說 load_command 在代碼層面上的表現。

代碼層面上,command 的基本結構體為:

struct load_command {
    uint32_t cmd;       /* type of load command */
    uint32_t cmdsize;   /* total size of command in bytes */
};

這個結構體相當于一個基類。類似的結構體還有很多,比如:dysymtab_command、segment_command 等等,都包含 cmd 和 cmdsize;

因為 load_command 包含的信息太少,編碼時不好用,所以在代碼層面上被使用更多的是 LC_SEGMENT 對應的結構體和其他類型的結構體,如:

LC_SEGMENT:

LC_SEGMENT

非 LC_SEGMENT 的 command 結構體如下:

LC_SYMTAB:

LC_SYMTAB

其他的還有 dysymtab_commanddylinker_command 等等,可以自行在源碼中查看。

舉個例子??:

這里以 fishhook 源碼來舉個實例,看如下代碼:

   // 定位到 LC 其實位置
  uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
    
    // 遍歷LC中的所有command,找出__LINKEDIT、LC_SYMTAB、LC_DYSYMTAB
  for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
    // 這里先直接強轉成 segment_command ,因為比load_command 更好用
    cur_seg_cmd = (segment_command_t *)cur;
      
    if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
        // __LINKEDIT是segment_command類型不需要再強轉
      if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
        linkedit_segment = cur_seg_cmd;
      }
        
    } else if (cur_seg_cmd->cmd == LC_SYMTAB) {
    // LC_SYMTAB是symtab_command類型需要強轉
      symtab_cmd = (struct symtab_command*)cur_seg_cmd;
    } else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
      // LC_DYSYMTAB是dysymtab_command類型需要強轉
      dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
    }
  }

上述代碼是 fishhook 中尋找 __LINKEDIT 段、LC_SYMTAB、LC_DYSYMTAB 的代碼,其中 __LINKEDIT 對應的類型就是 segment_command,LC_SYMTAB 對應的是 symtab_command 類型,LC_DYSYMTAB 對應的是 dysymtab_command 類型;

總結:

  1. Load Command 由多個 command 組成;
  2. command 主要有兩種類型:指向具體數據、不指向具體數據;
  3. 代碼層面上 load_command 結構體相當于基類,很少被使用;

4. Load Command 和 segment/section 的關系

上文中講到 Load Command 主要分為指向數據實體和不指向數據實體兩種類型。

不指向數據實體的 command 主要作用是為 dyld 提供信息,而指向數據實體的 command 才是 command 和 segment/section 關系的體現;

如 LC_SEGMENT 指向具體的 segment,這個 segment 的實體部分就是 Mach-O 文件的第三部分,主要內容是代碼和數據;

延伸官方的圖片,繪制如下:

Mach-O結構

如上圖, LC_SEGMENT 類型的 command 指向具體的 section data。常見的 segment_command 一般也就幾個:__TEXT、__DATA、__DATA_CONST、__LINKEDIT、__PAGEZERO;

_TEXT、__DATA、__DATA_CONST 這三個不用贅述了,指向代碼、數據、常量區(qū)等;

這里其實可以很簡單的理解成大數據都放在 Data 中并在 command 中添加相關的信息,使用時可以很方便的找到。小數據則直接存放在 command 中(再大你也放不下?。_@里的設計思想和索引/目錄的思想很類似,Load Command 就相當于目錄;

總結:

  1. __LINKEDIT 指向存放 link 操作必要的數據段,是鏈接操作奠基石般的存在;
  2. 非數據類型的 command 用于未 dyld 提供簡短的信息;
  3. 數據類型的 command 在提供信息的同時,指向了 Data 段具體的數據/代碼;
  4. 具體的數據使用 segment 和 section 進行分段和分組;

5. __LINKEDIT是否屬于段

__LINKEDIT 也屬于 segment, command 指向 __LINKEDIT 這個段。只是在 machOView 軟件上沒有體現:

machOView

而使用 image lookup memory 是可以看到的:

image lookup

另外,代碼層面上也有體現:

linkedit_data_command

從上圖可以看到,很多 command (LC_CODE_SIGNATURE等)都是用了 linkedit_data_command 這個結構體。而其中的 dataoff 則描述了對應數據在 __LINKEDIT 中的位置;

而 __LINKEDIT 這個 command 使用了 LC_SEGMENT ,對應著 segment_command 這個結構體。所以,這個 linkedit_data_command 更像是一個補充的作用。

四、segment 和 section

1. segment

segment 命令在 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 */
};

解讀:

segment

其實關于 segment 和 section 在上文中基本講的差不多了,一言以蔽之:

  • Data 段的代碼是一團一團(blob)的,而 segment 和 section 以段和組的維度指向 Data 區(qū)域的數據或代碼的實體,利于尋址和使用;

2. segment 和 section的關系

segment 相當于一個數組,section 相當于數組中的元素;

這里需要注意的是,segment_command 中的 nsects 。該字段起到了數組的作用,用于 section 的尋址。這個數組是采用(數量 + 大?。┑姆绞絹碇苯荧@取對應的地址,從而獲取到對應的 section 。

其實這種方式在 Mach-O 文件中很常見。比如 Header 后面跟的就是 Load Command, Load Command 地址 = Header 的初始地址 + Header.size ,這也是為什么 Header 結構體中包含 load commands 的個數,而 segment 結構體又包含 section 的個數的原因,fishhook 源碼中有體現:

  1. 計算load command的初始位置
// 計算load command的初始位置
// header 是一個地址,指向這個 mach-O object 的初始位置
// 頭部是一個Header(mach_header_t結構體),緊接著是Loac Command
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
  1. 遍歷 Load Command
// ncmds 表示load command 的個數
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
    // .....
}
  1. 遍歷 segment 中的 section
// nsects為number of sections
for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
    // ......
}

3. section

section 以“組”的維度指向 Data 部分中的數據。在 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 */
};

上文說了 segment 相當于一個數組,section 相當于數組中的元素。但是有一點需要注意:segment 本身是會存儲一些信息的,其實這一點在 Mach-O 文件中也可以看到:

  1. segment 初始地址 ≠ 第一個 section header 的地址;
segment初始地址
section header 初始地址

即:

  • segment 中不僅像數組一樣描述了該 segment 包含的 section,還存儲了 segment 段的一些信息;

4. 數據在 Data 的表現形式

再次強調一下,在 Data 中只有數據/代碼,并沒有描述信息,只有數據/代碼。

如下可以看到 Data 部分中的某個 section 初始地址 = 第一個 section 的地址:

section 初始地址
section第一個元素初始地址

即:

  • Data 部分的數據/代碼是一團一團的純數據按順序排列,沒有描述信息,segment 和 section 是一個統(tǒng)籌的概念;

來張圖吧:

segment/section

總結:

  1. 數據和代碼都是一坨一坨的存儲在 Data 中;
  2. segment 和 section 按照兩個維度劃分了 Data 部分,并描述了相關的信息;

5. 為什么要有 segment 和 section

從上文看,Data 中的數據都是一團一團的二進制,Mach-O 為此區(qū)分出了 section 和 segment。section 好理解,相似類型或者相同作用的數據作為一組數據嘛~~

比如懶加載符號都在 __la_symbol_ptr 這個 section 中,非懶加載符號都在 __got 這個 section 中,代碼都在 __text 這個 section 中,樁函數都在 __stub 中,樁函數的包裝函數都在 __stub_helper 中,這樣不就得了?為什么還要個 segment???

先說結論:

  • 功能細化,segment 負責內存對齊以及保持 section 相對位置不變。section 則只管數據/代碼的存儲;

怎么解釋呢?這里其實分為兩點:

  1. segment 和內存對齊;
  2. 位置相對不變;

首先說內存對齊,官方文檔描述如下:

segment align

即:segment 中的數據都會被印射到虛擬內存中,所以 segment 是按頁對齊的。

segment is bigger when placed into vm

即:segment 中的數據在虛擬內存中占得大小要比在磁盤中所占大小更大。

比如 __PAGEZERO 段,因為沒有數據,所以在磁盤中不占內存,但是在虛擬內存中占一個頁的內存。

這里需要解釋一下,__PAGEZERO 在 Load Command 中還是會占據少許磁盤空間的,即一個 command 結構體的大小。但是其描述的 segment 位于 Data 段,因為沒有具體數據,所以在磁盤中不占空間,即為 0;當 __PAGEZERO 動態(tài)鏈接器加載時,因為是 segment,所以要按頁對齊,最少分配一個 Page,所以雖然沒有數據,但是仍然占據了一個 Page;

至此我們知道 segment 在內存中是需要按照一定規(guī)則對齊的,以此實現 I/O 或者 CPU 指令的優(yōu)化;

再說說 section 的位置相對不變。

假設只有 section,那么內存對齊之后,section 如果未占滿一頁,那么該 section 后面的數據會留白,而在對齊之前,下一個 section 是緊跟著上一個 section 的。對齊之后,后面的 section 的位置就會發(fā)生變化。

這就是為什么 segment command 既有 vmaddr 又有 fileoff ,而 section 只有 fileoff(如symoff、stroff);

也就是說,section 只記錄相對于磁盤中文件初始位置的偏移,而 segment 已經根據對齊原則,算好了在虛擬內存中位置。如果是 segment 對齊后補 0,因為是補在最末尾,所以對當前 segment 中所有的 section 完全沒有影響,影響的只是下一個 segment 的位置,如下圖:

基地址的計算原理

即:使用 section 來記錄 vmaddr 理論上也是可以實現,但是相對復雜,而且功能劃分不夠明確,設計感更糟糕;

dyld 和 fishhook 中計算動態(tài)鏈接相關表的位置的公式就是基于 segment 的 vmaddr 和 fileoff 來計算基地址,最后加上 section 中的 fileoff,詳見(fishhool原理分析)[http://m.itdecent.cn/p/c856f5cbbadb]

五、常見的 command

LoadCommand.png

六、常見的 segment

常見的 segment 如下:

  1. __PAGEZERO;
  2. __TEXT;
  3. __DATA;
  4. __DATA_CONST;
  5. __LINKEDIT;

其實還有 __OBJC 、__IMPORT 等,具體定義在 loader.h 中,定義了常見的 segment 和 section:

loader.h

注釋中也說明了,這些 segment name 和 section name 對于鏈接器而言沒有什么意義。但是為了支持傳統(tǒng)的 UNIX 可執(zhí)行文件,需要鏈接器和匯編器使用約定的名稱;

注釋

所以,不需要糾結有哪些 segment,只需要關注幾點:

  1. command 分為指向具體的數據和不指向具體數據兩種類型;
  2. section 指向 data 中一團一團的數據,segment 整合 section,在虛擬內存的加載時,屏蔽掉分頁對 section 位置的影響;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Mach-O文件 Mach-O是Mach Object文件格式的縮寫,是mac以及iOS上可執(zhí)行文件的格式,例如當...
    YanZi_33閱讀 680評論 0 2
  • Mach-O Mach-O文件格式是 OS X 與 iOS 系統(tǒng)上的可執(zhí)行文件格式,類似于windows的 PE ...
    Joolybgo閱讀 1,155評論 0 1
  • 一. 先給出一個結構圖,大致了解一下內部的結構: 主要結構分成三個部分: Header部分:保存了該文件的一些基本...
    ldzSpace閱讀 2,622評論 1 2
  • 一、前言 本文簡要解析Mach-O文件格式、結構,主要是自己認識Mach-O文件,學習的一個過程,一些地方可能介紹...
    KinKen閱讀 1,720評論 0 4
  • 表情是什么,我認為表情就是表現出來的情緒。表情可以傳達很多信息。高興了當然就笑了,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 129,937評論 2 7

友情鏈接更多精彩內容