mach-O文件结构分析

2021-06-01  本文已影响0人  康小曹

一、概述

运行时架构(runtime architecture)是针对软件运行环境定义的一系列规则,包括但不限于:

  1. 如何为代码和数据(code and data)排位;
  2. 在内存中怎样去加载或者追踪程序的部分代码;
  3. 告诉编译器应该如何组装代码;
  4. 如何调用系统服务,如加载插件;

Mac 系统支持多种运行时架构,但是内核可以直接读取的可执行文件只有一种:Mach-O。因此,mac 的运行时架构也被命名为:Mach-O Runtime Architecture;因此,Mach-O 是一种存储标准,用于 Mach-O runtime architecture 架构中对程序的磁盘存储;

Mach-O 是 mach object 的缩写,在 -objc解决分类不加载的问题的官方文档中,明确指出所有的源文件都会被转化成一个 objcet,只不过最后经过链接操作,工程或被转化成静态库、动态库或者是可执行文件(类型不同的 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 类型。可以理解成多种类型的文件会被加载,而该 Image 如果值为特定的值,则该 Image 为 Mach-O 类型。

另外,如果该 Mach-O 的架构和编译该 Mach-O 文件的 CPU 字节序(大小端)一致,则使用 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 之类的区别,而 subtype 就是表示这个,部分定义如下:

image.png

4. filetype

filetype 就是我们熟知的 Mach-O 文件的类型,比如动态库、主工程生成可执行文件、bundle 等等,部分 type 如下:

type

举个例子🌰:

image.png

如上图,主工程生成的可执行文件就是 MH_EXECUTE、动态库则为 MH_DYLIB、ViewController.o 则为 MH_OBJECT,而 dyld 链接器 则为 MH_DYLINKER;

需要注意的是,静态库只是一个 mach-o object 的集合:

image.png

关于 fat 的格式和静态库为什么没有 header,暂时不深究???

5. ncmds && sizeofcmds

表示 header 之后的 Load Command 的段数和大小;

实例:

看看 CoreAutoLayout 动态库的 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 来描述符号表、动态符号表、字符串表的位置;

  1. 程序 main 线程的初始执行状态;

这里指的应该是 LC_MAIN 段描述的程序的入口函数位置;

  1. 主工程所导入的共享库信息;

这一点就不多说了,在 machOView 中可以直观的看到,也可以通过 otool 指令来获取;

2. Load Command 的理解

以上是官方文档对 Load Command 的表述,这里加上自己的理解。

Load Commands 由多个 command 组成,其大小由 command 的数量和 command 的 size 决定。Load Commands 更多的是一个统称的概念;

如果 Load Command 按照是否指向数据实体来分类,分为两种:

  1. 指向具体数据段

该种 command 存储了一些信息,且指向 Data 部分的具体数据。

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

再比如 LC_CODE_SIGNATURE 指向 Data 中的签名数据:

image.png

再比如动态链接相关的 __LINKEDIT 对应的 command 指向 Data 区域的 __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 这三个不用赘述了,指向代码、数据、常量区等;

这里其实可以很简单的理解成大数据都放在 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 在上文中基本讲的差不多了,一言以蔽之:

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 初始地址

即:

4. 数据在 Data 的表现形式

再次强调一下,在 Data 中只有数据/代码,并没有描述信息,只有数据/代码。

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

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

即:

来张图吧:

segment/section

总结:

  1. 数据和代码都是一坨一坨的存储在 Data 中;
  2. segment 和 section 按照两个维度划分了 Data 部分,并描述了相关的信息;

5. 为什么要有 segment 和 section

从上文看,Data 中的数据都是一团一团的二进制,Mach-O 为此区分出了 section 和 segment。section 好理解,相似类型或者相同作用的数据作为一组数据嘛~~

比如懒加载符号都在 __la_symbol_ptr 这个 section 中,非懒加载符号都在 __got 这个 section 中,代码都在 __text 这个 section 中,桩函数都在 __stub 中,桩函数的包装函数都在 __stub_helper 中,这样不就得了?为什么还要个 segment???

先说结论:

怎么解释呢?这里其实分为两点:

  1. segment 和内存对齐;
  2. 位置相对不变;

首先说内存对齐,官方文档描述如下:

segment align

即:segment 中的数据都会被印射到虚拟内存中,所以 segment 是按页对齐的。

segment is bigger when placed into vm

即:segment 中的数据在虚拟内存中占得大小要比在磁盘中所占大小更大。

比如 __PAGEZERO 段,因为没有数据,所以在磁盘中不占内存,但是在虚拟内存中占一个页的内存。

这里需要解释一下,__PAGEZERO 在 Load Command 中还是会占据少许磁盘空间的,即一个 command 结构体的大小。但是其描述的 segment 位于 Data 段,因为没有具体数据,所以在磁盘中不占空间,即为 0;当 __PAGEZERO 动态链接器加载时,因为是 segment,所以要按页对齐,最少分配一个 Page,所以虽然没有数据,但是仍然占据了一个 Page;

至此我们知道 segment 在内存中是需要按照一定规则对齐的,以此实现 I/O 或者 CPU 指令的优化;

再说说 section 的位置相对不变。

假设只有 section,那么内存对齐之后,section 如果未占满一页,那么该 section 后面的数据会留白,而在对齐之前,下一个 section 是紧跟着上一个 section 的。对齐之后,后面的 section 的位置就会发生变化。

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

也就是说,section 只记录相对于磁盘中文件初始位置的偏移,而 segment 已经根据对齐原则,算好了在虚拟内存中位置。如果是 segment 对齐后补 0,因为是补在最末尾,所以对当前 segment 中所有的 section 完全没有影响,影响的只是下一个 segment 的位置,如下图:

基地址的计算原理

即:使用 section 来记录 vmaddr 理论上也是可以实现,但是相对复杂,而且功能划分不够明确,设计感更糟糕;

dyld 和 fishhook 中计算动态链接相关表的位置的公式就是基于 segment 的 vmaddr 和 fileoff 来计算基地址,最后加上 section 中的 fileoff,详见(fishhool原理分析)[https://www.jianshu.com/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 对于链接器而言没有什么意义。但是为了支持传统的 UNIX 可执行文件,需要链接器和汇编器使用约定的名称;

注释

所以,不需要纠结有哪些 segment,只需要关注几点:

  1. command 分为指向具体的数据和不指向具体数据两种类型;
  2. section 指向 data 中一团一团的数据,segment 整合 section,在虚拟内存的加载时,屏蔽掉分页对 section 位置的影响;
上一篇下一篇

猜你喜欢

热点阅读