美文网首页
mach-O文件结构分析

mach-O文件结构分析

作者: 康小曹 | 来源:发表于2021-06-01 11:07 被阅读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 在上文中基本讲的差不多了,一言以蔽之:

    • Data 段的代码是一团一团(blob)的,而 segment 和 section 以段和组的维度指向 Data 区域的数据或代码的实体,利于寻址和使用;

    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 是一个统筹的概念;

    来张图吧:

    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???

    先说结论:

    • 功能细化,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 动态链接器加载时,因为是 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 位置的影响;

    相关文章

      网友评论

          本文标题:mach-O文件结构分析

          本文链接:https://www.haomeiwen.com/subject/kaqnrltx.html