12-MachO文件

前言

本篇文章主要分析MachO文件(也称作二进制可执行文件),相信大家在平时开发中都会碰到MachO文件这个概念,但是大部分人不清楚是个什么东西,本篇文章就和大家一起来具体分析它的由来以及它的内部结构

一、Mach-O

首先大家看看上面这张图,从左到右可以看出👇
1.不论是哪种高级语言(C OC Swift等),第一步都会生成AST语法树,只是编译器前端不同而已(有Clang、Swift或Rust

  1. 接着通过CIL MIR 或SIL生成器生成IR中间代码,这个中间代码都属于LLVM IR
  2. 最后交给MIR生成机器代码,这个机器代码就是MachO文件

整个过程其实都是LLVM帮我们完成的,至于LLVM是什么,大家可以参考我之前的文章 👉 LLVM编译流程

常见的Mach-O文件格式

  • 目标文件.o
  • 库文件
    • .a
    • .dylib
    • .framework
  • 可执行文件
  • dyld
  • .dsym

验证

.o.out、可执行文件

案例一
新建test.c文件,内容如下👇

#include <stdio.h>

int main() {
    printf("test\n");
    return 0;
}

验证.o文件👇

⚠️注意:不指定-c默认生成.out格式,如果报找不到'stdio.h' file not found,则可以指定-isysroot

验证.out可执行文件👇

验证可执行文件👇

再直接生成一个test3可执行文件👇

那么问题来了 👉 生成的a.out、test2、test3是一样的么?👇

可以看到生成的可执行文件md5相同

⚠️注意:原则上test3的md5应该和test2和a.out相同。源码没有变化,所以应该相同的。在指定-isysroot后生成的可能不同,推测和CommandLineTools有关(系统中有一个,Xcode中也有一个)。

案例二
再创建一个test1.c文件,内容如下👇

#include <stdio.h>

void test1Func() {
    printf("test1 func \n");
}

修改test.c👇

#include <stdio.h>

void test1Func();

int main() {
    test1Func();
    printf("test\n");
    return 0;
}

这个时候相当于多了个test1.c文件了,编译生成可执行文件demo、demo1、demo2👇

clang -o demo  test1.c test.c 
clang -c test1.c test.c 
clang -o demo1 test.o test1.o
clang -o demo2 test1.o test.o

查看他们的MD5👇

这里demo1和demo2的md5不同,是因为test.o和test1.o顺序不同

objdump命令查看Mach-O

objdump --macho -d demo

上图明显可见方法调用的顺序不同,这也就解释了md5不同的原因。这里很像Xcode中Build Phases -> Compile Sources源文件的顺序

⚠️注意:源文件顺序不同,编译出来的二进制文件不同( 大小相同),二进制排列顺序不同。

.a文件、

直接创建一个library库查看👇

//find /usr -name "*.a"
file libTestLibrary.a
libTestLibrary.a: current ar archive random library
.dylib文件
 file /usr/lib/libprequelite.dylib
/usr/lib/libprequelite.dylib: Mach-O 64-bit dynamically linked shared library x86_64

⚠️注意:dyld不是可执行文件,它是一个dynamic linker,由系统内核触发。

dyld文件
cd /usr/lib
file dyld
dyld: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamic linker x86_64] [i386:Mach-O dynamic linker i386]
dyld (for architecture x86_64): Mach-O 64-bit dynamic linker x86_64
dyld (for architecture i386):   Mach-O dynamic linker i386
.dsym文件
file TestDsym.app.dSYM
TestDsym.app.dSYM: directory

cd TestDsym.app.dSYM/Contents/Resources/DWARF

file TestDsym
TestDsym: Mach-O 64-bit dSYM companion file arm64

二、工程配置

2.1 查看Mach-O文件的类型

我们可以在工程配置中查看Mach-O文件的类型,如下图👇

也可以使用命令行查看👇

file your Mach-O文件路径

可以看到是支持2个架构的:arm64armv7。当然也可以在Xcode中直观的看到支持的架构👇

同时Xcode中架构设置在Build Settings -> Architectures中👇

主要有以下配置选项👇

  • Architectures 👉 支持的架构。
  • Build Active Architecture Only 👉 默认情况下debug模式下只编译当前设备架构,release模式下需要根据支持的设备。
  • $(ARCHS_STANDARD) 👉 环境变量,代表当前支持的架构。

如果我们要修改架构直接在Architectures中配置(增加armv7s)👇

2.2 通用二进制文件(Universal binary)

  • 苹果公司提出的一种程序代码,能同时适用多种架构的二进制文件。
  • 同一个程序包中同时为多种架构提供最理想的性能。
  • 因为需要储存多种代码,通用二进制应用程序通常比单一平台二进制的程序要大。
  • 由于多种架构有共同的非执行资源(代码以外的),所以并不会达到单一版本的多倍之多(特殊情况下,只有少量代码文件的情况下有可能会大于多倍)。
  • 由于执行中只调用一部分代码,运行起来不需要额外的内存。

当我们将通用二进制文件拖入Hopper时,能够看到让我们选择对应的架构👇

2.3 lipo命令

lipo是管理Fat File的工具,可以查看cpu架构,提取特定架构,整合和拆分库文件。

1. 查看MachO文件支持的架构

lipo -info MachO文件

lipo -info EvergrandeCustomerApp_Example
Architectures in the fat file: EvergrandeCustomerApp_Example are: armv7 arm64
2. lifo –thin 拆分某种架构

lipo MachO文件 –thin 架构 –output 输出文件路径

3. 使用lipo -create合并多种架构

lipo -create MachO1 MachO2 -output 输出文件路径

三、MachO文件结构

macho文件是mac osios系统可执行文件的格式,系统通过加载这个格式来执行代码。相关结构如下图👇

上图中Mach-O的组成结构如图所示包括👇

  • Header 包含该二进制文件的一般信息
    • 字节顺序、架构类型、加载指令的数量等
    • 使得可以快速确认一些信息,比如当前文件用于32位还是64位,对应的处理器是什么、文件类型是什么
  • Load commands 一张包含很多内容的表
    • 内容包括区域的位置、符号表、动态符号表等
  • Data 通常是对象文件中最大的部分
    • 包含Segement的具体数据

3.1 查看MachO文件的方式

有2种方式可查看MachO文件的结构👇

  1. 命令行 otool -f MachO文件
$ otool -f xxx.app/xxx
Fat headers
fat_magic 0xcafebabe
nfat_arch 2
architecture 0
    cputype 12
    cpusubtype 9
    capabilities 0x0
    offset 16384
    size 69642576
    align 2^14 (16384)
architecture 1
    cputype 16777228
    cpusubtype 0
    capabilities 0x0
    offset 69664768
    size 80306624
    align 2^14 (16384)
  1. MachO View可视化工具

3.2 MachO Header的结构

Fat Header

首先我们来看看Fat Header,什么是Fat Header👇

对于多架构MachO会有一个Fat Header,其中包含了CPU类型和架构OffsetSize代表了每一个架构二进制文件中的偏移大小

上图中,armv7偏移量大小分别是1638479315040,再看arm64的偏移量79347712,可以发现16384 + 79315040 = 79331424 < 79347712,但是79347712 - 16384 = 7933132879331328/(1024 * 16) = 4842,其中(1024 * 16)代表16k字节大小,因为👇

iOS中一页16K,MachO中都是以为单位对齐的。

这也验证了以页对齐,并且这也是Load Commands中可以插入LC_LOAD_DYLIB的原因。

Header的数据

上图是arm64架构下的Header,对应dyld的定义代码结构如下(loader.h)👇

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 魔数,快速定位属于64位还是32位
cputype CPU类型,比如ARM
cpusubtype CPU具体类型,arm64,armv7
filetype 文件类型,比如可执行文件
ncmds Number of Load Commands,即Load Commands总的条数
sizeofcmds Size of Load Commands,即Load Commands总的大小
flags 标识二进制文件支持的功能,主要是和系统加载、链接有关
reserved arm64特有,保留字段

其中,filetype的类型有👇

#define MH_OBJECT   0x1     /* relocatable object file */
#define MH_EXECUTE  0x2     /* demand paged executable file */
#define MH_FVMLIB   0x3     /* fixed VM shared library file */
#define MH_CORE     0x4     /* core file */
#define MH_PRELOAD  0x5     /* preloaded executable file */
#define MH_DYLIB    0x6     /* dynamically bound shared library */
#define MH_DYLINKER 0x7     /* dynamic link editor */
#define MH_BUNDLE   0x8     /* dynamically bound bundle file */
#define MH_DYLIB_STUB   0x9     /* shared library stub for static
                       linking only, no section contents */
#define MH_DSYM     0xa     /* companion file with only debug
                       sections */
#define MH_KEXT_BUNDLE  0xb     /* x86_64 kexts */
#define MH_FILESET  0xc     /* a file composed of other Mach-Os to
                       be run in the same userspace sharing
                       a single linkedit. */

3.3 Load Commands

dyld检索完Header之后就开始加载和解析Load Commands了,Load Comands的大致结构如下👇


2.3.1 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_command都需要包含👇

  1. cmd 👉 加载类型
  2. cmdsize 👉 加载的大小

2.3.2 所有load_command的具体信息

接下来我们仔细看看每个load_command具体包含哪些信息👇

__PAGEZERO

空指针陷阱,目的是为了和32位指令完全分开。(32位地址在4G以下64位地址大于4G,其中0xffffffff = 4G)。有以下几个重要的描述👇

  • Segment Name 👉 __PAGEZERO不占用数据(file size为0),唯一有的是VM Size(arm64 4G,armv7比较小)。
  • VM Addr 👉 虚拟内存地址
  • VM Size 👉 虚拟内存大小。运行时刻在内存中的大小,一般情况下和File size相同,__PAGEZERO例外。
  • File offset 👉 数据在文件中偏移量。
  • File size 👉 数据在文件中的大小。

一般我们定位地址是通过VM Addr + ASLR

__TEXT、__DATA、__LINKEDIT

他们的结构和__PAGEZERO大致差不多,用途是将文件中(32位/64位)的段映射到进程地址空间中。分为三大块 👉 分别对应DATA中的Section(__TEXT + __DATA)__LINKEDIT,告诉dyld占用多大空间。

LC_DYLD_INFO_ONLY

动态链接相关信息。

  • Rebase 👉 重定向(ASLR)偏移地址大小。从Rebase Info Offset + ASLR开始加载336个字节数据。
  • Binding 👉 绑定外部符号
  • Weak Binding 👉 弱绑定。
  • Lazy Binding 👉 懒绑定,用到的时候再绑定。
  • Export info 👉 对外开放的函数。
LC_SYMTAB

符号表地址。

  • Symbol Table Offset 👉 符号表地址偏移量
  • Number of Symbol 👉 符号总数量
  • String Table Offset 👉 字符串表地址偏移量
  • Symbol Table Size 👉 字符串表大小
LC_DSYMTAB

动态符号表地址。

也是包含一些索引、数量、地址偏移量等信息。

LC_LOAD_DYLINKER

使用谁加载,iOS系统是使用dyld加载,如下图👇

LC_UUID

文件的UUID,即MachO文件的唯一识别标识

LC_VERSION_MIN_IPHONES

支持最低的操作系统版本。

LC_SOURCE_VERSION

源代码的版本号。

LC_MAIN

程序主程序的入口地址和栈大小。

LC_ENCRYPTION_INFO_64

加密的信息。

LC_LOAD_DYLIB

依赖的库的路径,包含第三方的库。

系统的库👇

第三方库👇

LC_RPATH

Frameworks库的路径。

  • @executable_path 👇
  • @loader_path👇
LC_FUNCTION_STARTS

函数起始地址表。

LC_DATA_IN_CODE

定义在代码段内的非指令的表。

LC_DATA_SIGNATURE

代码签名。

3.4 Data

Data包含Section(__TEXT + __DATA)__LINKEDIT

3.4.1 __TEXT

__TEXT即代码段,就是我们写的代码。主要的几个子段有👇

1. __text: 代码节,存放机器编译后的代码
2. __stubs: 用于辅助做动态链接代码(dyld).
3. __stub_helper:用于辅助做动态链接(dyld).
4. __objc_methname:objc的方法名称
5. __cstring:代码运行中包含的字符串常量,比如代码中定义`#define kGeTuiPushAESKey        @"DWE2#@e2!"`,那DWE2#@e2!会存在这个区里。
6. __objc_classname:objc类名
7. __objc_methtype:objc方法类型
8. __ustring:
9. __gcc_except_tab:
10. __const:存储const修饰的常量
11. __dof_RACSignal:
12. __dof_RACCompou:
13. __unwind_info:

3.4.2 __DATA

__DATA数据段。主要的几个子段有👇

1. __got:存储引用符号的实际地址,类似于动态符号表,存储了`__nl_symbol_ptr`相关函数指针。
2. __la_symbol_ptr:lazy symbol pointers。懒加载的函数指针地址(C代码实现的函数对应实现的地址)。和__stubs和stub_helper配合使用。具体原理暂留。
3. __mod_init_func:模块初始化的方法。
4. __const:存储constant常量的数据。比如使用extern导出的const修饰的常量。
5. __cfstring:使用Core Foundation字符串
6. __objc_classlist:objc类列表,保存类信息,映射了__objc_data的地址
7. __objc_nlclslist:Objective-C 的 +load 函数列表,比 __mod_init_func 更早执行。
8. __objc_catlist: categories
9. __objc_nlcatlist:Objective-C 的categories的 +load函数列表。
10. __objc_protolist:objc协议列表
11. __objc_imageinfo:objc镜像信息
12. __objc_const:objc常量。保存objc_classdata结构体数据。用于映射类相关数据的地址,比如类名,方法名等。
13. __objc_selrefs:引用到的objc方法
14. __objc_protorefs:引用到的objc协议
15. __objc_classrefs:引用到的objc类
16. __objc_superrefs:objc超类引用
17. __objc_ivar:objc ivar指针,存储属性。
18. __objc_data:objc的数据。用于保存类需要的数据。最主要的内容是映射__objc_const地址,用于找到类的相关数据。
19. __data:暂时没理解,从日志看存放了协议和一些固定了地址(已经初始化)的静态量。
20. __bss:存储未初始化的静态量。比如:`static NSThread *_networkRequestThread = nil;`其中这里面的size表示应用运行占用的内存,不是实际的占用空间。所以计算大小的时候应该去掉这部分数据。
21. __common:存储导出的全局的数据。类似于static,但是没有用static修饰。比如KSCrash里面`NSDictionary* g_registerOrders;`, g_registerOrders就存储在__common里面

3.4.3 __LINKEDIT

__LINKEDIT主要包含👇

  • Dynamic Loader Info 👉 动态加载信息

  • Function Starts 👉 入口函数

  • Symbol Table 👉 符号表

  • Dynamic Symbol Table 👉 动态库符号表

  • String Table 👉 字符串表

  • Code Signature 👉 代码签名

验证

我们知道,获取类名可以用_objc_classname, 获取方法名可以用_objc_methname,但是这两个数据怎么串联匹配起来的?根据查相关资料,是通过__objc_classlist来映射的。

验证该问题需借助2个工具 👉 MachOViewHopper

  1. MachOView中打开Mach-O文件,直接看__objc_classlist👇

我们选择第一个地址,在Hopper中看看102725E28(按G搜索)👇

双击,对应到__objc_class👇

__objc_class对应的源码👇

typedef struct objc_class{
        struct __objc_class* isa;
        struct __objc_class* superclass;
        struct __objc_cache* cache;
        struct __objc_vtable* vtable;
        struct __objc_ data* data;
}objc_class;
  1. 第1个成员是isa指针,指向了MetaClass,对应的地址是102badf90👇
  1. 第2个成员是指向父类的指针,对应地址为0000000000000000
  2. 第5个成员指向__objc_ data,双击它,对应的地址为102737e30👇

接着我们看看__objc_data对应的数据结构源码👇

typedef struct objc_data{
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    uint32_t reserved;
    void* ivarlayout;
    char* name;
    struct __objc_method_list* baseMethod;
    struct __objc_protos* baseProtocol;
    struct __objc_ivars* ivars;
    struct __objc_ivars weakIvarLayout;
    struct __objc_ivars baseProperties;
}

主要的几个成员👇

  1. 第6个成员name 保存的类名,对应的地址是0x102445615

该地址对应的类名称是_AFURLSessionTaskSwizzling,至此,找到了类名称

  1. 第7个成员baseMethod 保存了类所有方法,一样,对应的地址是102737de0👇

接着查看__objc_method_list的源码👇

typedef struct objc_method_list{
    uint32_t flags;
    uint32_t count;
}

使用到的数据主要是count,对应数据为3,对应10进制数也是3,说明有3个方法👇

具体方法对应的数据结构为👇

typedef struct objc_method{
    char* name;
    char* signature;
    void* implementation;
}

objc_method_list结构体占用8(4+4)字节,而__objc_method_list的地址是0000000102737de0 + 8字节 = 第1个方法的地址0000000102737de8objc_method结构体占用24(8*3)字节,再加24字节得到第2个方法的地址0000000102737e00,同理再加24字节得到第3个方法的地址0000000102737e18

接着我们在MachOView中查看第1个方法的地址0000000102737de8👇

上图中,0000000102737de8中存储的第一个8字节地址是0102331F27,再去到Hopper中搜索该地址👇

image.png

同理,第2个8字节地址是01023326C4👇

第3个8字节地址是010232C51D👇

至此,我们从__objc_classlist中查找地址,首先通过__objc_classisa指针找到类名称,接着找到下面的成员变量base method的地址,找到objc_method_list方法列表,再根据objc_method结构体大小,计算内存平移后的地址,找到了所有的方法名称

以上就是dyld加载类名并关联方法列表的一个示例过程。

总结

  • Mach-O属于一种文件格式
    • 包含:可执行文件、静态库、动态库、dyld等
    • 可执行文件:
      • 通用二进制文件:集合了多种架构
      • lipo命令
        • -info 查看架构
        • ‐thin 拆分架构
        • ‐creat 合并架构
  • Mach-O结构
    • Header:用于快速确定文件的CPU类型、文件类型等
    • Load Commands:指示加载器(例如dyld)如何设置并且加载二进制数据
    • Data:存放数据👇
      • 代码
      • 数据
      • 字符串常量
      • 方法
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容