iOS 编译链接与 Mach-O 深度解析:静态库、动态库原理剖析,到工程化实践
日常业务开发中,多数开发者长期依赖 CocoaPods 自动化能力,很少关注工程 倘若你对以上问题一知半解,不妨带着疑问阅读本文,系统性吃透 iOS 工程编译、链接与依赖库底层核心逻辑。 传统的在 GUI 面板上操作不在本文研究范畴。对于 Build Settings 各个 key 不知道是什么作用的,可以查看 Xcode Build Settings Reference 和 Xcode Project Management Guide 举个例子,我们经常会遇到在不同环境下,根据环境的不同,网络接口请求不同的 url。常见的一幕是:给 QA 测试不同环境的 App,手动更改 baseUrl 地址,然后打包让下载。太低效了。 所以需要做: 为了区分不同环境,还需要设置 OC 和 Swift 各自的编译宏。 OC:需要在 OC 自带 要增加新的参数,选中对应的 Configuration,双击后在弹出的面板后,按照 DEBUG=1 的格式添加 对于 Swift 则有不同的编译器。在 Build Settings 中的 Other Swift Flags 里选择对应的 Configuration,按照 -DDEV 的格式添加。-D 是必须要存在的,后面要加的 Flag,就紧跟在后面 操作了一番下来,我们可以发现一些缺点: 要在不同的环境设置不同的 BASE_URL 在多 Scheme 方案下如何实现? 多 Scheme 可以选中 PROJECT,然后点击左下方的 +,点击「Duplicate "Debug" Configuration」,增加一列后,重命名为 Beta 分别选择不同的 Scheme,点击 Edit Scheme。让不同的 Scheme 对应不同的 Configuration 优点:不用像方式1一样,要配置某个值,需要切换不同的 TARGETS。通过多 Scheme 的方式,可以在同一个 Build Settings 中对不同的值进行配置,不会很分散。到时候只需要切换不同的 Scheme 即可。 Cocoapods 管理的项目在 install 之后便可以看到2份 所以我们也可以使用 另外 需求:设置不同环境对应不同的 BASE_URL 的值,在项目中正确读取。 Edit Scheme。让不同的 Scheme 选择对应的 Build Configuration 效果如下: 同样 Cocoapods 管理的工程,也会根据 Configuration 生成不同的 xcconfig 文件,我们可以修改或者创新新的 xcconfig 文件,但引入使用。 xcconfig (Xcode Configuration Settings File) 文件是用于管理 Xcode 项目构建设置的纯文本配置文件。遵循正确的编写规范可以确保项目配置的可维护性和一致性。 键值对声明 包含其他文件 注释规范 条件设置 SDK 条件 架构条件 构建配置条件 组合条件(上述条件可以自由组合) 设置条件后,如果在不符合的条件下,编译会报错 值引用和继承 文件组织结构 分层配置 路径管理规范 多平台支持 条件包含 命令行验证 终端输入 因为要编写脚本,研究 MachO 文件,但每次编译后的 MachO 还要 show in Finder,之后再切换到终端,然后执行脚本很繁琐。所以可以直接在 Xcode 的 Build Phases 中,增加脚本解决该问题。 但 Build Phases 中的 Run Script 长度有限,不能写很多脚本,所以还是需要结合 Xcconfig。 Xcconfig 中定义的变量在 Run Script 中是可以访问的到。 Demo 验证如下: 所以编写了一个脚本,进行输出重定向 xcode_run_cmd 使用的时候,需要搭配 符号的处理都是利用 LLVM Strip 的能力,对这部分好奇的,可以在源码中对 Strip 进行调试。 调试的时候要想更有意义,类似:真的在 strip 一个项目的符号,可以把某个可执行文件的路径配置到 LLVM-Strip 的启动项中 步骤:Edit Scheme - Arguments - 添加2个参数:-pa 和 需要脱符号的路径 关于 LLVM 工程如何编译、运行的具体步骤,可以查看 LLVM 上面已经看到了 Xcconfig 文件的身影。在 Xcode 中使用 Xcode 的 Build Settings 是一个多层叠加系统,优先级从高到低如下: 这是因为: 最终生效的意思是:Xcode 的 Build Settings 界面本质是一个实时计算的合并视图,它会展示: 举个例子: 假设在以下位置设置 最终生效值将是: 创建2个 编译后,查看如下: 所以可以看到:Xcode 的配置是遵循层级关系和继承的。 当配置完 Xcconfig 和 Xcode GUI 面板上操作完后,可以用 xcodebuild -showBuildSettings -configuration Debug 查看最终的结果是否符合预期 iOS 应用的构建过程本质上是将高级语言代码转化为设备可执行的 Mach-O 文件的过程,其中编译阶段和链接阶段是核心技术环节 前端编译: 词法分析(Lexical Analysis):将源代码拆分为 token 流(关键字、标识符、运算符等) 语法分析(Syntax Analysis):生成抽象语法树(AST) 语义分析(Semantic Analysis): 生成带类型信息的 AST 中间代码的生成与优化: LLVM IR 的生成:平台无关的中间表示(Objective-C 通过 Clang,Swift 通过 SILGen) 机器无关的优化。经历一系列 Pass。关键方面 目标代码生成:包含机器码 + 符号表 + 重定位信息 汇编代码生成:将 IR 转换为目标架构的汇编代码(.s 文件) 静态链接处理 合并过程: 动态链接处理 当 App 编译完成的时候,Mach-O 文件中会存在一些关键结构: 首次调用 NSLog 过程 桩代码读取 GOT 指针 此时 执行动态绑定器 dyld_stub_binder 工作流程: dyld 解析符号地址 dyld 执行下面步骤: 计算 ASLR 偏移后的实际地址。 更新 GOT 并跳转 后续调用 NSLog 流程 生成 Mach-O 可执行文件 编译、链接的对比 那看上去链接器做的事情很简单,不就是个打包工具?链接器最重要的工作就是决定符号(变量名、函数名)的定义 例如上面的代码 链接器打包所有的目标文件,因为链接器可以看到全局,具有上帝视角,因此链接器从依赖的库中去查找 NSLog 这个符号,如果找不到则会报经典的错误 编译器只能将 NSLog 这个函数的跳转地址暂时设置为0,随后在链接的时候再去修正它。 通过一个例子一步步验证下上面的过程: 第一步,将 main.m 编译为 main.o 文件。指令如下 第二步,已经得到了 main.o 目标文件,也就是二进制文件,利用指令 可以看到 main 函数中,callq 就是调用 NSLog 函数。后面的地址写为了 0,这里的0会在后面链接的过程中被修正。 第三步,另外为了能让链接器能够定位到这些需要被修正的地址,在代码块中可以看到一个重定位表。指令为 NSLog 位于偏移量为19的位置, 第四步,链接目标文件到可执行文件,指令为 因为目前 Foundation 已经存在于系统目录,所以不需要额外指定动态库/静态库路径了。 符号可以理解为程序中各种元素的抽象表示。它就像是一个标识符,用来代表函数、变量、类等编程元素 编译 使用 定义:在目标文件中显式导出的符号,可被其他模块引用。 特点: Mach-O 标记: 针对全局符号: Common Symbol:Common Symbol 是全局符号的“临时状态” 编译器将未初始化的全局变量(如 Common Symbol(公共符号)和全局符号(Global Symbol)本质上是同一符号在不同编译阶段的表现形式,二者既有紧密联系又有关键区别 编译期与链接期的形态转换 链接器设置: 有趣的 feature: 上面的代码不会编译报错。当存在2个同名的全局符号,一个初始化了,一个未初始化,不会报错。在编译链接阶段,当找到定义之后,未定义的会被删掉。 定义:仅在当前目标文件中可见,不会暴露给其他模块 特点: Mach-O 标志:无 定义:当前目标文件声明但未定义的符号,需由链接器在其他目标文件或库中解析。 特点:链接时若找不到符号定义,则会报错:Undefined Symbol Weak Reference Symbol:表示此 未定义符号是弱引用。如果动态链接器找不到该符号的定义,则将其设置为0。链接器会将该符号设置弱链接标志 Weak definition Symbol:表示此符号为弱定义符号。如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则该弱定义将被忽略。只能将合并部分中的符号标记为弱定义 当把其中一个符号通过 \__attribute__((weak)) 改为 weak symbol 的时候,再次编译,发现没有问题。 弱引用符号。 主要作用: 向后兼容性 运行时安全检查。使用前需显式检查符号是否可用: 避免链接错误 实验不方便模拟动态库和 App 的情况。就看同一个 Mach-O 中,只有函数声明,没有实现的情况 但告诉编译器该符号是弱引用符号,就可以编译链接成功 生成目标文件分过程中,只需要头文件信息(头文件路径),只需要重定位符号表,知道哪些符号需要重定位,链接的时候,会自动将符号重定位。 符号在 Mach-O 中占一定的体积,所以需要脱符号。那么哪些符号不能脱? 动态库的全局符号不做处理,默认就是导出符号。链接之后,这些导出符号可能就是间接符号表中的后续经由 dyld_stub_binder 去查找所需的符号。所以动态库的全局符号不能 strip 掉。 看个 Demo:OC 对象默认就是全局符号。 链接器也提供了能力,将导出符号变为不导出的符号:-Xlinker -unexported_symbol -Xlinker _OBJC_CLASS_$_Person 如果有一批符号需要隐藏,链接器提供了更方便的参数: -Xlinker -unexported_symbols_list -Xlinker ${PROJECT_DIR}/LDExploreDemo/hidden_symbols.list 按照先后顺序 操作: 操作: 操作: 将 AST 转换成一种与具体硬件架构无关的、更低级的表示形式,称为 中间代码。LLVM 使用的中间代码是 LLVM IR。 操作: 应用一系列优化通道对 LLVM IR 进行转换,目标是在不改变程序行为的前提下,提升性能、减小代码体积。常见优化包括: 输入: 操作: Tips: 后续有很多终端使用指令的场景,为了查找方便高效,分享一个技巧 man 指令,比如 进入 vim 模式了,看到左下角有 比如查找 代码中使用了 Foundation 库的 NSLog,NSLog 对于业务代码来说,就是一个导入的符号,对于 Foundation 库来说,就是一个导出的符号。 什么符号可以是导出的符号?全局符号可以是导出符号。 App 或者一个 MachO 中,所有使用到的动态库的符号,都保存在间接符号表中,这些间接符号表中的数据,来自于动态库中。 动态库,全局符号 -> 导出符号 间接符号表 -> 动态库符号 所以,Strip 符号的时候,可能不能 Strip 全局符号。 OC 代码,默认都是导出的全局符号,所以容易占空间,想让体积变小,就可以尽量不想暴露的符号,使用链接器的能力,将不需要暴露的符号不暴露出去。 静态库 = QA:从符号角度出发,动态库还是静态库对于 App 瘦身较好(更有抓手)?使用动态库还是静态库会提及更小? App 在链接静态库的时候,静态库就是 .o 文件的合集,会把 静态库链接的时候,除了间接符号表,其他区域都有可能放。所以链接静态库占用体积更小。 所以大家在写 SDK 的时候,可以从符号角度出发想想,是选择静态库还是动态库。针对动态库,可以 strip 导出符号。默认 OC 的符号都是导出的。 写在前面: 这3个的一个或者多个造成的问题,着手去排查问题即可。 clang 编译、链接的参数解释如下: 说明: 系统头文件参数: 做个实验,验证下静态库其实就是 第一步,编写 oc 代码,就一个 Person 类,写一个类方法,编译为静态库。 第二步,将 其实,这里就已经可以验证「静态库就是 .o 文件的合集」。 利用 第三步,编写代码 第四步,利用 clang 将 第五步,将第四步得到的 注意:上面第二步,得到的 查找规则:先找 第六步,查看第五步得到的可执行文件,然后执行,看看是否正常? 静态库本质就是一堆 利用 Clang 指令将 .m 编译为 .o 目标文件 再利用 clang 指令将 .o 目标文件,链接为静态库 利用 libtool 合并2个静态库 用 ar -t libPersonCat 指令,验证合并后的静态库所包含的 .o 目标文件 链接器有一个特性,Auto-Link,启动这个特性后,当我们 import <模块> 不需要我们再去往链接器配置这个链接参数。比如 静态库( 静态库的本质是目标文件( 就会抛出 其实上述存在前提,链接的时候需要指定为 一个 cocoapods 组织的工程,以 pod 的形式引入了 AFNetworking 库。再将另一个 现象:工程编译成功。编译链接后,运行输出打印信息。 直接在 Xcode 中手动拖入一个静态库/动态库,本质是将库的路径写到 OTHER_LDFLAGS 后面。Xcode 会按照指定的路径去加载静态库/动态库。 一个 cocoapods 组织的工程,以 pod 的形式引入了 AFNetworking 库,再将另一个 现象:工程链接失败。报错: 现象:工程链接失败。报错: 问题:为什么上次的链接成功,这次却链接失败了,报符号重复的错误? 2个静态库有符号冲突怎么办?且一个静态库没有源码。llvm-objcopy 这个工具刚好有能力在静态库基础上直接修改符号名称。 通过 objdump --macho -t ${MACH_PATH} 就可以输出符号信息。Demo 中需要重命名的符号如下: 关于 LLVM 如何编译运行,或者开发 pass 可以查看 LLVM 编译运行 打开 LLVM 工程,Scheme 切换为 然后改造 Demo 工程静态库的脚本为 llvm-objcopy --prefix-symbols=FantasticLBP_ ${MACH_PATH} 去给冲突的符号加前缀 结果:但发现运行报错,提示 改进:将 打开 llvm 源码进行调试,配置启动参数。 上面演示了单个修改符号名称的过程。llvm-objcopy 可以批量修改。 用一个文件,格式为空格和换行来区别。 修改后运行,发现批量符号修改成功。 利用 llvm-objcopy 的能力,修改重名的符号后测试下。 可以看到: 通过 llvm-objcopy 只是修改了 所以这种方式存在巨大风险,这里只是从技术研究角度出发,证明可以这么做,但是影响面太大。 方案 1:单个库转为动态库 方案 2:指定模块为动态框架 实验:我的电脑 cocoapods 版本较低,采用下面写法 效果: 动态库:拥有独立的符号空间(LC_ID_DYLIB 标识) 利用动态库的二级命名空间特性,将冲突的静态库转换为动态库,实现符号隔离 静态库的 xcconfig 为 动态库的 xcconfig 为 静态库/ .o 文件 Strip 流程: 处理 Strip 的过程,就是在修改 Mach-O 文件中的内容。 动态库 All Symbols Non-Global Symbols(非全局符号): Mac OS/iOS 平台还可以使用 Framework,Framework 实际上是一种打包方式,将库的:二进制文件、头文件、和有关资源打包到一起,方便管理和分发。 Framework 和系统 UIKit.Framework 还是有很大区别的。 因此,Apple 把这种 Framework 又叫做 不同的 App 使用相拥的动态库,并不会只在系统中存在一份。而是在各个 App 中各自打包、签名、加载一份。 根据 Apple 审核要求,上传到 App Store 的 ipa 可执行文件有大小限制。这里的大小不是指二进制(Mach-O)文件大小,而是指去其中 为了实现该效果,很多公司在组件化或者开源库,都采用动态链接的方式。因为动态链接的部分,不算在当前 Mach-O 的 继续做个实验,验证下 Framework 的结构(上面做了静态库),所以我们可以沿用上面的成果,将静态库包装成 Framework 第一步,新建一个文件夹 第二步,模仿 Framwork 文件结构目录。因为 Framework 的结构里有 Header 信息,所以创建 第三步,根据 第四步,再根据 说明:test.o 链接 Person.framework 生成 main 可执行文件 查找规则:先找 第五步:成功得到了 test 可执行文件,说明模仿 Framwork 搭建的静态库 Framwork 成功了。然后测试下可执行文件运行结果。进行 double check 静态库是一堆 Mach header 包括一些基础信息,所以 Mach header 存在冗余(大小端序、CPU 类型等),这也就是为什么静态库比动态库体积大的原因之一。 动态库在 Mach header 这里有改进,AFNetworking 动态库格式如下: 将公共的信息放到一起,公用一个 Mach header。 但「同一份代码,一个库制作成动态库体积会比静态库小」不绝对。 对于 iOS9,Load Commands Segment vmsize 默认是一个内存页,也就是16k 对于 iOS10,Load Commands Segment vmsize 默认是 32k 假设项目只有1个源文件,可能打包后的静态库要比动态库体积大。 dead strip 触发条件: 由于 OC 是动态性语言,比如 Category 是在运行时创建的,所以即使开启了 dead strip 也没办法脱掉 OC 符号。 链接器很方便,提供了 -why_live 参数,来查看为什么某个符号没有被脱掉。用法:-Xlinker -why_live -Xlinker _global_Function dead code strip 和 xlinker 提供的4个参数: tbd 全称是 text-based stub libraries,本质就是一个 YAML 描述的文本文件。 作用:用于描述动态库的链接信息,包括导出的符号、动态库的架构信息、动态库的依赖信息 用于避免在真机开发过程中直接使用传统的 dylib。 对于真机来说,由于动态库都是在设备上的,在 Xcode 上使用基于 tbd 格式的伪 framework,可以大大减少 Xcode 的大小 一个典型的 TBD 文件(如 继续通过小实验来研究动态库的创建与使用 第一步:创建 dylib 文件夹,下面创建 第二步:对 main.m 编译成 main.o 文件,指令为 第三步:到 dylib 文件夹下,对 Person 编译为 Person.o 文件,指令为 第四步:将 Person.o 编译为动态库,指令为 第五步,将 main.o 和 LibPerson.dylib 链接,成为 main 可执行文件 每次针对动态库的操作都是这些差不多的指令,就是一些参数的不同,写个 Shell 脚本,命名为 结果如下: 第六步:对生成的 main 可执行文件进行调试运行,使用 lldb 指令 咦,为什么我用动态库链接后还是无法使用???带着问题研究下 因为: 所以:能不能推导出这样一个结论:静态库也可以链接成动态库。 做个小实验验证看看: 和上面的材料没有差别,区别在于脚本,其中一步是将静态库链接为动态库。 使用链接器 LD 能力,链接静态库为动态库指令如下 执行完脚本,又出现了奇怪的现象: 发现,在利用动态库链接成可执行文件时报错了 链接器在链接阶段,默认使用 知道具体原因那就好办了,修改指令 再次运行 build 脚本,然后对可执行文件执行,还是报错 😂 通过上面的实验可以得出结论: 为什么动态库链接后的可执行文件运行,会报 不得不聊聊动态库加载原理 也就是说:当 dyld 加载一个可执行文件(main) 的时候,在 Mach-O 中有一些名字叫 用 MachOView 打开另一个 App 看看 对于我们自己链接的可执行文件 main 进行查看,利用 可以发现: 如何解决? 需要在编译链接生成动态库的时候,有个东西保存动态库路径,这个就是 Mach-O 文件中的另一个 Load Command,即 通过改变动态库 name 来修改动态库的路径。具体指令为: 修改动态库的 name 之后,再次 动态库有了正确的 name 后,再重新链接生成可执行文件。可执行文件可以正确运行,查看所以来的动态库路径,均正确加载。 方式一有明显缺点,因为路径是绝对路径,因此没办法迁移。 关键步骤:谁链接动态库,谁来提供 rpath 前提说明:模拟下 App 真实环境。创建一个文件夹 文件夹,将 dylib 文件夹下的文件复制过去。结构如下 第一步,在 第二步,将 第三步,给动态库利用 第四步,利用 第五步,回到根目录,将 第六步,将 此时的可执行文件虽然链接成功了,但是可执行文件需要用到动态库的功能,直接运行会报错 第七步,给可执行文件 添加后验证是否添加成功,指令 第八步,链接成功后 下面是上面全部步骤的截图说明。 给动态库添加 核心:谁链接动态库, 反思:上面的方案还是有缺点的,因为可执行文件提供的 可以将可执行文件的 rpath 修改为灵活的,而不是写死的路径,指令格式为:install_name_tool -rpath oldPath newPath 这个可不是花里胡哨的烧操作,Cocoapods 也是这么干的 注意: 假设背景是: 这样一个场景,代码模拟下,文件目录如下 得到 main 可执行文件,运行报错。 得到新的命题:可执行文件中引入动态库 A,动态库的功能实现依赖动态库 B,链接器该如何链接呢? 第一种尝试: 对 Cat.framework 下的 build.sh 修改脚本,指定 对 Person.framework 下的 build.sh 修改脚本,指定 对可执行文件 main 根目录,指定 运行报错如下: 为什么还错误了?都已经给可执行文件添加了 思路:因为报错是说动态库 Person 找不到动态库 Cat,那是不是聚焦下 Person,在 Person 的链接指令中,给 Person 添加 动手实践下,修改 Person 动态库的 build.sh 脚本 在 Person.framework运行下 build.sh,然后在根目录下运行 build.sh,得到新的可执行文件,然后可以成功运行 反思:可执行文件依赖动态库 A,动态库 A 依赖动态库 B,上面的配置很繁琐: 好繁琐啊,有没有简化的方法。此时 在当前场景下,Person 动态库的指令可以简化下。 修改脚本后,分别在 Person.framework 和 main 可执行文件根目录下执行 build.sh 脚本,然后验证可执行文件是否可以正确运行,加载所需动态库 注:为了方便看清楚脚本执行情况和可执行文件执行结果,这次运行注释了 otool 的打印脚本。 链接器可真强大啊,同时 Mach-O 开的口子也多,也就是可以随意修改已经编译好的可执行文件的 上述的操作,其实本质就是修改 Mach-O 文件的 Load Command,按照 Apple 的机制,Mach-O 修改后,必须重新签名才可以运行使用破解软件时,总是要求我们重新签名,背后的命令如下: 那么主工程可以直接调用 Cat 动态库的能力吗? 正常写代码肯定可以,但是从链接器角度分析下,如何实现 调用的本质就是符号的发现。也就是 Cat 的符号有没有导出?可执行文件 mian 使用的能力就是动态库导出后,自己导入的。 因为 main 引入了 进入 Cat.framework 也查看下 Cat 动态库的导出符号,使用指令 发现 Person 没有导出 Cat 的符号。那在可执行文件中调用不了 Cat 的能力了。 怎么办呢?链接器 LD 已经是很成熟的东西了,对于处理动态库依赖了动态库,且需要将被依赖动态库的符号导出,这样的需求早已满足了。具体是什么参数?终端输入 其中,我们需要用的是 -reexport_framework 这个参数。指令为 对此,修改 Person.framework 的 build.sh 脚本 执行脚本输出如下: 结构如下: 理论上来讲,Person.framework 把 Cat.framework 导出了,实现方式是通过给 Mach-O 的一个叫做 完整验证下,还需要做2件事情: 修改 main.m 的代码,因为要访问 Cat.framework 的能力,测试能否正常运行 修改 main.m 的 build.sh 脚本,因为 Person.framework 已经暴露了 Cat.framework 的能力, 修改完从里到外一次性执行 build.sh,得到 main 可执行文件。一切顺利,输出如下: <span id="twoLevelNamespace"></span> 在 iOS 系统中,动态库的二级命名空间是一种关键的符号管理机制,它解决了不同库之间可能发生的符号冲突问题。这个机制是 macOS 和 iOS 平台动态链接的核心特性。 二级命名空间通过2个维度来唯一标识符号: 动态库名称(Dylib Identity) 每个动态库都有一个唯一标识符,通常包含: 兼容版本号(Compatibility Version) 当动态链接器(dyld)加载符号的时候: 在符号表中查找唯一匹配 在二级命名空间下,符号表使用分层结构 当两个库定义同名符号时: 在二级命名空间作用下: 禁用二级命名空间: 使用唯一前缀命名符号 符号可见性控制 使用适当的路径变量: 完整说法是:同样的代码,静态库格式的体积可能要比动态库大一些。为什么? 静态库的内部结构是: 静态库的本质是一堆 静态库 = N 个独立 .o 文件打包在一起,每个 .o 都自带一套完整的 Mach-O Header 信息: 每个 动态库(.dylib/.framework`)= 所有代码合并成 1 个 Mach-O,只有 1 套 Header + Load Commands。 结构如下: 也就是说:静态库中冗余的头信息,就是静态库更大的根本原因。 但还有另一个说法:静态库被 App 链接后,反而对于 App 体积影响可能更小一些,为什么? 为什么加了可能,因为静态库全部加载的话才是存在多个“冗余” Mach-O 头信息(也不一定是冗余,只是刚好结构如此),但如果通过 other link flag 控制的话,在符号侧链接的少一些(裁剪的多一些),可能就会比动态库这种链接形式,对 App 的体积影响更小一些。 静态库链接前(.a 内部) ld 链接后(进入主可执行文件):ld 扔掉所有 .o 的头 只把代码段合并 → 最终可执行文件 静态库(开启裁剪,默认) 动态库 XCFramework:是苹果官⽅推荐的、⽀持的,可以更⽅便的表示⼀个多平台和架构的分发⼆进制库的格式。专⻔在 2019 年提出的framework 的另⼀种先进格式。 需要 Xcode11 以上⽀持。是为了更好的⽀持 Mac Catalyst 机制和 ARM 芯⽚的 macOS。 胖二进制:Fat Binary,通用二进制格式(Universal Binary)。通用二进制文件实际上就是将支持不同架构的二进制文件打包成一个文件,系统在加载运行时,会根据通用二进制文件中提供的架构,选择和当前系统匹配的二进制文件。 动态库是可以合并的。前提是不同的 CPU 架构。合并之后还是多个不同的动态库,只不过 mach-header 是挨在一起的,所有的库文件也是挨着的。可以理解成是“压缩”。 lipo 指令的缺点: 文件。所以使用 lipo 处理动态库的合并、拆分,都需要管理 基于此,Apple 在 2019 诞生了 和传统的 framework 相⽐: 第一步:先创建一个动态库 第二步:利用 先打出模拟器的包,指令如下: 再打出真机的包,指令如下 打包成功的输出如下: 实体目录如下: 注意:我们打包归档后生成的符号表只有 因为 第三步:利用 结果如下: 可以看到打包后的 xcframework 自动处理了文件夹。但是缺点是我们提供的 framework,别人使用可能会 crash,所以为了一些使用场景需要在 xcframework 中提供 第四步:加入 结果如下 可以看到 xcframework 里面不只有不同的动态库,还携带了对应的 第五步:制作好的 xcframework 接入使用下。 import 头文件并使用 编译运行,查看 products 下面的产物,因为选择的是模拟器运行,所以验证 可以看到我们打包的 好处有三: 注意:该部分代码,在 先做个小实验 第一步:创建一个 iOS App 工程,然后把上面生成的 第二步:不引入动态库,而是通过 xcconfig 文件,告诉链接器,关于动态库的三要素:头文件位置、动态库名称。 第三步:编译运行。 结论:编译正常,但是运行会报错 第一种解决方案是给 xcconfig 添加 rpath 的具体路径。 第二种解决方案是将库声明为“弱链接”。输入 运行时行为: dyld 加载: 符号解析: 错误处理: 主要功能: 修改 xcconfig 文件为 修改后编译运行,发现没有报 我们对添加了 查看 Mach-O 发现,被 通常,当链接一个库时,链接器会尝试解析所有从该框架中引用的符号。如果某个符号在库不存在或者没有导出,链接器会报错,因为这是一个强链接(strong linking)的要求。 使用 这样,应用程序在运行时可以优雅地处理缺少的符号,例如,某个特性可能因为缺少实现而不可用,但应用程序的其他部分仍然可以正常工作。 探索下静态库符号冲突的情况下,怎么解决? 来个简单的小实验。 第一步:准备2个 AFNetworking 的静态库,符号一模一样,但是静态库名称不同。 第二步:创建 Demo 工程,将静态库放到根目录下。 第三步:创建 xcconfig 文件。配置参数用于配制一些编译、链接信息。 第四步:尝试编译,编译成功。 QA:为什么链接2个同名同符号的静态库,编译不报错? 会有二级命名空间吗?不是的。静态库本质就是一堆 核心原因是因为,链接器针对静态库,在 deac code strip 专门为静态库,设置为 可以验证下,在链接的时候修改链接参数为 如何解决?? LD 提供了 修改 LD 链接参数 具体代码见: 第一步,创建一个名字叫 第二步,给动态库添加 AFNetworking 依赖。 第三步:目的是为了研究 App -> NetworkManager 动态库 -> AFNetworking 动态库的情况,所以 App 可以用测试工程代替,测试工程就是一个可执行文件,类似一个 App。 发现编译通过,运行报错。 为什么?原因 因为 运行报错是因为 dyld 找不到 AFNetworking 动态库所在的位置。 使用的 AFNetworking 动态库,所以 遵循原则是谁链接库,谁就提供库所需要的 rpath 信息。所以 dyld 在运行起来后,会根据 如何解决? 方式一:low 一点,直接给测试工程也 方式二:不是找不到 AFNetworking 吗?因为 xcconfig 提供的路径找不到,那直接给 方式三:观察标准做法,比如一个 App 使用了 AFNetworking,这个 case Cocoapods 是怎么处理的? 是通过 shell 脚本来处理的。该脚本肯定是 Cocoapods 生成的。属于工程化范畴。核心代码如下 具体怎么做呢?编写 shell 脚本,编译 Person 动态库、AFNetworking 动态库,然后将产物复制到 Frameworks 文件夹下。 Tips 来个有趣的操作:NetworkManager.Framework 如何使用主工程的功能?也就是反向依赖 功能的本质就是符号,dyld 在链接的时候,会把所有的导出符号放到一起,只要运行的时候找到所有的符号,就可以动态链接。 那怎么做呢? 第一步:在 App(我们的实验中就是测试工程)创建 NetworkObject 类(OC 类默认就是全局符号,反向依赖后,可供外部使用) 第二步:在 framework 的 HEADER_SEARCH_PATHS 中增加 App 的头文件查找路径 第三步:framework 中增加实现代码,使用 App 中的符号 发现编译报错?符号找不到 所以,如何链接成功?如何处理这个未定义的符号? LD 链接器支持符号的处理。 方法一: 方法二: 模拟:App -> NetworkManager 动态库 -> AFNetworking 静态库。将上面的工程中,NetworkManager 中的 podfile 动态库依赖静态库(动态库作为宿主),则会把静态库中所有导出符号链接到动态库中。同时动态库的符号都被保留了(静态库被引用到的符号 + 动态库的符号,存放于动态库的导出符号表中)所以工程可以正常编译、链接。 如何在 App 中引入静态库 AFNetworking 中的符号?因为链接后,动态库中已经包含了静态库中的符号,所以只需要让 Xcode 编译通过即可。符号查找无需关心。 打开 NetworkManager 动态库的 Mach-O 查看下符号,可以看到动态库 NetworkManager 中已经包含了静态库 AFNetworking 的符号。 如何成功编译?告诉链接器 HEADER_SEARCH_PATH 信息即可。 思考:动态库链接静态库后,静态库中暴露的导出符号,在动态库中也是导出符号。假设动态库不想把静态库的符号暴露出来,该怎么做? LD 链接器提供了能力,将静态库的符号不暴露出来。指令为: -Xlinker -hidden-l"AFNetworking" 模拟:App -> NetworkManager 静态库 -> AFNetworking 静态库 之后编译报错 静态库链接的本质是:链接器只提取被直接引用的目标文件。此时 NetworkManager 静态库中只有自己的符号,没有所依赖的 AFNetworking 中的符号。 App 链接 NetworkManager 静态库,需要告诉链接器:头文件、库名称、库所在位置。 但 NetworkManager 静态库链接 AFNetworking 静态库,没有配置信息告诉 App。所以需要额外配置。App 直接依赖的静态库,静态库所依赖的静态库没有对 App 可见。 方法一:Build Settings -> 查找 Other Linker Flags,添加 方法二:直接给 App install 静态库。Cocoapods 处理这些依赖关系。 具体代码见: 模拟:App -> NetworkManager 静态库 -> AFNetworking 动态库。效果等价于:App + 动态库 编译报错,符号未定义 。 App 调用静态库,静态库中被 App 引用的符号最终会被链接到 App 里。所以问题演变为:App 如何使用动态库里面的符号? 上面代码运行,NetworkManager 静态库调用 AFNetworking 动态库的能力,就相当于 App 直接访问 AFNetworking 动态库的符号一样。 App 没有配置关于 AFNetworking 的链接三要素,所以找不到 AFNetworking。 修改后编译没问题,运行报错 怎么处理? 参考其他使用 AFNetworking 的项目,Cocoapods 工程化模版写好了脚本。 运行成功。同时查看测试过程的 Product 里 framework 目录下确实存在了 AFNetworking 具体代码见: 代码见: 在 iOS/macOS 开发领域的编译链接体系中,Module 是一项由 Clang/LLVM 提供的语义化头文件封装技术,其核心目标是通过强隔离的预编译单元解决传统 编译器实现: 特征: 一个 Module 是机器代码和数据的最小单位,可以独立于其他代码单元进行链接。 通常,Module 是通过编译单个源文件生成的目标文件。例如,当前的 但是,Module 在调用的时候会产生开销,比如我们在使用一个静态库的时候。 Module 生成的 模块元数据 编译环境指纹: 语义信息: 依赖关系图 编译器配置快照 平台强耦合性: 内容敏感性 说人话,平时什么情况下使用的最多?导入库文件的时候,就是 module 的主战场。 当在 App 里导入一个库的时候,发生了什么事情? 先谈谈导入方式,导入方式有2种: 假设没有开启 module 能力,当使用 当编译 module 是 clang 提供的能力。可以把头文件编译成二进制文件,缓存到系统目录中。这样的好处是,在使用某个 当开启 module 能力后,下面3种写法都会被转换为 moudle 的写法 假设存在一个 c.h 的文件,需求是想使用模块 A 和模块 B 中定义的内容。 方法一:直接 import 模块对应的头文件 方法二:因为模块 B 已经暴露了模块 A,所以导入 B 就可以使用模块 A 中定义的类和函数 AFNetworking 的 Framework/module.modulemap 看一个例子,AsyncDisplayKit 中 使用:子模块可以通过大模块访问到,比如 注意: umbrella 有2种用法: 更多关于 Module 的信息,可以查看 Clang::Modules 第一步:创建动态库 第二步:给动态库添加 第三步:给主工程、动态库设置在同一个 Xcode 项目中编译调试。 结论: 写法1 写法2 写法3 写法3是将需要暴露的头文件,挨个显示声明子模块,指明头文件,然后导出。 背景:探索 Swift Framework 中,没有桥接文件,Swift 如何访问 OC?如何处理 modulemap 导出文件? 问题1:Swift Framework 中 Swift 里如何访问 OC? 利用 modulemap 解决。 但是,上面的做法有副作用。虽然 Framework 内部 Swift 可以访问 OC 了,但是使用 Framework 的地方,也可以访问到 OCStudent 类。如何解决该问题? 如何实现只在 Swift Framework 内 Swift 代码可以访问 OC 类,在使用 framework 的地方,访问不到 oc 类? module 提供 private module 的能力。 文件内容是不希望通过正常预设模块暴露出去的子模块。 什么是正常预设模块?比如 具体代码见: Xcode 9 之后,Swift 开始支持静态库。 Swift 没有头文件的概念,Swift 要用 public 修饰的类和函数怎么办? Swift 库引入了一个全新的文件 第一步:准备2个 Swift 静态库。其中 FLSwiftWorker 2个类完全一样,FLSwiftA、FLSwiftB 同名方法,方法实现不一样。 编写脚本: 第二步,打算用 libtool 指令 因为存在同名符号,但是又不存在2级命名空间,所以如何处理符号问题?? 需要找 Headers 头文件信息和 Modules 文件夹里面的信息。 第三步,新建一个 iOS App 工程,引入合并后的静态库 libSwiftC.a 然后一步步解决符号冲突问题 编辑 xcconfig 配置头文件信息,不然编译会报错(引入了头文件)。 为什么 App 工程中的 OC 类中导入头文件编译成功,但还是无法访问里面的类? 同样,需要一个新的参数,告诉 LD modulemap 的信息,然后系统根据 module.modulemap 去关联查找 这个时候就可以在 App oc 代码中访问静态库里 Swift 类了。 为什么 App 工程中的 Swift 类中导入头文件报错? 因为上面的配置是 Swift 编译后产生的 modulemap 和 swiftmodule 是配置 而 Swift 编译器是 swiftc,需要额外配置。 但是配置后还是报错,因为看上去文件路径是对的,Framework 生成的时候,就是这么划分文件夹的。但实际编译的时候 Headers和 Swift 静态库编译后确实会生成两个核心目录: 配置 Swift Module 信息: Module 预编译生成的 缓存管理复杂度上升 需维护全局 引入头文件的方式: 思考:平时 Xcode 写代码的时候 本地目录优先搜索: 递归子目录搜索 Header Search Paths 遍历 如果本地目录中没有找到文件,编译器会根据 Xcode 项目的设置中的 "Header Search Paths" 来确定接下来搜索的目录。这些路径通常包括: 路径类型: Framework Search Paths 遍历 如果 Header Maps(hmap) 加速 为了提高查找效率,项目可能使用 Header Maps。这些是预先生成的文件,列出了目录中所有头文件的索引,帮助编译器快速定位。 编译器缓存优化 编译器可能会缓存头文件的查找结果,以避免在后续编译中重复搜索 文件未找到处理 如果编译器在所有指定的搜索路径中都没有找到 最早期是 include,再到后来的 import,区别是什么? 这段缓存相关逻辑,可以在 LLVM:HeaderSearch.cpp 中查看 import 举个例子吧。在 在找到上面的内容后,编译器将其复制粘贴到 编译器发现存在3个 import,则继续查找其内容 编译器会把其内容复制到 这样的步骤,直到整个文件(Perosn.m)中的所有 import 被替换掉。同时 存在2个问题:健壮性、拓展性。 我们大多数情况下,经常需要引入一些头文件来助力实现某些功能,假设某个类只有一个方法,方法本身10行。但是因为导入了某些头文件,最后这个文件按照上述 import 的查找替换过程,最后该文件可能存在10万行代码(import 了 Foundation、UIKit、MapKit 等库)。太浪费、太不合理了 为了优化前面提到的问题,一种折中的技术方案诞生了,它就是 日常开发中,我们经常可以看到某些组件的头文件会频繁的出现,例如 UIKit,而这很容易让人联想到一个优化点,我们是不是可以通过某种手段,避免重复编译相同的内容呢? 而这就是 PCH 为预编译流程带来的改进点。大体原理就是,在我们编译任意 虽然这种技术有一定的优势,但实际应用起来,还存在不少的问题。 首先,它的维护是有一定的成本的: 为了解决上面的问题,Clang Module 技术诞生了。 一个 Module 是机器代码和数据的最小单位,可以独立于其他代码单元进行链接。 通常,Module 是通过编译单个源文件生成的目标文件。例如,当前的 在实际编译之时,编译器会创建一个全新的目录,用它来存放已经编译过的 Module 产物。如果在编译的文件中引用到某个 Module 的话,系统将优先在这个列表内查找是否存在对应的中间产物,如果能找到,则说明该文件已经被编译过,则直接使用该中间产物,如果没找到,则把引用到的头文件进行编译,并将产物添加到相应的空间中以备重复使用。 相关的一些使用,可以查看上面的 section。 还是存在问题,因此诞生了 Use Header Maps 技术 但存在一个问题,如果同时存在多个 基于此,诞生了 Header Maps 技术,通过 hmap 文件,让 Xcode 在查找头文件的时候更快速。 编译器 -I 参数可以跟 Header Search Paths 也可以跟 hmap 文件路径。 QA:Xcode 自己会生成 hmap 文件,为什么我们还需要自己生成? 创建一个 iOS App,默认模版的基础上添加一个 Person 类,一个继承自 Person 的 Student 类。然后编译 从 Xcode Compile log 可以看到通过 iOS hmap 文件会根据文件分类和使用方式主要与项目结构和编译需求有关。可以分为项目级别 Header Maps、组件级别 Header Maps、公共与私有 Header Maps等等。 可以看到 hmap 结构类似 Mach-O ,顶部 HMapHeader 告诉系统,当前有几个 Bucket,下面是 Bucket 信息。然后最下方是 string 区域。 在 HMapBucket 中: 不就是1个文件依赖4个头文件吗?为什么设计成 hMap 这么复杂的结构?要表示路径的话,可以有很多种方式,比如最基础的 Map。hMap 有啥优点? 内存占用对比(10,000 头文件场景) 查找性能对比(单次查找) 假设 100 个头文件在相同目录: 有事:空间节省,目录路径节省 99% 存储。类似 Trie 树。 其结构、工作原理都类似 Mach-O 文件。读取思路为: 参考 Mach-O 文件结构和 LLVM 源码中对于 HMap 的处理 LLVM:HeaderMap.cpp 的实现,编写一个读取 hmap 文件的代码。 具体可以运行的代码可以在 github 这个 Repo 中查看并运行 BlogDemos:HMapDump 运行调试的时候,把需要读取的 HMapFile 拷贝到项目根目录。然后 Edit Scheme - Run - Arguments Passed On Launch 这里我把 github Textture 编译日志中的 hmap 拖到项目的根目录下了 QA:如何做到该工具做到像系统自带的 效果如下: 如果2个 clang 可以使用 iOS 导入方式: Tips:Xcode 设置 Search Path 有三处 Xcode 会主动生成 有必要。下面来个例子进行说明。虽然平时开发中经常以二进制组件的方式构建 App,但在某些场景下(精准测试、覆盖率统计等),打包构建还是需要以全源码编译的方式进行。而且在实际开发过程中,大多是以源码的方式进行开发,所以我们将实验对象设置为基于全源码编译的流程。 创建一个静态库 然后查看编译日志中的 分析:在 App 使用 Static Library 的情况下,假设开启了 组件、库使用 所以可以回答上面的问题了。虽然一个静态库、动态库项目中, 一个大型项目有300个 Pod,每个 Pod 有100个头文件,也就是共 30000个头文件,在30000个头文件中,如果没有享受 既然知道某些情况下会存在 hmap 文件没有命中的情况,那有必要修改吗?让静态库的情况,也可以使用 hmap 带来的编译红利。 LLVM 真是好东西,把 Xcode 编译、链接等一些幕后的事情变成白盒,有迹可循,感兴趣的可以去查看源码并带着一些关键词来搜索源码进行查看,可能会发现一些平时了解不到的细节。 可以查看 HeaderMapTest.cpp 和 HeaderMapTestUtils.h 编写 hmap 的生成代码。编写后的具体代码可以查看 BlogDemos:HMapWritor 为了方便平时使用,按照上面的方式,也将二进制放到 本节讨论:如何为静态库自定义 hmap 开启编译加速? 为了方便观察,开启2组对照实验。 整体目录结构和设置如下: 注意:因为我的 Demo 都是在一个 Xcode 工程,不同的 TARGETS,所以即使打包好的静态库产物,复制到 App 工程 StaticLib 文件夹下,系统在编译的时候还是会以源码的形式编译。 解决办法: 问题修复完,最终编译链接效果如下: 整体目录结构和设置如下: 注意:因为我的 Demo 都是在一个 Xcode 工程,不同的 TARGETS,所以即使打包好的静态库产物,复制到 App 工程 StaticLib 文件夹下,系统在编译的时候还是会以源码的形式编译。 解决办法:同一个 Project,不同的 Targets,选中当前的 Target,Edit Scheme,在 Build 下取消勾选 Find Implicit Depedencies 最终编译链接效果如下: 源码查看: 上面是理论上分析并思考了2个问题: 完整流程如下: Cocoapods 提供了很多钩子,可以自定义编写 Ruby 脚本。 完整步骤: UIKit、CoreGraphics 等。从 iOS13 开始,为了提高性能,绝大部分的系统动态库文件都被打包存放到了一个缓存文件中(dyld shared cache)中。 所有指令集原则是向下兼容的。动态库共享缓存一个非常明显的好处是节省内存。 具体底层源码可以查看,dyld 源码中 某个 App 被第一次打开时候, dyld 根据动态库的依赖,循环加载动态库到动态库共享缓存中(内存),后续其他 App 第一次打开发现使用了动态库A已经在动态库共享缓存中存在,则不需要加载。这一步调用方法为 早期:应用程序的可执行文件是存储在磁盘上的,启动应用,则需要将可执行文件加载进内存,早期计算机是没有虚拟内存的,一旦加载就会全部加载到内存中,并且进程都是按照顺序排列的,这样存在两个问题: 基于上述2个问题,诞生了虚拟内存技术。App 进程通过内存管理单元(Memory Management Unit, MMU)来管理的。MMU 使用一张特殊的表来跟踪虚拟内存地址和物理内存地址之间的映射关系。这张表通常被称为页表(Page Table) 内存分页: 当物理内存满的时候,会发生覆盖。用户使用的活跃的数据,覆盖内存中最不活跃的数据那一页。对应现实的表现就是:iPhone 上永远可以较好的打开一个 App,比如打开了 App1、App2、App3...,等打开使用了一阵子后,内存紧张,我们尝试切换打开最早的 App1,会发现 App1 重新启动了,之前用的功能 A 的页面1,已经不见了,经历一个新的启动流程。 但虚拟内存方案带来一个问题。比如黑客不断探索发现,某个重要的功能位于第3页,是不是完全可以通过固定的地址去访问?? 因为早期物理内存方案下,App 启动后位于什么地址是不确定的。有了虚拟内存后,App 内符号的地址都是从0到4G,都是相对地址。 为了解决该问题,Apple 又推出了 ASLR 技术。核心就是为了解决虚拟内存地址固定不变的问题。就不能通过固定的地址访问内存或者数据了。 可执行文件 Header 的内存地址,就是 也可以使用 利用 MachOView 查看如下: _PAGEZERO _TEXT _DATA_CONST: _DATA _LINKEDIT File Offset:在 Mach-O 文件中的位置 File Size:在 Mach-O 文件中的占据的大小 从下面的图可以看出, 我们会发现根据 Mach-O 文件中的信息,File Size、File Offset、VM Address、VM Size 可以判断出 Address Space Layout Randomization,地址空间布局随机化。是一种针对缓冲区溢出的安全保护技术,通过堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码的位置,达到阻止溢出攻击目的的一种技术。在 iOS 4.3 引入。 在有 ASLR 的时候:__TEXT 代码段地址 = __ 在 Mach-O 文件中的地址是原始地址 dyld 是一个动态链接程序。是 iOS 和 macOS 系统中的动态链接器(Dynamic Loader)。它负责在程序运行时加载和链接动态库。简单来说,dyld 就如同一个「中间人」,将你的程序代码和它所依赖的所有动态库整合在一起,最终让你的程序能够正常运行。 懒加载的符号表项通常包括一个或多个指向符号的间接符号(Indirect Symbols),这些间接符号在首次访问时会被动态链接器替换为实际的内存地址。这个过程称为符号绑定(Symbol Binding)。由于懒加载的符号在首次使用前不会被绑定,因此可以减少应用程序的启动时间,并按需加载所需的资源 dyld 的初始化:在系统内核完成初始化后,从 Mach-O 的 内存映射:dyld 将匹配 Mach-O 文件到内存空间,确保每个依赖库都被映射到正确的地址 解析 Mach Header,判断当前 Mach-O 文件是否可用 比如上面的 Mach header 中 根据 Mach Header,解析 load commands,根据解析的结果,将程序各个部分加载程序到指定的地址空间,同时设置保护标志 执行自身初始化配置加载环境。 加载当前程序链接的所有动态库到指定的内存中。 通过下面指令便可以获取到 Load Command 为 LC_LOAD_DYLIB 的记录,也就是可以看到所需要的全部动态库 搜索所有动态库,绑定需要在调用程序之前用的符号(非懒加载符号)。 使用下面指令便可以查看 LC_DYSYMTAB 相关信息 定位了间接符号表(用于方法调用和动态绑定) 如何定位间接符号表? 结构类似于: 提供了重定位信息(地址修正数据) 包含模块表(动态链接遗留信息) 在 indirect symbol table 中,将需要绑定的导入符号真实地址替换。 工作流程: 动态库、静态库在使用类似 NSLog 等系统动态库符号的时候,由于系统动态库是是共享缓存技术,所以编译阶段没办法确定这些系统符号的地址。源码调用 NSLog,编译器会生成 这时候间接符号表就登场了,存储了在符号表中的索引,符号的真实地址存储在 链接阶段同样无法确认系统符号地址。 所以总的来说,编译链接后,App 启动的时候,第一次调用了 NSLog,汇编层面会发生类似调用 这也是 Fishhook 可以正常 work 的基础。 dyld 调用程序入口函数,开始执行程序。 实验一: 实验二: 可以看到创建的 然后利用 gcc 指令 当然除了 gcc,很多嵌入式平台,可以在代码中指定 c 程序的起点。 比如 STM32,专门有个汇编文件,用于系统的初始化。 总结一下: iOS 侧,dyld 默认以 main 函数作为函数起点。 QA:有没有这样一个疑问:dyld 的工作流程有一部分是:加载所有手动插入的动态库、链接程序需要的动态库、链接插入的库、应用插入的函数。为什么 dyld 的工作流程为什么和链接器的有些重合?链接动态库为什么是 dyld 在做的事情 链接器(Linker)和动态链接器(Dynamic Linker)在程序构建和运行过程中的不同角色,它们的工作看似有重合,实则处于不同阶段且分工明确 时间点不同: 地址确定性: 工作内容本质差异: dyld 完整流程 由于 ASLR 的存在 静态链接器: 动态链接器: 窥探系统库底层实现的时候可能需要从动态库共享缓存中提取出某个 Framework,比如 UIKit。这时候要么用第三方工具,要么用 dyld 的能力。 查看 然后用 clang++ 编译,命令为 将编译后的产物复制到动态库共享缓存目录下去。然后执行命令 dyld 本身就是一种 MachO 文件,MachO 文件类型为7,是一种包含多种架构的结构,如下图: 可以加载以下类型的 Mach-O 文件:MH_EXECUTE、MH_DYLIB、MH_BUNDLE 工作原理:使用 动态库的加载过程通常是在程序启动时进行的,因此 第一步:创建 第二步:创建 iOS App 测试工程,接入 Inject 动态库。将 第三步:Edit scheme -> Run -> Environment Variables。Name 为 第四步:运行输出如下 不只是这一种方式可以插入动态库,图上是 GUI 的方式,给 Xcode Enviornment Variables 中加入 工作原理: 在 iOS 中, 第一步:创建 第二步:创建 iOS App 测试工程,接入 InjectFunction 动态库。将 第三步:Edit scheme -> Run -> Environment Variables。Name 为 第四步:运行输出。发现 NSLog 确实被 hook 做了替换。 应用场景:不能上架,但可以去做探索、验证源码等场景。 第一种是替换系统的 dyld,风险大,需要感知源码。需要准备带调试信息的 第二种方式是通过 lldb 调试 dyld。dyld 提供了一些环境变量,用于控制 dyld 在运行过程中输出感兴趣的信息。 lldb 保留了一个库列表,避免在按名称设置断点时出现问题,而 dyld 与 libdyld.dyllib 就在该列表上。 有2种方式可以强制在 dyld 上设置断点: br set -n dyldbootstrap::start -s dyld set target.breakoints-use-platform-avoid-list 0:禁用 LLDB 的系统断点保护机制 设置为 推荐使用 dyld 提供的环境变量来控制 dyld 在运行过程中输出感兴趣的信息。格式为:DYLD_PRINT_APIS=1 ${MachOPath} 怎么用? 另一种方式是利用 Xcode -> Edit Scheme,增加或修改 dyld 环境变量 Mach Object 的缩写,是 iOS/MacOS 上用于存储程序、库的标准格式。 Mach-O 格式⽤来替代 BSD 系统的 在 XNU 源码中可以查看 Mach-O 的定义。 属于 Mach-O 格式的文件类型有: MH_OBJECT MH_DYLIB:动态库文件 Xcode 中也可以查看 Mach-O 文件类型 源代码(比如c文件),编译变为目标文件(比如.o文件),再经过链接变为可执行文件。 Tips: 通用二进制文件,可以同时适用于多种架构的二进制文件,包含多种不同架构的独立的二进制文件。 因为通用二进制文件比原来的大,所以被成为“胖二进制文件”(Fat Binary),使用 Hopper 打开会显示 “FAT archive” 信息查看: Xcode 中可以修改指令集。由 Build Settings 的2个配置决定: 一个 Mach-O 文件包含3块 可以用 MachOView: 和系统自带的 atool 查看 Mach-O 信息 比如我用 otool 查看我编写的一个 SwiftUIDemo 所依赖的共享缓存库 用 MachOView 查看 DDD Mach-O 文件 可以看到在 Mach-O 文件上, 在没有 ASLR 的时候:__TEXT 代码段地址 = __PAGEZEROR 地址 第一步,编写一个名为 第二步,将其转换为可执行文件,指令为 第三步,分析查看可执行文件 第四步,将其转为目标文件,指令为 第五步,查看目标文件指令信息 分析下指令信息: 可以看到源码中从上到下声明的函数,在目标文件和可执行文件中顺序是一致的 test1、test2 2个函数都存在,地址不一样,为什么都是 以 test1 为例。 第一步:根据 B2 计算得到原码,然后计算出原码。 iOS 是小端序,从右向左看, 补码如何到原码?所有的1取反,然后加1。第一步计算结果为 第二步:根据近地址相对位移技术, 完整的 第一步,先用指令查看全局变量的地址值。 第二步,手动计算。根据近地址相对位移技术原理, 去掉最开始的 可见 至此,可以了解到 Mach-O 文件是按照不同 Section 去加载数据,访问数据的。但不同 Section 是语义上更方便理解的,程序执行最本质的是:不管处于什么 section,只看偏移量。 DWARF 是被众多编译器和调试器使用的,用于支持源码级别调试的调试文件格式。 第一步,新建 第二步:Xcode 每次编译运行都会生成新的 第三步:查看 Mach-O 中,包含 可以看到:编译阶段,编译器会把调试信息放到一个单独的段中,该段名为 我们知道:链接阶段,Strip 会把调试信息从 .o 文件中剥离出来,单独放在符号表中 继续向下窥探验证 第四步:编译成可执行文件,指令为 第五步:查看可执行文件中是否包含 第六步:查看可执行文件的符号表。指令为: 可以看到:链接完成后,所有的调试符号、使用的符号,都放在符号表里了。 看到了调试符号和 DWARF 信息,那如何生成 .dSYM 文件?因为 DWARF 是一种调试格式,所以知道调试符号,只要按照 DWARF 格式组织,写入一个文件就可以了。 结论: 编译器会在编译阶段,把调试信息放在单独的 打包上线的时候会把调试符号等裁剪掉,但是线上统计到的堆栈我们仍然要能够知道对应的源代码,这时候就需要把符号写到另外一个单独的文件里,这个文件就是DSYM。 调试信息位置:直接嵌入在 文件结构: 调试过程:LLDB 直接从可执行文件中读取调试信息 优点:调试速度快(无需加载额外文件) 缺点:增加30%~50%的 App 体积,源码信息泄漏 文件结构: 调试过程: 在崩溃日志中 第一步:新建 iOS 项目,Buidl Setting 切换为 第二步:Mac 自带 思考:假设线上 crash 了,如何根据 crash 堆栈中没有符号化的地址,找到符号的真实地址? 分析: 问题1: 问题2:.dSYM 文件保存的是虚拟地址还是偏移后的地址? 处于安全原因,Apple 设计了 ASLR 机制。是 App 启动,经由 dyld 后对加载的动态库做了地址随机偏移,而 .dSYM 文件是在编译、链接后生成的,所以肯定保存的是虚拟地址。 问题3:怎么计算原始地址? 所以为了还原原始地址,也就是符号偏移量, 需要经过计算:RuntimeAddress = ImageBaseAddress + SymbolOffset + PCOffset => SymbolOffset = RuntimeAddress - ImageBaseAddress - PCOffset 如上 Crash 数据: Crash 发生的的符号所在动态库为:ExploreDSYM。Crash 日志下面的 Binary Images 列出来所依赖动态库信息: 所以 ImageBaseAddress = 0x103b83000 3种验证方式: 在终端切目录到 App 内用:nm -a ExploreDSYM | grep 1ee0 验证,发现找到该符号的信息 在终端切目录到 App 内用:objdump -d --start-address=0x100001ee0 --stop-address=0x100001f17 ExploreDSYM.app/ExploreDSYM 验证,发现找到该符号的信息 指令格式为: 核心公式:RuntimeAddress = ImageBaseAddress + SymbolOffset + PCOffset 拿到真实地址,然后利用 dwarfdump --lookup 0x0000000100001cb0 ./ExploreDSYM.app.dSYM 找到对应的信息,可以看到 可以确认:由于 iOS ASLR 的存在,符号真正的地址,是基于 image 的开始地址进行偏移得到的。 RuntimeAddress = ImageBaseAddress + SymbolOffset + PCOffset,类似一个 注意:PCOffset 理论上一直存在,但表示上可能缺失: 核心结论:在绝大多数情况下,链接静态库对 iOS App 包体积影响更小(即产生的 App 更小) 原因分析: 静态库的链接机制与优化: 动态库的打包机制: 符号表与 Stripping: 剥离后: 系统动态库的例外: 它存储的是索引,不是地址! a) 启动时绑定 (Non-Lazy / Load-Time Binding): 绑定对象: 过程: 根据 b) 延迟绑定 (Lazy Binding / Run-Time Binding): 机制 (基于 第一次调用发生: 桩代码执行: 后续调用: 优点: 不会。每个 Mach-O 模块(App 或动态库)拥有独立的 GOT: 主程序 (App) 动态库 (如 lib.dylib) 数据符号绑定: 符号解析: 指针更新: 因此,对于 iOS App 包体积优化: 所以,链接静态库对于体积影响更小。关键在于 这也解释了 Apple 的官方建议:iOS 开发优先使用静态库。 报错日志为: 根本在于: 实际上, 根本问题:链接顺序错误导致符号解析失败 系统库怎么可能会出错??虽然代码都是系统库,但最终都在 App 的链接配置的地方声明链接顺序的,链接顺序影响符号的发现能力。现在的工程都依赖 Cocoapods 构建。Cocoapods 会对依赖的库名称进行字典排序 字典序排序使 "CoreServices" 排在 "Foundation" 前。生成的 Cocospods::Xcodeproj/lib/xcodeproj /config.rb 源码也可证明该问题: 链接器的工作方式: 在 Xcode 工程的 Build Settings > Other Linker Flags 中: 确保生成确定性(Determinism) 简化内部处理逻辑 当排序与链接器的顺序敏感性冲突时,问题显现: 链接器的工作机制 典型案例 bash 此时链接器在 CocoaPods 选择排序,本质是权衡后的工程决策: CocoaPods 的排序不是“缺陷”,而是为确定性牺牲局部灵活性的典型设计。它解决了协作与维护的核心痛点,代价是将链接顺序的责任转移给开发者。在工程实践中,这种妥协是合理的——毕竟,可预测的构建环境比偶发的链接错误更可控。 正如软件开发中的许多决策:没有完美解,只有最适,ROI 也相对较高.xcconfig 配置细节,也对静态库、动态库的符号裁剪、编译链接底层流程知之甚少。不妨先思考几个工程进阶高频问题Library not loaded: 'libPerson.dylib' 错误?一、工程多环境配置
1. 多 Target 的方式
Build Settings 中的 Preprocessor Macros 中添加,可以区分不同的 Configuration(工程自带的是 Debug、Release。可以在 PROJECT 中添加新的 Configuration。假设在 Configuration 里再增加一个 Beta。那就可以在这里设置)。DEBUG 宏定义来区分是否是 Debug 环境。只有在 Beta 下 BETA 宏为1,其他为0。2. 多 Scheme 方式
LDExploreDemoBeta、LDExploreDemoRelease 2个 Scheme。LDExploreDemo,点击 Run 验证3. .xcconfig 方式
1. 基础使用
.xcconfig 文件。命名格式为:Pods-{ProjectName}.debug.xcconfig 和 Pods-{ProjectName}.release.xcconfig.xcconfig 的方式管理工程。命名格式为:Pods-{ProjectName}.${ConfigurationName}.xcconfig.xcconfig 可以给 PROJECT 设置,也可以给 TARGETS 设置。${ProjectName}Debug 、${ProjectName}Beta 、`${ProjectName}Release.xcconfig 文件,命名格式为:Pods-{ProjectName}.${ConfigurationName}.xcconfig。比如:Config-LDExploreDemo.Debug.xcconfig、Config-LDExploreDemo.Beta.xcconfig、Config-LDExploreDemo.Release.xcconfig。并编辑里面的内容2. xcconfig 编写规范
// 基本键值对
BUILD_SETTING_NAME = value
// 带引号的值(包含空格时必需)
OTHER_LDFLAGS = -ObjC -all_load
FRAMEWORK_SEARCH_PATHS = "$(inherited)" "$(PROJECT_DIR)/Frameworks"// 包含其他配置文件
#include "Base.xcconfig"
#include "../Configurations/SharedSettings.xcconfig"// 单行注释
/*
多行注释
用于详细说明
*/
// 设置分组标题
////////////////////////////
// MARK: - 路径设置
////////////////////////////// iOS 真机配置
LIBRARY_SEARCH_PATHS[sdk=iphoneos*] = $(inherited) "$(SRCROOT)/iOS/Libs"
// iOS 模拟器配置
LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*] = $(inherited) "$(SRCROOT)/Simulator/Libs"
// macOS 配置
OTHER_CFLAGS[sdk=macosx*] = -DMAC_ENV// ARM64 架构
GCC_PREPROCESSOR_DEFINITIONS[arch=arm64] = ARM_OPTIMIZED=1
// x86_64 架构
SWIFT_COMPILATION_MODE[arch=x86_64] = wholemodule// Debug 配置
OTHER_SWIFT_FLAGS[config=Debug] = -D DEBUG -enable-testing
// Release 配置
CODE_SIGN_IDENTITY[config=Release] = "Apple Distribution"// 仅对 iOS 模拟器的 Debug 配置生效
ENABLE_UI_TESTS[sdk=iphonesimulator*][config=Debug] = YES // 引用其他设置的值
NEW_SETTING = $(EXISTING_SETTING)/subpath
// 继承上级设置(重要!)
OTHER_LDFLAGS = $(inherited) -framework CryptoKitProject/
├── Configurations/
│ ├── Base.xcconfig
│ ├── Debug.xcconfig
│ ├── Release.xcconfig
│ ├── Development.xcconfig
│ └── Production.xcconfig// Base.xcconfig - 通用基础设置
SDKROOT = iphoneos
IPHONEOS_DEPLOYMENT_TARGET = 15.0
// Debug.xcconfig
#include "Base.xcconfig"
GCC_PREPROCESSOR_DEFINITIONS = DEBUG=1 COCOAPODS=1
SWIFT_OPTIMIZATION_LEVEL = -Onone
// Release.xcconfig
#include "Base.xcconfig"
SWIFT_OPTIMIZATION_LEVEL = -O
GCC_OPTIMIZATION_LEVEL = 3// 使用 PROJECT_DIR 作为根目录
PROJECT_DIR = $(SRCROOT)
// 框架搜索路径
FRAMEWORK_SEARCH_PATHS = $(inherited) \
"$(PROJECT_DIR)/ThirdParty" \
"$(PROJECT_DIR)/Frameworks"
// 头文件搜索路径
HEADER_SEARCH_PATHS = $(inherited) \
"$(PROJECT_DIR)/Includes" \
"$(PROJECT_DIR)/Generated"// 通用设置
COMMON_SETTINGS = -DCOMMON_FEATURE
// iOS 特定
BUILD_SETTING[sdk=iphoneos*] = $(COMMON_SETTINGS) -DIOS
BUILD_SETTING[sdk=iphonesimulator*] = $(COMMON_SETTINGS) -DIOS_SIMULATOR
// macOS 特定
BUILD_SETTING[sdk=macosx*] = $(COMMON_SETTINGS) -DMACOS// 根据配置包含不同文件
#if $(CONFIGURATION) == Debug
#include "DebugOverrides.xcconfig"
#elif $(CONFIGURATION) == Release
#include "ReleaseOverrides.xcconfig"
#endif# 检查语法
plutil -lint Config.xcconfig
# 查看最终设置
xcodebuild -project YourProject.xcodeproj -showBuildSettings3. 编译链接日志输出重定向
tty 敲回车,然后在 Xcode 脚本中,就可以将 echo 输出的结果重定向到终端 echo "message" > /dev/ttys000.xcconfig 文件,需要设置3个参数:4. LLVM Strip 调试
二、Xcode 配置层级机制
.xcconfig 文件设置 OTHER_LDFLAGS = -framework "AFNetworking" 后,该值会出现在项目的 Build Settings 界面中,这是由 Xcode 配置系统的设计机制决定的1. Xcode 配置层级机制
.xcconfig 配置是比较通用的,比较基础的.xcconfig.xcconfig 导入的值$(inherited))OTHER_LDFLAGS:配置位置 设置的值 Project 层 -framework "CoreData"Target 层 -framework "AFNetworking".xcconfig 文件 -framework "OpenSSL"OTHER_LDFLAGS = -framework "AFNetworking"2. 验证 Xcode 的配置层级系统
.xcconfig 文件Base.xcconfig:只链接一个 UI 基础库 WantUIKitDev.xcconfig:引入 Base.xcconfig 文件,同时在此基础上,引入:SDWebImage、AFNetworking、PrismClient 3个库Dev.xcconfig三、符号的导入与导出
1. 从源文件到可执行文件做了什么?
1. 编译阶段(Compilation) - 将源代码转化为机器码的中间形态
int main() {
return add(2, 3);
}
->
FunctionDecl: main
└─ CompoundStmt
└─ ReturnStmt
└─ CallExpr: add
├─ IntegerLiteral: 2
└─ IntegerLiteral: 3add 函数存在且参数匹配); add 函数的 IR 表示
define i32 @add(i32 %a, i32 %b) {
%1 = add i32 %a, %b
ret i32 %1
}- 优化前:
%1 = mul i32 %a, 1 ; a*1
%2 = add i32 %1, 0 ; +0
+ 优化后:
%result = add i32 %a, %b.s 文件,输出 .o 文件2. 链接阶段(Linking)- 构建完整可执行文件
```c++
// main.c
extern int add(int, int); // 外部符号声明
int main() {
return add(2, 3);
}
```
符号解析过程:
```mermaid
graph LR
A[main.o] -- 寻找 add 符号 --> B[math.o]
B -- 返回符号地址 --> A
```
重定位操作:
```assembly
- 链接前: call 0x00000000 ; 未知地址
+ 链接后: call 0x1000F2A0 ; 解析后的add函数地址
```
文件类型 处理方式 示例 目标文件(.o) 直接合并 main.o + utils.o 静态库(.a) 按需提取 libMath.a 中的 add.o Swift 模块 消除重复符号 __TEXT.__swift5_types 0x1000: _main (来自 main.o)
0x2000: _add (来自 math.o)__TEXT.__stubs section(PLT 桩代码); NSLog 的桩代码
_NSLog_stub:
jmp qword ptr [rip + _NSLog_ptr] ; 跳转到GOT中的指针
nop__DATA._la_symbol_ptr section(GOT 懒符号指针)_NSLog_ptr:
.quad _dyld_stub_binder ; 初始指向绑定器__DATA._nl_symbol_ptr section(非懒加载指针); 启动时立即绑定的符号
_malloc_ptr: .quad _malloc_realNSLog(@"hello world"); 编译后调用的是 _NSLog_stubjmp qword ptr [rip + _NSLog_ptr]_NSLog_ptr 指向 dyld_stub_binder_NSLog基地址: 0x7FFFE4500000
偏移: 0x0000000000004a30
实际地址: 0x7FFFE4504a30; 更新GOT指针
mov [rip + _NSLog_ptr], rax ; rax=0x7FFFE4504a30
; 跳转到真实实现
jmp raxMach-O Header
┌───────────────────────┐
│ Load Commands │ → 描述段信息
├───────────────────────┤
│ __TEXT (代码段) │ → 只读机器码
│ __text: 主程序代码 │
│ __stubs: PLT存根 │
├───────────────────────┤
│ __DATA (数据段) │ → 可读写数据
│ __data: 全局变量 │
│ __la_symbol_ptr: │ → 延迟绑定指针
├───────────────────────┤
│ Dynamic Loader Info │ → dyld 所需信息
└───────────────────────┘阶段 核心任务 关键技术 输出 编译 代码转换 AST 生成 IR 优化 指令选择 .o 目标文件链接 模块集成 符号解析 地址绑定 重定位 Mach-O 文件 #include <Foundation/Foundation.h>
int main() {
NSLog(@"Hello world");
return 0;
}main.m。编译器在编译 main.m 时遇到 NSLog,根本不知道这个 NSLog 符号定义在哪里,这不是编译器该关心的事情。因此,编译器只能看到局部,只聚焦关心一个当前的源文件。到底谁来关心这个 NSLog 符号定义在哪呢?这就是链接器。undefined reference to ***clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-c main.m -o main.oobjdump -r main.o 查看目标文件中的内容objdump -r main.oclang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
main.o -o main2. 符号
1. 符号的种类
Symbol Type 说明 U undefined(未定义) A absolute(绝对符号) T text section symbol(__TEXT.__text) D data section symbol(__DATA.__data) B bss section symbol(__DATA.__bss) C common symbol(只能出现在 MH_OBJECT 类型的Mach-O⽂件中)- debugger symbol table S 除了上⾯所述的,存放在其他 section的内容,例如未初始化的全局变量存放在(__DATA,__common)中I indirect symbol(符号信息相同,代表同⼀符号) u 动态共享库中的⼩写u表示⼀个未定义引⽤对同⼀库中另⼀个模块中私有外部符号 main.m 到 main.oclang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-c main.m -o main.onm -pa .o文件路径 命令来查看符号2. 符号的大分类
1. 全局符号(Global Symbols)
duplicate symbol 错误(除非使用弱符号)。// C/C++/Objective-C 中未加 static 的函数/全局变量
int globalVar = 10;
void publicFunction() { ... }N_EXT(外部符号)。int global_age; 初始化为 int global_age = 0;Common Symbol
int x;)暂存为 Common Symbol,链接时再决定其最终形态:.bss 段)阶段 Common Symbol 全局符号 (Global Symbol) 编译期 ✅ 未初始化的全局变量被标记为 Common Symbol ❌ 此时未形成强定义 链接期 ❌ 被转化或合并 ✅ 最终成为强定义的全局符号 int global_int_age = 28;
int global_int_age;
void main() {
print("Hello world");
}2. 本地符号(Local Symbols / Static Symbols)
static int localVar = 5; // 静态全局变量
static void privateFunc() { ... } // 静态函数N_EXT 标志3. 未定义符号(Undefined Symbol)
extern int externalVar; // 声明外部变量
void undefinedFunc(); // 声明未实现的函数4. Weak Symbol
__attribute__((weak_import)) 是 Clang/GCC 编译器的一个属性,用于在 iOS/macOS 开发中实现弱链接(Weak Linking)。它的核心作用是:允许代码引用高版本 SDK 中的符号(函数/变量/类),同时在低版本系统中运行时安全地处理这些符号的缺失,避免崩溃NULL。if (&NewFunction != NULL) { // 检查弱导入函数指针
NewFunction(); // 安全调用
}
if ([NewClass class] != nil) { // 检查弱导入类
// 安全使用 NewClass
}weak_import 时:低版本系统因找不到符号会直接崩溃NULL,由开发者处理3. 脱符号
4. 符号可见性
-O1、O2、O3、Os、Oz四、编译阶段做了什么
1. 预处理
.m, .mm, .c, .cpp, .swift 等)。#define 定义的宏。#import 或 #include 指令替换为对应头文件的内容(递归处理嵌套包含)。#if, #ifdef, #ifndef, #else, #elif, #endif 等指令决定哪些代码块被包含或排除(常用于区分 Debug/Release 版本、不同平台、功能开关)。#pragma 等特殊指令。2. 词法分析
int result = a + b; 分解成 int (关键字), result (标识符), = (运算符), a (标识符), + (运算符), b (标识符), ; (符号)。3. 语法分析
4. 语义分析
int + float 是允许的,int + NSString* 是不允许的)。const int size = 10 * 20;)。5. 生成中间代码
.ll 文本文件或 .bc 二进制文件中)。这是优化发生的主要阶段6. IR 优化
@inlinable 函数)。retain/release)、Swift 的泛型特化等。-O0/-Onone - 无优化, -O1 - 基础优化, -O2/-O - 常用优化, -O3 - 激进优化, -Os/-Osize - 优化大小)控制优化的级别和类型7. 生成目标汇编代码
.s 文件)。8. 生成 .o 目标文件
.s)。.o 文件)。.o 文件)。9. 链接
.o)。.a 文件,本质是 .o 文件的集合)。UIKit.framework, Foundation.framework, libSystem.dylib 等)的导入信息(头文件声明和链接库 stub)。ld64)。__TEXT, 数据段 __DATA)以及符号分配最终的内存地址(虚拟地址)。此时地址是相对的或基于段的起始地址。.text, .data, .rodata, .bss)合并到输出文件的对应段中。.o 文件,避免包含未使用的代码。UIKit)及其版本信息(LC_LOAD_DYLIB 加载命令),并设置符号绑定信息(延迟绑定通过 __DATA,__la_symbol_ptr),但不将动态库代码复制到最终可执行文件中。动态库在运行时由 dyld 加载。start 函数,最终调用 main 或 UIApplicationMain)。YourApp)和/或 动态库(.dylib)、Bundle(.bundle, .framework 内部)man nm:: 光标,如果想查看当前 nm 命令的参数,可以快速查找,输入 / + 具体参数,敲回车即可跳转到要匹配到的位置,如果有多个结果,且当前自动跳转到的不是正确的位置,vim 模式下可以输入 n 跳转到下一个匹配到的位置(n 即 next),输入 N 则跳转到上一个匹配到的位置。-p,则输入 /-p,敲回车的效果如下符号的导入导出及 App 瘦身
OTHER_LDFLAGS=$(inherited) -Xinker -unexported_symbol -Xlinker _OBJC_CLASS_$_Person.o 文件的合集 + 重定位符号表。但重定位符号表中的符号不能 strip,所以只能 strip .o 文件的调试符号。.o 中的符号(包括可以重定位的符号),都放到 App 自身的符号表中,也就意味着可能是:本地符号、全局符号、导出符号。根据 Strip 的原理,Strip 可以脱离除了间接符号表之外的所有符号。五、静态库
.a 静态库,.dylib 动态库使用有问题,一般是:1. clang 指令
clang命令参数:
-x: 指定编译文件语言类型
-g: 生成调试信息
-c: 生成目标文件,只运行preprocess,compile,assemble,不链接
-o: 输出文件
-isysroot: 使用的SDK路径
1. -I<directory> 在指定目录寻找头文件 header search path
2. -L<dir> 指定库文件路径(.a\.dylib库文件) library search path
3. -l<library_name> 指定链接的库文件名称(.a\.dylib库文件)other link flags -lAFNetworking
-F<directory> 在指定目录寻找framework framework search path
-framework <framework_name> 指定链接的framework名称 other link flags -framework AFNetworking-I 参数# Xcode
Header Search Paths: /path/Headers
# 等价于 Clang
clang -I/path/Headers-I + 通配符:# Xcode
Header Search Paths: /path/libs/**
# 等价于 Clang
clang -I/path/libs/subdir1 -I/path/libs/subdir2 ...-isystem# Xcode
Header Search Paths: system /path/system_headers
# 等价于 Clang
clang -isystem /path/system_headers ...-framework 参数# Xcode
Framework Search Paths: /path/framework/Headers
# 等价于 Clang
clang -F /path/framework/Headers2. 静态库就是 .o 文件的合集
.o 文件的合集。Person.m 编译为 Person.oPerson.o 重命名为 Person.dylibobjdump --macho --private-header Person.dylib 查看静态库依旧是 Object Filemain.m 代码,导入静态库 <Person.h>#import <Foundation/Foundation.h>
#import <Person.h>
int main(int argc, char * argv[]) {
Person *p = [[Person alloc] init];
[p sayHi];
NSLog(@"%@", p);
}main.m 编译为 main.o 文件。注意,因为用到了 NSLog 和导入了静态库的头文件,所以需要加参数指定 NSLog 该符号从哪确定,也需要指定静态库所需的信息。clang -x objective-c \
-target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-I./StaticLibrary \
-c main.m -o main.omain.o 文件和前面编译好的 Person 静态库clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
> -L./StaticLibrary \
> -lPerson \
> main.o -o mainPerson 静态库需要重命名下,因为 clang 指令 -l 参数的 Person,其实就是去找 libPerson 的动态库或者静态库。lib+<library_name> 的动态库,找不到,再去找 lib+<library_name> 的静态库,还找不到,就报错.o 文件的集合,单个 main.o 文件,修改拓展名就可以变为静态库3. 静态库的合并
.o 的合集,那么2个或者2个以上的静态库是可以合并的。clang -x objective-c \
-target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-I./CatStaticLibray \
-c ./CatStaticLibrary/Cat.m -o Cat.olibtool -static \
-o libPersonCat \
libPerson libCat4. Auto-Link
import <framework> 我们在代码里使用的是 *.framework 这个目标文件时,就自动在目标文件的 Mach-O 中插入一个 Load Command 格式是 LC_LINKER_OPTION,存储这样一个链接器参数 -framework FrameworkName5. duplicate symbol
.a 文件)在链接阶段出现 duplicate symbol 错误,本质是链接器在合并多个目标文件时发现了重复的全局符号定义.o)的集合。当链接器将主工程和静态库的代码合并时,如果发现:duplicate symbol 错误all_load\-Objc 时才会存在6. 静态库冲突
1. 情景模拟
1. Demo1
libAFNetworking.a 以静态库的形式引入了 AFNetworking。
问题:为什么2个同样的静态库,没有链接失败?
分析:OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking" "/Users/unix_kernel/Desktop/编译链接/LDAndFramework/StaticLibConflictsSolution/StaticLibConflictsDemo/AFNetworking"`OTHER_LDFLAGS = $(inherited) -ObjC -l"AFNetworking" 中的 -ObjC 改为 -all_load 也不会存在链接问题。本质是链接器对于同名的库,找到第一个后,后面的就停止查找,不会被链接了,所以不存在符号重复相关的问题2. Demo2
libAFNetworking.a 静态库改名为 libAFNetworkingCopy.a,然后引入 Xcode 工程。Issues223 duplicate symbols for architecture x86_64
问题:为什么上次的链接成功,这次却链接失败了,报符号重复的错误?
分析:3. Demo3
Static LibraryAFNetworkingMock 文件。不过类里面还存在一个类 AFURLSessionManager 和一个全局函数 global_functionerror: duplicate interface definition for class 'AFURLSessionManager'
分析:2. 解决方案
1. 收集需要重命名的符号
2. 编译 llvm-objcopy 产出工具
llvm-objcopy 然后编译运行,从 Products 目录下将产物拷贝到个人电脑的 CustomTools 文件夹,后续就可以在终端访问 llvm-objcopy 指令。(因为在 .zshrc 文件里配置过 export PATH=~/CustomTools:$PATH)
这样就说明已经成功了FantasticLBP_(当然这个前缀名字可以是 SDK 名称简写、业务线简写,我这里是 Github ID ,验证问题而已)。llvm-objcopy: command not foundllvm-objcopy 移动到源码根目录下,现在 Command not found 的问题解决了,但是报错: error: option is not supported for MachO
说明 --prefix-symbols 并不支持 MachO 格式。而 --redefine-sym 符合要求。
发现修改成功--redefine-syms。_OBJC_CLASS_$_AFURLSessionManager FantasticLBP__OBJC_CLASS_$_AFURLSessionManager
_OBJC_METACLASS_$_AFURLSessionManager FantasticLBP__OBJC_METACLASS_$_AFURLSessionManager
_OBJC_IVAR_$_AFURLSessionManager._session FantasticLBP__OBJC_IVAR_$_AFURLSessionManager._session3. 使用工具修改符号名称
4. 为什么存在2处类定义?
_OBJC_CLASS_$ 符号而忽略 _OBJC_METACLASS_$ 和类名字符串,运行时仍会看到两个类定义,导致冲突未解决
而且和运行时系统相关的所有功能都会受影响,比如:Category、KVC/KVO 内部使用类名字符串查找、Archive/Unarchive 崩溃等。
另外一种方式:2个不同名的静态库,但存在相同符号。可以通过 cocoapods 的配置,将某个静态库改为动态库。因为动态库存在二级命名空间,就可以避免符号重复的问题。
关于动态库二级命名空间的细节,可以查看这里# Podfile
use_frameworks! # 启用框架支持
target 'YourApp' do
pod 'AFNetworking', '~> 4.0' # 默认静态库
# 将冲突库转为动态框架
pod 'ConflictingLib', :modular_headers => true, :linkage => :dynamic
enddynamic_frameworks = ['ConflictingLib']
target 'YourApp' do
# 先声明所有库
pods = [
'AFNetworking',
'ConflictingLib',
'OtherLib'
]
# 动态处理
pods.each do |pod|
if dynamic_frameworks.include?(pod)
pod pod, :modular_headers => true, :linkage => :dynamic
else
pod pod
end
end
endplatform :ios, '9.0'
use_frameworks! # 确保启用框架支持
target 'StaticLibConflictsDemo' do
# 动态链接 AFNetworking(兼容所有版本)
pod 'AFNetworking', :modular_headers => true
# 添加后安装钩子脚本
post_install do |installer|
# 将 AFNetworking 设为动态框架
installer.pods_project.targets.each do |target|
if target.name == 'AFNetworking'
# 设置 Mach-O 类型为动态库
target.build_configurations.each do |config|
config.build_settings['MACH_O_TYPE'] = 'mh_dylib'
config.build_settings['DYLIB_INSTALL_NAME_BASE'] = '@rpath'
config.build_settings['LD_RUNPATH_SEARCH_PATHS'] = ['$(FRAMEWORK_SEARCH_PATHS)']
end
end
end
# 确保框架被正确嵌入
installer.pods_project.build_configurations.each do |config|
config.build_settings['EMBEDDED_CONTENT_CONTAINS_SWIFT'] = 'YES'
end
end
end
可以看到不光是解决了符号重复的问题,也解决了同一个类,存在2处实现的问题。5. 符号冲突总结
1. 最佳实践方案:动态库隔离
# Podfile
use_frameworks! # 关键:启用框架支持
target 'StaticLibConflictsDemo' do
# 将冲突库转为动态 framework
pod 'ConflictingLib', :modular_headers => true, :linkage => :dynamic
# 其他静态库
pod 'NonConflictingLib'
end2. 源码级解决方案:添加类前缀
// 原始类名
@interface AFURLSessionManager : NSObject
// 修改后
@interface ABC_AFURLSessionManager : NSObject3. 构建系统级解决方案:Bazel 命名空间
objc_library(
name = "AFNetworking",
namespace = "ABC", # 自动添加前缀
srcs = glob(["*.m"]),
hdrs = glob(["*.h"]),
)六、Strip 流程
_DWARF 段七、Framework
1. 定义
Embedded Framework。开发中使用的动态库会被放到 ipa 的 framework 目录下,基于沙盒运行。__TEXT 段的大小。__TEXT 部分总和不得超过 80M__TEXT 部分不得超过 60M__TEXT 部分的总和不超过 500M__TEXT 段。2. Framework 2种结构
.dylib + 签名 + 资源文件.a + 签名 + 资源文件3. 静态库转 Framework
Framworks,下面创建一个 Person.Framework 文件夹,把之前得到的静态库 Person 移动到该目录下。并创建一个 test.m 移动过去。#import <Foundation/Foundation.h>
#import "Person.h"
int main () {
Person *person = [[Person alloc] init];
NSLog(@"Person is %@", person);
}Headers 文件夹,把 Person.h 文件放进去。Headers 同层目录,把 Person 静态库放进去。test.m 和 framework 信息,编译成 test.oclang -x objective-c \
-target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-I ./Frameworks/Person.framework/Headers \
-c test.m -o test.otest.o 和 framework 去完成链接。链接三要素:库的头文件、库所在目录、库的名称。只不过在处理 Framework 的时候,参数不一样clang \
-target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-F ./Frameworks \
-framework Person \
test.o -o test-F./Frameworks 在当前目录的子目录 Frameworks 查找需要的库文件。类似 Xcode 里的 Framwork search path-framework Person 链接的名称为 Person.framework 的动态库或者静态库。类似 Other linker flags -frameworkPerson.framework 的动态库,找不到,再去找 Person.framework 的静态库,还找不到,就报错4. QA:通常情况下,同一份代码,一个库制作成动态库体积会比静态库小。为什么?
.o 文件的集合。假设 AFNetworking 有15个 .m 文件,编译后产生 15个 .o 文件。每个 .o 文件的都存在下面3部分:Unix_Kernel ~/Desktop/OCExplore/OCExplore file Person.o
Person.o: Mach-O 64-bit object x86_64
Unix_Kernel ~/Desktop/OCExplore/OCExplore otool -h Person.o
Person.o:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777223 3 0x00 1 4 1880 0x000020005. dead strip
clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-Xlinker -dead_strip \
-Xlinker -all_load \
-Xlinker -why_live -Xlinker _global_Function \
-L ./StaticLibrary \
-l Person \
test.o -o test-noall_load: 完全不加载、直接去优化。OTHER_LDFLAGS=-Xlinker -noall_load-all_load: 完全加载、不要去优化掉。OTHER_LDFLAGS=-Xlinker -all_load-ObjC : 排除ObjC的代码、其他的都优化掉。OTHER_LDFLAGS=-Xlinker -ObjC -force_load : 指定哪些静态库不要优化掉;八、动态库
1. tbd
UIKit.tbd)包含以下部分:--- !tapi-tbd-v3
archs: [ arm64, arm64e ]
platform: ios
install-name: /System/Library/Frameworks/UIKit.framework/UIKit
current-version: 61000
compatibility-version: 1.0
exports:
- archs: [ arm64, arm64e ]
symbols: [ _UIApplicationMain, _UIViewSetFrame ]
weak-symbols: [ _UISomeOptionalAPI ]
objc-classes: [ UIViewController, UIView ]
objc-ivars: [ UIViewController.view ]
...字段 描述 archs支持的架构 (arm64, armv7, x86_64 等) platform目标平台 (ios, ios-simulator, macos) install-name运行时加载路径 current-version库的当前版本 compatibility-version兼容版本 exports导出的符号信息 symbols导出的 C 函数和全局变量 objc-classes导出的 Objective-C 类 objc-ivars导出的实例变量 objc-eh-typesObjective-C 异常类型 re-exports重新导出的其他库 2. 直接链接动态库
Person.h Person.m 类。在 dylib 同层目录创建 main.m 文件。代码如下clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-I./dylib \
-c main.m -o main.oclang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-c Person.m -o Person.oclang -dynamiclib \
-target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
Person.o -o LibPerson.dylibclang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-L./dylib \
-lPerson \
main.o -o mainbuild.shecho "---------------- start --------------"
echo "第一步:先对 main.m 编译为 main.o"
clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-I./dylib \
-c main.m -o main.o
echo "第二步,再对 Person 编译为 Person.o"
pushd ./dylib
clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-c Person.m -o Person.o
echo "第三步,将 Person.o 编译为 libPerson.dylib 动态库"
clang -dynamiclib \
-target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
Person.o -o libPerson.dylib
echo "第四步,将 main.o 和 libPerson.dylib 链接为可执行文件 main"
popd
clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-L./dylib \
-lPerson \
main.o -o main
echo "---------------- Done --------------"lldb -file 可执行文件,然后输入 r 进行运行:3. 静态库链接成动态库
.o 文件可以链接成静态库.o 文件可以链接成动态库ld -dylib -arch x86_64 \
-macosx_version_min 13.1 \
-syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-lsystem -framework Foundation \
libPerson.a -o libPerson.dylibbuild.sh 完整脚本如下echo "---------------- start --------------"
echo "第一步:先对 main.m 编译为 main.o"
clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-I./dylib \
-c main.m -o main.o
echo "第二步,再对 Person 编译为 Person.o"
pushd ./dylib
clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-c Person.m -o Person.o
echo "第三步,对 Person.o 编译为 LibPerson.a 静态态库"
libtool -static -arch_only x86_64 Person.o -o libPerson.a
echo "第四步,LD 链接器将 libPerson.a 链接为 libPerson.dylib 动态库"
ld -dylib -arch x86_64 \
-macosx_version_min 13.1 \
-syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-lsystem -framework Foundation \
libPerson.a -o libPerson.dylib
echo "第五步,将 main.o 和 libPerson.dylib 链接为可执行文件 main"
popd
clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-L./dylib \
-lPerson \
main.o -o main
echo "---------------- Done --------------"undefined symbols for **,然后利用 objdump --macho --exports-trie libPerson.dylib 查看动态库的导出符号,居然是空。为什么?-noall_load 参数。共4个参数:-noall_load :默认是 -noall_load,顾名思义就是不会所有符号的加载,而是链接器链接一个静态库之前去扫描静态库文件,找到需要的代码再进行链接-all_load:链接所有符号,不管代码有没有使用-force_load: 可以指定要载入所有方法的库,后面必须跟一个只想静态库的路径-ObjC:告诉链接器把库中定义的 Objective-C 类和 Category 都加载进来,这样编译之后的可执行文件会变大(因为加载了其他的 OC 代码进来)。由于 OC语言符号链接的基本单位是类,静态库链接时首先会链接本类,而 Category 是运行时才会被加载的,因此会被静态链接器直接忽略掉,通过 -ObjC 命令是告知链接器链接所有的 OC 代码ld -dylib -arch x86_64 \
-macosx_version_min 13.1 \
-syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-lsystem -framework Foundation \
-all_load \
libPerson.a -o libPerson.dylib.o 文件的合集.o 文件链接后的产物4. 动态库 Library not loaded?
Library not loaded: 'libPerson.dylib' 错误?LC_LOAD_DYLIB 的 Load Command,里面保存了所使用到的动态库的路径。可执行文件所依赖的动态库,就是靠 dyld 根据动态库路径进行加载的。otool -l main | grep 'DYLIB' -A 5 指令LC_ID_DYLIB。方式一:通过
install_name_tool 指令install_name_tool -id 动态库路径 动态库名称。即:install_name_tool -id /Users/unix_kernel/Desktop/LDAndFramework/StaticLibraryLinkedIntoDynamicLibrary/dylib/libPerson.dylib libPerson.dylibotool -l libPerson.dylib | grep 'DYLIB' -A 5 查看路径信息方式二:
rpathrpath 即 Runpath search paths,是 dyld 搜索路径。运行时,@rpath 指示 dyld 按顺序搜索路径列表,以找到动态库。@rpath 是保存一个或多个路径的变量。Frameworks,内部继续创建 Person.framework 文件夹,其内部继续创建 Headers// tree -L 4
.
├── Frameworks
│ └── Person.framework
│ ├── Headers
│ │ └── Person.h
│ ├── Person.h
│ ├── Person.m
├── build.sh
├── dylib
│ ├── Person.h
│ ├── Person.m
│ ├── Person.o
│ ├── libPerson.a
│ └── libPerson.dylib
├── main.m Frameworks/Person.framework 下面执行命令,将 Person.m 编译为 Person.oclang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-c Person.m -o Person.oPerson.o 链接为动态库clang -dynamiclib \
-target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
Person.o -o Personinstall_name_tool 修改 id,id 指定 @rpath 信息(在做这么一件事:动态库需要 rpath 信息,用来加载访问动态库真正的实现)install_name_tool -id @rpath/Frameworks/Person.framework/Person /Users/unix_kernel/Desktop/LDAndFramework/StaticLibraryLinkedIntoDynamicLibrary/Frameworks/Person.framework/Personotool 查看动态库的 name 是否修改好了 @rpath 信息otool -l Person | grep 'ID' -A 5main.m 编译为 main.oclang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-I./Frameworks/Person.framework/Headers \
-c main.m -o main.omain.o 和 Person.framework 链接为可执行文件clang -target x86_64-apple-macos13.1 \
-fobjc-arc \-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-F./Frameworks \
-framework Person \
main.o -o mainLibrary not loaded: '@rpath/Frameworks/Person.framework/Person'。所以需要给可执行文件添加 rpath 信息main 添加 rpath 信息(在做这么一件事:宿主(App)提供 rpath 信息,用于提供路径信息给动态库)install_name_tool -add_rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLibraryLinkedIntoDynamicLibrary mainotool -l main | grep 'RPATH' -A 5main 可执行文件即可执行rpath 信息。rpath 谁来提供,比如一个 Person.framework 路径为: /usr/meiying/desktop/DynamicExplore/Person.frameworkLoad Command 中存在 LC_RPATH ,值为 /usr/meiying/desktop/DynamicExplore/Person.framework@rpath/Frameworks/Person.framework/Personrpath 还是一个绝对路径。方式三:@execute_path、@loader_path
@executable_path:表示可执⾏程序所在的⽬录,解析为可执⾏⽂件的绝对路径。@loader_path:表示被加载的Mach-O 所在的⽬录,每次加载时,都可能被设置为不同的路径,由上层指定install_name_tool -rpath /Users/unix_kernel/Desktop/LDAndFramework/StaticLibraryLinkedIntoDynamicLibrary @executable_path mainLD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks'@loader_path 还是比较抽象的,举一个实际场景例子来看看[cat sleep] 能力[person sayHi] 能力,动态库 Person 使用了动态库 Catlldb -file main
(lldb) target create "main"
Current executable set to '/Users/unix_kernel/Desktop/LDAndFramework/DynamicLibraryUseDynamicLibrary/main' (x86_64).
(lldb) r
Process 54157 launched: '/Users/unix_kernel/Desktop/LDAndFramework/DynamicLibraryUseDynamicLibrary/main' (x86_64)
dyld[54157]: Library not loaded: 'Person'
Referenced from: '/Users/unix_kernel/Desktop/LDAndFramework/DynamicLibraryUseDynamicLibrary/main'
Reason: tried: 'Person' (no such file), '/usr/local/lib/Person' (no such file), '/usr/lib/Person' (no such file), '/Users/unix_kernel/Desktop/LDAndFramework/DynamicLibraryUseDynamicLibrary/Person' (no such file), '/usr/local/lib/Person' (no such file), '/usr/lib/Person' (no such file)
Process 54157 stopped
* thread #1, stop reason = signal SIGABRT
frame #0: 0x000000010005698e dyld`__abort_with_payload + 10
dyld`:
-> 0x10005698e <+10>: jae 0x100056998 ; <+20>
0x100056990 <+12>: movq %rax, %rdi
0x100056993 <+15>: jmp 0x100013150 ; cerror_nocancel
0x100056998 <+20>: retq
Target 0: (main) stopped.@rpath 信息, -Xlinker -install_name -Xlinker @rpath/Cat.framework/Catecho "1: 编译 Cat.m 为 Cat.o 可执行文件"
clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-c Cat.m -o Cat.o
echo "2: 链接 Cat.o 为 Cat 动态库"
clang -dynamiclib \
-target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-Xlinker -install_name -Xlinker @rpath/Cat.framework/Cat \
Cat.o -o Cat
echo "3: 输出动态库的 name(路径)信息"
otool -l Cat | grep 'ID' -A 5@rpath 信息, -Xlinker -install_name -Xlinker @rpath/Person.framework/Personecho "1:编译 Person.m 为 Person.o 可执行文件"
clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-I./Headers \
-I./Frameworks/Cat.framework/Headers \
-c Person.m -o Person.o
echo "2: 链接 Person.o 为 Person 动态库"
clang -dynamiclib \
-target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-Xlinker -install_name -Xlinker @rpath/Person.framework/Person \
-F./Frameworks/ \
-framework Cat \
Person.o -o Person
echo "3: 输出动态库的 dylib 信息"
otool -l Person | grep 'DYLIB' -A 5
echo "-----------"
echo "4: 输出动态库的 name 信息"
otool -l Person | grep 'ID' -A 5
@executable_path 信息,-Xlinker -rpath -Xlinker @executable_path/Frameworksecho "---------------- start --------------"
echo "第一步:先对 main.m 编译为 main.o"
clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-I./Frameworks/Person.framework/Headers \
-c main.m -o main.o
echo "第二步,将 main.o 和 Person.dylib 链接为可执行文件 main"
clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-Xlinker -rpath -Xlinker @executable_path/Frameworks \
-F./Frameworks \
-framework Person \
main.o -o main
echo "第三步,输出信息"
otool -l main | grep 'RPATH' -A 3
otool -l main | grep 'DYLIB' -A 3
echo "---------------- Done --------------"@executable_path,给2个动态库都添加了 @rpath,怎么办?@rpath 就可以了?echo "1:编译 Person.m 为 Person.o 可执行文件"
clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-I./Headers \
-I./Frameworks/Cat.framework/Headers \
-c Person.m -o Person.o
echo "2: 链接 Person.o 为 Person 动态库"
clang -dynamiclib \
-target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-Xlinker -install_name -Xlinker @rpath/Person.framework/Person \
-Xlinker -rpath -Xlinker @executable_path/Frameworks/Person.framework/Frameworks \
-F./Frameworks/ \
-framework Cat \
Person.o -o Person
echo "3: 输出动态库的 dylib 信息"
otool -l Person | grep 'DYLIB' -A 5
echo "-----------"
echo "4: 输出动态库的 name 信息"
otool -l Person | grep 'ID' -A 5
rpath,指令为: -Xlinker -rpath -Xlinker @executable_path/Frameworks-Xlinker -install_name -Xlinker @rpath/Person.framework/Person,同时又因为依赖了动态库 B,所以同时又要提供 rpath,指令为 -Xlinker -rpath -Xlinker @executable_path/Frameworks/Person.framework/Frameworks -Xlinker -install_name -Xlinker @rpath/Cat.framework/Cat@loader_path 呼之欲出了。-Xlinker -rpath -Xlinker @executable_path/Frameworks/Person.framework/Frameworks 可以替换为 -Xlinker -rpath -Xlinker @loader_path/Frameworks loader_path 是标准解决方案。随便打开 AFNetworking 工程看看rpath ,自己也可以创建一个同名的动态库(name)把原有的动态库替换了,这也是为什么 MacOS 那些破解软件的工作原理。(当然,这些也要结合逆向技术)sudo codesign --force --deep --sign - (应用路径)5. 动态库如何导出所引用的动态库的符号
import <Person.h> ,查看下 Person 动态库的导出符号,使用指令 objdump --macho --exports-trie Personobjdump --macho --exports-trie Catman ld 查看下指令 -Xlinker -reexport_framework -Xlinker Catecho "1:编译 Person.m 为 Person.o 可执行文件"
clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-I./Headers \
-I./Frameworks/Cat.framework/Headers \
-c Person.m -o Person.o
echo "2: 链接 Person.o 为 Person 动态库"
clang -dynamiclib \
-target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-Xlinker -install_name -Xlinker @rpath/Person.framework/Person \
-Xlinker -rpath -Xlinker @executable_path/Frameworks/Person.framework/Frameworks \
-Xlinker -reexport_framework -Xlinker Cat \
-F./Frameworks/ \
-framework Cat \
Person.o -o Person
echo "3: 输出动态库的 dylib 信息"
otool -l Person | grep 'DYLIB' -A 5
echo "-----------"
echo "4: 输出动态库的 name 信息"
otool -l Person | grep 'ID' -A 5
LC_REEXPORT_DYLIB 的 Load Command。也就是可执行文件,通过 Person.framework 的 LC_REEXPORT_DYLIB load Command 可以实现访问 Cat.framework 的符号。#import <Foundation/Foundation.h>
#import "Person.h"
#import <Cat.h>
int main(int argc, char * argv[]) {
Person *p = [[Person alloc] init];
[p sayHi];
Cat *cat = [[Cat alloc] init];
[cat sleep];
return 0;
}mian.m 中引入了 Cat.Framework 的符号,LD 链接指令需要加 Cat.framework 头文件的参数echo "---------------- start --------------"
echo "第一步:先对 main.m 编译为 main.o"
clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-I./Frameworks/Person.framework/Headers \
-I./Frameworks/Person.framework/Frameworks/Cat.framework/Headers \
-c main.m -o main.o
echo "第二步,将 main.o 和 Person.dylib 链接为可执行文件 main"
clang -target x86_64-apple-macos13.1 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk \
-Xlinker -rpath -Xlinker @executable_path/Frameworks \
-F./Frameworks \
-framework Person \
main.o -o main
# echo "第三步,输出信息"
# otool -l main | grep 'RPATH' -A 3
# otool -l main | grep 'DYLIB' -A 3
echo "---------------- Done --------------"6. 二级命名空间
1. 核心原理
graph TD
A[符号解析] --> B{是否在二级命名空间?}
B -->|是| C[符号名称 + 库标识符]
B -->|否| D[仅符号名称]
C --> E[唯一符号]
D --> F[可能冲突]1. 库标识符(Dylib Identity)
# 查看库标识符
otool -l libExample.dylib | grep -A 3 LC_ID_DYLIB
# 输出示例:
# cmd LC_ID_DYLIB
# cmdsize 56
# name @rpath/libExample.dylib (offset 24)
# time stamp 1 Wed Dec 31 19:00:01 1969
# current version 1.0.0
# compatibility version 1.0.02. 使用过程
3. 符号表结构
Symbol Table
├── LibA Symbols
│ ├── foo
│ ├── bar
│ └── baz
├── LibB Symbols
│ ├── foo
│ ├── xyz
│ └── abc
└── ...2. 应用场景
1. 解决符号冲突
// libNetwork.dylib
void process_data() {
// 网络处理实现
}
// libAudio.dylib
void process_data() {
// 音频处理实现
}
所以,可以安全的使用2个库,而不会发生符号冲突问题2. Xcode 配置
OTHER_LDFLAGS = $(inherited) -twolevel_namespaceOTHER_LDFLAGS = $(inherited) -flat_namespace
查看命名空间状态:# 检查二进制是否使用二级命名空间
otool -lv YourApp | grep TWOLEVEL
# 输出示例:
# flags TWOLEVEL3. 动态库开发 tips
1. 动态库设计原则
// 良好的命名实践
void TaoBaoNetwork_processData() {
// 实现
}// 只导出必要的符号
__attribute__((visibility("default")))
void public_api();
// 隐藏内部实现
__attribute__((visibility("hidden")))
void internal_function();2. 运行时路径管理
@executable_path: 可执行文件所在目录@loader_path: 当前加载模块所在目录@rpath: 运行时搜索路径7. 静态库比动态库体积大
.o 文件的合集。Header + Load Commands + 段信息 + 符号表libXXX.a
├── A.o (Header + LC + __TEXT + __DATA + 符号表)
├── B.o (Header + LC + __TEXT + __DATA + 符号表)
├── C.o (Header + LC + __TEXT + __DATA + 符号表)
└── ....o 文件也是 Mach-O Object 类型的 Mach-O,所以都需要:libXXX.dylib
└── 1 个完整的 Mach-O
├── Header(只 1 份)
├── Load Commands(只 1 份)
├── 合并后的 __TEXT
├── 合并后的 __DATA
└── 统一的符号表LC_LOAD_DYLIB,只能按照 mmap 整库加载,没办法选择链接哪部分符号.o → Mach Header + Load Commands + 代码段
.o → Mach Header + Load Commands + 代码段
.o → Mach Header + Load Commands + 代码段只有一个 Mach Header + 一套 Load Commands九、xcframework
1. 诞生背景
2. 优势
dSYM 文件BCSymbolMapsdSYM、BCSymbolMaps、库文件,较为繁琐。xcframework 技术。.xcframework ⽂件提供多个平台的分发⼆进制⽂件Fat Header 相⽐,可以按照平台划分,可以包含相同架构的不同平台的⽂件3. 如何制作 xcframework
pod lib create Person。里面就一个 Person 类, 包含2个方法。xcodebuild 指令打包。xcodebuild 指令执行的是打包当前的工程目录,会读取 -workspace 参数指定的项目配置,然后根据指定的 -scheme 和 -configuration 模式去打包工程。xcodebuild -workspace Person.xcworkspace \
-scheme 'Person-Example' \
-configuration Release \
-destination 'generic/platform=iOS Simulator' \
-archivePath '../archives/Person.framework-iphonesimulator.xcarchive' \
SKIP_INSTALL=NO \
archivexcodebuild -workspace Person.xcworkspace \
-scheme 'Person-Example' \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath '../archives/Person.framework-iphoneos.xcarchive' \
SKIP_INSTALL=NO \
archive.dSYM 文件,没有 BCSymbolMaps 文件,是因为工程没有开启 bitcode,当开启 bitcode 后的,打包会生成 BCSymbolMaps 文件,作用也是用于 Crash 后解析堆栈用的。.dSYM 文件是默认生成的,但是 bitcode 需要分别开启(Pod 和 Demo 工程)。开启 bitcode 后重新打包,看看生成的产物里有没有 BCSymbolMaps 和相关的 .bcsymbolmap 文件xcodebuild 打包成 xcframeworkxcodebuild -create-xcframework \
-framework 'archives/Person.framework-iphoneos.xcarchive/Products/Library/Frameworks/Person.framework' \
-framework 'archives/Person.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/Person.framework' \
-output 'https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/xcframework/Person.xcframework'.dSYM 文件或者 .bcsymbolmap 文件。.dSYM 和 .BCSymbolMaps 文件,重新生成 xcframework,其参数为 -debug-symbols,后面路径必须是绝对路径。xcodebuild -create-xcframework \
-framework 'archives/Person.framework-iphoneos.xcarchive/Products/Library/Frameworks/Person.framework' \
-debug-symbols '/Users/unix_kernel/Desktop/LDAndFramework/MultipleArchMerge/Person/archives/Person.framework-iphoneos.xcarchive/BCSymbolMaps/3C61A3F4-4398-322F-8AC9-F078B196C381.bcsymbolmap' \
-debug-symbols '/Users/unix_kernel/Desktop/LDAndFramework/MultipleArchMerge/Person/archives/Person.framework-iphoneos.xcarchive/BCSymbolMaps/B34138DB-2F8F-372C-93D3-7ADDDFC7BDA1.bcsymbolmap' \
-debug-symbols '/Users/unix_kernel/Desktop/LDAndFramework/MultipleArchMerge/Person/archives/Person.framework-iphoneos.xcarchive/dSYMs/Person.framework.dSYM' \
-framework 'archives/Person.framework-iphonesimulator.xcarchive/Products/Library/Frameworks/Person.framework' \
-debug-symbols '/Users/unix_kernel/Desktop/LDAndFramework/MultipleArchMerge/Person/archives/Person.framework-iphonesimulator.xcarchive/dSYMs/Person.framework.dSYM' \
-output 'https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/xcfrmaework/Person.xcframework'.dSYM 和 bcsymbolmap 文件,用于堆栈、符号还原。XCFrameworkUsageDemo 的 Xcode iOS App 工程Person.xcframeworkPerson.framework 里面的动态库文件大小,是否和 Person.xcframework 里面模拟器目录下 Person 动态库的大小一致Person.xcframework 可以正常使用,除此之外,Person.xcframework 包含了模拟器和真机的动态库文件和对应的 .dSYM 和 .bcsymbolmap 文件,当导入到项目中的时候,Xcode 会根据当前编译的架构,自动从里面选择合适的架构文件。LDAndFramework/XCFramework 目录下。4. 弱链接(weakly linked)
Person.framework 拖到工程根目录下Library not loaded: @rpath/Person.framework/Personman ld 查看具体的参数和说明: -weak_framework name[,suffix]
This is the same as the -framework name[,suffix] but forces the framework and all references to it to be marked as weak imports. Note: due to a
clang optimizations, if functions are not marked weak, the compiler will optimize out any checks if the function address is NULL.NULLEXC_BAD_ACCESS// 引用一个动态库,3要素:头文件、动态库名称、动态库路径
// 知道了链接的原理之后,就知道可以不用把动态库托到项目中去,指定了3要素就可以链接了。
// 1. -I:头文件信息
HEADER_SEARCH_PATHS = $(inherited) ${SRCROOT}/Person.framework/Headers
LD_RUNPATH_SEARCH_PATHS = $(inherited)
// 2. -F:framework
FRAMEWORK_SEARCH_PATHS = $(inherited) ${SRCROOT}
// 3. -weak_framework: 允许该库在运行时消失
OTHER_LDFLAGS = $(inherited) -Xlinker -weak_framework -Xlinker "Person"Library not loaded: 这样的错。_weak-framework 这个链接器参数的可执行文件查看下,指令为 otool -l WeakImportDemo-weak-framework 声明后,cmd 从 LC_LOAD_DYLIB 变为 LC_LOAD_WEAK_DYLIB-weak_framework 选项,可以告诉链接器,即使框架中缺少某些符号,也不要报错,而是允许链接继续进行。5. 静态库符号冲突原因及其解法
//// -I
HEADER_SEARCH_PATHS = $(inherited) "${SRCROOT}/AFNetworking" "$(SRCROOT)/AFNetworking2"
//// -L
LIBRARY_SEARCH_PATHS = $(inherited) "${SRCROOT}/AFNetworking" "$(SRCROOT)/AFNetworking2"
//// -l
OTHER_LDFLAGS = $(inherited) -l"AFNetworking" -l"AFNetworking2".o 文件,所有的 .o 最后都会和主工程的可执行文件进行合并,所以不存在二级命名空间的问题。-noall_load,意思是:完全不加载、直接去优化-all_load 就会报错 -load_hidden 参数。 -load_hidden path_to_archive
Uses specified static library as usual, but treats all global symbols from the static library to as if they are visibility hidden.
Useful when building a dynamic library that uses a static library but does not want to export anything from that static library.OTHER_LDFLAGS = $(inherited)
-l"AFNetworking"
-l"AFNetworking2"
-Xlinker -force_load // 强制加载静态库中所有目标文件(即使未使用)
-Xlinker "${SRCROOT}/AFNetworking/libAFNetworking.a"
-Xlinker -load_hidden // 隐藏静态库符号
-Xlinker "${SRCROOT}/AFNetworking2/libAFNetworking2.a"LDAndFramework/StaticLibConflictsSolution/StaticLibConflictsDemo十、动态库/静态库的搭配使用
1. 动动
NetworkManager 的 Framework,勾选测试。里面就添加一个 NetworkDetector 类,有1个类方法 +sharedManagerpod init ,初始化工程pod 'AFNetworking'Pods-NetworkManager.debug.xcconfig 里面的配置信息告诉链接器关于编译的3要素:头文件位置、动态库名称、rpath 信息。所以可以链接成功,NetworkManager.Framework 的 Load Command LC_LOAD_DYLIB 中的 name 就告诉外部,AFNetworking 将会在 @rpath/AFNetworking.framework/AFNetworking 下面查找。NetworkManager.Framework 提供 rpath 信息。xcconfig 文件的 LD_RUNPATH_SEARCH_PATHS 是 Xcode 项目中的一个设置,它在编译时告诉链接器在生成的可执行文件的运行时路径(rpath)中包含特定的目录( install_name_tool -add_rpath 是在二进制文件生成后对其进行修改)LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' '@executable_path/.https://raw.githubusercontent.com/FantasticLBP/knowledge-kit/master/Frameworks'LD_RUNPATH_SEARCH_PATHS 提供的 rpath 信息,和 AFNetworking 的 name 拼接去查找。但我们面前的例子,路径下没有 AFNetworking。pod AFNetworking。这是在探究原理,简单解决问题可以这么做。LD_RUNPATH_SEARCH_PATHS 配置一个可以找到的地方。然后运行成功。这只是为了研究定位问题后,简单解决问题的方案。往一个 Framework 里通过 Cocoapods 导入一个库,并不会真的导入,而是生成链接器链接所需的参数,并不会把依赖的库文件放到自身的 Framework 中。
-Xlinker -undefined -Xlinker dynamic_lookup 修改动态库 xcconfig 添加未定义的符号为动态查找。但风险较大,所有未定义的都不会报错误伤较大(比如随便敲的符号,也检测不出问题)OTHER_LDFLAGS = $(inherited) -framework "AFNetworking" -Xlinker -undefined -Xlinker dynamic_lookup-Xlinker -U -Xlinker _OBJC_CLASS_$_NetworkObject 只对特定的未定义的符号采用动态查找OTHER_LDFLAGS = $(inherited) -framework "AFNetworking" -Xlinker -U -Xlinker _OBJC_CLASS_$_NetworkObject2. 动静
# use_frameworks! 注释掉,就会以静态库的形式链接 AFNetworking。OTHER_LDFLAGS = $(inherited) -ObjC -Xlinker -hidden-l"AFNetworking"3. 静静
Build Settings -> Mach-OType 改为 Static Libraryuse_frameworks! 注释掉,就会以静态库的形式链接 AF-lAFNetworking 指明链接哪个库;查找 Libarary Search Path,设置库的查找路径 ${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/AFNetworkingLDAndFramework/StaticLibUseStaticLib4. 静动
Static Libraryuse_frameworks! 注释打开import <AFNetworking/AFNetworking.h> 会自动在目标文件的 Mach-O 中 OTHER_LDFLAGS 拼接 -framework 参数FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AFNetworking",展开为 "${BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/AFNetworking"。将其设置到 Build Settings 的 framework search path 中。image not found,因为编译后的 AFNetworking 没有拷贝到 App 所需目录下。(通过配置,链接器会在 build/Debug-iphoneos/AFNetworking 的位置查找 AFNetworking,目前找不到)Scripts 文件夹Scripts 目录下,重命名为 handle-frameworks.sh"${SRCROOT}/Scripts/handle-frameworks.sh"LDAndFramework/StaticLibUseDynamicLib5. 同时依赖静态库、动态库
platform :ios, '14.1'
target 'InstallDyanmicAndStaticFramework' do
use_frameworks!
$static_framework = ['AFNetworking']
pre_install do |install|
puts install
install.pod_targets.each {| pod |
if $static_framework.include?(pod.name)
def pod.build_type;
Pod::BuildType.static_framework # 使用静态库
end
end
}
end
# Pods for InstallDyanmicAndStaticFramework
pod 'SDWebImage'
pod 'AFNetworking'
endLDAndFramework/InstallDyanmicAndStaticFramework十一、Module
1. 定义
#include 机制的头文件污染与低效问题export)与依赖(requires)#define)不泄漏到导入方.pcm(Precompiled Module)加速编译test.m 被编译成目标文件 test.o 时,当前的目标文件就代表了一个 Module 。2. .pcm 文件
1. 结构
.pcm(Precompiled Module)文件是 Clang 对 C++ Module 的预编译二进制表示,其内容远非简单的头文件文本封装,而是包含编译器所需的完整语义信息。.pcm 文件包含:export 声明的函数/类)Target: x86_64-apple-darwin22.4.0
SDK: MacOSX13.3.sdk
Language: C++17.pcm 文件的 60-70%(主要膨胀源)import 直接导入的其他模块(如 import std.core;)<vector> 依赖 <memory>)-D 宏定义、-I 包含路径、-std=c++20 等-mabi=sysv、-fPIC 等影响二进制兼容性的参数-Werror、-fno-exceptions 等策略2. 技术特性
.pcm 文件与以下环境绑定,不可跨平台/编译器/配置复用:// 不同配置生成不同 .pcm
-arch arm64 ≠ -arch x86_64
-O2 ≠ -O0
-DDEBUG ≠ -DRELEASE.pcm 失效(因 AST 变化)-I 路径顺序变化即触发重新生成.pcm 本质是 AST 的反序列化过程3. 场景
.h 文件也是需要编译的(里面也会承载一些方法信息)。include:#include "*.h" 的时候, 头文件 A、B 在 use.c another-use.c 中,include 几次就要编译几次。use.c 的时候,就要编译 include 进来的 A.h、B.h。编译 another-use.c 同样要再编译一次 A.h、B.h。被 include 了几次,就要编译几次,重复编译,效率低下。#include 是纯粹的文本替换指令.c 文件都获得头文件的完整副本import:与 include 相对应,import 语句用于导入模块,而不是简单的文本包含。使用模块可以减少编译时间,因为编译器只需要编译模块的接口而不是整个模块的实现clang -fmodules -fmodule-map-file=mo dule.modulemap -fmodules-cache-path=./moduleCache -c use.c -o use.o:-fmodules 用于告诉 clang 启用 module 编译-fmodules-cache-path 后面的路径中可以看到 A、B2个头文件,编译缓存也存在2个,分别以 A、B 开头-fmodule-map-file 指明 modulemap 文件路径.h 的多个 .m 中,就不会因为多处引入 .h 而编译多次 .h#import <AFNetworking/AFNetworking.h>
#include "AFNetworking/AFNetworking.h"
@import AFNetworking.AFNetworking;
// 转换为
@import AFNetworking.AFNetworking;4. modulemap 编写规范
case 1:上例中的 modulemap
/* module.modulemap */
module A { // 定义了一个名字叫 A 的模块
header "A.h" //模块 A 的公共头文件为 A.h。这意味着任何想要使用模块 A 中定义的类或函数的代码,都需要导入 A.h 文件
}
module B { // 定义了一个名为 B 的模块。。
header "B.h" // 模块 B 的公共头文件是 B.h
export A // 模块 B 向外暴露模块 A。这意味着任何导入模块 B 的代码,也可以使用模块 A 中定义的类或函数。export A 将模块 A 作为模块 B 的一部分公开,以便在使用模块 B 时,可以隐式使用模块 A 中的内容。
}#import "A.h"
#import "B.h"#import "B.h"case2:AFNetworking Demo 中的 modulemap
framework module AFNetworking { // 定义一个名为 AFNetworking 的框架模块
umbrella header "AFNetworking.h" // 指定了框架的伞头文件,这个文件是包含了所有公共头文件的文件,方便外部调用
export * // 表示框架中的所有公共接口(类、结构体、枚举、协议等)都被导出,
module * { export * } // 意味着框架内部的所有子模块(即 AFNetworking.h 中头文件代表的子模块都会被匹配到),子模块的名称就是 .h 文件的名称,都将被导出,可以被外部所访问
}case3:AsyncDisplayKit 的 modulemap
framework module AsyncDisplayKit { // 定义了一个名为 AsyncDisplayKit 的框架模块
umbrella header "AsyncDisplayKit.h" // 指定了框架的伞头文件,这个文件是包含所有公共头文件的文件,方便外部调用
export * // 表示框架中所有公共借口(类、结构体、枚举、协议等)都被导出,可以被外部代码访问
module * { // 意味着框架内部的所有子模块(即 AsyncDisplayKit.h 中头文件代表的子模块都会被匹配到),子模块的名称就是 .h 文件的名称,都将被导出,可以被外部所访问
export *
}
explicit module ASControlNode_Subclasses { // 如果需要显示指名,则必须使用 explicit。本行定义了一个名为 ASControlNode_Subclasses 的子模块,它包含了与 ASControlNode 相关的子类
header "ASControlNode+Subclasses.h" // 指定了子模块的头文件,这个头文件包含了该子模块中所有需要对外暴露的头文件的定义
export * // 将 ASControlNode+Subclasses.h 中的 .h 头文件,名称不变,导出出去
}
explicit module ASDisplayNode_Subclasses { // 定义了一个名为 ASDisplayNode_Subclasses 的子模块,它包含了所有与 ASDisplayNode 相关的子类
header "ASDisplayNode+Subclasses.h" // 指定了子模块的头文件,这个头文件包含了子类的定义
export *
}
}umbrella header 伞头文件,即 AsyncDisplayKit.h 的内容#import <AsyncDisplayKit/ASAvailability.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
#import <AsyncDisplayKit/ASDisplayNode+Convenience.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import <AsyncDisplayKit/ASControlNode.h>
#import <AsyncDisplayKit/ASImageNode.h>
#import <AsyncDisplayKit/ASTextNode.h>
#import <AsyncDisplayKit/ASButtonNode.h>
#import <AsyncDisplayKit/ASMapNode.h>
#import <AsyncDisplayKit/ASVideoNode.h>
#import <AsyncDisplayKit/ASVideoPlayerNode.h>
#import <AsyncDisplayKit/ASEditableTextNode.h>
#import <AsyncDisplayKit/ASImageProtocols.h>
#import <AsyncDisplayKit/ASBasicImageDownloader.h>
#import <AsyncDisplayKit/ASPINRemoteImageDownloader.h>
#import <AsyncDisplayKit/ASMultiplexImageNode.h>
#import <AsyncDisplayKit/ASNetworkImageNode.h>
#import <AsyncDisplayKit/ASPhotosFrameworkImageRequest.h>
#import <AsyncDisplayKit/ASTableView.h>
#import <AsyncDisplayKit/ASTableNode.h>
#import <AsyncDisplayKit/ASCollectionView.h>
#import <AsyncDisplayKit/ASCollectionNode.h>
#import <AsyncDisplayKit/ASCollectionViewLayoutInspector.h>
#import <AsyncDisplayKit/ASCollectionViewLayoutFacilitatorProtocol.h>
#import <AsyncDisplayKit/ASCellNode.h>
#import <AsyncDisplayKit/ASSectionContext.h>
#import <AsyncDisplayKit/ASElementMap.h>
#import <AsyncDisplayKit/ASCollectionLayoutContext.h>
#import <AsyncDisplayKit/ASCollectionLayoutState.h>
#import <AsyncDisplayKit/ASCollectionFlowLayoutDelegate.h>
#import <AsyncDisplayKit/ASSectionController.h>
#import <AsyncDisplayKit/ASSupplementaryNodeSource.h>
#if AS_IG_LIST_KIT
#import <AsyncDisplayKit/IGListAdapter+AsyncDisplayKit.h>
#import <AsyncDisplayKit/AsyncDisplayKit+IGListKitMethods.h>
#endif
#import <AsyncDisplayKit/ASScrollNode.h>
#import <AsyncDisplayKit/ASPagerFlowLayout.h>
#import <AsyncDisplayKit/ASPagerNode.h>
#import <AsyncDisplayKit/ASNodeController+Beta.h>
#import <AsyncDisplayKit/ASViewController.h>
#import <AsyncDisplayKit/ASNavigationController.h>
#import <AsyncDisplayKit/ASTabBarController.h>
#import <AsyncDisplayKit/ASRangeControllerUpdateRangeProtocol+Beta.h>
#import <AsyncDisplayKit/ASDataController.h>
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/ASDimension.h>
#import <AsyncDisplayKit/ASDimensionInternal.h>
#import <AsyncDisplayKit/ASDimensionDeprecated.h>
#import <AsyncDisplayKit/ASLayoutElement.h>
#import <AsyncDisplayKit/ASLayoutSpec.h>
#import <AsyncDisplayKit/ASBackgroundLayoutSpec.h>
#import <AsyncDisplayKit/ASCenterLayoutSpec.h>
#import <AsyncDisplayKit/ASRelativeLayoutSpec.h>
#import <AsyncDisplayKit/ASInsetLayoutSpec.h>
#import <AsyncDisplayKit/ASOverlayLayoutSpec.h>
#import <AsyncDisplayKit/ASRatioLayoutSpec.h>
#import <AsyncDisplayKit/ASAbsoluteLayoutSpec.h>
#import <AsyncDisplayKit/ASStackLayoutDefines.h>
#import <AsyncDisplayKit/ASStackLayoutSpec.h>
#import <AsyncDisplayKit/_ASAsyncTransaction.h>
#import <AsyncDisplayKit/_ASAsyncTransactionGroup.h>
#import <AsyncDisplayKit/_ASAsyncTransactionContainer.h>
#import <AsyncDisplayKit/_ASDisplayLayer.h>
#import <AsyncDisplayKit/_ASDisplayView.h>
#import <AsyncDisplayKit/ASDisplayNode+Beta.h>
#import <AsyncDisplayKit/ASTextNode+Beta.h>
#import <AsyncDisplayKit/ASTextNodeTypes.h>
#import <AsyncDisplayKit/ASBlockTypes.h>
#import <AsyncDisplayKit/ASContextTransitioning.h>
#import <AsyncDisplayKit/ASControlNode+Subclasses.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASEqualityHelpers.h>
#import <AsyncDisplayKit/ASHighlightOverlayLayer.h>
#import <AsyncDisplayKit/ASImageContainerProtocolCategories.h>
#import <AsyncDisplayKit/ASLog.h>
#import <AsyncDisplayKit/ASMutableAttributedStringBuilder.h>
#import <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/ASRunLoopQueue.h>
#import <AsyncDisplayKit/ASTextKitComponents.h>
#import <AsyncDisplayKit/ASTraitCollection.h>
#import <AsyncDisplayKit/ASVisibilityProtocols.h>
#import <AsyncDisplayKit/ASWeakSet.h>
#import <AsyncDisplayKit/ASEventLog.h>
#import <AsyncDisplayKit/CoreGraphics+ASConvenience.h>
#import <AsyncDisplayKit/NSMutableAttributedString+TextKitAdditions.h>
#import <AsyncDisplayKit/UICollectionViewLayout+ASConvenience.h>
#import <AsyncDisplayKit/UIView+ASConvenience.h>
#import <AsyncDisplayKit/UIImage+ASConvenience.h>
#import <AsyncDisplayKit/NSArray+Diffing.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/UIResponder+AsyncDisplayKit.h>
#import <AsyncDisplayKit/AsyncDisplayKit+Debug.h>
#import <AsyncDisplayKit/ASDisplayNode+Deprecated.h>
#import <AsyncDisplayKit/ASCollectionNode+Beta.h>@AsyncDisplayKit.ASEventLog;modulemap 文件中逐个列出文件名,提高了开发的灵活性和效率。5. 实战:Framework 使用 modulemap
PersonFramework,创建一个 iOS App Demo 工程。PersonFramework.modulemap 文件PersonFramework.modulemap 命名的 modulemap,在编译后被 Xcode 重命名为 module.modulemap,系统只认这个.modulemap 文件有多种写法,就拿上面的举例,存在3种写法:framework module PersonFramework {
// umbrella 不加 Header,则说明后面要跟文件夹路径。
umbrella "Headers"
export *
module * {
export *
}
}nubrella 后不加 Header,说明跟得是文件夹,文件夹中里面的内容和 Build Phases 中 Header public 部分的头文件描述的一致。会将 public 的头文件,最后放到 Headers 文件夹中。framework module PersonFramework {
umbrella header "PersonFramework.h"
export *
module * {
export *
}
}umbrella header + 伞头文件 意味着伞头文件里所有的 .h 将会被放到 Headers 文件夹中。且给外部访问framework module PersonFramework {
umbrella header "PersonFramework.h"
explicit module Worker {
header "Worker.h"
export *
}
explicit module Student {
header "Student.h"
export *
}
}6. Swift Framework modulemap
说明:PersonSwiftFramework.private.modulemap 的 private.modulemap 格式private.modulemap 模块名必须为 PersonSwiftFramework_Private/* module.modulemap */
framework module PersonSwiftFramework_Private {
module OCStudent {
header "OCStudent.h"
export *
}
}PersonSwiftFramework 是正常预设模块,通过 @import PersonSwiftFramework. 访问均符合预期。@import PersonSwiftFramework_Private.OCStudent;访问到的LDAndFramework/module-collections/ModulePractice7. Swift module - Swift 静态库的合并
1. Swift module 概念
.swiftmodule,其包含序列化过的 AST,也包含 SIL(Swift Intermediate Language,Swift 中间语言)2. 实战:Swift 静态库合并
cp -Rv -- "${BUILT_PRODUCTS_DIR}/" "${SOURCE_ROOT}/../Products",把产物拷贝到 products 目录下libtool -static FLSwiftLibA.framework/FLSwiftLibA FLSwiftLibB.framework/FLSwiftLibB -o libFLSwiftC.a 进行合并,发现报错.
├── FLSwiftLibA
├── Headers
│ ├── FLSwiftLibA-Swift.h
│ └── FLSwiftLibA.h
├── Info.plist
├── Modules
│ ├── FLSwiftLibA.swiftmodule
│ │ ├── Project
│ │ ├── x86_64-apple-ios-simulator.abi.json
│ │ ├── x86_64-apple-ios-simulator.swiftdoc
│ │ └── x86_64-apple-ios-simulator.swiftmodule
│ └── module.modulemap
└── _CodeSignature
├── CodeDirectory
├── CodeRequirements
├── CodeRequirements-1
├── CodeResources
└── CodeSignature// 头文件信息
HEADER_SEARCH_PATHS = $(inherited) "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibA.framework/Headers" "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibB.framework/Headers"FLSwiftLibA.swiftmodule 里面的 x86_64-apple-ios-simulator.swiftmodule swiftmodule 文件信息。OTHER_CFLAGS,其实就是配置 c、oc 编译器也就是 clang 的关于 swift 的信息。SWIFT_INCLUDE_PATHS = "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibA.framework/Modules/" "${SRCROOT}/FLStaticLibC/Public/FLSwiftLibB.framework/Modules/"module.modulemap 、.swiftmodule 文件夹在同一层目录。所以我们手动编译静态库之后,需要处理静态库产物的文件目录信息。3. 为什么 Swift 静态库在链接的时候需要配置2个信息?
Headers (包含 Objective-C/C 头文件) 和 .swiftmodule (包含 Swift 的模块接口信息)。 要让使用方(宿主 App 或其他模块)正确链接和使用这个静态库,必须同时处理好这两个部分的路径配置Headers/ 目录下)OTHER_CFLAGS = "-fmodule-map-file={path}"8. 静态库使用 Module 带来的问题
1. 存储与 I/O 开销显著增加
.pcm 文件体积膨胀.pcm 文件包含头文件的完整 AST(抽象语法树)及依赖元数据,其体积通常达原始头文件的 30–40 倍(例如 <vector> 头文件 20 KB → .pcm 800 KB)。大型项目累积 .pcm 可达 GB 级(如 LLVM 编译超 10 GB),加剧存储压力与 I/O 延迟(机械硬盘随机读写性能骤降)。.pcm 缓存路径(-fmodules-cache-path),多版本编译环境易因缓存失效导致重复生成,消耗额外磁盘与计算资源2. 编译效率降低
十二、hmap文件与 Header Maps
1. 头文件导入技术发展历史
1. 头文件查找流程
具体文件.h.h 放到 module,采用预编译的方式,产出二进制,节省编译时间。比如 #import <UIKit/UIKit.h>#import "ViewController.h" 背后是怎么工作的?是如何找到具体的文件内容的阶段1:基础路径搜索
"ViewController.h" 进行导入时,编译器会首先在当前源文件所在的本地目录(即当前 .m 或 .mm 文件所在的目录)查找头文件AppDelegate.m 中导入 ViewController.h 时,优先在 AppDelegate.m 同级目录查找ViewController.h,编译器会自动递归地搜索所有子目录-I- 编译选项禁用递归阶段2:工程配置路径搜索
.xcodeproj)的位置来解析ViewController.h 是某个框架或库的一部分,编译器还会在 "Framework Search Paths" 中指定的目录下查找。阶段3:高级优化机制
阶段4:错误处理机制
ViewController.h,它会报错指出找不到文件。fatal error: 'ViewController.h' file not found
#import "ViewController.h"
^~~~~~~~~~~~~~~~~~
1 error generated.2. 早期的 import
include: 是基础引入,编译过程中会被直接展开,其内容会插入到 #include 指令的位置。将 目标.h 文件中的内容一字不落地拷贝到当前文件中,并替换掉这句 #includeimport: 只引入一次。用于导入模块化的头文件,它遵循模块化的结构,可以提供更好的封装性。会加入缓存,判断 import 的内容之前已经引入过则不再引入。// Cache all of the lookups performed by this method. Many headers are
// multiply included, and the "pragma once" optimization prevents them from
// being relex/pp'd, but they would still have to search through a
// (potentially huge) series of SearchDirs to find it.
LookupFileCacheInfo &CacheLookup = LookupFileCache[Filename];Person.m#import <DynamicLibA/DynamicLibA.h>
@implementation Person
- (void)eat {
NSLog(@"Person eat");
}
@endDynamicLibA.h 内容如下#import <DynamicLibA/Student.h>
#import <DynamicLibA/Worker.h>
#import <DynamicLibA/Teacher.h>Person.m 中#import <DynamicLibA/Student.h>
#import <DynamicLibA/Worker.h>
#import <DynamicLibA/Teacher.h>
@implementation Person
- (void)eat {
NSLog(@"Person eat");
}
@end// Student.h
@interface Student: NSObject
- (void)study;
@endPerson.m 中。@interface Student: NSObject
- (void)study;
@end
#import <DynamicLibA/Worker.h>
#import <DynamicLibA/Teacher.h>
@implementation Person
- (void)eat {
NSLog(@"Person eat");
}
@end.m 可能会变得非常长。3. PCH(PreCompiled Header)
PreCompiled Header。早期做 iOS 开发的都看过 pch 文件。.m 文件前, 编译器会先对 .pch 里的内容进行预编译,将其变为一种二进制的中间格式缓存起来,便于后续的使用。当开始编译 .m 文件时,如果需要 PCH 里已经编译过的内容,直接读取即可,无须再次编译。.pch 文件中4. clang module
test.m 被编译成目标文件 test.o 时,当前的目标文件就代表了一个 Module 。2. 诞生背景
header search path 的时候,假设一个工作项目有10000个文件,一个类使用了10个头文件,要去10000个文件中分别查找10个头文件具体位置,这个查找过程是发生在编译阶段的。无疑会增加编译耗时。-I 参数提供 hmap 文件路径。来享受编译加速带来的红利。3. hmap 结构
-I 参数来指定 hmap 文件。复制目录路径查看所有的 hmap 文件,可以看到,hmap 文件不可读,必须用对应的工具解析,按照指定的格式解析。因为属于编译器的 scope,所以查看 LLVM 源码窥探下。uint32_t 类型的成员,表示 bucket 中键的哈希值或者是一个特殊值表示 bucket 的状态(例如,是否为空或者是一个冲突的bucket)uint32_t 类型的成员,通常用于存储键的前缀偏移量,它与 Suffix 一起定义了键在整个 Header Map 中的完整字符串uint32_t 类型的成员,用于存储键的后缀长度,与 Prefix 结合使用可以定位到键的具体字符串4. why hmap?
{
"ViewController.m": ['a.h', 'b.h', 'c.h', 'd.h']
}1. 文本映射方案的致命缺陷
方案 存储量 内存占用 问题 文本映射 (JSON) 约 5 MB 50+ MB 解析耗时 + 碎片化 HMap 二进制 约 800 KB 2 MB 直接内存映射 操作 JSON 方案 HMap 方案 加载文件 5 ms (读5MB) 0.01 ms (mmap) 解析数据 15 ms (JSON parse) 0 ms (直接访问) 查找键值 0.5 ms (遍历) 0.001 ms (哈希计算) 总耗时 ~20 ms ~0.01 ms 2000倍差距:当 ViewController.m 引入 4 个头文件时,仅查找就相差 80ms vs 0.04ms
2. HMap 的优点
1. 零解析内存映射
2. 路径压缩艺术
// 文本方案存储:
{"File1.h": "/path/to/project/Sources/File1.h", ...} // 重复100次"/path/to/project/Sources/"
// HMap 存储:
前缀: "/path/to/project/Sources/" (存储1次)
后缀: "File1.h", "File2.h" ... (只存文件名)3. CPU 缓存友好
5. 编写工具分析 hmap 文件
1. 读取 HMap 文件的代码
二进制数据
sizeOf(HMapHeader)
从二进制数据中读取出:header -----> 根据 Header 信息,按照 Bucket 格式,读取出 Bucket 信息 ------> 再根据 Bucket 的 Key、prefix、suffix 去 String Payload 区域读取数据2. 变成终端可使用的能力
objdump 一样,在终端命令行使用:CustomTools 文件夹.zshrc 文件里将路径添加进去。export PATH=~/CustomTools:$PATH.zshrc 文件后,在终端执行 souce .zshrc5. 编写工具生成 hmap 文件
1. 为什么要生成 hmap 文件
.m 文件有相同的头文件代码,造成编译浪费。-I 来指定 Header Search Path 信息。-I 后面可以跟:目录文件夹、.hmap 文件import <> :本质上就是 LD 编译参数 HEADERS_SEARCH_PATHS -Iimport "" :本质上就是 LD 编译参数 USER_HEADER_SEARCH_PATHS -iquoteHeader Search Path、System Header Search Path、User Header Search Path,区别是什么?<> 方式引入的文件"" 方式引入的文件.hmap 文件,那为什么还需要研究生成 hmap 文件?HMapStaticLib 里面就包含2个类文件 Person 类、继承自 Person 类的 Student 类。再创建一个使用 iOS App,使用该静态库。编译该 App,查看编译日志。 HMapStaticLibApp-project-headers.hmap 文件。利用上面制作的 HMapDump 工具。Use Header Map,静态库中所有头文件类型为 Project(只有 Project、Private、Public 3种类型,public 就是字面意思的公开,private 则代表 In Progress, project 才是通常意义上的 Private 含义)的情况,最终生成的 .hmap 文件中只会包含类似 #import "Student.h" 的键值引用。也就是说使用的地方,只有 #import "Student.h" 的这种方式才会走 hmap 策略,否则还是走 Header Search Path 来寻找头文件路径。#import <SomeFramework/SomeFrameworkExportedHeader.h> 是访问的标准做法。好处有3点:Build Setting 中虽然 Use Header Maps 为 YES,但是某些情况下 Header Maps 带来的加速福利没有享受到。Header Maps 带来的福利,老老实实依靠 Header Search Path 中提供的头文件所在的文件夹信息查找,或者递归查找。可能存在 n*m 的循环查找过程,效率很低,涉及的 IO 影响大型项目的编译构建时间,影响分发和发生问题时候的及时热修复。2. 如何生成 HMap 文件
/Users/unix_kernel/CustomTools 目录下,同时设置 .zshrc 可访问性。6. hmap 助力提升 iOS 项目编译速度
1. 实验一:静态库 + Header Search Paths + App 编译
1. 静态库设置
2. App 以 Header Search Paths 方式编译链接静态库
${SRCROOT}/HMapBenchMark-HeaderSearchPath/StaticLib/HMapStaticLib-HeaderSearchPath/HeadersViewController.m 中,#import "HMapStaticLib_HeaderSearchPath.h" 引入静态库的公共头文件,然后使用里面的类
会看到以下的编译日志。
编译日志里时间先放着。等以 HeaderMap 编译链接实现结束一起对比看看时间节省了多少。2. 实验二:静态库 + HeaderMap + App 编译
1. 静态库设置
${SRCROOT}/HMapStaticLib-HeaderMap/hmap(hmap 文件所在目录)2. App 以 HeaderMap 方式编译链接静态库
${SRCROOT}/HMapBenchMark-HeaderMap/StaticLib/HMapStaticLib-HeaderMap/HeadersViewController.m 中,#import "HMapStaticLib_HeaderMap.h" 引入静态库的公共头文件,然后使用里面的类
会看到以下的编译日志。3. 数据分析及结论
可以看到静态库先用 Header Search Paths 的方式,App 编译耗时6.5s。使用了自定义的 Header Map 文件后,App 编译耗时6.1s。编译耗时减少了0.4s,节省了6.6%。这是只有1个静态库且只有7个头文件的情况下测试得到的数据。真实项目中,如果静态库数量越多、项目文件目录长且嵌套复杂、头文件数量越多,以自定义 hmap 的方式将会在编译阶段节省更多的时间7. 工程化问题
如果每个工程项目都这么做,效率有点低,所以需要站在工程化的角度去设计,如何优化?post_install 钩子header_mappings_by_file_accessor 遍历所有头文件和 header_dir,由header_dir/header.h和header.h为key,以头文件搜索路径为value,组装成一个Hash<key,value>,生成所有组件pod头文件的json文件,再通过hmap工具将json文件转成hmap文件。.xcconfig 文件的 HEADER_SEARCH_PATHS 值,仅指向生成的新生成的 .hmap 文件,删除原来添加的搜索目录;USE_HEADERMAP 值,关闭对默认的 .hmap 文件的访问pod install 执行过程;Pods-MainApp-prebuilt.hmap);HEADER_SEARCH_PATHS = ${PODS_ROOT}/Headers/HMap/Pods-MainApp-prebuilt.hmap);USE_HEADERMAP = NO)。十一、dyld 及其工作流程
1. DYLD(dyld shared cache) 动态库共享缓存
存放路径为:/System/Libarey/Caches/com.apple.dyld/dyld_shared_cache_armX
其中,X 代表 ARM 处理器的指令集架构。V6、V7、V7s、arm64、arm64e。不同架构,对应的动态库缓存不一致。dyld2.cpp 文件,函数入口为 load 方法。ImageLoader* load(const char* path, const LoadContext& context, unsigned& cacheIndex);findInSharedCacheImage2. ASLR
1. 虚拟内存、物理内存、内存分页、ASLR
2. 未使用 ASLR 的问题
__TEXT 段__DATA 段0x0LC_SEGMENT(__TEXT) 中的 VM Address0x1000000000x4000size -l -m -x DDD 指令来查看 Mach-O 的内存分布_PAGEZERO 在真实的 Mach-O 文件中不存在,不占据大小。只在虚拟内存中存在。__TEXT 段内函数信息,这样子不够安全3. ASLR 诞生
0x10005000PAGEZEROR 地址代码运行起来函数真实地址 = ASLR-Offset + __PAGEZERO(arm64:0x100000000,其他:0x4000)+ 函数基于 Mach-O 的地址3. 概念
libdyld.dylib 给我们的程序提供在 runtime 期间能使用动态链接功能。比如 dlopen 能力,又或者是系统 lazy-binding 符号表,在运行之后,动态修正符号地址。objdump --macho --private-headers DSYMDemoInfo.plist 文件,加载相关信息,例如启动画面等,并建立沙盒环境以及进行权限检查LC_LOAD_DYLINKER load command 中,根据 name 路径信息,然后加载,dyld 开始执行。它的主要任务是加载应用程序的主可执行文件以及其他依赖的动态库Load command 14
cmd LC_LOAD_DYLIB
cmdsize 88
name /System/Library/Frameworks/Foundation.framework/Foundation (offset 24)
time stamp 2 Thu Jan 1 08:00:02 1970
current version 1953.255.0
compatibility version 300.0.0X86_64,dyld 会判断该架构能否在当前系统上运行EXECUTE,是不是一个可执行文件Load command 1
cmd LC_SEGMENT_64 // 指定加载命令的类型是 LC_SEGMENT_64,这代表一个 64 位的内存段命令。
cmdsize 792 // 表示这个加载命令的总大小是 792 字节。
segname __TEXT // 段名称,这里是 __TEXT 段,通常包含程序的指令和只读数据。
vmaddr 0x0000000100000000 // 指定了该段在内存中的虚拟地址
vmsize 0x0000000000004000 // 定义了该段在内存中的大小,这里是 16 KiB(4000 十六进制转换为十进制是 16384)。
fileoff 0 // 表示该段在文件中的偏移量,这里是文件的起始位置
filesize 16384 // 表示该段在文件中的实际大小,以字节为单位
maxprot r-x // 定义了该段(代码段)的最大保护级别,这里是 r-x(读和执行),表示该段可以被读取和执行,但不能写入。如果是数据段,则可读可写 rw-
initprot r-x // 定义了该段在加载到内存时的初始保护级别,与最大保护级别相同
nsects 9 // 表示该段包含的节(sections)数量,这里是 9 个
flags (none) // 该段没有设置任何特殊标志+load 方法和 C 的构造函数,以初始化静态变量LC_MAIN 命令,获取程序的入口地址,并调用 main 函数,启动应用程序的主执行流程4. dyld 到底做了什么
LC_DYLD_INFO_ONLYLoad command 4
cmd LC_DYLD_INFO_ONLY // 表示这是一个只包含 dyld 信息的加载命令,它通常不包含实际的 rebase、bind 等信息,而是提供给 dyld 用于优化链接过程的信息
cmdsize 48 // 这个命令的总大小是 48 字节
rebase_off 0 // 指示 rebase 信息在文件中的位置
rebase_size 0 // 指示 rebase 信息在文件中的大小
bind_off 0 // 指示 bind 信息的位置
bind_size 0 // 指示 bind 信息的大小
weak_bind_off 0 // 指示弱绑定信息的位置
weak_bind_size 0 // 指示弱绑定信息的大小
lazy_bind_off 0 // 指示延迟绑定信息的位置
lazy_bind_size 0 // 指示延迟绑定信息的大小
export_off 32768 // 导出表(export table)的位置
export_size 48 // 导出表(export table)的大小LC_LOAD_DYLIBLoad command 12
cmd LC_LOAD_DYLIB // 指明了 load command 的类型是 LC_LOAD_DYLIB,意味着接下来的信息是关于加载一个动态库的
cmdsize 56 // 这个命令的总大小是56字节
name /usr/lib/libSystem.B.dylib (offset 24) // 指定了要加载的动态库的路径和文件名。这里是 libSystem.B.dylib,位于 /usr/lib/ 目录下。(offset 24) 表示文件名在命令数据中的偏移量
time stamp 2 Thu Jan 1 08:00:02 1970 // 这是动态库的时间戳,用于版本检查。不过,这个时间戳通常是0或一个固定值,因为系统库的加载不依赖于时间戳
current version 1319.0.0 // 这是动态库的当前版本号,用于确保应用程序加载的是兼容的版本
compatibility version 1.0.0 // 这是动态库的兼容版本号,表明应用程序至少需要这个版本的库才能正常运行objdump --macho --private-headers /Users/unix_kernel/Library/Developer/Xcode/DerivedData/LLDBWithDSYM-bhspsnzkxcmhjjcekslnvrepukag/Build/Products/Debug-iphoneos/LLDBWithDSYM.app/LLDBWithDSYM | grep -A 7 'LC_LOAD_DYLIB'LC_DYSYMTABobjdump --macho --private-headers /Users/unix_kernel/Library/Developer/Xcode/DerivedData/LLDBWithDSYM-bhspsnzkxcmhjjcekslnvrepukag/Build/Products/Debug-iphoneos/LLDBWithDSYM.app/LLDBWithDSYM | grep -A 30 'LC_DYSYMTAB'LC_DYSYMTAB 对于动态链接非常重要,因为:indirectsymoff 69744 # 间接符号表在文件中的偏移地址
nindirectsyms 25 # 间接符号表包含25个条目文件布局:
0x0000 ┌───────────────────┐
│ Mach-O 头部 │
├───────────────────┤
│ 加载命令区域 │
│ (包含LC_DYSYMTAB) │
├───────────────────┤
│ __TEXT段 │
├───────────────────┤
│ __DATA段 │
├───────────────────┤
0x69744├───────────────────┤ ← 间接符号表开始位置
│ 条目0: 4字节 │
│ 条目1: 4字节 │
│ ... │
│ 条目24: 4字节 │
0x69844├───────────────────┤ ← 间接符号表结束位置
│ 其他数据 │
└───────────────────┘extreloff 0 # 外部重定位表偏移=0 → 不存在
nextrel 0 # 外部重定位条目数=0
locreloff 0 # 本地重定位表偏移=0 → 不存在
nlocrel 0 # 本地重定位条目数=0ilocalsym 0 # 本地符号起始索引=0
nlocalsym 173 # 本地符号数量=173
iextdefsym 173 # 外部定义符号起始索引=173
nextdefsym 8 # 外部定义符号数量=8
iundefsym 181 # 未定义符号起始索引=181
nundefsym 22 # 未定义符号数量=22LC_DYSYMTABcall _NSLog,标记为未定义符号(在 Mach-O 中体现为 n_type 的 N_UNDF 标志)。_DATA 段的 _la_symbol_ptr,一开始 _la_symbol_ptr 中的地址,指向 dyld_stub_bindercall _NSLog 去实现,然后 dyld 通过 dyld_stub_binder 去扫描加载的动态库的导出符号表信息,查询真正地址后发生 NSLog 调用,并把真实地址写入懒加载符号表(_la_symbol_prt)中。第二次及以后调用的时候,便可以从懒加载符号表中直接获取到真实地址了。LC_LOAD_DYLIB 提供)LC_MAINLoad command 11
cmd LC_MAIN // 里的 cmd 指明了加载命令的类型是 LC_MAIN,这个命令用于指定程序的主入口点
cmdsize 24 // 这个命令的大小是24字节,这是命令头加上任何尾部数据的总和
entryoff 16288 // 指定了程序入口点的偏移量,它是相对于文件的开始位置的。在这个例子中,入口点位于文件开头的第 16288 字节处。入口点通常是 main 函数的地址
stacksize 0 // 这个字段指定了为程序的主线程初始栈分配的大小。在这个例子中,栈大小被设置为0,这可能意味着栈的大小将使用默认值,或者在其他地方指定(例如,通过链接器的其他设置或命令行选项)5. main 函数
编写2个函数,main 函数和 test 函数,除了方法名不同,在汇编侧是一样的。test.c 文件中,只有一个 test 方法。没有 main 方法,然后用 gcc 发现链接报错。 gcc -nostartfiles -e_test test.c 发现可以编译通过,运行也没问题。6. dyld 加载过程
fork 函数,创建⼀个进程execve 或其衍⽣函数,在该进程上加载,执⾏ Mach-O⽂件Mach-O ⽂件加载到内存Mach-O 中的 mach_header,以确认它是有效的 Mach-O ⽂件mach_header 解析 load commands。根据解析结果,将程序各个部分加载到指定的地址空间,同时设置保护标记LC_LOAD_DYLINKEN中加载 dylddyld开始⼯作__dyld_start() 函数,通知 dyld 开始工作dyldbootstrap::start 函数,使 dyld ⾃身进⼊可运⾏状态dyld::_main 函数,dyld 的入口函数instantiateMainExecutable ,为主可执行文件创建镜像__attribute__((constructor)))LC_MAIN 查找设置程序⼊⼝函数,将胶⽔地址设置成⼊⼝函数地址,否则胶⽔地址为0dyld::_main 函数中继续执行dyld::_main→dyldbootstrap::start→__dyld_start(),dyld 配置完成,把控制权交给可执⾏⽂件的⼊⼝函数main(),继续后面的流程功能 链接器 (ld) dyld 静态库处理 ✅ 合并到可执行文件 ❌ 不处理 动态库依赖记录 ✅ 写入 LC_LOAD_DYLIB ❌ 只读取 地址分配 ❌ 仅预留空间 ✅ 实际分配内存地址 符号绑定 ❌ 只设置占位符 ✅ 实际解析地址 共享缓存 ❌ 无法访问 ✅ 直接映射使用 7. dyld 应用
dsc_extractor.cpp 代码可以看到是具备抽取能力的。修改源码,将 if 宏定义去掉。如下#include <stdio.h>
#include <stddef.h>
#include <dlfcn.h>
typedef int (*extractor_proc)(const char* shared_cache_file_path, const char* extraction_root_path,
void (^progress)(unsigned current, unsigned total));
int main(int argc, const char* argv[])
{
if ( argc != 3 ) {
fprintf(stderr, "usage: dsc_extractor <path-to-cache-file> <path-to-device-dir>\n");
return 1;
}
//void* handle = dlopen("/Volumes/my/src/dyld/build/Debug/dsc_extractor.bundle", RTLD_LAZY);
void* handle = dlopen("/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/usr/lib/dsc_extractor.bundle", RTLD_LAZY);
if ( handle == NULL ) {
fprintf(stderr, "dsc_extractor.bundle could not be loaded\n");
return 1;
}
extractor_proc proc = (extractor_proc)dlsym(handle, "dyld_shared_cache_extract_dylibs_progress");
if ( proc == NULL ) {
fprintf(stderr, "dsc_extractor.bundle did not have dyld_shared_cache_extract_dylibs_progress symbol\n");
return 1;
}
int result = (*proc)(argv[1], argv[2], ^(unsigned c, unsigned total) { printf("%d/%d\n", c, total); } );
fprintf(stderr, "dyld_shared_cache_extract_dylibs_progress() => %d\n", result);
return 0;
}clang++ dsc_extractor.cpp./dsc_extractor dyld_shared_cache_armv7s armv7s,代表将动态库提取到 armv7s 目录下。1. 插入动态库
__attribute__((constructor)) 是 GCC 和 Clang 编译器支持的一个属性,用于标记一个函数为构造函数,该函数会在程序加载动态库时自动执行。__attribute__((constructor)) 属性标记的函数会在 main 函数执行之前被调用。Inject 动态库,新建 Inject.m 文件#import <Foundation/Foundation.h>
__attribute__((constructor))
static void customConstructor(int argc, const char **argv) {
NSLog(@"Hello,I am an injected dynamic library!");
}Inject.framework 里的动态库拖到 App 工程根目录。DYLD_INSERT_LIBRARIES,value 为 InjectFunction 动态库路径,${SRCROOT}/Inject。
具体 DemoDYLD_INSERT_LIBRARIES 的方式实现。还可通过 dlopen("/path/to/your_lib.dylib", RTLD_NOW) 实现。2. 函数替换
__DATA,__interpose 是一个特殊的 Mach-O 段,用于实现函数插桩 (Function Interposing)。它允许你在不修改原始库代码的情况下,拦截并替换库中的某个函数Mach-O 文件结构
├── Header
├── Load Commands
└── Segments (每个段包含多个节)
├── __TEXT (代码段)
│ ├── __text
│ └── __cstring
└── __DATA (数据段) <-- __interpose 所在位置
├── __data
├── __bss
└── __interpose <-- 拦截功能的核心__interpose 通过在 __DATA 段中声明特殊结构,通知 dyld(动态链接器)进行函数重定向// 拦截结构体定义
struct interpose_t {
void* replacement; // 替换函数地址
void* original; // 原始函数地址
};__attribute__((used)) static struct { ... } ... __attribute__ ((section("__DATA, __interpose"))) = { ... }; 定义一个结构体,它包含两个指向函数的指针:replacement 和 replacee。结构体使用 __attribute__((used)) 属性标记,以避免编译器将其优化掉。同时,它被放置在 __DATA, __interpose 段,这个段是专门为函数插桩而设计的。InjectFunction 动态库,新建 InjectFunction.m 文件#import <Foundation/Foundation.h>
#define INTERPOSE(_replacement, _replacee) \
__attribute__((used)) static struct { \
const void* replacement; \
const void* replacee; \
} _interpose_##_replacee __attribute__ ((section("__DATA, __interpose"))) = { \
(const void*) (unsigned long) &_replacement, \
(const void*) (unsigned long) &_replacee \
};
void my_NSLog(NSString *format, ...) {
NSLog(@"Injected Function ---> %@", format);
}
INTERPOSE(my_NSLog, NSLog);InjectFunction.framework 里的动态库托到 App 工程根目录。DYLD_INSERT_LIBRARIES,value 为 InjectFunction 动态库路径,当有多个动态库的时候,中间用 : 把路径隔开。${SRCROOT}/Inject:${SRCROOT}/InjectFunction。8. 调试 dyld
dyld/libdyld.dylib/libclosured.dylib ,与系统做替换,风险较大。br set:breakpoint set 的简写,表示设置断点-n dyldbootstrap::start:-n 指定函数名称,dyldbootstrap::start 是 macOS/iOS 系统动态链接器 dyld 的入口函数。当程序启动时,系统会先执行此函数完成动态库加载和程序初始化-s dyld:-s 限定共享库范围,dyld 是 macOS/iOS 的核心动态链接器(路径通常为 /usr/lib/dyld),所有用户进程的启动都依赖它dyld、系统框架关键函数)。禁止在这些敏感位置设置断点,防止系统崩溃。0 (禁用),强制允许在任何地址设置断点,包括:dyld) 内部libobjc 的运行时函数)DYLD_PRINT_APIS: 打印 dyld 内几乎所有发生的调用。格式为:DYLD_PRINT_APIS=1 ${MachOPath}DYLD_PRINT_APIS=1 Users/unix_kernel/Library/Developer/Xcode/DerivedData/LLDBWithDSYM-bhspsnzkxcmhjjcekslnvrepukag/Build/Products/Debug-iphoneos/LLDBWithDSYM.app/LLDBWithDSYM DYLD_PRINT_LIBRARIES :打印在应用程序启动期间正在加载的所有动态库DYLD_PRINT_WARNINGS:打印 dyld 运行过程中的辅助信息DYLD_PATH:显示 dyld 搜索动态库的目录顺序DYLD_PRINT_ENV:显示 dyld 初始化的环境变量DLYD_PRINT_SEGMENTS:打印当前程序的 segment 信息DYLD_PRINT_STATISTICS:打印 pre-main 耗时DYLD_PRINT_INITIALIZERS:会在执行每个镜像(image)的初始化器(initializer)时打印出一行信息。这些初始化器包括 C++ 的静态构造函数以及使用 __attribute__((constructor)) 标记的函数。这个环境变量对于调试和分析程序启动时的初始化顺序和行为非常有用。十二、Mach-O
a.out 格式。Mach-O ⽂件格式保存了在编译过程和链接过程中产⽣的机器代码和数据,从⽽为静态链接和动态链接的代码提供了单⼀⽂件格式。loader.h#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 */1. 常见的 Mach-O 文件类型
.o 合并在一起.dSYM/Contents/Resource/DWARF/xx 常用于还原堆栈file 命令可以查看文件类型。find . -name "*.c" 比如在当前路径查找 .c 文件2. Universal Binary
lipo -info Testlipo Test -thin arm64 -o Test_arm64lipo -create Test_arm64 Test_armv7 -output Test_universalArchitectures->Architectures User-Defined->VALID_ARCHS,取2者的交集。
其中 Architectures->Architectures 的值 $(ARCHS_STANDARD) 是 Xcode 内置的环境变量,不同版本的 Xcode 值不一样,是通用的一些架构值。比如 Xcode9下,$(ARCHS_STANDARD) 可能为 arm64、armv7。Xcode 4下,$(ARCHS_STANDARD) 可能为 armv73. Mach-O 结构
__PAGEZERO 的 VM Size 有值,但是 File Size 为0,也就是说 __PAGEZERO 在 Mach-O 中不占据内存,但是程序运行起来之后,会占据虚拟内存。所以代码段在 Mach-O 中 File Offset 为0(如果前面的 __PAGEZERO 的 File size 有值,这里的 File Offset 就不为0)。4. Mach-O 文件如何查找地址
main.m 的代码void test1 () {}
void test2() {}
int globalValue;
int main () {
globalValue = 21;
globalValue = 20;
test1();
test2();
return 0;
}clang main.m -o mainobjdump --macho -d mainclang -c main.m -o main.oobjdump --macho -d main.o e8 00 00 00 00 callq _test1,其中 e8 是固定指令,代表 callq。后面的 00 00 00 00 是相对地址,test1 真正的地址是 下面 4e + 上面 00 00 00 00 的结果。这是一种近地址相对位移技术00 00 00 00 ?那系统如何确定函数真实地址?需要找个地方将这些符号存起来,然后再找个时机去把真实的地址写进去。需要重定位符号表,告诉链接器在链接阶段需要重定位objdump --macho --reloc main.o 指令查看 main.o 的重定位符号表。test1 的地址就是 0000004a 对应的数据。49 后面就是 4atest2 的地址就是 0000004f 对应的数据。4e 后面就是 4fglobalValue 的地址就是 0000003f 对应的数据,3c 3d 3e 3f,经过 48 8b 05 到 3f1. 如何找到函数的真实地址
100003f93: c7 00 14 00 00 00 movl $20, (%rax)
100003f99: e8 b2 ff ff ff callq _test1
100003f9e: e8 bd ff ff ff callq _test2
100003fa3: 31 c0 xorl %eax, %eaxb2 ff ff ff 可以看到后4位是 ff,所以是一个经过计算后的补码信息。所以需要计算得到原码信息。0x4E下一条指令地址+ b2 ff ff ff 就是函数 test1 的真实地址。但是 FF 是负的,所以 0x100003f9e - 0x4E = 0x100003F50 ,也就是 Text 段中 test1 的真实地址。2. 如何找到全局变量地址
objdump --macho -s main,可以看到地址值为 0x100008000下一条指令 + 0x407a = 0x100003f86 +0x407a = 0x10000800048 8d 05 ,iOS 小端序,所以是 0x407a100003f70: 55 pushq %rbp
100003f71: 48 89 e5 movq %rsp, %rbp
100003f74: 48 83 ec 10 subq $16, %rsp
100003f78: c7 45 fc 00 00 00 00 movl $0, -4(%rbp)
100003f7f: 48 8d 05 7a 40 00 00 leaq _globalValue(%rip), %rax
100003f86: c7 00 15 00 00 00 movl $21, (%rax)
100003f8c: 48 8d 05 6d 40 00 00 leaq _globalValue(%rip), %rax
100003f93: c7 00 14 00 00 00 movl $20, (%rax)0x100008000 和数据段的地址是一样的。十三、.dSYM 文件
1. 定义
.dSYM 文件就是保存 DWARF 格式的调试信息的文件。.DSYM (debugging symbol)文件是保存十六进制函数地址映射信息的中转文件,调试信息(symbols)都包含在该文件中。Xcode 工程每次编译运行都会生成新的 .DSYM 文件。默认情况下 debug 模式时不生成 .DSYM ,可以在 Build Settings -> Build Options -> Debug Information Format 后将值 DWARF 修改为 DWARF with DSYM File,这样再次编译运行就可以生成 .DSYM 文件。2. 探索
1. 如何生成
.dSYM 文件main.m 文件.DSYM 文件。使用 clang -g 参数也可以生成 DSYM 文件。指令为: clang -g -c main.m -o main.o__DWARF 的段,使用指令查看:objdump --macho --private-headers main.o__DWARFclang -g main.m -o main__DWARF 段。objdump --macho --private-headers mainnm -pa main。红色区域代表调试符号。clang -g1 main.m -o main,参数 -g1 用于生成 .dSYM 文件。dwarfdump main.dSYM 用于查看 .dSYM 文件debug map.o 文件中加载 DWARF.dSYM bundle__DWARF 段中。当去链接的时候,会把 __DWARF 段删掉,同时把所有的调试信息放在符号表中。2. Xcode Build Settings 中的
DWARF 和 DWARF with dSYM File 有啥区别?1. DWARF
.o 目标文件和最终可执行文件中App (Mach-O)
├── __TEXT (代码段)
├── __DATA (数据段)
└── __DWARF (调试段) // 包含所有调试信息2. DWARF with dSYM File
App (精简Mach-O) // 无调试信息
App.dSYM // 独立调试信息包
└── Contents
└── Resources
└── DWARF
└── App // 实际DWARF数据特性 DWARF DWARF with dSYM File 调试信息位置 嵌入在可执行文件中 分离到独立的.dSYM文件 文件大小 可执行文件体积大 可执行文件体积小 调试速度 调试启动快 调试启动稍慢 符号化能力 需要完整可执行文件 使用.dSYM文件即可 适用场景 开发调试阶段 发布/测试阶段 安全隐私 源码信息易暴露 保护源码信息 Bitcode支持 不支持 支持 默认配置 Debug模式默认 Release模式默认 3. 深入理解程序技术器
4 ExploreDSYM 0x103b84f17 -[ViewController visitArray] + 55 (ViewController.m:25) 的 +55 表示程序计数器(PC)距离函数入口点的字节偏移1. 程序执行流程
2. 为什么需要这个偏移量?
1. 精确代码定位
2. 优化代码分析
4. 剖析 .dSYM 符号化
DWARF with dSYM File 。设置模拟 crash(数组越界)console App 查看崩溃报告,因为有 .dSYM 文件,所以可以看到方法信息-------------------------------------
Translated Report (Full Report Below)
-------------------------------------
Incident Identifier: 2AA5A2DD-78A3-4FCC-807D-4523F88DBD0E
CrashReporter Key: B8015533-4AEE-8714-C435-0ABAB8898AE5
Hardware Model: MacBookPro11,4
Process: ExploreDSYM [23538]
Path: /Users/USER/Library/Developer/CoreSimulator/Devices/C099153B-7736-4241-A734-2514070F8E90/data/Containers/Bundle/Application/E85716C6-245A-4B16-BA20-1A030036DE46/ExploreDSYM.app/ExploreDSYM
Identifier: com.unix.kernel.ExploreDSYM
Version: 1.0 (1)
Code Type: X86-64 (Native)
Role: Foreground
Parent Process: launchd_sim [18357]
Coalition: com.apple.CoreSimulator.SimDevice.C099153B-7736-4241-A734-2514070F8E90 [8102]
Responsible Process: SimulatorTrampoline [624]
Date/Time: 2025-06-19 17:15:50.0523 +0800
Launch Time: 2025-06-19 17:15:47.7752 +0800
OS Version: macOS 12.7.6 (21H1320)
Release Type: User
Report Version: 104
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Triggered by Thread: 0
Last Exception Backtrace:
0 CoreFoundation 0x7ff80042889b __exceptionPreprocess + 226
1 libobjc.A.dylib 0x7ff80004dba3 objc_exception_throw + 48
2 CoreFoundation 0x7ff8004b0019 -[__NSCFString characterAtIndex:].cold.1 + 0
3 CoreFoundation 0x7ff80035282d +[NSConstantArray automaticallyNotifiesObserversForKey:] + 0
4 ExploreDSYM 0x103b84f17 -[ViewController visitArray] + 55 (ViewController.m:25)
5 ExploreDSYM 0x103b84e64 __29-[ViewController viewDidLoad]_block_invoke + 36 (ViewController.m:20)
6 libdispatch.dylib 0x7ff80013ca3a _dispatch_client_callout + 8
7 libdispatch.dylib 0x7ff80013fe87 _dispatch_continuation_pop + 715
8 libdispatch.dylib 0x7ff80015534a _dispatch_source_invoke + 2046
9 libdispatch.dylib 0x7ff80014c1e9 _dispatch_main_queue_drain + 1015
10 libdispatch.dylib 0x7ff80014bde4 _dispatch_main_queue_callback_4CF + 31
11 CoreFoundation 0x7ff800387b1f __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
12 CoreFoundation 0x7ff800382436 __CFRunLoopRun + 2482
13 CoreFoundation 0x7ff8003816a7 CFRunLoopRunSpecific + 560
14 GraphicsServices 0x7ff809cb128a GSEventRunModal + 139
15 UIKitCore 0x10c80aad3 -[UIApplication _run] + 994
16 UIKitCore 0x10c80f9ef UIApplicationMain + 123
17 ExploreDSYM 0x103b851be main + 110 (main.m:17)
18 dyld_sim 0x103dc12bf start_sim + 10
19 dyld 0x1055ce52e start + 462
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 libsystem_kernel.dylib 0x7ff83611cfce __pthread_kill + 10
1 libsystem_pthread.dylib 0x7ff8361741ff pthread_kill + 263
2 libsystem_c.dylib 0x7ff800132fe0 abort + 130
3 libc++abi.dylib 0x7ff800258742 abort_message + 241
4 libc++abi.dylib 0x7ff80024995d demangling_terminate_handler() + 266
5 libobjc.A.dylib 0x7ff800032082 _objc_terminate() + 96
6 libc++abi.dylib 0x7ff800257b65 std::__terminate(void (*)()) + 8
7 libc++abi.dylib 0x7ff800257b16 std::terminate() + 54
8 libdispatch.dylib 0x7ff80013ca4e _dispatch_client_callout + 28
9 libdispatch.dylib 0x7ff80013fe87 _dispatch_continuation_pop + 715
10 libdispatch.dylib 0x7ff80015534a _dispatch_source_invoke + 2046
11 libdispatch.dylib 0x7ff80014c1e9 _dispatch_main_queue_drain + 1015
12 libdispatch.dylib 0x7ff80014bde4 _dispatch_main_queue_callback_4CF + 31
13 CoreFoundation 0x7ff800387b1f __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
14 CoreFoundation 0x7ff800382436 __CFRunLoopRun + 2482
15 CoreFoundation 0x7ff8003816a7 CFRunLoopRunSpecific + 560
16 GraphicsServices 0x7ff809cb128a GSEventRunModal + 139
17 UIKitCore 0x10c80aad3 -[UIApplication _run] + 994
18 UIKitCore 0x10c80f9ef UIApplicationMain + 123
19 ExploreDSYM 0x103b851be main + 110 (main.m:17)
20 dyld_sim 0x103dc12bf start_sim + 10
21 dyld 0x1055ce52e start + 462
Thread 1:
0 libsystem_pthread.dylib 0x7ff83616ff48 start_wqthread + 0
Thread 2:
0 libsystem_pthread.dylib 0x7ff83616ff48 start_wqthread + 0
Thread 3:
0 libsystem_pthread.dylib 0x7ff83616ff48 start_wqthread + 0
Thread 4:: com.apple.uikit.eventfetch-thread
0 libsystem_kernel.dylib 0x7ff83611693a mach_msg_trap + 10
1 libsystem_kernel.dylib 0x7ff836116ca8 mach_msg + 56
2 CoreFoundation 0x7ff80038788e __CFRunLoopServiceMachPort + 145
3 CoreFoundation 0x7ff800381fdf __CFRunLoopRun + 1371
4 CoreFoundation 0x7ff8003816a7 CFRunLoopRunSpecific + 560
5 Foundation 0x7ff800c568b4 -[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 213
6 Foundation 0x7ff800c56b2d -[NSRunLoop(NSRunLoop) runUntilDate:] + 72
7 UIKitCore 0x10c8e0286 -[UIEventFetcher threadMain] + 535
8 Foundation 0x7ff800c8011b __NSThread__start__ + 1009
9 libsystem_pthread.dylib 0x7ff8361744e1 _pthread_start + 125
10 libsystem_pthread.dylib 0x7ff83616ff6b thread_start + 15
Thread 5:
0 libsystem_pthread.dylib 0x7ff83616ff48 start_wqthread + 0
Thread 6:
0 libsystem_pthread.dylib 0x7ff83616ff48 start_wqthread + 0
Thread 7:
0 libsystem_pthread.dylib 0x7ff83616ff48 start_wqthread + 0
Thread 0 crashed with X86 Thread State (64-bit):
rax: 0x0000000000000000 rbx: 0x0000000105649600 rcx: 0x00007ff7bc379b98 rdx: 0x0000000000000000
rdi: 0x0000000000000103 rsi: 0x0000000000000006 rbp: 0x00007ff7bc379bc0 rsp: 0x00007ff7bc379b98
r8: 0x00007ff7bc379a60 r9: 0x00007ff7bc379cc0 r10: 0x0000000000000000 r11: 0x0000000000000246
r12: 0x0000000000000103 r13: 0x0000003000000008 r14: 0x0000000000000006 r15: 0x0000000000000016
rip: 0x00007ff83611cfce rfl: 0x0000000000000246 cr2: 0x0000000000000000
Logical CPU: 0
Error Code: 0x02000148
Trap Number: 133
Binary Images:
0x7ff836115000 - 0x7ff83614cfff libsystem_kernel.dylib (*) <2fe67e94-4a5e-3506-9e02-502f7270f7ef> /usr/lib/system/libsystem_kernel.dylib
0x7ff83616e000 - 0x7ff836179ff7 libsystem_pthread.dylib (*) <5a5f7316-85b7-315e-baf3-76211ee65604> /usr/lib/system/libsystem_pthread.dylib
0x7ff8000b5000 - 0x7ff800139ff7 libsystem_c.dylib (*) <8a60f5c1-ea1f-352b-b778-967be44e3677> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/system/libsystem_c.dylib
0x7ff800248000 - 0x7ff80025dffb libc++abi.dylib (*) <ae8cbd53-0926-3251-b648-6f32d9330a50> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libc++abi.dylib
0x7ff80002c000 - 0x7ff80005ffe9 libobjc.A.dylib (*) <2a7a213a-fdb2-311c-81d7-efdfd9ddf25a> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libobjc.A.dylib
0x7ff80013a000 - 0x7ff800185ff3 libdispatch.dylib (*) <59be51c1-e9f3-3a60-8108-cd70ae082897> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/system/libdispatch.dylib
0x7ff800303000 - 0x7ff80068bffc com.apple.CoreFoundation (6.9) <2be0f79f-8b25-3614-9e7e-dbac565f72dd> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
0x7ff809cae000 - 0x7ff809cb5ff2 com.apple.GraphicsServices (1.0) <16365e42-1d5c-363d-84d1-3bb290a43253> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/GraphicsServices.framework/GraphicsServices
0x10b9c9000 - 0x10d494fff com.apple.UIKitCore (1.0) <adb282b1-2fb2-38e0-8492-47f9443eb1ef> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore
0x103b83000 - 0x103b86fff com.unix.kernel.ExploreDSYM (1.0) <be3ba671-c69f-3a68-9d76-310c4e150256> /Users/USER/Library/Developer/CoreSimulator/Devices/C099153B-7736-4241-A734-2514070F8E90/data/Containers/Bundle/Application/E85716C6-245A-4B16-BA20-1A030036DE46/ExploreDSYM.app/ExploreDSYM
0x103dbf000 - 0x103e1efff dyld_sim (*) <6fb74554-3370-3677-93d4-7f7a01ea6a80> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim
0x1055c9000 - 0x105634fff dyld (*) <eea022bb-a6ab-3cd1-8ac1-54ce8cfd3333> /usr/lib/dyld
0x7ff8006fe000 - 0x7ff80102eff4 com.apple.Foundation (6.9) <86cd050d-44fc-3045-a1f3-8ad5047b329e> /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework/Foundation
EOF
-----------
Full Report
-----------
{"app_name":"ExploreDSYM","timestamp":"2025-06-19 17:15:55.00 +0800","app_version":"1.0","slice_uuid":"be3ba671-c69f-3a68-9d76-310c4e150256","build_version":"1","platform":7,"bundleID":"com.unix.kernel.ExploreDSYM","share_with_app_devs":0,"is_first_party":0,"bug_type":"309","os_version":"macOS 12.7.6 (21H1320)","incident_id":"2AA5A2DD-78A3-4FCC-807D-4523F88DBD0E","name":"ExploreDSYM"}
{
"uptime" : 110000,
"procLaunch" : "2025-06-19 17:15:47.7752 +0800",
"procRole" : "Foreground",
"version" : 2,
"userID" : 501,
"deployVersion" : 210,
"modelCode" : "MacBookPro11,4",
"procStartAbsTime" : 110816195256181,
"coalitionID" : 8102,
"osVersion" : {
"train" : "macOS 12.7.6",
"build" : "21H1320",
"releaseType" : "User"
},
"captureTime" : "2025-06-19 17:15:50.0523 +0800",
"incident" : "2AA5A2DD-78A3-4FCC-807D-4523F88DBD0E",
"bug_type" : "309",
"pid" : 23538,
"procExitAbsTime" : 110818470471618,
"cpuType" : "X86-64",
"procName" : "ExploreDSYM",
"procPath" : "\/Users\/USER\/Library\/Developer\/CoreSimulator\/Devices\/C099153B-7736-4241-A734-2514070F8E90\/data\/Containers\/Bundle\/Application\/E85716C6-245A-4B16-BA20-1A030036DE46\/ExploreDSYM.app\/ExploreDSYM",
"bundleInfo" : {"CFBundleShortVersionString":"1.0","CFBundleVersion":"1","CFBundleIdentifier":"com.unix.kernel.ExploreDSYM"},
"storeInfo" : {"deviceIdentifierForVendor":"9BE90473-1F72-514D-A6F8-2A4F5F1C42D7","thirdParty":true},
"parentProc" : "launchd_sim",
"parentPid" : 18357,
"coalitionName" : "com.apple.CoreSimulator.SimDevice.C099153B-7736-4241-A734-2514070F8E90",
"crashReporterKey" : "B8015533-4AEE-8714-C435-0ABAB8898AE5",
"responsiblePid" : 624,
"responsibleProc" : "SimulatorTrampoline",
"wakeTime" : 27225,
"sleepWakeUUID" : "231A480C-858C-466F-AA42-E97E3B5B94AB",
"sip" : "enabled",
"isCorpse" : 1,
"exception" : {"codes":"0x0000000000000000, 0x0000000000000000","rawCodes":[0,0],"type":"EXC_CRASH","signal":"SIGABRT"},
"asiBacktraces" : ["0 CoreFoundation 0x00007ff8004288ab __exceptionPreprocess + 242\n1 libobjc.A.dylib 0x00007ff80004dba3 objc_exception_throw + 48\n2 CoreFoundation 0x00007ff8004b0019 -[__NSCFString characterAtIndex:].cold.1 + 0\n3 CoreFoundation 0x00007ff80035282d +[NSConstantArray automaticallyNotifiesObserversForKey:] + 0\n4 ExploreDSYM 0x0000000103b84f17 -[ViewController visitArray] + 55\n5 ExploreDSYM 0x0000000103b84e64 __29-[ViewController viewDidLoad]_block_invoke + 36\n6 libdispatch.dylib 0x00007ff80013ca3a _dispatch_client_callout + 8\n7 libdispatch.dylib 0x00007ff80013fe87 _dispatch_continuation_pop + 715\n8 libdispatch.dylib 0x00007ff80015534a _dispatch_source_invoke + 2046\n9 libdispatch.dylib 0x00007ff80014c1e9 _dispatch_main_queue_drain + 1015\n10 libdispatch.dylib 0x00007ff80014bde4 _dispatch_main_queue_callback_4CF + 31\n11 CoreFoundation 0x00007ff800387b1f __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9\n12 CoreFoundation 0x00007ff800382436 __CFRunLoopRun + 2482\n13 CoreFoundation 0x00007ff8003816a7 CFRunLoopRunSpecific + 560\n14 GraphicsServices 0x00007ff809cb128a GSEventRunModal + 139\n15 UIKitCore 0x000000010c80aad3 -[UIApplication _run] + 994\n16 UIKitCore 0x000000010c80f9ef UIApplicationMain + 123\n17 ExploreDSYM 0x0000000103b851be main + 110\n18 dyld 0x0000000103dc12bf start_sim + 10\n19 ??? 0x00000001055ce52e 0x0 + 4384941358"],
"extMods" : {"caller":{"thread_create":0,"thread_set_state":0,"task_for_pid":0},"system":{"thread_create":0,"thread_set_state":627,"task_for_pid":9},"targeted":{"thread_create":0,"thread_set_state":0,"task_for_pid":0},"warnings":0},
"lastExceptionBacktrace" : [{"imageOffset":1202331,"symbol":"__exceptionPreprocess","symbolLocation":226,"imageIndex":6},{"imageOffset":138147,"symbol":"objc_exception_throw","symbolLocation":48,"imageIndex":4},{"imageOffset":1757209,"symbol":"-[__NSCFString characterAtIndex:].cold.1","symbolLocation":0,"imageIndex":6},{"imageOffset":325677,"symbol":"+[NSConstantArray automaticallyNotifiesObserversForKey:]","symbolLocation":0,"imageIndex":6},{"imageOffset":7959,"sourceLine":25,"sourceFile":"ViewController.m","symbol":"-[ViewController visitArray]","imageIndex":9,"symbolLocation":55},{"imageOffset":7780,"sourceLine":20,"sourceFile":"ViewController.m","symbol":"__29-[ViewController viewDidLoad]_block_invoke","imageIndex":9,"symbolLocation":36},{"imageOffset":10810,"symbol":"_dispatch_client_callout","symbolLocation":8,"imageIndex":5},{"imageOffset":24199,"symbol":"_dispatch_continuation_pop","symbolLocation":715,"imageIndex":5},{"imageOffset":111434,"symbol":"_dispatch_source_invoke","symbolLocation":2046,"imageIndex":5},{"imageOffset":74217,"symbol":"_dispatch_main_queue_drain","symbolLocation":1015,"imageIndex":5},{"imageOffset":73188,"symbol":"_dispatch_main_queue_callback_4CF","symbolLocation":31,"imageIndex":5},{"imageOffset":543519,"symbol":"__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__","symbolLocation":9,"imageIndex":6},{"imageOffset":521270,"symbol":"__CFRunLoopRun","symbolLocation":2482,"imageIndex":6},{"imageOffset":517799,"symbol":"CFRunLoopRunSpecific","symbolLocation":560,"imageIndex":6},{"imageOffset":12938,"symbol":"GSEventRunModal","symbolLocation":139,"imageIndex":7},{"imageOffset":14949075,"symbol":"-[UIApplication _run]","symbolLocation":994,"imageIndex":8},{"imageOffset":14969327,"symbol":"UIApplicationMain","symbolLocation":123,"imageIndex":8},{"imageOffset":8638,"sourceLine":17,"sourceFile":"main.m","symbol":"main","imageIndex":9,"symbolLocation":110},{"imageOffset":8895,"symbol":"start_sim","symbolLocation":10,"imageIndex":10},{"imageOffset":21806,"symbol":"start","symbolLocation":462,"imageIndex":11}],
"faultingThread" : 0,
"threads" : [{"triggered":true,"id":1036283,"threadState":{"r13":{"value":206158430216},"rax":{"value":0},"rflags":{"value":582},"cpu":{"value":0},"r14":{"value":6},"rsi":{"value":6},"r8":{"value":140701991410272},"cr2":{"value":0},"rdx":{"value":0},"r10":{"value":0},"r9":{"value":140701991410880},"r15":{"value":22},"rbx":{"value":4385445376,"symbolLocation":0,"symbol":"_main_thread"},"trap":{"value":133},"err":{"value":33554760},"r11":{"value":582},"rip":{"value":140704035753934,"matchesCrashFrame":1},"rbp":{"value":140701991410624},"rsp":{"value":140701991410584},"r12":{"value":259},"rcx":{"value":140701991410584},"flavor":"x86_THREAD_STATE","rdi":{"value":259}},"queue":"com.apple.main-thread","frames":[{"imageOffset":32718,"symbol":"__pthread_kill","symbolLocation":10,"imageIndex":0},{"imageOffset":25087,"symbol":"pthread_kill","symbolLocation":263,"imageIndex":1},{"imageOffset":516064,"symbol":"abort","symbolLocation":130,"imageIndex":2},{"imageOffset":67394,"symbol":"abort_message","symbolLocation":241,"imageIndex":3},{"imageOffset":6493,"symbol":"demangling_terminate_handler()","symbolLocation":266,"imageIndex":3},{"imageOffset":24706,"symbol":"_objc_terminate()","symbolLocation":96,"imageIndex":4},{"imageOffset":64357,"symbol":"std::__terminate(void (*)())","symbolLocation":8,"imageIndex":3},{"imageOffset":64278,"symbol":"std::terminate()","symbolLocation":54,"imageIndex":3},{"imageOffset":10830,"symbol":"_dispatch_client_callout","symbolLocation":28,"imageIndex":5},{"imageOffset":24199,"symbol":"_dispatch_continuation_pop","symbolLocation":715,"imageIndex":5},{"imageOffset":111434,"symbol":"_dispatch_source_invoke","symbolLocation":2046,"imageIndex":5},{"imageOffset":74217,"symbol":"_dispatch_main_queue_drain","symbolLocation":1015,"imageIndex":5},{"imageOffset":73188,"symbol":"_dispatch_main_queue_callback_4CF","symbolLocation":31,"imageIndex":5},{"imageOffset":543519,"symbol":"__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__","symbolLocation":9,"imageIndex":6},{"imageOffset":521270,"symbol":"__CFRunLoopRun","symbolLocation":2482,"imageIndex":6},{"imageOffset":517799,"symbol":"CFRunLoopRunSpecific","symbolLocation":560,"imageIndex":6},{"imageOffset":12938,"symbol":"GSEventRunModal","symbolLocation":139,"imageIndex":7},{"imageOffset":14949075,"symbol":"-[UIApplication _run]","symbolLocation":994,"imageIndex":8},{"imageOffset":14969327,"symbol":"UIApplicationMain","symbolLocation":123,"imageIndex":8},{"imageOffset":8638,"sourceLine":17,"sourceFile":"main.m","symbol":"main","imageIndex":9,"symbolLocation":110},{"imageOffset":8895,"symbol":"start_sim","symbolLocation":10,"imageIndex":10},{"imageOffset":21806,"symbol":"start","symbolLocation":462,"imageIndex":11}]},{"id":1036302,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":1}]},{"id":1036303,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":1}]},{"id":1036304,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":1}]},{"id":1036305,"name":"com.apple.uikit.eventfetch-thread","frames":[{"imageOffset":6458,"symbol":"mach_msg_trap","symbolLocation":10,"imageIndex":0},{"imageOffset":7336,"symbol":"mach_msg","symbolLocation":56,"imageIndex":0},{"imageOffset":542862,"symbol":"__CFRunLoopServiceMachPort","symbolLocation":145,"imageIndex":6},{"imageOffset":520159,"symbol":"__CFRunLoopRun","symbolLocation":1371,"imageIndex":6},{"imageOffset":517799,"symbol":"CFRunLoopRunSpecific","symbolLocation":560,"imageIndex":6},{"imageOffset":5605556,"symbol":"-[NSRunLoop(NSRunLoop) runMode:beforeDate:]","symbolLocation":213,"imageIndex":12},{"imageOffset":5606189,"symbol":"-[NSRunLoop(NSRunLoop) runUntilDate:]","symbolLocation":72,"imageIndex":12},{"imageOffset":15823494,"symbol":"-[UIEventFetcher threadMain]","symbolLocation":535,"imageIndex":8},{"imageOffset":5775643,"symbol":"__NSThread__start__","symbolLocation":1009,"imageIndex":12},{"imageOffset":25825,"symbol":"_pthread_start","symbolLocation":125,"imageIndex":1},{"imageOffset":8043,"symbol":"thread_start","symbolLocation":15,"imageIndex":1}]},{"id":1036306,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":1}]},{"id":1036307,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":1}]},{"id":1036308,"frames":[{"imageOffset":8008,"symbol":"start_wqthread","symbolLocation":0,"imageIndex":1}]}],
"usedImages" : [
{
"source" : "P",
"arch" : "x86_64",
"base" : 140704035721216,
"size" : 229376,
"uuid" : "2fe67e94-4a5e-3506-9e02-502f7270f7ef",
"path" : "\/usr\/lib\/system\/libsystem_kernel.dylib",
"name" : "libsystem_kernel.dylib"
},
{
"source" : "P",
"arch" : "x86_64",
"base" : 140704036085760,
"size" : 49144,
"uuid" : "5a5f7316-85b7-315e-baf3-76211ee65604",
"path" : "\/usr\/lib\/system\/libsystem_pthread.dylib",
"name" : "libsystem_pthread.dylib"
},
{
"source" : "P",
"arch" : "x86_64",
"base" : 140703129358336,
"size" : 544760,
"uuid" : "8a60f5c1-ea1f-352b-b778-967be44e3677",
"path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/usr\/lib\/system\/libsystem_c.dylib",
"name" : "libsystem_c.dylib"
},
{
"source" : "P",
"arch" : "x86_64",
"base" : 140703131009024,
"size" : 90108,
"uuid" : "ae8cbd53-0926-3251-b648-6f32d9330a50",
"path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/usr\/lib\/libc++abi.dylib",
"name" : "libc++abi.dylib"
},
{
"source" : "P",
"arch" : "x86_64",
"base" : 140703128797184,
"size" : 212970,
"uuid" : "2a7a213a-fdb2-311c-81d7-efdfd9ddf25a",
"path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/usr\/lib\/libobjc.A.dylib",
"name" : "libobjc.A.dylib"
},
{
"source" : "P",
"arch" : "x86_64",
"base" : 140703129903104,
"size" : 311284,
"uuid" : "59be51c1-e9f3-3a60-8108-cd70ae082897",
"path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/usr\/lib\/system\/libdispatch.dylib",
"name" : "libdispatch.dylib"
},
{
"source" : "P",
"arch" : "x86_64",
"base" : 140703131774976,
"CFBundleShortVersionString" : "6.9",
"CFBundleIdentifier" : "com.apple.CoreFoundation",
"size" : 3706877,
"uuid" : "2be0f79f-8b25-3614-9e7e-dbac565f72dd",
"path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/System\/Library\/Frameworks\/CoreFoundation.framework\/CoreFoundation",
"name" : "CoreFoundation",
"CFBundleVersion" : "1953.300"
},
{
"source" : "P",
"arch" : "x86_64",
"base" : 140703292907520,
"CFBundleShortVersionString" : "1.0",
"CFBundleIdentifier" : "com.apple.GraphicsServices",
"size" : 32755,
"uuid" : "16365e42-1d5c-363d-84d1-3bb290a43253",
"path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/System\/Library\/PrivateFrameworks\/GraphicsServices.framework\/GraphicsServices",
"name" : "GraphicsServices",
"CFBundleVersion" : "1.0"
},
{
"source" : "P",
"arch" : "x86_64",
"base" : 4489777152,
"CFBundleShortVersionString" : "1.0",
"CFBundleIdentifier" : "com.apple.UIKitCore",
"size" : 28098560,
"uuid" : "adb282b1-2fb2-38e0-8492-47f9443eb1ef",
"path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/System\/Library\/PrivateFrameworks\/UIKitCore.framework\/UIKitCore",
"name" : "UIKitCore",
"CFBundleVersion" : "6209"
},
{
"source" : "P",
"arch" : "x86_64",
"base" : 4357369856,
"CFBundleShortVersionString" : "1.0",
"CFBundleIdentifier" : "com.unix.kernel.ExploreDSYM",
"size" : 16384,
"uuid" : "be3ba671-c69f-3a68-9d76-310c4e150256",
"path" : "\/Users\/USER\/Library\/Developer\/CoreSimulator\/Devices\/C099153B-7736-4241-A734-2514070F8E90\/data\/Containers\/Bundle\/Application\/E85716C6-245A-4B16-BA20-1A030036DE46\/ExploreDSYM.app\/ExploreDSYM",
"name" : "ExploreDSYM",
"CFBundleVersion" : "1"
},
{
"source" : "P",
"arch" : "x86_64",
"base" : 4359712768,
"size" : 393216,
"uuid" : "6fb74554-3370-3677-93d4-7f7a01ea6a80",
"path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/usr\/lib\/dyld_sim",
"name" : "dyld_sim"
},
{
"source" : "P",
"arch" : "x86_64",
"base" : 4384919552,
"size" : 442368,
"uuid" : "eea022bb-a6ab-3cd1-8ac1-54ce8cfd3333",
"path" : "\/usr\/lib\/dyld",
"name" : "dyld"
},
{
"source" : "P",
"arch" : "x86_64",
"base" : 140703135948800,
"CFBundleShortVersionString" : "6.9",
"CFBundleIdentifier" : "com.apple.Foundation",
"size" : 9637877,
"uuid" : "86cd050d-44fc-3045-a1f3-8ad5047b329e",
"path" : "\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Library\/Developer\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/System\/Library\/Frameworks\/Foundation.framework\/Foundation",
"name" : "Foundation",
"CFBundleVersion" : "1953.300"
}
],
"sharedCache" : {
"base" : 140703128616960,
"size" : 3002335232,
"uuid" : "229cb26a-91c2-3a54-ac19-858f5ac3393c"
},
"vmSummary" : "ReadOnly portion of Libraries: Total=647.6M resident=0K(0%) swapped_out_or_unallocated=647.6M(100%)\nWritable regions: Total=612.5M written=0K(0%) resident=0K(0%) swapped_out=0K(0%) unallocated=612.5M(100%)\n\n VIRTUAL REGION \nREGION TYPE SIZE COUNT (non-coalesced) \n=========== ======= ======= \nActivity Tracing 256K 1 \nColorSync 32K 4 \nFoundation 16K 1 \nKernel Alloc Once 8K 1 \nMALLOC 216.4M 40 \nMALLOC guard page 32K 7 \nMALLOC_NANO (reserved) 384.0M 1 reserved VM address space (unallocated)\nSTACK GUARD 56.0M 8 \nStack 11.6M 8 \nVM_ALLOCATE 28K 3 \n__DATA 4836K 257 \n__DATA_CONST 23.0M 270 \n__DATA_DIRTY 26K 12 \n__FONT_DATA 4K 1 \n__LINKEDIT 350.8M 18 \n__OBJC_RO 28.4M 1 \n__OBJC_RW 882K 1 \n__TEXT 296.8M 278 \ndyld private memory 1280K 2 \nmapped file 37.1M 7 \nshared memory 16K 1 \n=========== ======= ======= \nTOTAL 1.4G 922 \nTOTAL, minus reserved VM space 1.0G 922 \n",
"legacyInfo" : {
"threadTriggered" : {
"queue" : "com.apple.main-thread"
}
},
"trialInfo" : {
"rollouts" : [
{
"rolloutId" : "6112e14f37f5d11121dcd519",
"factorPackIds" : {
"SIRI_TEXT_TO_SPEECH" : "634710168e8be655c1316aaa"
},
"deploymentId" : 240000231
},
{
"rolloutId" : "6112dda2fc54bc3389840642",
"factorPackIds" : {
"SIRI_DICTATION_ASSETS" : "620aec83b02b354d3afd2f50"
},
"deploymentId" : 240000145
}
],
"experiments" : [
]
}
}4 ExploreDSYM 0x103b84f17 -[ViewController visitArray] + 55 (ViewController.m:25) Crash 日志中的符号地址是偏移后的还是偏移前的?4 ExploreDSYM 0x103b84f17 -[ViewController visitArray] + 55 (ViewController.m:25),其中 +55 偏移量表示程序计数器(PC)距离函数入口点的字节偏移量,所以 RuntimeAddress = 0x103b84f17,PCOffset = 0x37(十进制55,十六进制37)ExploreDSYM 0x103b83000 - 0x103b86fff com.unix.kernel.ExploreDSYM (1.0) <be3ba671-c69f-3a68-9d76-310c4e150256> /Users/USER/Library/Developer/CoreSimulator/Devices/C099153B-7736-4241-A734-2514070F8E90/data/Containers/Bundle/Application/E85716C6-245A-4B16-BA20-1A030036DE46/ExploreDSYM.app/ExploreDSYMdwarfdump --lookup 地址 --arch 架构 {AppName}.app.dSYM 来找到相关信息,里面有符号名、文件名、代码行数,具体为:dwarfdump --lookup 0x100001EE0 ExploreDSYM.app.dSYM 验证,发现找到该符号的信息objdump -d --start-address={0x100001ee0} \
--stop-address={0x100001ee0+0x37} \
ExploreDSYM
---->
objdump -d --start-address=0x100001ee0 \
--stop-address=0x100001F17 \
ExploreDSYM5. 计算原始地址
#import <mach-o/dyld.h>
#import <objc/runtime.h>
// 获取 ASLR 后的地址,也就是 RuntimeAddress
uintptr_t get_slide_address(void) {
uintptr_t vmAddress_slide = 0;
// 遍历所有加载过的 image,包括 ipa 中的可执行文件(也就是主 App 的可执行文件) + 依赖的动态库
for (uint32_t i = 0; i < _dyld_image_count(); i++) {
// 处理当前 image
// 当前 image 的名称
const char *imageName = (char *)_dyld_get_image_name(i);
const struct mach_header *header = _dyld_get_image_header(i);
if (header->filetype == MH_EXECUTE) {
// 获取 image 当前的偏移地址。偏移是基于该符号所在 image 的。vmAddress_slide 也就是 ImageBaseAddress
vmAddress_slide = _dyld_get_image_vmaddr_slide(i);
NSString *str = [NSString stringWithUTF8String:imageName];
// 当前 image 名如果包含 ExploreDSYM,则说明是要找的 image 地址
if ([str containsString:@"ExploreDSYM"]) {
NSLog(@"image name %s at address 0x%llx and ASLR slide 0x%lx.\n", imageName, (mach_vm_address_t)header, vmAddress_slide);
break;
}
}
}
return vmAddress_slide;
}
// 计算虚拟内存地址 SymbolOffset = RuntimeAddress - ImageBaseAddress
- (void)getRuntimeAddress {
// 获取 sel 的 ASLR 后的地址,因为启动后经过 dyld 做了偏移
IMP imp = class_getMethodImplementation(self.class, @selector(visitArray));
unsigned long imppos = (unsigned long)imp;
unsigned long slide = get_slide_address();
// SymbolOffset = RuntimeAddress - ImageBaseAddress
unsigned long addr = imppos - slide;
NSLog(@"%lu", addr);
}y = 1x + b 的运算十四、编译链接综合实战题目
1. 动态库还是静态库,对于 App 包体积影响更大?
libxxx.a) 本质上是一个 .o 文件的归档。ld) 并不会把整个静态库文件都塞进最终的可执行文件。.o 文件中的代码和数据。Dead Code Stripping 选项)会进行 Dead Code Stripping (死代码剥离)。这意味着即使链接器提取了一个 .o 文件,它也会分析这个 .o 文件内部的符号(函数、全局变量等),只保留那些最终被 App 或其他库代码实际使用到的符号。没有被任何地方引用的符号会被安全地移除。libxxx.dylib 或 .framework) 是一个独立的、预链接好的可执行代码单元。LC_LOAD_DYLIB 加载命令和符号表中。xxx.dylib 或 xxx.framework/xxx) 必须作为一个完整的文件被包含在 App Bundle 中。Mach-O) 都会包含一个符号表。Strip Linked Product (通常设置为 YES)。这个步骤会移除对运行时非必需的符号信息。NSClassFromString 需要的类名)。大部分原始符号名称都被移除,只保留地址信息。strip) 发生在链接之后,它确实能减小可执行文件的大小,但符号表本身在整个二进制中占比相对较小。影响包体积的最大因素还是代码段 (__TEXT) 和数据段 (__DATA) 的大小。 静态库通过 Dead Code Stripping 在链接阶段就移除了未使用的代码/数据,这是它节省体积的主要手段。动态库缺少这种精细的按需提取机制。UIKit, Foundation, libSystem.dylib) 不需要打包到你的 App Bundle 中。它们已经存在于 iOS 设备上。1. 间接符号表(Indirect Symbol Table)的本质
__LINKEDIT 段中。uint32_t 数组。Dynamic Symbol Table, LC_DYSYMTAB Load Command 指向)中的索引。__stubs, __got, __la_symbol_ptr) 和 符号定义(在某个动态库中) 的桥梁。2. 绑定时机:启动时绑定 vs 延迟绑定
dyld 解析符号地址的时机由符号的 绑定信息(Binding Info) 决定。这些信息存储在 __LINKEDIT 段中,由 LC_DYLD_INFO 或 LC_DYLD_INFO_ONLY Load Command 指向。__DATA, __got 或 __DATA_CONST, __got): 这些数据符号必须在程序启动、任何代码访问它们之前就被解析并填入正确的地址。因为数据访问通常没有像函数调用那样的“桩”机制来延迟处理。-bind_at_load 链接器标志,但在 iOS/macOS 上通常不鼓励)。dyld 在 App 启动过程的 bind phase 完成这些绑定。dyld 加载所有依赖的动态库。bind phase,dyld 遍历 bind opcodes(一种紧凑的指令序列,描述了哪些位置的指针需要绑定到哪个库的哪个符号)。bind opcode 找到:__got 中的某个条目地址)。dyld 在目标库的导出符号表(通常是 Trie 树)中查找该符号,得到其实际内存地址(库基地址 slide + 符号偏移)。__got 中的条目)。main 函数执行时,这些非延迟绑定的符号地址已经确定。访问这些数据或调用这些函数(如果绑定了函数)不会有额外延迟。__stubs 和 __la_symbol_ptr):__stubs Section (__TEXT 段):stub)。每个桩代码对应一个需要延迟绑定的函数。stub 的指令是跳转到 dyld_stub_binder 函数(由 dyld 提供),并传递一个关键参数:该桩对应的 延迟绑定信息偏移量(Lazy Binding Info Offset)。__la_symbol_ptr Section (__DATA 段):Pointer)。__stubs 中那个桩代码的地址。这确保了第一次调用函数时,CPU 会跳转到 __stubs 中的桩代码。NSLog (编译后通常是 callq _NSLog)。_NSLog 符号在编译时被解析为指向 __stubs 中 NSLog 桩代码的地址(或者在某些架构/优化下直接指向 __la_symbol_ptr 条目)。__stubs 中的 NSLog 桩代码。dyld_stub_binder。dyld_stub_binder 工作:lazy bind opcodes)。opcode:确定要绑定的符号名(通过间接符号表索引)和目标库。Trie)中查找该符号,得到 NSLog 的实际内存地址。dyld_stub_binder 将 __la_symbol_ptr 中对应 NSLog 的条目内容,从指向桩代码的地址,改写为 NSLog 函数的实际地址!dyld_stub_binder 直接跳转到 NSLog 函数的实际地址执行。__la_symbol_ptr 中的 NSLog 条目已被更新为真实地址。NSLog 时,CPU 会直接跳转到 NSLog 的真实地址,不再经过 __stubs 桩代码和 dyld_stub_binder。绑定只发生一次!dyld 只需要处理非延迟绑定(主要是数据)。成千上万的函数绑定被推迟到实际使用时,显著减少了启动时间。LC_DYLD_INFO 中的 lazy_bind_off/size: 专门存储描述哪些函数符号需要延迟绑定以及如何绑定的 opcode 指令。3. GOT 的核心作用和工作原理
1. 解决数据访问的位置无关性问题
extern int g_globalVar;)最终会位于哪个内存地址。g_globalVar(如 mov eax, [0x12345678]),在加载地址随机化后,这个地址肯定是错误的。dyld 填充为变量的真实绝对地址。2. GOT 的结构与位置
__DATA,__got 或 __DATA_CONST,__got section 中。现代系统倾向于使用 __DATA_CONST 以提高安全性(只读)。uintptr_t 数组)。每个需要动态解析的全局或静态数据符号(变量)在 GOT 中独占一个条目(slot)。dyld 解析后填充。3. 动态库与 App 的全局变量会写入同一个 GOT 吗?
在 __DATA_CONST,__got 中存储:global_in_dylib)
在自身的 __DATA,__got 中存储:global_in_dylib),该变量地址不写入 GOT,而是存储在动态库的数据段(__DATA,__data),供其他模块通过其 GOT 间接访问。4. 总结
__DATA/__got 中为每个需要动态解析的全局数据变量预留一个指针槽位。dyld 在启动阶段 (非延迟绑定) 查找并计算出每个全局数据变量的真实内存绝对地址。dyld 将计算得到的全局变量的真实绝对地址写入其对应的 GOT 条目。4. Non-Lazy Binding(启动时绑定) 和 Lazy Binding(延迟绑定)
1. 启动阶段 - 非延迟绑定(蓝色区域)
errno 变量)2. 首次调用 - 延迟绑定(橙色区域)
_dyld_stub_binder 并传递符号索引3. 后续调用 - 直接访问(绿色区域)
__la_symbol_ptr 跳转5. 总结对比
特性 静态库 ( libxxx.a)动态库 ( libxxx.dylib / .framework)包含机制 按需提取:只链接 App 实际用到的 .o 文件中的符号全量打包:整个库文件必须包含在 App Bundle 中 核心优化 Dead Code Stripping:移除库中未使用的代码/数据 无:即使只用一个函数,整个库文件也得打包 对 App 可执行文件大小影响 较小 (仅包含实际使用的代码+剥离后符号表) 很小 (仅包含引用信息+剥离后符号表) 对 App 整体包体积影响 小 大 (库文件本身显著增加体积) 符号表作用 链接时定位;剥离后大部分移除 App 中:运行时绑定;库文件中:自身需要 系统库处理 通常不直接链接系统静态库 不增加体积 (库已存在于系统) 主要体积来源 App 可执行文件中链接进来的有效代码/数据 App Bundle 中完整的动态库文件 Dead Code Stripping 设置为 YES (默认通常是),并且 Strip Linked Product 在 Release 模式下设置为 YES (默认也是)。对于动态库本身,也要配置其 Stripping 选项。Dead Code Stripping 机制允许链接器只包含 App 实际需要的代码,而动态库则必须完整包含整个文件。符号表的影响相对较小,剥离 (strip) 操作对两者都有效,但无法弥补动态库全量包含带来的根本性体积增加。2. 运行报错:Symbol not found 类问题
1. 符号定位错误
dyld[5466]: Symbol not found: _NSUserActivityTypeBrowsingWeb
Referenced from: <61E75C66-9BA7-3070-B783-EADF1665E135> /private/var/containers/Bundle/Application/51B572E2-1311-459F-89F1-11B96C2B48C4/xxxx.app/xxxx.debug.dylib
Expected in: <2C72BAF6-60AA-38C5-BC25-04F76D3EAAC2> /System/Library/Frameworks/CoreServices.framework/CoreServices
Symbol not found: _NSUserActivityTypeBrowsingWeb
Referenced from: <61E75C66-9BA7-3070-B783-EADF1665E135> /private/var/containers/Bundle/Application/51B572E2-1311-459F-89F1-11B96C2B48C4/xxxx.app/xxxx.debug.dylib
Expected in: <2C72BAF6-60AA-38C5-BC25-04F76D3EAAC2> /System/Library/Frameworks/CoreServices.framework/CoreServices
dyld config: DYLD_LIBRARY_PATH=/usr/lib/system/introspection DYLD_INSERT_LIBRARIES=/usr/lib/libLogRedirect.dylib:/usr/lib/libBacktraceRecording.dylib:/usr/lib/libRPAC.dylib:/usr/lib/libViewDebuggerSupport.dylib:/System/Library/PrivateFrameworks/GPUToolsCapture.framework/GPUToolsCapture Symbol not found: _NSUserActivityTypeBrowsingWeb
Expected in: CoreServices.frameworkNSUserActivityTypeBrowsingWeb 属于 Foundation.framework(iOS 10+ 引入),而不是 CoreServices.framework2. 链接问题
NSUserActivityTypeBrowsingWeb 时,Foundation 尚未加载3. 系统库为什么出现这种链接错误?
# CocoaPods 处理逻辑
# 原始顺序
frameworks = ['CoreServices', 'Foundation', 'UIKit']
# 排序后的顺序
sorted_frameworks = frameworks.sort # => ['CoreServices', 'Foundation', 'UIKit']OTHER_LDFLAGS 变为:-framework CoreServices
-framework Foundationdef to_hash(prefix = nil)
list = []
list += other_linker_flags[:simple].to_a.sort
modifiers = {
:frameworks => '-framework ',
:weak_frameworks => '-weak_framework ',
:libraries => '-l',
:arg_files => '@',
:force_load => '-force_load',
}
[:libraries, :frameworks, :weak_frameworks, :arg_files, :force_load].each do |key|
modifier = modifiers[key]
sorted = other_linker_flags[key].to_a.sort
if key == :force_load
list += sorted.map { |l| %(#{modifier} #{l}) }
else
list += sorted.map { |l| %(#{modifier}"#{l}") }
end
end
result = attributes.dup
result['OTHER_LDFLAGS'] = list.join(' ') unless list.empty?
result.reject! { |_, v| INHERITED.any? { |i| i == v.to_s.strip } }
result = @includes.map do |incl|
path = File.expand_path(incl, @filepath.dirname)
if File.readable? path
Xcodeproj::Config.new(path).to_hash
else
{}
end
end.inject(&:merge).merge(result) unless @filepath.nil? || @includes.empty?
if prefix
Hash[result.map { |k, v| [prefix + k, v] }]
else
result
end
endld 按从左到右顺序加载框架4. 解决方案:调整链接顺序
-framework Foundation$(inherited) 继承 Pods 的设置OTHER_LDFLAGS = (
-framework Foundation,
$(inherited) # Pods 生成的标志
)5. 既然存在问题,Cocoapods 为什么这么设计
1. 排序功能的设计目的
CocoaPods 的核心目标之一是保证 跨环境一致性。通过字典序排序链接标志,无论依赖库的声明顺序如何,最终生成的 .xcconfig 文件内容始终保持一致。这避免了:pod install 后出现无关的 Git 差异;
排序后更容易实现:
多数情况下,链接顺序不影响结果(例如独立的功能库)。排序在提升可维护性的同时,对大部分项目无负面影响2. 排序引发 Bug 的本质原因
链接器(ld)按从左到右顺序解析符号:CoreServices 依赖 Foundation,但 CoreServices < Foundation 按字母序排在前面)7。
用户遇到的 NSUserActivityTypeBrowsingWeb 符号丢失:# 排序后错误顺序
-framework CoreServices # 先链接,需 Foundation 中的符号
-framework Foundation # 后链接,符号未被引用CoreServices 中遇到未定义符号,但右侧无 Foundation,因此报错3. 排序的取舍:为何不取消?
优点 代价 ✅ 保证多环境一致性 ⚠️ 破坏隐式链接顺序 ✅ 简化依赖管理逻辑 ⚠️ 需手动调整关键框架顺序 ✅ 减少冗余标志 $(inherited) 或手动调整)解决