MachO && dyld(三)

目录

Data(数据)

  • 简介

    通过前面对 MachO 文件 Header 和 LoadCommands 的介绍,可知:
    Header 区域主要用于存储 MachO 文件的一般信息,并且描述了 LoadCommands 区域
    而 LoadCommands 区域则详细描述了 Data 区域
    如果说 Header 区域和 LoadCommands 区域的主要作用是:
    ① 让系统内核加载器知道如何读取 MachO 文件
    ② 并指定动态链接器来完成 MachO 文件后续的动态库加载
    ③ 然后设置好程序入口等一些列程序启动前的信息
    那么,Data 区域的作用,就是当程序运行起来后,为每一个映射到虚拟内存中的指令操作提供真实的物理存储支持

    Data 区域通常是 MachO 文件中最大的部分,主要包含:代码段、数据段,链接信息等
    注意:不要把 Data 区域与数据段搞混掉了,Data 区域指的是广义上的数据,而不是特指数据段的数据

    在 MachO 文件中,Data 为第三个区域
    与 LoadCommands 区域紧接着 Header 区域不同的是,Data 区域并没有紧接着 LoadCommands 区域。LoadCommands 与 Data 之间还留有不少的空间,为代码注入提供了很大的便利
    MachO && dyld(三)
    以下我们通过几个小示例,来简单地探索一下 MachO 文件的 Data 区域

  • MachOView 中的 Segment 与 Section

    我们要分清楚:位于 LoadCommands 区域的段加载指令 && 位于 Data 区域的段数据
    在 MachO 文件中,通常约定:
    segment 的名称为双下划线加全大写字母(如 __TEXT)
    section 的名称为双下划线加全小写字母(如 __text)
    MachO && dyld(三)

  • MachO 文件的 Header 区域和 LoadCommands 区域也会被映射到进程的虚拟地址空间中

    LoadCommands 区域的 LC_SEGMENT_64 命令用于将 MachO 文件中 64 位的 Segment(段)映射到进程的虚拟地址空间中(即加载命令)
    特别地,在 MachO 文件中,并不是只有 Data 区域会被映射到进程的虚拟地址空间中
    Header 区域和 LoadCommands 区域也会被映射到进程的虚拟地址空间中
    MachO && dyld(三)
    请看以下分析:

    LoadCommands 区域的第一条加载命令 LC_SEGMENT_64(__PAGEZERO):加载空指针陷阱段(不可读,不可写,不可执行)
    用于将 MachO 文件中,起始地址为 0x0,大小为 0x0 的区域 映射到
    进程的虚拟地址空间 [0x0, 0x1 0000 0000 (4GB)]
    即规定了进程地址空间的前 4GB:不可读、不可写、不可执行
    MachO && dyld(三)
    LoadCommands 区域的第二条加载命令LC_SEGMENT_64(__TEXT):加载代码段(可读,可执行,不可写)
    用于将 MachO 文件中,起始地址为 0x0,大小为 0x8000 的区域 映射到
    进程的虚拟地址空间 [0x1 0000 0000 (4GB), 0x1 0000 8000]
    MachO && dyld(三)
    ③ 我们注意到:
    在 MachO 文件中,Header 区域并上 LoadCommands 区域的起始地址(0x0)和结束地址(0x0B77),包含在 LC_SEGMENT_64(__TEXT) 对 MachO 文件的映射范围 [0x0, 0x8000]
    也就是说,Header 区域和 LoadCommands 区域会被当成代码段的一部分,从而映射到进程的虚拟地址空间中
    MachO && dyld(三)
    ④ 特别地,Data 区域中的第一个节 Section64(__TEXT, __text) ,在 MachO 文件中的起始地址为 0x6158,大小为 0x2A4,将会被映射到进程的虚拟地址空间 0x1 0000 6158
    也就是说,进程的可用虚拟空间起始处(0x1 0000 0000 (4GB)),最先存储的是 MachO 文件的 Header 区域和 LoadCommands 区域的数据([0x1 0000 0000 (4GB), 0x1 0000 0B77]),然后接着是一段留白的区域([0x1 0000 0B78, 0x1 0000 6157]),接着才是 Data 区域中代码段的第一个节 Section64(__TEXT, __text)0x1 0000 6158
    MachO && dyld(三)
    其内存分布如下图所示:
    MachO && dyld(三)
    ⑤ 这里还有一点需要注意:
    既然在内存中 MachO 文件的 Header 区域和 LoadCommands 区域被映射成代码段的一部分
    那么在内存中 Header 和 LoadCommands 的数据的访问权限就跟代码段是一样的(可读,可执行,不可写)

  • 查看 Dynamic Loader Info

    LoadCommands 区域的 LC_DYLD_INFO_ONLY 命令中记录着动态链接器加载动态库所需的必要信息的位置和大小
    Data 区域的 Dynamic Loader Info 则实际存储着动态链接器加载动态库所需的必要信息
    MachO && dyld(三)
    MachO && dyld(三)
    MachO && dyld(三)
    MachO && dyld(三)
    MachO && dyld(三)

  • 查看主线程的入口

    主线程的入口即 main 函数的入口
    MachO && dyld(三)
    MachO && dyld(三)

  • 一个简单的 Demo

    在项目 MachODemoViewController.m 里面,有以下代码:

    1. 静态 C 字符串 和 静态 OC 字符串
    2. 无参数和带参数的 C 函数
    3. 无参数和带参数的 OC 方法
    4. 一个动态库 HcgServices

    如下图所示:
    MachO && dyld(三)
    根据以上代码,对项目的 MachO 文件进行探索:

    ① 静态的 C 字符串 和 静态的 OC 字符串,都被存储在 Section64(__TEXT, __cstring) 里面
    不仅如此,函数和方法里面用到的字符串(哪怕是像 %d%s 这样的占位符),也都被存储在 Section64(__TEXT, __cstring) 里面。并且所有相同的字符串,只会被保存一次
    MachO && dyld(三)MachO && dyld(三)
    ② 由于 C 语言是静态语言,在程序构建时,所有的 C 函数都被编译成汇编代码了,所以在 MachO 文件中,找不到 无参数和带参数的 C 函数

    ③ 所有的 OC 方法名都被存储在 Section64(__TEXT, __objc_methname)
    MachO && dyld(三)
    ④ 在 LoadCommands 区域,有加载动态库 libHcgServices.dylib 的命令
    MachO && dyld(三)

  • 查看 Symbol Table

    LoadCommands 区域的 LC_SYMTAB 命令中记录了:
    Symbol Table 的偏移量为 50272(0x0000C460)
    String Table 的偏移量为 54004(0x0000D2F4)
    MachO && dyld(三)
    Data 区域的 Symbol Table 的起始地址正好为 0x0000C460
    Symbol Table 的每一个元素都是一个 struct nlist_64 结构体

    #import <mach-o/nlist.h>
    
    struct nlist_64 {
        union {
            uint32_t  n_strx;	/* 符号的名称在 string table 中的索引 */
        } n_un;
        uint8_t n_type;			/* 符号的类型标识,可选的值有:N_STAB、N_PEXT、N_TYPE、N_EXT */
        uint8_t n_sect;        	/* 符号所在的 section 的索引。如果没有对应的 section,则为 NO_SECT */
        uint16_t n_desc;       	/* 符号的描述,see <mach-o/stab.h> */
        uint64_t n_value;      	/* 符号所在的地址 或 stab 的偏移量 */
    };
    

    MachO && dyld(三)
    Data 区域的 String Table 的起始地址正好为 0x0000D2F4
    String Table 存储着所有符号的名字,以 . 作为分隔符
    MachO && dyld(三)
    这里以类 AppDelegate 为例进行演示(注意:这里说的 AppDelegate 是一个类)
    AppDelegate 的符号在 Symbol Table 中所对应的 struct nlist_64 结构体如下图所示
    MachO && dyld(三)
    由步骤 ③ 可知:
    符号 AppDelegate 所对应的字符串在 String Table 中的偏移量为 0x00000ABF
    又因为 String Table 的起始地址为 0x0000D2F4
    所以符号 AppDelegate 所对应的字符串在 String Table 中的位置为 0x0000D2F4 + 0x00000ABF = 0x0000DDB3
    MachO && dyld(三)
    String Table0x0000DDB3 处,确实存储着类 AppDelegate 的符号名称
    这与步骤 ③ 中 MachOView(Value 列)的显示结果不谋而合

    同样由步骤 ③ 可知:
    符号 AppDelegate 位于第 0x15(21) 个 Section 里面,并且符号的地址为 0x00009568
    MachOView 已经帮我们解析出第 21 个 Section 就是 Section64(__DATA, __objct_data)
    Section64(__DATA, __objct_data) 的第二个条目中(地址为 0x00009568),记录着类 AppDelegate 的基本信息:

    1. AppDelegateisa 指针 指向了 _OBJC_METACLASS_$_AppDelegate
    2. AppDelegate 的父类是 _OBJC_CLASS_$_UIResponder(也就是我们所熟知的 UIResponder
    3. 此时类 AppDelegate 的缓存为空
    4. AppDelegate 的 VTable 数量为 0
    5. AppDelegate 的详细信息在地址 0x100008CF0

    MachO && dyld(三)
    到这里,是不是觉得特别熟悉呢?我们通过 clangAppDelegate.m 重写为 AppDelegate.cpp ,可以看到:

    ~/Desktop/AppDelegate > ls
    AppDelegate.h 	AppDelegate.m
    
    ~/Desktop/AppDelegate >
    clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk AppDelegate.m
    
    ~/Desktop/AppDelegate > ls
    AppDelegate.h 	AppDelegate.m	AppDelegate.cpp
    
    [AppDelegate.cpp...]
    
    extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_AppDelegate __attribute__ ((used, section ("__DATA,__objc_data"))) = {
    	0, // &OBJC_METACLASS_$_AppDelegate,
    	0, // &OBJC_CLASS_$_UIResponder,
    	0, // (void *)&_objc_empty_cache,
    	0, // unused, was (void *)&_objc_empty_vtable,
    	&_OBJC_CLASS_RO_$_AppDelegate,
    };
    static void OBJC_CLASS_SETUP_$_AppDelegate(void ) {
    	OBJC_METACLASS_$_AppDelegate.isa = &OBJC_METACLASS_$_NSObject;
    	OBJC_METACLASS_$_AppDelegate.superclass = &OBJC_METACLASS_$_UIResponder;
    	OBJC_METACLASS_$_AppDelegate.cache = &_objc_empty_cache;
    	OBJC_CLASS_$_AppDelegate.isa = &OBJC_METACLASS_$_AppDelegate;
    	OBJC_CLASS_$_AppDelegate.superclass = &OBJC_CLASS_$_UIResponder;
    	OBJC_CLASS_$_AppDelegate.cache = &_objc_empty_cache;
    }
    
    [AppDelegate.cpp...]
    

    这与我们通过 MachOView 看到的结果不谋而合

    根据步骤 ⑤ 中 MachOView 的显示结果可知,类 AppDelegate 的详细信息在地址 0x100008CF0
    MachO && dyld(三)
    同样地,根据步骤 ⑤ 中 AppDelegate.cpp 的代码可知,0x100008CF0 就是 _OBJC_CLASS_RO_$_AppDelegate 的地址
    _OBJC_CLASS_RO_$_AppDelegate 实际上是一个 struct class_ro_t 结构体
    struct class_ro_t 结构体的原始定义位于 runtime 的源码中
    AppDelegate.cppstruct class_ro_t 结构体的描述为

    [AppDelegate.cpp...]
    
    struct _class_ro_t {
        unsigned int flags;
        unsigned int instanceStart;
        unsigned int instanceSize;
        const unsigned char *ivarLayout;
        const char *name;
        const struct _method_list_t *baseMethods;
        const struct _objc_protocol_list *baseProtocols;
        const struct _ivar_list_t *ivars;
        const unsigned char *weakIvarLayout;
        const struct _prop_list_t *properties;
    };
    
    [AppDelegate.cpp...]
    

    到这里,我们已经可以获取到类 AppDelegate 的所有信息了

    比如,我们想看看类 AppDelegate 的方法列表
    由步骤 ⑥ 中 MachOView 的显示结果可知:Base Methods = 0x100008C48
    MachO && dyld(三)
    这里的每一个条目(item)都对应一个 struct objc_method 结构体
    同样地,struct objc_method 结构体的原始定义也位于 runtime 的源码中
    AppDelegate.cppstruct objc_method 结构体的描述为

    [AppDelegate.cpp...]
    
    struct _objc_method {
    	struct objc_selector * _cmd;
    	const char *method_type;
    	void  *_imp;
    };
    
    [AppDelegate.cpp...]
    

    比如,我们想看看类 AppDelegate 的属性列表
    由步骤 ⑥ 中 MachOView 的显示结果可知:Base Properties = 0x100008C98
    MachO && dyld(三)
    我们来梳理一下:
    一开始的时候,我们根据 LoadCommands 区域的 LC_SYMTAB 命令知道了 Symbol TableString Table 的位置和大小,并在 Data 区域实际找到了 Symbol TableString Table
    接下来,我们以类 AppDelegate 为例,说明 Symbol Table 是如何工作的
    首先,我们根据类 AppDelegateSymbol Table 找到了 AppDelegate 的符号名称
    其次,我们根据类 AppDelegateSymbol Table 知道了 AppDelegate 位于 Section64(__DATA, __objct_data)

    Section64(__DATA, __objct_data) 中,我们获取到了类 AppDelegate 的一般信息(比如,isa 指针、父类、类缓存)
    并且知道了类 AppDelegate 详细的信息存储在 Section64(__DATA, __objct_constant)
    之后我们根据 struct class_ro_t 结构体的描述,找到类 AppDelegate 的详细信息(比如,类名,方法列表,属性列表)

    这里有一点需要注意:
    如果 Project 通过 Release 配置构建,那么 MachO 中 Section64(__DATA, __objct_data) 里面的内容会被抹去

  • 查看 Dynamic Symbol Table

    LoadCommands 区域的 LC_DYSYMTAB 命令中记录了所有动态链接时需要用到的符号的信息:
    MachO && dyld(三)
    LC_DYSYMTAB 命令可知,MachO 文件内部的符号在符号表(Symbol Table)中的起始索引为 0 ,数量为 201 个,如下图所示:
    MachO && dyld(三)
    LC_DYSYMTAB 命令可知,MachO 文件导出给外部使用的符号在符号表(Symbol Table)中的起始索引为 201 ,数量为 1 个,如下图所示:
    MachO && dyld(三)
    LC_DYSYMTAB 命令可知,MachO 文件用于懒绑定的符号在符号表(Symbol Table)中的起始索引为 202 ,数量为 24 个,如下图所示:
    MachO && dyld(三)
    LC_DYSYMTAB 命令可知,间接符号表在 MachO 文件中的偏移量为 0x0000D280(53888),共有 0x1D(29) 个元素,如下图所示:
    MachO && dyld(三)

    乍一看,MachOView 中的 Indirect Symbols 里面似乎存储了大量的信息
    但实际上,Indirect Symbols 里面所存储的每一个元素,都只是一个 8Byte 的数据
    这个 8Byte 的数据,代表的是 和动态库相关的符号 在符号表(Symbol Table)的索引值

    而 MachOView Value 列的数据,都是 MachOView 根据这个 8Byte 的索引值,一步一步解析出来的
    通过这个 8Byte 的索引值,可以解析出来的数据有:

    1. Symbol:符号的名称
    2. Section:符号所处在的 section,一般有 section64(__TEXT, __stubs)section64(__DATA, __got)section64(__DATA, __la_symbol_ptr)
    3. Indirect Address:可以通过 Indirect Address 找到符号在 section64(__TEXT, __stubs)section64(__DATA, __got)section64(__DATA, __la_symbol_ptr) 中的地址

    fishhook 的原理就用到了这个,后面会单独写一篇文章来讲

  • 查看 Function Starts

    LoadCommands 区域的 LC_FUNCTION_STARTS 命令用于描述函数的起始地址信息,指向了 Data 区域的链接信息段(__LINKEDIT)中 Function Starts 的首地址
    Function Starts 定义了一个函数起始地址表,调试器和其他程序通过该表可以很容易地判断出一个地址是否在函数内
    MachO && dyld(三)
    通过 Hopper Disassembler 能解析出同样的结果
    MachO && dyld(三)

iOS 系统的懒绑定机制

  • iOS 的懒绑定流程 && MachO 相关的数据结构

    虽然 iOS 系统的懒绑定思路和 Linux 系统的懒绑定思路基本相同
    但是在懒绑定的具体流程以及所使用的数据结构上却略有不同

    在 XCode 中新建一个 iOS Project:LazyBindingDemo
    并使用如下代码来探究 iOS 系统动态库 Foundation.framworkNSLog 函数是如何被懒绑定的
    MachO && dyld(三)
    使用 Release 配置 Build 项目 LazyBindingDemo,并使用 MachOView 打开主程序的 MachO 文件
    MachO && dyld(三)
    ① 在 iOS 系统中,当程序调用动态库的函数时,它实际上是执行 Section64(__TEXT, __stubs) 处的代码(你也可以将 Section64(__TEXT, __stubs) 理解成 ELF 文件中从 plt[1] 开始的部分)
    下图红框标出的部分便是用来调用 NSLog 函数的 Symbol Stub
    它的地址是 0x100006524 (先记住这个地址,后面通过调试验证的时候会用到)
    MachO && dyld(三)
    ② 外部函数的地址被放在 Section64(__DATA, __la_symbol_ptr) 中,而 Symbol Stub 的作用便是找到相应的 Lazy Symbol Pointer 并跳转到它所包含的地址
    此处 NSLog 函数的 Lazy Symbol Pointer 所记录的地址为 0x00000001 000065E4
    MachO && dyld(三)
    ③ 当我们第一次调用 NSLog 函数时, Lazy Symbol Pointer 尚未记录 NSLog 函数的地址,而是指向 Section64(__TEXT, __stub_helper) 中相关的内容。在 Section64(__TEXT, __stub_helper) 中,它将懒绑定函数 dyld_stub_binder 所需的参数放到 寄存器 w16 中,之后跳转到地址 0x000065CC 处,也就是 Section64(__TEXT, __stub_helper) 的头部,然后调用懒绑定函数 dyld_stub_binder 进行符号绑定,最后会将 NSLog 函数的真实地址回写到 Section64(__DATA, __la_symbol_ptr) 中对应的 Lazy Symbol Pointer
    MachO && dyld(三)
    ④ 仔细观察后发现,寄存器 w16 实际上是存放一个 int 类型的值,那么这个 int 类型的值究竟代表什么呢?为什么懒绑定函数 dyld_stub_binder 可以利用它来绑定符号?实际上,它是相对于 Lazy Binding Info 的偏移量(在 LINKEDIT 段的 Dynamic Loader Info 中)
    懒绑定函数 dyld_stub_binder 根据这个偏移量便可从 Lazy Binding Info 中找到绑定过程所需的信息(比如到系统的 Foundation 动态库中寻找 NSLog 函数)
    MachO && dyld(三)

  • 通过 LLDB 的调试,验证懒绑定流程

    上面介绍了 iOS 的懒绑定流程 && MachO 文件中用于支持懒绑定的数据结构
    接下来通过 LLDB 调试项目:LazyBindingDemo 来验证以上的内容
    在两个 NSLog 输出语句中都下断点
    并在调试的时候显示汇编代码(XCode - Debug - Debug Wrokflow - Always Show Disassembly
    MachO && dyld(三)
    ① 程序运行到 NSLog(@"First"); 处,我们可以看到程序实际上是跳转到地址 0x104116524 处的 Symbol Stub 代码。但是等等,我们之前在 MachOView 中观察到程序此时应该跳转到地址 0x100006524 处,但是为什么这里的地址却是 0x104116524 呢?
    这是因为 iOS 系统加载 MachO 文件的时候,使用了 ASLR 技术(地址空间布局随机化)。通过计算 0x104116524 - 0x100006524 可以得到程序此次加载的偏移量为 0x04110000
    MachO && dyld(三)
    ② 根据上一小节的介绍:
    当程序调用动态库的函数时,它实际上是执行 Section64(__TEXT, __stubs) 处的代码
    那么地址 0x104116524 处对应的汇编代码,应该就是 NSLog 函数的 Symbol Stub
    我们通过 LLDB 打印地址 0x104116524 处对应的汇编代码
    LLDB 的输出结果,与前面用 MachOView 显示的 NSLog 函数的 Symbol Stub是一致的
    MachO && dyld(三)
    ③ 因为是首次调用 NSLog 函数,所以地址 0x104116524 处记录的,应该是 Section64(__TEXT, __stub_helper) 中 NSLog 函数执行懒绑定前,用于准备懒绑定的参数的代码
    我们通过 LLDB 打印地址 0x00000001 04116524 处对应的汇编代码
    LLDB 的输出结果,与前面用 MachOView 显示的 NSLog 函数执行懒绑定前,准备参数的代码是一致的
    MachO && dyld(三)
    ④ 那么可想而知,地址0x1041165cc 应该就是 Section64(__TEXT, __stub_helper) 的首地址,其记录的应该就是对懒绑定函数 dyld_stub_binder 的调用
    我们通过 LLDB 打印地址 0x1041165cc 处对应的汇编代码
    果不其然,地址 0x1041165cc 记录的就是对懒绑定函数 dyld_stub_binder 的调用
    出于好奇,我们通过地址 0x00000001 a986 e08c 进去看一下懒绑定函数长什么样子
    MachO && dyld(三)
    ⑤ iOS 系统首次调用 NSLog 函数进行懒绑定的流程,我们已经验证完了
    接来下清空 LLDB 的输出并过掉第一个断点,程序运行到 NSLog(@"Secondt");
    我们接着探索 iOS 系统第二次调用 NSLog 函数的流程
    可想而知,地址 0x104116524 记录的应该还是 NSLog 函数在 Section64(__TEXT, __stubs)Symbol Stub
    我们通过 LLDB 打印地址 0x104116524 处对应的汇编代码,LLDB 的输出结果与预期相符
    MachO && dyld(三)
    ⑥ 我们注意到, 此时 Section64(__TEXT, __stubs)NSLog 函数的 Symbol Stub 记录的不再是指向 Section64(__TEXT, __stub_helper) 的调用,而是 NSLog 函数的真是地址
    这也证实了,懒绑定只会在函数首次调用的时候执行一次
    最后,出于严谨,我们还是要验证一下地址 0x00000001 a9e6253c 出是不是存储着 NSLog 函数对应的汇编代码
    MachO && dyld(三)
    ⑦ 懒绑定函数 dyld_stub_binder 位于动态链接器 dyld 中,大致的绑定过程如下:

    (libdyld.dylib) dyld_stub_binder -->
    (libdyld.dylib) _dyld_fast_stub_entry -->  
    dyld::fastBindLazySymbol --> 
    ImageLoaderMachO::bindLocation
    

如何获取到 Lazy Symbol Pointers 对应的函数名

  • 思考

    先回忆起上节在讲 iOS 系统的懒绑定机制 时的这张图
    这里是通过 MachOView 解析的,项目 LazyBindingDemo 的 MachO 文件中的 Lazy Symbol Pointers
    在调用 NSLog 函数时,会到这里取 NSLog 函数的地址,然后跳转执行

    根据上节对 iOS 系统懒绑定机制的介绍:
    第一次从这里取到的地址值会指向 stub_helper
    第二次从这里取到的地址值会指向 NSLog 函数的入口
    MachO && dyld(三)
    留意到上图最右边的 Value 列,这里 MachOView 已经帮我们解析出:地址 0x10000C000 是指向 NSLog 函数的调用
    这里需要注意一点:MachOView 仅仅是帮我们解析出了地址 0x10000C000 调用的函数名是 NSLog,而不是解析出了 NSLog 函数的真实地址。因为 MachOView 打开的仅仅是存储在磁盘上的静态 MachO 文件,而不是装载到内存中的动态进程,所以 MachOView 是无法获取到位于动态库中的 NSLog 函数的真实地址的

    言归正传:在 Lazy Symbol Pointers 中,MachOView 是如何解析出地址 0x10000C000 对应的函数名就是 NSLog 的呢?

  • Section64 Header 中的 Indirect Sym Index

    在开始介绍函数名获取的流程之前,这里先补充一个知识点

    我们知道,MachO 文件的 Data 区域是分段(Segment)管理的,每个段(Segment)会有 0 到 多个节(Section
    其中,用于描述节(Section)的数据结构如下所示:

    // (64 位的)节
    struct section_64 {
    	char		sectname[16];	/* 16 Byte 的节名 */
    	char		segname[16];	/* 该节所属的段,16 Byte 的段名 */
    	uint64_t	addr;			/* 节的虚拟内存起始地址 */
    	uint64_t	size;			/* 节所占内存空间的大小(Byte) */
    	uint32_t	offset;			/* 节数据在文件中的偏移 */
    	uint32_t	align;			/* 节的内存对其边界(2 的次方) */
    	uint32_t	reloff;			/* 重定位信息在文件中的偏移 */
    	uint32_t	nreloc;			/* 重定位信息的条数 */
    	uint32_t	flags;			/* 标志信息(节的类型与属性。一个节只能有一个类型,但是可以有多个属性,可以通过位运算分别获取节的类型和属性) */
    	uint32_t	reserved1;		/* 保留字段 1(可以用来表示偏移量或者索引,一般用来表示 Indirect Symbol Index,也就是首元素在间接索引表的位置) */
    	uint32_t	reserved2;		/* 保留字段 2(可以用来表示数量或者大小,比如,在 Section64(__TEXT, __sutbs) 中就用来表示 stub 的个数 */
    	uint32_t	reserved3;		/* 保留字段 3(无任何用处,真正的保留字段)*/
    };
    

    留意到 uint32_t reserved1 字段:保留字段 1(可以用来表示偏移量或者索引,一般用来表示 Indirect Symbol Index,也就是首元素在间接索引表的位置)。什么意思呢?uint32_t reserved1 字段其实是一个索引偏移量,指的是 Section 的第 0 个元素对应 Indirect Symbols 表中的第几个元素。以 Section64 Header(__stubs) 为例进行说明:
    MachO && dyld(三)
    MachO && dyld(三)

  • 由 Lazy Symbol Pointers 获取函数名的过程

    ① 由 LoadCommands 区域的 LC_SEGMENT_64(__DATA).Section64 Header(__la_symbol_ptr).Indirect Sym Index = 0xF(15) 可知,Lazy Symbol Pointers 的第 0 个元素对应 Indirect Symbols 的第 15 个元素。因为 NSLog 函数正好为 Lazy Symbol Pointers 的第 0 个元素,所以 NSLog 函数对应 Indirect Symbols 的第 15 个元素,如下图所示:
    MachO && dyld(三)
    MachO && dyld(三)
    ② 留意上图 NSLog 函数在 Indirect Symbols 中对应的条目,其 Data 列的值为 0x00000C0(192),说明 NSLog 函数的符号在符号表 Symbol Table 中的索引为 192,找到 Symbol Table 的第 192 个元素,如下图所示:
    MachO && dyld(三)
    ③ 留意上图 NSLog 函数在 Symbol Table 中对应的条目,其 String Table Index 为 16,说明 NSLog 函数在符号表 String Table 中的起始位置为 16,找到 String Table 的第 16 个位置,正好为 _NSLog,如下图所示:
    MachO && dyld(三)
    ④ 整体的解析顺序为:
    Section64 Header(__la_symbol_ptr) -> Lazy Symbol Pointers -> Indirect Symbols -> Symbols -> String Table
    Section64 Header(__stubs) -> Symbol Stubs -> Indirect Symbols -> Symbols -> String Table

通用二进制文件(多层 MachO 文件)

  • 通用二进制文件简介

    思考一个问题:
    不同的 iOS 设备,可能具有不同的 CPU 架构(armv7、armv7s、arm64)
    那么,通过 XCode 存档(Archive)出来的同一个 IPA 包为什么可以运行在具有不同 CPU 架构的所有 iOS 设备上呢?
    因为,通过 XCode 存档(Archive)出来的 IPA 包中,包含了不同 CPU 架构的二进制代码

    可以在 XCode - Target - Build Setttings - Architectures 中,设置项目所支持的 CPU 架构
    MachO && dyld(三)
    各个选项的含义如下:

    1. Architectures 选项,指示项目将被编译成支持哪些 CPU 指令集的二进制代码。Standard architectures 表示标准架构,里面包含 armv7 和 arm64
    2. Valid Architectures 选项:指示项目可支持的 CPU 指令集。Architectures 选项 和 Valid Architectures 选项的交集,将是 XCode 最终生成的 IPA 包所支持的指令集
    3. Build Active Architecture Only 选项:指示是否只编译出当前真机调试的设备所对应的指令集,该选项 Debug 模式下默认为 YES,Release 模式下默认为 NO
      开发调试时,为了加快编译速度,一般只需编译出调试设备的 CPU 型号所对应的二进制代码即可
      测试发布时,为了涵盖大部分机型,一般需要编译出所有主流机型的 CPU 型号所对应的二进制代码

    通用二进制文件(Universal Binary)也叫胖二进制文件(Fat Binary),是由苹果公司提出的 能同时适用多种 CPU 架构的二进制文件,具有以下特点:

    1. 使用 通用二进制文件 的同一个程序包能同时为多种 CPU 架构提供最理想的性能
    2. 因为 通用二进制文件 需要储存多种 CPU 架构的二进制代码,所以(通用二进制应用程序)通常比(单一平台二进制应用程序)要大
    3. 因为两种 CPU 架构的二进制代码有共同的非可执行资源,所以(通用二进制应用程序)的大小并不会达到(单一平台二进制应用程序)的两倍之多
    4. 因为一个 iOS 设备只会有一种 CPU 架构,所以(通用二进制应用程序)在执行时只会调用一种 CPU 架构的代码。因此,(通用二进制应用程序)运行起来不会比(单一平台二进制应用程序)耗费额外的内存

    可以通过 file 指令,查看 MachO 文件支持哪些 CPU 指令集

    ~/Desktop/lipo_demo > file MachODemo
    MachODemo: Mach-O universal binary with 3 architectures: [arm_v7:Mach-O executable arm_v7] [arm_v7s:Mach-O executable arm_v7s] [arm64:Mach-O 64-bit executable arm64]
    MachODemo (for architecture armv7):	Mach-O executable arm_v7
    MachODemo (for architecture armv7s):	Mach-O executable arm_v7s
    MachODemo (for architecture arm64):	Mach-O 64-bit executable arm64
    
  • 通用二进制文件的结构

    接着再思考一个问题:
    由上面的介绍可知,IPA 包中含有不同 CPU 架构的二进制代码
    那么,这些不同 CPU 架构的二进制代码存放在 IPA 包中的哪里呢?
    这些不同 CPU 架构的二进制代码存放在 IPA 包中的主可执行文件中

    那么,通用二进制文件的结构和普通 MachO 文件的结构,有哪些区别,又有哪些联系呢?
    接下来,通过 MachOView 查看通用二进制文件的内部结构:
    MachO && dyld(三)
    由上图可知,通用二进制文件由:
    ① Fat Header
    ② 存储不同 CPU 架构代码的 MachO 文件
    按次序组成。因此,有时候,通用二进制文件也被叫做多层 MachO 文件

    通用二进制文件与单一架构的 MachO 文件的关系,如下图所示:
    MachO && dyld(三)
    在 fat.h 中用于描述 Fat Header 的数据结构,如下所示:

    struct fat_header {
    	uint32_t	magic;			/* 魔数,用于描述通用二进制文件的字节顺序 */
    	uint32_t	nfat_arch;		/* 通用二进制文件所包含的架构数量 */
    };
    
    struct fat_arch {
    	cpu_type_t	cputype;		/* cpu 类型 */
    	cpu_subtype_t	cpusubtype;	/* cpu 子类型 */
    	uint32_t	offset;			/* 当前架构在通用二进制文件中的偏移量 */
    	uint32_t	size;			/* 当前架构的大小 */
    	uint32_t	align;			/* 内存对齐边界(2 的次方) */
    };
    
    #define FAT_MAGIC	0xcafebabe 	/* 小端模式(arm cpu 默认工作在小端模式) */
    #define FAT_CIGAM	0xbebafeca	/* 大端模式(需要转换成小端模式) */
    
  • lipo 命令

    ① 使用 lipo 命令查看 MachO 文件的信息

    # lipo -info 命令只能查看 MachO 文件包含哪些 CPU 架构
    # 如果要查看 MachO 文件的详细信息,可以使用 OTool 或者 MachOView
    ~/Desktop/lipo_demo > lipo -info MachODemo
    Architectures in the fat file: MachODemo are: armv7 armv7s arm64
    

    ② 使用 lipo 命令拆分多层 MachO 文件(通用二进制文件)

    ~/Desktop/lipo_demo > lipo MachODemo -thin arm64 -output MachO_arm64
    

    ③ 使用 lipo 命令合并 MachO 文件

    ~/Desktop/lipo_demo > lipo -create MachO_armv7 MachO_arm64 -output MachO_standard
    
上一篇:关于Python修改列表的值的问题


下一篇:iOS关于字符数字类型的转换