美文网首页iOS
Objective-C runtime机制(前传)——Mach-

Objective-C runtime机制(前传)——Mach-

作者: 无忘无往 | 来源:发表于2019-01-31 15:32 被阅读9次

    Mach-O

    Mach-O是Mach Object文件格式的缩写。它是用于可执行文件,动态库,目标代码的文件格式。作为a.out格式的替代,Mach-O格式提供了更强的扩展性,以及更快的符号表信息访问速度。

    Mach-O格式为大部分基于Mach内核的操作系统所使用的,包括NeXTSTEP, Mac OS X和iOS,它们都以Mach-O格式作为其可执行文件,动态库,目标代码的文件格式。

    具体到我们的iOS程序,当用XCode打包后,会生成一个.app为扩展名的文件(位于工程目录/Products文件夹下),其实.app是一个文件夹,我们用鼠标右键选择‘Show Package contents’,就可以查看文件夹的内容,其中会发现有一个和我们工程同名的unix 可执行文件,这个就是iOS可执行文件,它是符合Mach-O格式的。


    image

    Mach-O文件结构

    关于Mach-O的文件格式,在苹果官网已经找不到相关说明了,但是你可以通过下面链接获取PDF版说明:
    Mach-O File Format Reference

    Mach-O格式如下图所示,它被分为headerload commandsdata三大部分:

    image

    header:对Mach-O文件的一个概要说明,包括Magic Number, 支持的CUP类型等。

    load commands: 当系统加载Mach-O文件时,load command会指导苹果的动态加载器(dyld)h或内核,该如何加载文件的Data数据。

    data: Mach-O文件的数据区,包含代码和数据。其中包含若干Segment块(注意,除了Segment块之外,还有别的内容,包括code signature,符号表之类,<font color='red'>不要被苹果的图所误导!</font>),每个Segment块中包含0个或多个seciton。Segment根据对应的load command被dyld加载入内存中。

    我们可以使用MachOView(一个查看MachO 格式文件信息的开源工具)工具来查看一个具体的文件的Mach-O格式。

    header

    我们以一个普通的iOS APP为例,看看Mach-O文件header部分的具体内容。通过MachOView打开可执行文件,可以看到header的结构:

    image

    是不是有些懵?下面我们就结合Darwin内核源码,来了解下Mach header的定义。

    Mach header的定义位于Darwin源码中的 EXTERNAL_HEADERS/mach-o/loader.h 中:

    32位:

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

    64位:

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

    可以看到,32位和64位的Mach header基本相同,只不过64位header中多了一个保留参数reserved。

    • magic:魔数,用来标识这是一个Mach-O文件,有32位和64位两个版本:
    #define MH_MAGIC    0xfeedface  /* the mach magic number */
    #define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
    
    • cputype:支持的CUP架构类型,如arm。
    • cpusubtype:在支持的CUP架构类型下,所支持的具体机器型号。在我们的例子中,APP是支持所有arm64的机型的:CUP_SUBTYPE_ARM64_ALL
    • filetype: Mach-O的文件类型。包括:
    #define MH_OBJECT   0x1     /* Target 文件:编译器对源码编译后得到的中间结果 */
    #define MH_EXECUTE  0x2     /* 可执行二进制文件 */
    #define MH_FVMLIB   0x3     /* VM 共享库文件(还不清楚是什么东西) */
    #define MH_CORE     0x4     /* Core 文件,一般在 App Crash 产生 */
    #define MH_PRELOAD  0x5     /* preloaded executable file */
    #define MH_DYLIB    0x6     /* 动态库 */
    #define MH_DYLINKER 0x7     /* 动态连接器 /usr/lib/dyld */
    #define MH_BUNDLE   0x8     /* 非独立的二进制文件,往往通过 gcc-bundle 生成 */
    #define MH_DYLIB_STUB   0x9     /* 静态链接文件(还不清楚是什么东西) */
    #define MH_DSYM     0xa     /* 符号文件以及调试信息,在解析堆栈符号中常用 */
    #define MH_KEXT_BUNDLE  0xb     /* x86_64 内核扩展 */
    
    • ncmds:load command的数量
    • sizeofcmds: 所有load command的大小
    • flags: Mach-O文件的标志位。主要作用是告诉系统该如何加载这个Mach-O文件以及该文件的一些特性。有很多值,我们取常见的几种:
    #define MH_NOUNDEFS 0x1     /* Target 文件中没有带未定义的符号,常为静态二进制文件 */
    #define MH_SPLIT_SEGS   0x20  /* Target 文件中的只读 Segment 和可读写 Segment 分开  */
    #define MH_TWOLEVEL 0x80        /* 该 Image 使用二级命名空间(two name space binding)绑定方案 */
    #define MH_FORCE_FLAT   0x100 /* 使用扁平命名空间(flat name space binding)绑定(与 MH_TWOLEVEL 互斥) */
    #define MH_WEAK_DEFINES 0x8000 /* 二进制文件使用了弱符号 */
    #define MH_BINDS_TO_WEAK 0x10000 /* 二进制文件链接了弱符号 */
    #define MH_ALLOW_STACK_EXECUTION 0x20000/* 允许 Stack 可执行 */
    #define MH_PIE 0x200000  /* 加载程序在随机的地址空间,只在 MH_EXECUTE中使用 */
    #define MH_NO_HEAP_EXECUTION 0x1000000 /* 将 Heap 标记为不可执行,可防止 heap spray 攻击 */
    

    MH_PIE 随机地址空间

    每次系统加载进程后,都会为其随机分配一个虚拟内存空间。
    在传统系统中,进程每次加载的虚拟内存是相同的。这就让黑客有可能篡改内存来破解软件。

    dyld

    dyld是苹果公司的动态链接库,用来把Mach-O文件加载入内存。

    二级命名空间

    表示其符号空间中还会包含所在库的信息。这样可以使得不同的库导出通用的符号。与其相对的是扁平命名空间。

    Load commands

    load commands 紧跟在header之后,用来告诉内核和dyld,如何将各个Segment加载入内存中。
    load command被源码表示为struct,有若干种load command,但是共同的特点是,在其结构的开头处,必须是如下两个属性:

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

    苹果为cmd定义了若干的宏,用来表示cmd的类型,下面列举出几种:

    // 描述该如何将32或64位的segment 加载入内存,对应segment command类型
    #define LC_SEGMENT  0x1
    #define LC_SEGMENT_64   0x19    
    // UUID, 2进制文件的唯一标识符
    #define LC_UUID     0x1b
    // 启动动态加载器dyld
    #define LC_LOAD_DYLINKER 0xe
    

    Segment load command

    在这么多的load command中,需要我们重点关注的是segment load command。segment command解释了该如何将Data中的各个Segment加载入内存中,而和我们APP相关的逻辑及数据,则大部分位于各个Segment中。

    而和我们的Run time相关的Segment,则位于__DATA类型Segment下。

    Segment load command分为32位和64位:

    struct segment_command { /* for 32-bit architectures */
        uint32_t    cmd;        /* LC_SEGMENT */
        uint32_t    cmdsize;    /* includes sizeof section structs */
        char        segname[16];    /* segment name */
        uint32_t    vmaddr;     /* memory address of this segment */
        uint32_t    vmsize;     /* memory size of this segment */
        uint32_t    fileoff;    /* file offset of this segment */
        uint32_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 */
    };
    
    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 */
    };
    

    32位和64位的Segment load command基本类似,只不过在64位的结构中,把和寻址相关的数据类型,由32位的uint32_t改为了64位的uint64_t类型。

    结构体的定义,看注释基本能够看懂,就是maxprot, initprot不太明白啥意思。

    这里介绍一个特殊的‘Segment’,叫做__PAGEZERO Segment。 这里说它特殊,是因为这个Segment其实是苹果虚拟出来的,只是一个逻辑上的段,而在Data中,根本没有对应的内容,也没有占用任何硬盘空间。

    __PAGEZERO Segment在VM中被置为Read only,逻辑上占用APP最开始的4GB空间,用来处理空指针。

    我们用MachOV点开__PAGEZERO Segment所对应的Segment load command,LC_SEGMENT_64(__PAGEZERO):


    image

    可以看到其vm size是4GB,但其真正的物理地址File size和offset都是0。

    Section header

    在Data中,程序的逻辑和数据是按照Segment(段)存储,在Segment中,又分为0或多个section,每个section中在存储实际的内容。而之所以这么做的原因在于,在section中,可以不用内存对齐达到节约内存的作用,而所有的section作为整体的Segment,又可以整体的内存对齐。

    在Mach-O文件中,每一个Segment load command下面,都会包含对应Segment 下所有section的header。
    section header的定义如下:

    struct section { /* for 32-bit architectures */
        char        sectname[16];   /* name of this section */
        char        segname[16];    /* segment this section goes in */
        uint32_t    addr;       /* memory address of this section */
        uint32_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) */
    };
    
    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 */
    };
    
    

    这样,关于load commonds部分,其真正的结构其实和苹果提供的图片有些许的差异:

    image

    Data

    Mach-O的Data部分,其实是真正存储APP 二进制数据的地方,前面的header和load command,仅是提供文件的说明以及加载信息的功能。

    Data部分也被分为若干的部分,除了我们前面提到的Segment外,还包括符号表,代码签名,动态加载器信息等。

    而程序的逻辑和数据,则是放在以Segment分割的Data部分中的。我们在这里,仅关心Data中的Segment的部分。

    Segment根据内容的不同,分为若干类型,类型名称均是以“双下划线+大写英文”表示,有的Segment下面还会包含若干的section,section的命名是以"双下划线+小写英文"表示。

    先来看Segment,Mach-O中有如下几种Segment:

    #define SEG_PAGEZERO    "__PAGEZERO" /* 当时 MH_EXECUTE 文件时,表示空指针区域 */
    #define SEG_TEXT    "__TEXT" /* 代码/只读数据段 */
    #define SEG_DATA    "__DATA" /* 数据段 */
    #define SEG_OBJC    "__OBJC" /* Objective-C runtime 段 */
    #define SEG_LINKEDIT    "__LINKEDIT" /* 包含需要被动态链接器使用的符号和其他表,包括符号表、字符串表等 */
    

    这里面注意到到SEG_OBJC,是和OC的runtime相关的。但是根据这篇文章中说所,在OC 2.0中已经废弃掉__OBJC段,而是将其放入到了__DATA段中以__objc开头的section中。这些和runtime相关的sections是本文的重点,我们稍后再分析。我们先看看其他的段。

    __TEXT段

    __TEXT是程序的只读段,用于保存我们所写的代码和字符串常量,const修饰常量等。
    下面是__TEXT段下常见的section:
    Section | 用途

    • | :-: | -:
      __TEXT.__text | 主程序代码
      __TEXT.__cstring | C 语言字符串
      __TEXT.__const | const 关键字修饰的常量
      __TEXT.__stubs | 用于 Stub 的占位代码,很多地方称之为桩代码。
      __TEXT.__stubs_helper | 当 Stub 无法找到真正的符号地址后的最终指向
      __TEXT.__objc_methname | Objective-C 方法名称
      __TEXT.__objc_methtype | Objective-C 方法类型
      __TEXT.__objc_classname | Objective-C 类名称

    例如,我们点击__TEXT.__objc_classname, 会看到我们程序中所使用到的类的名称:

    image

    而在__TEXT.__cstring section中,则看到我们定义的字符串常量(如@"I'm a cat!! miao miao"):

    image

    值得注意的是,这些都是以明文形式展现的。如果我们将加密key用字符串常量或宏定义的形式存储在程序中,可以想象其安全性是得不到保障的。

    __DATA段

    __DATA段用于存储程序中所定义的数据,可读写。__DATA段下常见的sectin有:

    Section 用途
    __DATA.__data 初始化过的可变数据
    __DATA.__la_symbol_ptr lazy binding 的指针表,表中的指针一开始都指向 __stub_helper
    __DATA.nl_symbol_ptr 非 lazy binding 的指针表,每个表项中的指针都指向一个在装载过程中,被动态链机器搜索完成的符号
    __DATA.__const 没有初始化过的常量
    __DATA.__cfstring 程序中使用的 Core Foundation 字符串(CFStringRefs)
    __DATA.__bss BSS,存放为初始化的全局变量,即常说的静态内存分配
    __DATA.__common 没有初始化过的符号声明
    __DATA.__objc_classlist Objective-C 类列表
    __DATA.__objc_protolist Objective-C 原型
    __DATA.__objc_imginfo Objective-C 镜像信息
    __DATA.__objc_selfrefs Objective-C self 引用
    __DATA.__objc_protorefs Objective-C 原型引用
    __DATA.__objc_superrefs Objective-C 超类引用

    可见,在__DATA段下,有许多以__objc开头的section,而这些section,均是和runtime的加载有关的。


    image

    我们将在后续的文章中,继续探讨这些section和runtime的关系。

    总结

    这次我们一起了解了XNU内核下的二进制文件格式Mach-O。它由header,load command以及data三部分组成:


    image

    我们重点应该了解的应该是data部分,因为这里存储着我们程序真正的数据和代码。
    在data部分中,又区分为以Segment划分的部分以及代码签名等其他部分。
    在Segment下,有区分有若干的section。
    常用的Segment有__PAGE_ZERO, __TEXT, __DATA(注意区分Mach-O的data和这里的__DATA段名称)。

    参考资料

    趣探 Mach-O:文件格式分析
    深入理解Macho文件(二)- 消失的__OBJC段与新生的__DATA段
    mach-o格式分析
    Mach-O 文件格式探索
    Mach-O 维基百科

    相关文章

      网友评论

        本文标题:Objective-C runtime机制(前传)——Mach-

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