Mach-O 探究

Mach-OMach Object文件格式的缩写,它是一种用于可执行文件,目标代码,动态库,内核转储的文件格式。作为a.out格式的替代,Mach-O提供了更强的扩展性,并提升了符号表中信息的访问速度。
Mach-O格式为大部分基于Mach内核的操作系统所使用的,包括NeXTSTEP, Mac OS XiOS,它们都以Mach-O格式作为其可执行文件,动态库,目标代码的文件格式。

Mach-O简介

在iOS开发中,我们的代码在编译后会生成一个.app的文件(Product文件夹下),而.app文件我们可以把它看做是一个文件夹,内部存放了APP正常运行所需要的文件,通常比较容易识别的是一些资源文件。如图:

我们通过显示包内容看下.app文件夹内都有什么:

当然我们这片文章的主角也在这个文件夹内:

系统识别Mach-O(这个名字是项目的名字)为可执行文件(Mach-O是一种可执行文件格式),我们来看下这个文件,Mach-O文件是无法直接打开或者查看包内容,这里我们需要借助MachOView工具来查看,工具是开源的如果你想看具体的实现,你可以看工具的源码,当然你可以直接下载使用。

打开后的页面是这样的:

Mach-O结构

实际上我们从使用MachOView打开后的文件目录也可以看出,Mach-O的文件结构分为三大部分:Header,Load Commands,Data

下面是官方提供的一张结构图:

根据上图,我们将我们看到的目录大致划分为:

  • Mach-O 头(Mach Header):这里描述了 Mach-O 的 CPU 架构、文件类型以及加载命令等信息;
  • 加载命令(Load Command):当系统加载Mach-O文件时,load command会指导苹果的动态加载器(dyld)h或内核,该如何加载文件的Data数据。
  • 数据区(Data):Mach-O文件的数据区,包含代码和数据。其中包含若干Segment块,每个Segment块中包含0个或多个seciton。Segment根据对应的load command被dyld加载入内存中。

注意:通过对比我们发现实际上官网给出的结构并不准确,在实际结果中还包含了Dynamic Loader Info,Function Starts,Symbol Table,Data In Code Entries,Dynamic Symbol Table,String Table,Code Signature等。

下面我们来详细看下每部分的内容

Mach64 Header

这里我们可以和苹果开源的Darwin源码一起看方便理解,源码在这里

32位

/*
 * The 32-bit mach header appears at the very beginning of the object file for
 * 32-bit architectures.
 */
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位:

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

我们看到32位和64位的mach_header基本是一致的,只是在64位中新增了reserved字段,下面我们来看下其中每个字段所表示的意义。

Magic Number
offsetdatadescriptionvalue
00000000FEEDFACFMagic NumberMH_MAGIC_64

我们可以将其直译为魔数,他的值(Value)有两个:

#define	MH_MAGIC	0xfeedface	/* the mach magic number */ 32#define MH_MAGIC_64 0xfeedfacf /* the 64-bit mach magic number */ 64

用于这个Mach-O文件的标识,有32位和64位两个值。由此可以看出我们的示例是一个64位的Mach-O文件。

CPU Type ,CPU SubType
offsetdatadescriptionvalue
000000040100000CCPU TypeCPU_TYPE_64
0000000800000000CPU SubType
00000000CPU_SubType_ARM64_ALL

CPU Type和CPU SubType 表示支持的CUP架构类型和子类型,如ARM。而具体的类型有哪些我们可以通过查询/mach/machine.h.中的定义查看这里不做过多的扩展,具体可以看这里
我们的示例中,APP是支持所有arm64的机型的:CUP_SUBTYPE_ARM64_ALL。

File Type
offsetdatadescriptionvalue
0000000C00000002File TypeMH_EXECUTE

File Type 表示 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 内核扩展 */

这里类型均是在loader.h文件中定义的

对于我们示例中的我们的File Type为 MH_EXECUTE表示 可执行的二进制文件。

ncmds(Number of Load Commands)
offsetdatadescriptionvalue
0000001000000017Number of Load Commands23

ncmds表示load command的数量。在我们的示例中表示数量为23个。

sizeofcmds(Size of Load Commands)
offsetdatadescriptionvalue
0000001400000AF8Size of Load Commands2808

sizeofcmds表示所有load command的总大小。示例中总大小为2808。

Flags
offsetdatadescriptionvalue
0000001800200085Flags
00000001MH_NOUNDEFS
00000004MH_DYLDLINK
00000080MH_TWOLEVEL
00200000MH_PIE

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 攻击 */

结合我们的示例,我们共有4个Flags:

  • MH_NOUNDEFS
  • MH_DYLDLINK dyld是苹果公司的动态链接库,用来把Mach-O文件加载入内存
  • MH_TWOLEVEL 表示其符号空间中还会包含所在库的信息。这样可以使得不同的库导出通用的符号。与其相对的是扁平命名空间。
  • MH_PIE 每次系统加载进程后,都会为其随机分配一个虚拟内存空间(在传统系统中,进程每次加载的虚拟内存是相同的。这就让黑客有可能篡改内存来破解软件)

注意:flags的值也定义在loader.h文件中 都可以通过源码查看。

Load Commands

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

/*
 * The load commands directly follow the mach_header.  The total size of all
 * of the commands is given by the sizeofcmds field in the mach_header.  All
 * load commands must have as their first two fields cmd and cmdsize.  
 * The cmd
 * field is filled in with a constant for that command type.  
 * Each command type
 * has a structure specifically for it.  
 * The cmdsize field is the size in bytes
 * of the particular load command structure plus anything that follows it that
 * is a part of the load command (i.e. section structures, strings, etc.). 
 *  To
 * advance to the next load command the cmdsize can be added to the offset or
 * pointer of the current load command.  
 * The cmdsize for 32-bit architectures
 * MUST be a multiple of 4 bytes and for 64-bit architectures MUST be a multiple
 * of 8 bytes (these are forever the maximum alignment of any load commands).
 * The padded bytes must be zero.  All tables in the object file must also
 * follow these rules so the file can be memory mapped.  Otherwise the pointers
 * to these tables will not work well or at all on some machines.  With all
 * padding zeroed like objects will compare byte for byte.
 */
struct load_command {
	uint32_t cmd;		/* type of load command */
	uint32_t cmdsize;	/* total size of command in bytes */
};

对应我们示例中的Load Commands

我们在尝试去理解load_command的注释:

load commands紧跟着mach_header,load commands的总大小由mach_header汇总的sizeofcmds字段给出,所有的load commands都必须以cmd和cmdsize两个字段作为前两个字段(结合我们的示例也得到验证),cmd字段的值为commandtype常量,每一个commandtype都有一种特定的结构。cmdsize字段以字节为单位包含loadcommand结构和额外的其他字段(例如  section structures,strings等)。要前进到下一个加载命令,可以将cmdsize添加到当前加载命令的偏移量或指针。对于32位体系结构的cmdsize
 必须是4字节的倍数,并且对于64位架构,必须是8字节的倍数(这些永远是所有装入命令的最大对齐),填充字节必须为零。
Segment

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

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

Segment load command也分为32位和64位:

32位

/*
 * The segment load command indicates that a part of this file is to be
 * mapped into the task's address space.  The size of this segment in memory,
 * vmsize, maybe equal to or larger than the amount to map from this file,
 * filesize.  The file is mapped starting at fileoff to the beginning of
 * the segment in memory, vmaddr.  The rest of the memory of the segment,
 * if any, is allocated zero fill on demand.  The segment's maximum virtual
 * memory protection and initial virtual memory protection are specified
 * by the maxprot and initprot fields.  If the segment has sections then the
 * section structures directly follow the segment command and their size is
 * reflected in cmdsize.
 */
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 */
};

64位

/*
 * The 64-bit segment load command indicates that a part of this file is to be
 * mapped into a 64-bit task's address space.  If the 64-bit segment has
 * sections then section_64 structures directly follow the 64-bit segment
 * command and their size is reflected in cmdsize.
 */
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_command基本一致,只是在64位的结构中把和寻址相关的数据类型由uint32_t改为uint64_t

我们先看下示例中,和Segment相关的Command:

结合源码我们可以看到:

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

根据前面结构图我们知道Load Commands实际上是一个二级结构:Segment->Section,正如示例中所示

因此,下面我们在看下section的结构

Section
/*
 * A segment is made up of zero or more sections.  Non-MH_OBJECT files have
 * all of their segments with the proper sections in each, and padded to the
 * specified segment alignment when produced by the link editor.  The first
 * segment of a MH_EXECUTE and MH_FVMLIB format file contains the mach_header
 * and load commands of the object file before its first section.  The zero
 * fill sections are always last in their segment (in all formats).  This
 * allows the zeroed segment padding to be mapped into memory where zero fill
 * sections might be. The gigabyte zero fill sections, those with the section
 * type S_GB_ZEROFILL, can only be in a segment with sections of this type.
 * These segments are then placed after all other segments.
 *
 * The MH_OBJECT format has all of its sections in one segment for
 * compactness.  There is no padding to a specified segment boundary and the
 * mach_header and load commands are not part of the segment.
 *
 * Sections with the same section name, sectname, going into the same segment,
 * segname, are combined by the link editor.  The resulting section is aligned
 * to the maximum alignment of the combined sections and is the new section's
 * alignment.  The combined sections are aligned to their original alignment in
 * the combined section.  Any padded bytes to get the specified alignment are
 * zeroed.
 *
 * The format of the relocation entries referenced by the reloff and nreloc
 * fields of the section structure for mach object files is described in the
 * header file <reloc.h>.
 */
struct section { /* for 32-bit architectures */
	char		sectname[16];	/* name of this section 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];	/* Section 名字 */
	char		segname[16];	/* 所在的Segment名称*/
	uint64_t	addr;		/* Section 所在的内存地址 */
	uint64_t	size;		/* Section 的大小 */
	uint32_t	offset;		/* Section 所在的文件偏移 */
	uint32_t	align;		/* Section 的内存对齐边界 (2 的次幂) */
	uint32_t	reloff;		/* 重定位信息的文件偏移 */
	uint32_t	nreloc;		/* 重定位条目的数目 */
	uint32_t	flags;		/* 标志属性 (section type and attributes)*/
	uint32_t	reserved1;	/* 保留字段1 (for offset or index) */
	uint32_t	reserved2;	/* 保留字段2 (for count or sizeof) */
	uint32_t	reserved3;	/* 保留字段3 */
};

在64位和32位的section定义中,64位新增了一个reserved3保留字段,以及将section的addr和size字段由原来的uint32_t类型升级为uint64_t。

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

结合我们示例中的一个section结构如下图:

DATA(数据)

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

前面我们介绍过,我们通过Load Commands从DATA中读取数据,而Load Commands被划分成了多个Segment,也就是说 我们通过不同的Load Commands从DATA中读取不同的数据。

在介绍Segment的时候我们说过Segment被划分成__PAGEZERO,__TEXT,__DATA,__LINKEDIT这几段。

结合我们的示例,我们发现DATA被划分为:__TEXT,__DATA

下面我们来看下这几个数据段(section):

__TEXT段

__TEXT是程序的只读段,用于保存我们所写的代码和字符串常量,const修饰常量等。

下面是几个我们常见的section:

Section存储内容
__TEXT.__text主程序代码
__TEXT.__cstringC 语言字符串
__TEXT.__constconst 关键字修饰的常量
__TEXT.__stubs用于 Stub 的占位代码,很多地方称之为桩代码。
__TEXT.__stubs_helper当 Stub 无法找到真正的符号地址后的最终指向
__TEXT.__objc_methnameObjective-C 方法名称
__TEXT.__objc_methtypeObjective-C 方法类型
__TEXT.__objc_classnameObjective-C 类名称

我们来结合示例看下这几个section的内容:

cstring

我们可以从中看到lw_property,lw_publicproperty这两个属性名。以及我们打印的NSLog中的内容,同时我们发现,我们可能定义的某些三方key或者appid在这里都暴露在外部。

static const NSString *lw_constsecretKey = @"11234455556";

objc_methname

我们可以看到我们自定义的方法名lw_publicMethod,lw_privateMethod以及lw_property,lw_publicproperty重写的setter和getter方法。

classname

我们可以看到我们自定义的类的类:LWCustomClass

_DATA

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

下面我们看下常见的__DATA下的section:

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

这些以objc开头的DATA字段都是跟runtime有关的,后面我们会详细分析。

__objc_imageinfo

typedef struct objc_image_info {
    uint32_t version; // currently 0
    uint32_t flags;

#if __cplusplus >= 201103L
  private:
  // 位移枚举 
    enum : uint32_t {
        IsReplacement       = 1<<0,  // used for Fix&Continue, now ignored
        SupportsGC          = 1<<1,  // 是否支持垃圾回收
        RequiresGC          = 1<<2,  // 镜像是否需要回收
        OptimizedByDyld     = 1<<3,  // image is from an optimized shared cache
        CorrectedSynthesize = 1<<4,  // used for an old workaround, now ignored
        IsSimulated         = 1<<5,  // image compiled for a simulator platform
        HasCategoryClassProperties  = 1<<6,  // class properties in category_t

        SwiftVersionMaskShift = 8,
        SwiftVersionMask    = 0xff << SwiftVersionMaskShift  // Swift ABI version

    };
   public:
    enum : uint32_t {
        SwiftVersion1   = 1,
        SwiftVersion1_2 = 2,
        SwiftVersion2   = 3,
        SwiftVersion3   = 4
    };

  public:
    bool isReplacement()   const { return flags & IsReplacement; }
    bool supportsGC()      const { return flags & SupportsGC; }
    bool requiresGC()      const { return flags & RequiresGC; }
    bool optimizedByDyld() const { return flags & OptimizedByDyld; }
    bool hasCategoryClassProperties() const { return flags & HasCategoryClassProperties; }
    bool containsSwift()   const { return (flags & SwiftVersionMask) != 0; }
    uint32_t swiftVersion() const { return (flags & SwiftVersionMask) >> SwiftVersionMaskShift; }
#endif
} objc_image_info;

我们发现objc_image_info中主要是有version字段和flag字段,

__objc_classlist

这个section列出了所有的class,包括meta class。

图中的value值是就是这个类结构体的地址(包括元类),类结构体的结构为objc中的objc_class结构体,结构如下:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}
__objc_catlist

这里可以查看代码中的所有分类,其value的值为指向分类结构体的指针

对应oc中的结构为category_t,具体结构如下:

struct category_t {
    // 是指 class_name 而不是 category_name
    const char *name;
    // 要扩展的类对象,编译期间是不会定义的,而是在运行时通过 * name 对应到对应的类对象。
    classref_t cls;
    // 对象方法列表
    struct method_list_t *instanceMethods;
    // 类方法列表
    struct method_list_t *classMethods;
    // 协议列表
    struct protocol_list_t *protocols;
    // 实例属性
    struct property_list_t *instanceProperties;
    // Fields below this point are not always present on disk.
    // 类属性(这个结构体以_开头命名???)
    struct property_list_t *_classProperties;
    // methodsForMeta 返回类方法列表或者对象方法列表
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    // 属性列表返回方法
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
__objc_protolist

该Section中记录了项目中所有的协议。 其value值为指向协议的指针

协议的结构体为protocol_t,具体如下:


struct protocol_t : objc_object {
    const char *mangledName;
    struct protocol_list_t *protocols;
    method_list_t *instanceMethods;
    method_list_t *classMethods;
    method_list_t *optionalInstanceMethods;
    method_list_t *optionalClassMethods;
    property_list_t *instanceProperties;
    uint32_t size;   // sizeof(protocol_t)
    uint32_t flags;
    // Fields below this point are not always present on disk.
    const char **_extendedMethodTypes;
    const char *_demangledName;
    property_list_t *_classProperties;
}
__objc_classrefs

该section记录了哪些class被引用了,这里记录了所有被实例化的class,有些类虽然在包里,但是我们并未使用,因此这里不会有。

__objc_selrefs

这section记录哪些SEL对应的字符串被引用了,有系统方法,也有自定义方法:

__objc_superrefs

该section记录了调用super方法的类。

比如,在子类方法中,我们调用了父类的方法,就会将子类记录在这里。

__objc_const

该section用来记录在OC内存初始化过程中的不可变内容。这里所谓的不可变内容并不是我们在程序中所写的const NSInteger k = 5这种常量数据(它存在__TEXT的const section中),而是在OC内存布局中不可变得部分。

应用启动

根据上面介绍的在应用启动期间,dyld和kern会读取Mach-O文件中的Load Command去读取和加载_DATA数据段下的内容,而这一切都发生在main函数之前。所以我们看下main函数之前都发生了什么?

启动调用堆栈

添加一个符号断点(Symbolic BreakPoint)让应用在执行到_objc_init方法是断点执行。

这样我们就能看到下面的这个调用栈:

因为_objc_init方法是runtime的入口,因此在这之前调用的方法都是dyld和ImageLoader的操作

dyld

dyld(the dynamic link editor)动态链接器,系统 kernel 做好启动程序的初始准备后,交给 dyld 负责,dyld的主要工作内容为(参考 dyld: Dynamic Linking On OS X ):

  • 从 kernel 留下的原始调用栈引导和启动自己
  • 将程序依赖的动态链接库递归加载进内存,当然这里有缓存机制
  • non-lazy 符号立即 link 到可执行文件,lazy 的存表里
  • Runs static initializers for the executable
  • 找到可执行文件的 main 函数,准备参数并调用
  • 程序执行中负责绑定 lazy 符号、提供 runtime dynamic loading services、提供调试器接口
  • 程序main函数 return 后执行 static terminator
  • 某些场景下 main 函数结束后调 libSystem 的 _exit 函数
ImageLoader

这里的image不是图片的意思,它是一个二进制文件,你可以把他理解为一个镜像文件。内部是被编译过的符号、代码等,因此ImageLoader作用是将这些文件加载进内存,且每一个文件对应一个ImageLoader实例来负责加载。

他的主要工作为:

  • 在程序运行时它先将动态链接的 image 递归加载 (也就是上面测试栈中一串的递归调用的时刻)
  • 再从可执行文件 image 递归加载所有符号
ImageLoaderMachO

顾名思义这里应该是去加载MachO文件,从堆栈中我们可以看到主要跟doInitialization方法和doModInitFunctions方法。

doInitialization

这个方法的主要作用是:获取Mach-O的init方法的地址并调用

bool ImageLoaderMachO::doInitialization(const LinkContext& context)
{
	CRSetCrashLogMessage2(this->getPath());

	// mach-o has -init and static initializers
	doImageInit(context);
	doModInitFunctions(context);
	
	CRSetCrashLogMessage2(NULL);
	
	return (fHasDashInit || fHasInitializers);
}
void ImageLoaderMachO::doImageInit(const LinkContext& context)
{
	if ( fHasDashInit ) {
		// mach-o文件中指令的个数
		const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
		// 遍历指令
		for (uint32_t i = 0; i < cmd_count; ++i) {
			switch (cmd->cmd) {
				case LC_ROUTINES_COMMAND:
					// 获取macho_routines_command的init_address
					Initializer func = (Initializer)(((struct macho_routines_command*)cmd)->init_address + fSlide);
					// 执行-init方法
					func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
					break;
			}
			// 计算下一个指令((char*)cmd)+cmd->cmdsize
			cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
		}
	}
}
doModInitFunctions

这个方法的主要作用是:获取Mach-O的static initializer的地址并调用


void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
{
	if ( fHasInitializers ) {
	   // mach-o文件中指令的个数
		const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
		// 遍历所有的指令
		for (uint32_t i = 0; i < cmd_count; ++i) {
		   // 如果指令是Mach-o中的LC_SEGMENT_COMMAND
			if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
			     // 从sectionsStart到sectionsEnd
					for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
					const uint8_t type = sect->flags & SECTION_TYPE;
					if ( type == S_MOD_INIT_FUNC_POINTERS ) {
						
						for (size_t i=0; i < count; ++i) {
							if ( context.verboseInit )
								dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());
							// 执行initializer方法
							func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
						}
					}
				}
			}
			// 根据指令的地址+指令大小获取到下一个指令
			cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
		}
	}
}

总结

上述我们介绍了Mach-O文件的主要结构,以及每个segment和section的功能和字段的作用,结尾处我们通过查看应用启动调用堆栈来确认Mach-O文件何时被ImageLoader解析并加载到内存中,提供给后续的runtime使用。鉴于main函数之前系统内核,dyld,ImageLoader,rumtime做了很多准备,我们决定新开一篇文章来讲述这个过程发生了什么,敬请期待!

参考文章

XNU源码
探秘 Mach-O 文件
Mach-O文件结构理解
Mach-O 与动态链接
iOS 程序 main 函数之前发生了什么

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值