zoukankan      html  css  js  c++  java
  • 学习Mach-O文件类型

    1. Mach-O定义

    Mach-O(Mach Object File Format)是macOS上的可执行文件格式,它是一种用于可执行文件,目标代码,动态库,内核转储的文件格式。

    2. Mach-O 文件格式

    根据官网的描述,Mach-O文件的结构如下图:

    主要分为三个部分:

    • Header:记录了Mach-O文件的基本信息,包括CPU架构、文件类和Load Commands等信息。
    • Load Commands:描述了怎样加载每个 Segment 的信息,
    • Data:Data 中每一个Segment的数据都保存在此,每个Segment拥有一个或多个 Section ,用来存放数据和代码

    这里我们借助MachOView来观察文件结构,先写一段简单的cpp代码:

    #import <stdio.h>
    
    int main() {
        printf("Mach-O Test");
        return 0;
    }
    

    使用 clang -g main.cpp -o main 生成执行文件,随后通过MachOView观察:

    根据<mach-o/loader.h>中的源码,我们可以一起看看这三个部分的结构体

    2.1 Header

    Mach-O 文件头主要目的是为加载命令提供信息。加载命令过程紧跟在头之后,并且 ncmds 和 sizeofcmds 来能个字段将会用在加载命令的过程中。

    /*
     * 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 */
    }
    
    • magic:魔术,根据宏定义,标识当前Mach-O位32位(0xfeedface)/ 64位 (0xfeedfacf)
    #define MH_MAGIC 0xfeedface /* the mach magic number */`
    #define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */
    
    • cputype / cpusubtype:CPU类型/CPU子类型
    • filetype:文件类型,常用的如下
     * Constants for the filetype field of the mach_header
     */
    #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 Commands数量
    • sizeofcmds:Load Commands的总大小
    • flag:标识位,记录文件的详细信息
    #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  /* 对可执行的文件类型启用地址空间 layout 随机化 */
    #define MH_NO_HEAP_EXECUTION 0x1000000 /* 将 Heap 标记为不可执行,可防止 heap spray 攻击 */
    
    • reserved:64位文件特有的保留字段
      对于刚才生成的可执行文件,其Header信息如下:

    2.2. Load Commands

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

    一起来看看Load Commands在MachOView中的构成,以LC_SEGMENT_64这个cmd为例(cmd类型为:LC_SEGMENT_64,size为72):

    2.3 Data

    从整体架构图中可以看到,Data又分为SegmentSection两个部分

    2.3.1 Segment

    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 */
    };
    
    • cmd :Load Commands部分中提到的cmd类型
    • cmdsize :同上
    • segname[16] :段名称
    • vmaddr :段虚拟地址(未偏移),真实虚拟地址要加上ASLR的偏移量
    • vmsize :段的虚拟地址大小
    • fileoff :段在文件内的地址偏移
    • filesize :段在文件内的大小
      加载segment的过程,就是从文件偏移 fileoff 处,将大小为 filesize 的段,加载到虚拟机 vmaddr 处。
    • nsects :段内section数量
    • flags :标志位,用于描述详细信息

    大家看到后面几个地址和偏移肯定会头晕,其实这几个变量主要作用在加载segment的时候。
    加载segment的过程,就是从文件偏移 fileoff 处,将大小为 filesize 的段,加载到虚拟机 vmaddr 处。

    segment[16]其实我们刚才在MachOView中就有见到:

    可以看到,LC_SEGMENT_64中包含了五种类型,分别是: __PAGEZERO, __TEXT, __DATA, __DATA_CONST, __LINKEDIT:

    • PAGEZERO:可执行文件捕获空指针的段
    • TEXT:代码段和只读数据
    • DATA_CONST:常态变量
    • DATA:全局变量和静态变量
    • LINKEDIT:包含动态链接器所需的符号字符串表等数据

    而对于__TEXT__DATA这两个Segment,则可以继续分解为Section,从而形成Segment->Section的结构。之所以要这样设计,是因为在同一个Segment下的Section可以拥有相同的控制权限,并且可以不完全按照Page的大小进行内存对齐,从而达到节约内存的效果。

    2.3.2 Section

    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 */
    };
    
    • sectname :section名称
    • segname :所属的segment名称
    • addr :section在内存中的地址
    • size :section大小
    • offset :section在文件中的偏移
    • align :内存对齐边界
    • reloff :重定位入口在文件中的偏移
    • nreloc :重定位入口数量

    以LC_SEGMENT_64为例,其中的Section64 Header(__text),大写的 __TEXT 代表 segment ,小写的 __text 代表 section ,其中的不同的Section代表着不同的含义,列举一下常见的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 类名称
    __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 超类引用

    3. Mach-O实验

    3.1 验证__TEXT.__text的加载

    上文提到,__TEXT.__text的含义是主程序代码,更值得一提的是这个section的加载过程可以观察得到。

    首先通过MachOView来看Load Commonds中__TEXT.__text的数据,为什么要先看Load Comands呢,因为Load Commands记录了Data是如何加载的,即作为Data加载结果的预期值,所以当结果=预期时我们就达到了验证效果。

    从图中可以看到,此时__TEXT.__text的address(section在内存中的地址)为0000000100003F60,这是预期值。

    随即通过vtool命令来查看汇编之后的代码起始地址:

    otool -vt MachOTest
    

    可见加载同样起始于0000000100003F60,验证完毕。

    3.2 探索__DATA.__la_symbol_ptr和__TEXT.__stubs的关系

    在上文中,我们提到了常见section的用途。其中
    __DATA.__la_symbol_ptr的用途为: lazy binding 的指针表,表中的指针一开始都指向 __stub_helper。因为这些用途都是从资料上搜集的,有些不知道其具体含义。但是对于这条尤其困惑,借此例子理解一下。

    首先,我们从MachOView上点开__TEXT.__stubs,选择其中一个stub,取其Data。由于我们demo比较简单,只存在一个stub,所以选择这个就OK了。

    接下来在Hopper Disassembler中打开这个之前demo的Mach-O文件,在其中搜索刚才的地址FF2570400000,得到:

    找到了对应代码的汇编表示,我们双击进入:

    可以看出,这个stub的含义就是跳转到以__la_symbol_ptr 对应表项数据所指向地址的代码,我们再取地址100008000回到MachOView中看一下:

    得到这个地址中的Data为0000000100003FA0,在通过Hopper查看这个Data:

    果然落在了__stub_helper这个section!

    回想一下我们刚才走过的链路

    • __TEXT.__stubs中取出一个stub,取其Data(FF2570400000),在Hopper中打开
    • 通过Hopper中找到Data并继续进入后,发现最终指向__la_symbol_ptr的某一项
    • 取这一项的地址(100008000),在MachOView中找到对应的Data(0000000100003FA0)
    • 发现这个Data最终落在__TEXT.__stubs_helper

    也就是说, __DATA.__la_symbol_ptr 里面的所有表项的数据在开始时都会被 binding 成 __stub_helper 。而一旦被首次调用,找到地址后,就会将 __DATA.__la_symbol_ptr内的占位符binding为真实的地址,便可以直接执行函数,后续无需再走binding的流程。这就如其名一样完成了lazy binding的过程。

  • 相关阅读:
    [leetcode] Combination Sum and Combination SumII
    nginx随着passenger构造ruby on rails页
    form 为什么上传文件enctype现场
    ftk学习记录(多形式的文章)
    Android setDisplayOptions 具体的使用说明
    存储结构二叉树
    SQLSERVER存储过程语法的具体解释
    iOS多用连接、反向协议、安全
    struts2于validate要使用
    Oracle存储过程实现返回多个结果集 在构造函数方法中使用 dataset
  • 原文地址:https://www.cnblogs.com/DaiShuSs/p/15169304.html
Copyright © 2011-2022 走看看