最新消息:图 床

深入淺出 MachO

COOL IAM 55浏览 0评论

作者:evilpan
原文鏈接:https://evilpan.com/2020/09/06/macho-inside-out/

之前寫了一篇深入淺出ELF,作為姊妹篇這次就來聊聊MacOS的可執行文件格式MachO。

Mach-O 101

之前的文章中我們說過,可執行文件的使命有兩個,一是方便開發者在編譯、鏈接時提供可擴展的封裝結構;二是在執行時能給操作系統(內核)提供內存映射信息。MachO也不例外。

MachO本身沒有什麼特別的含義,就是Mach object的簡寫,而Mach是早期的一個微內核。和ELF一樣,MachO也極具拓展性,從全局視角來看,MachO文件可以分為三個部分,分別是:

  1. Mach Header: 文件頭信息
  2. 可變長度的LOAD_COMMAND信息
  3. 上述LOAD_COMMAND中所用到的具體信息(segments)

這裡的segment可以理解為一段連續的內存空間,擁有對應的讀/寫/執行權限,並且在內存中總是頁對齊的。每個segment由一個或者多個section組成,section表示特定含義數據或者代碼的集合(不需要頁對齊)。在macOS中,通常約定segment的名稱為雙下劃線加全大寫字母(如__TEXT),section的名稱為雙下劃線加小寫字母(如__text)。

下面對這三個部分進行分別介紹。

注: MachO文件結構的表示通常分為32位和64位兩種,本文以64位為例,畢竟這是歷史的進程。

Header

文件頭信息參考mach-o/loader.h中的定義如下:

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

filetype表示類型,常見的有:

  • MH_OBJECT: 可重定向的目標文件
  • MH_EXECUTE: 可執行文件
  • MH_DYLIB: 動態綁定的共享庫文件

flags為不同的文件標籤的組合,每個標籤佔一個位,可以用位或來進行組合,常見的標籤有:

  • MH_NOUNDEFS: 該文件沒有未定義的引用
  • MH_DYLDLINK: 該文件將要作為動態鏈接器的輸入,不能再被靜態鏈接器修改
  • MH_TWOLEVEL: 該文件使用兩級名字空間綁定
  • MH_PIE: 可執行文件會被加載到隨機地址,只對MH_EXECUTE有效

另外一個值得關注的就是ncmdssizeofcmds,分別指定了 LOAD_COMMAND 的個數以及總大小,從這裡也大概能猜到,每個 command 的大小是可變的。

Command

LOAD_COMMAND是體現MachO文件拓展性的地方,每個 command 的頭兩個word分別表示類型和大小,如下:

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

不同的cmd類型都會有其對應的結構體來描述其內容,cmdsize表示的是整個cmd的大小,即包括頭部和內容。也就是說在處理的時候當前cmd的位置加上cmdsize就是下一個cmd的位置。注意每個command的大小(即cmdsize)需要word對齊,對於32位CPU來說是4字節,64位則是8字節;同時對齊末尾的填充部分必須是0。

loader.h中絕大部分的篇幅就是用來定義各種不同command類型的結構體了,這裡挑選一些比較常見的來進行介紹。

LC_SEGMENT

LC_SEGMENT/LC_SEGMENT64可以說是最重要的一個command。表示當前文件的一部分將會映射到目標進程(task)的地址空間中,包括程序運行所需要的所有代碼和數據。假設當前MachO文件的起始地址為begin,則映射的內容為:

  • 原始地址(文件地址): begin + fileoff,大小為filesize
  • 目的地址(進程虛址): vmaddr,大小為vmsize

其中vmsize >= filesize,如果有多出來的部分需要填充為零。segment_command的結構體表示如下:

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

maxprot/initprot表示對應segment虛擬地址空間的RWX權限。如果segment包含一個或者多個section,那麼在該segment結構體之後就緊跟着對應各個section頭,總大小也包括在cmdsize之中,其結構如下:

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

每個section頭對應一個section,位置在相對文件起始地址offset處,大小為size字節,對應的虛擬地址為addr。這裡的align對齊指的是在虛擬地址空間中的對齊,實際上在文件中是連續存放的,因為有size指定大小。reloff和nreloc與符號的重定向有關,在下面的加載過程一節中再進行介紹。

從這裡可以看出,section的內容和segment是不連續存放的,只是section header在對應segment之後。而segment的vmsize實際上會大於segment+section_header的大小(即cmdsize),猜測多出來的空間是內核加載MachO時將對應section內容填充進去,後面將會對這一猜測進行驗證。

TEXT

__TEXT段包含__text__stubs__stub_helper__cstring等section,一般用來存放不可修改的數據,比如代碼和const字符串,可以用otool查看對應的section內容:

$ otool -V main -s __TEXT __stubs main: Contents of (__TEXT,__stubs) section 0000000100000f6a   jmpq    *0xa8(%rip) ## literal pool symbol address: _printf 0000000100000f70    jmpq    *0xaa(%rip) ## literal pool symbol address: _set_foo 

在實際的MachO可執行文件中觀察發現TEXT的fileoff為0,也就是說TEXT段映射的時候會將當前文件頭部分也映射到進程空間中。

(lldbinit) image dump sections main Sections for '/Users/evilpan/temp/macho-test/main' (x86_64):  SectID     Type             Load Address                             Perm File Off.  File Size  Flags      Section Name  ---------- ---------------- ---------------------------------------  ---- ---------- ---------- ---------- ----------------------------  0x00000100 container        [0x0000000000000000-0x0000000100000000)* ---  0x00000000 0x00000000 0x00000000 main.__PAGEZERO  0x00000200 container        [0x0000000100000000-0x0000000100001000)  r-x  0x00000000 0x00001000 0x00000000 main.__TEXT  0x00000001 code             [0x0000000100000ee0-0x0000000100000f6a)  r-x  0x00000ee0 0x0000008a 0x80000400 main.__TEXT.__text  0x00000002 code             [0x0000000100000f6a-0x0000000100000f76)  r-x  0x00000f6a 0x0000000c 0x80000408 main.__TEXT.__stubs  0x00000003 code             [0x0000000100000f78-0x0000000100000f9c)  r-x  0x00000f78 0x00000024 0x80000400 main.__TEXT.__stub_helper  0x00000004 data-cstr        [0x0000000100000f9c-0x0000000100000fb0)  r-x  0x00000f9c 0x00000014 0x00000002 main.__TEXT.__cstring  0x00000005 compact-unwind   [0x0000000100000fb0-0x0000000100000ff8)  r-x  0x00000fb0 0x00000048 0x00000000 main.__TEXT.__unwind_info  0x00000300 container        [0x0000000100001000-0x0000000100002000)  rw-  0x00001000 0x00001000 0x00000000 main.__DATA  0x00000006 data-ptrs        [0x0000000100001000-0x0000000100001008)  rw-  0x00001000 0x00000008 0x00000006 main.__DATA.__nl_symbol_ptr  0x00000007 data-ptrs        [0x0000000100001008-0x0000000100001018)  rw-  0x00001008 0x00000010 0x00000006 main.__DATA.__got  0x00000008 data-ptrs        [0x0000000100001018-0x0000000100001028)  rw-  0x00001018 0x00000010 0x00000007 main.__DATA.__la_symbol_ptr  0x00000009 zero-fill        [0x0000000100001028-0x000000010000102c)  rw-  0x00000000 0x00000000 0x00000001 main.__DATA.__common  0x00000400 container        [0x0000000100002000-0x0000000100007000)  r--  0x00002000 0x00004a90 0x00000000 main.__LINKEDIT 

上面例子中__TEXT段的的vm_size和file_size都是0x1000,這個大小在文件中正好是第一個__DATAsection的起始地址:

後記

本文通過對MachO文件的文件格式研究,介紹了MacOS和iOS中可執行文件的加載過程,從內核中的處理一直到動態連接器dyld的代碼分析。可以看出MachO與ELF相比實現方式各有千秋,但是在內核中原生增加了對代碼的簽名和加密,其實ELF也很容易實現類似的功能,但開放系統需要更多考慮兼容性的問題,不像蘋果可以大刀闊斧的隨便改。

對於MachO的深入理解其實也有助於日常的相關研究,比如Apple Store的加密實現以及代碼簽名的大致原理,還有針對dyld_cache的處理等,其中每一項都值得去深入挖掘。而且本文也沒有介紹到全部的MachO特性,比如Objective-C相關的段,具體的實戰部分後面有時間會再去整理一下。

參考資料


转载请注明:IAMCOOL » 深入淺出 MachO

0 0 vote
Article Rating
Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x