MachO && dyld(三)

Data(数据)

  • 简介

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

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

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

  • MachOView 中的 Segment 与 Section

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

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

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

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

  • 查看 Dynamic Loader Info

    LoadCommands 区域的 LC_DYLD_INFO_ONLY 命令中记录着动态链接器加载动态库所需的必要信息的位置和大小
    Data 区域的 Dynamic Loader Info 则实际存储着动态链接器加载动态库所需的必要信息
    1 - LC_DYLD_INFO_ONLY
    2 - Rebase Info
    3 - Binding Info
    4 - Lazy Binding Info
    5 - Exprot Info

  • 查看主线程的入口

    主线程的入口即 main 函数的入口
    main 函数入口 00
    main 函数入口 01

  • 一个简单的 Demo

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

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

    如下图所示:
    一个简单的 Demo
    根据以上代码,对项目的 MachO 文件进行探索:

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

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

  • 查看 Symbol Table

    LoadCommands 区域的 LC_SYMTAB 命令中记录了:
    Symbol Table 的偏移量为 50272(0x0000C460)
    String Table 的偏移量为 54004(0x0000D2F4)
    1 -  LC_SYMTAB
    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 的偏移量 */
    };
    

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

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

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

    5 - AppDelegate 所处的 Section
    到这里,是不是觉得特别熟悉呢?我们通过 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
    6 - struct class_ro_t
    同样地,根据步骤 ⑤ 中 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
    7 - Base Methods
    这里的每一个条目(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
    8 - Base Properties
    我们来梳理一下:
    一开始的时候,我们根据 LoadCommands 区域的 LC_SYMTAB 命令知道了 Symbol TableString Table 的位置和大小,并在 Data 区域实际找到了 Symbol TableString Table
    接下来,我们以类 AppDelegate 为例,说明 Symbol Table 是如何工作的
    首先,我们根据类 AppDelegateSymbol Table 找到了 AppDelegate 的符号名称
    其次,我们根据类 AppDelegateSymbol Table 知道了 AppDelegate 位于 Section64(__DATA, __objc_data)

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

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

  • 查看 Dynamic Symbol Table

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

    乍一看,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 定义了一个函数起始地址表,调试器和其他程序通过该表可以很容易地判断出一个地址是否在函数内
    LC_FUNCTION_STARTS
    通过 Hopper Disassembler 能解析出同样的结果
    Hoppe

iOS 系统的懒绑定机制

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

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

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

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

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

    (libdyld.dylib) dyld_stub_binder ->
    (libdyld.dylib) dyld::fastBindLazySymbol(...) ->  
    (libdyld.dylib) ImageLoader::doBindFastLazySymbol(...) ->  
    (libdyld.dylib) ImageLoaderMachOCompressed::doBindFastLazySymbol(...)
    

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

  • 思考

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

    根据上节对 iOS 系统懒绑定机制的介绍:
    第一次从这里获取到的地址值会指向 stub_helper
    第二次从这里获取到的地址值会指向 NSLog 函数的入口
    Lazy Symbol Pointers
    留意到上图最右边的 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) 为例进行说明:
    Section64 Header(__stubs) 的 Reserved1 属性
    Symbol Stubs 与 Indirect Symbols 的对应关系

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

    ① 由 LoadCommands 区域的 LC_SEGMENT_64(__DATA).Section64 Header(__la_symbol_ptr).Indirect Sym Index = 0x0000000F(15) 可知,Lazy Symbol Pointers 的第 0 个元素对应 Indirect Symbols 的第 15 个元素。因为 NSLog 函数正好为 Lazy Symbol Pointers 的第 0 个元素,所以 NSLog 函数对应 Indirect Symbols 的第 15 个元素,如下图所示:
    Section64 Header(__la_symbol_ptr) 的 Reserved1 属性
    Lazy Symbol Pointers 与 Indirect Symbols 的对应关系
    ② 留意上图 NSLog 函数在 Indirect Symbols 中对应的条目,其 Data 列的值为 0x00000C0(192),说明 NSLog 函数的符号在符号表 Symbol Table 中的索引为 192,找到 Symbol Table 的第 192 个元素,如下图所示:
    2 - Symbol Table
    ③ 留意上图 NSLog 函数在 Symbol Table 中对应的条目,其 String Table Index0x16,说明 NSLog 函数在符号表 String Table 中的起始位置为 0x16,找到 String Table 的第 0x16 个位置,正好为 _NSLog,如下图所示:
    3 - String Table
    ④ 整体的解析顺序为:
    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 架构
    Build Settings - Architectures
    各个选项的含义如下:

    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 文件
    由上图可知,通用二进制文件由:
    ① Fat Header
    ② 存储不同 CPU 架构代码的 MachO 文件
    按次序组成。因此,有时候,通用二进制文件也被叫做多层 MachO 文件

    通用二进制文件与单一架构的 MachO 文件的关系,如下图所示:
    通用二进制文件结构
    在 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
    
  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
引用:iOSAPP瘦身需要掌握的技能包括XCode构建技术、LLVM编译器技术、CocoaPods构建技术、图片压缩技术、持续集成技术。这些技术可以在开发和构建过程中优化App的大小和性能,从而提升用户体验。 引用:iOS工程结构包括壳工程和Pod模块,壳工程由主Target和Apple插件Target构成,而模块包括静态库和动态库。IPA包是iOS上传到AppStore的包格式,它包含了MachO可执行文件、.framework、Assets.car等文件。从iOS工程到IPA包的构建过程主要包括编译和文件拷贝。 引用:iOS工程构建产物是MachO文件,其中的TEXT段存放了只读的数据段,__cstring段存放了普通的CString,__objc_methtype和__objc_methname存放了Objc的方法签名和方法名。对这些数据进行压缩可以显著减少App的大小。 引用:iOS的ODR技术(OnDemandResource)允许在运行时动态下载资源文件。这意味着如果某些资源在启动时用不到,可以通过ODR技术进行处理,从而减少App的大小。 综上所述,iOS IT技术涵盖了多个方面,包括XCode构建技术、LLVM编译器技术、CocoaPods构建技术、图片压缩技术、持续集成技术,以及对iOS工程结构的理解和优化、MachO文件的压缩、ODR技术的应用等。这些技术和策略可以帮助开发者优化App的大小和性能,提升用户体验。<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span>

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值