Xcode 15下MonkeyDev动态库注入调试适配指南 上周把开发机上的 Xcode 升到 15随手打开那个安静了半年的 MonkeyDev 工程CommandR 之后红色报错一路刷到底那种感觉就像你出差回来发现自家门锁换了。MonkeyDev 这套免越狱设备上的动态库注入调试方案在 Xcode 15 下确实需要一批针对性的改造才能重新跑起来——它既不是简单换个版本号就能糊弄过去也不是彻底不能用。这篇就把我这两天从零到跑通的完整过程记下来包括 Xcode 15 到底改了哪几处底层行为、Build Settings 里必须动刀的十几个键、Run Script 的重写要点、签名顺序为什么不能乱以及一堆只有真机跑起来才会暴露的坑。如果你手上也有这类注入调试工程或者正在做自有 App 的插件化拆解、安全研究学习、授权范围内的测试验证这篇应该能帮你省掉一整个下午。1. 先搞清楚 MonkeyDev 在 Xcode 里到底干了什么1.1 一条完整链路模板、dylib target、Run Script、重签名很多人用 MonkeyDev 用了很久其实没细看它在工程里布了什么局。装好之后Xcode 的新建工程面板里会多出几个自定义模板选中之后生成出来的并不是一个普通 App 工程而是「一个宿主 App target 一个动态库 target」的组合体。宿主 target 负责把壳装到设备上动态库 target 才是你真正写业务逻辑的地方——所有的类声明、方法替换、界面改动都写在这边。两者之间靠 Build Phases 里的一个 Run Script 连接起来这个脚本会调用 MonkeyDev 自带的一套命令行工具做三件固定动作把编译出来的 dylib 拷贝进.app包内、往宿主可执行文件的 Mach-O 头里插入一条加载指令、对改动过的产物重新签名。做完这三步Xcode 的安装流程才会把一个「自带插件」的壳推进设备。理解这条链路非常关键因为 Xcode 15 的所有破坏性改动基本都打在这条链路的某一环上。第一环是模板本身模板里塞着一堆历史遗留的 Build Settings这些键在新版本里有的被废弃、有的默认值翻转、有的直接消失第二环是 dylib target 的编译参数涉及架构切片、链接器行为、模块校验第三环是 Run Script 的执行环境Xcode 15 给它加了一个沙盒第四环是签名Xcode 15 对签名的校验更严顺序错了或者漏签任何一个 Mach-O 都会在安装阶段被拦下来。四环里任何一环断掉你看到的都是同一个结果——真机上闪退或者压根装不进去但根因完全不同。我踩的第一个坑就在这里一开始看到闪退就去查 hook 代码折腾了两小时才发现是签名顺序导致的code signature invalid跟业务逻辑一行关系都没有。所以后面的章节我会按这条链路的顺序来讲方便你对照自己的工程定位断点在哪一环。1.2 Xcode 15 掐断的四根线和它们各自的表现第一根线是用户脚本沙盒。Xcode 15 新建工程默认把ENABLE_USER_SCRIPT_SANDBOXING打开一旦开启Run Script 只能在工程目录和派生数据目录里读写任何越界访问都会被系统直接拒绝。MonkeyDev 的脚本恰恰需要访问工程外的东西——命令行工具在安装目录下dylib 产物在派生数据里App 壳在另一个路径。表现就是构建日志里出现一串Sandbox: bash(...) deny(1) file-read-data然后脚本中途退出但 Xcode 未必给你一个显眼的红叉很多时候只是一个 warning 级别的提示产物看起来「构建成功」装上去必然崩。第二根线是位码彻底退场。老工程里ENABLE_BITCODE YES几乎是默认配置Xcode 15 已经不认这个键了留着会在构建时报出明确的错误而不是警告。这一条比较好处理删掉即可但它会掩盖其他问题——因为构建一旦在早期失败你就看不到后面链接阶段的报错了。第三根线是架构收敛。VALID_ARCHS这个键已经废弃ARCHS里如果还写着armv7Xcode 15 会直接告诉你该架构不再支持。更隐蔽的是模拟器切片问题如果你的工程里手动配了EXCLUDED_ARCHS或者依赖了只有 x86_64 的静态库在新架构组合下会报building for iOS Simulator, but linking in object file built for iOS这个错看起来像库的问题实际是架构没对齐。第四根线是链接器换代。Xcode 15 换上了新链接器行为和老链接器有一批差异重复链接同一个库会报警告、-lstdc的查找路径变了、对LC_LOAD_DYLIB的处理更严格、对 arm64 切片的合并规则也不同。这些差异单独看都不致命但叠在一起会让老工程的构建产物悄悄变形——比如插入了加载指令但符号表对不上运行时 dyld 直接拒绝加载。2. 动手前先把环境矩阵定死2.1 版本组合与踩雷概率适配这种事情最忌讳的就是「边改边猜」。先把版本组合确定下来能规避一半的玄学问题。下面这张表是我实测下来比较稳的搭配以及几个常见组合的雷点组合项推荐选择备注与雷点Xcode 版本15.2 或 15.415.0 早期版本对新链接器的兼容略差15.3 之后部分工具链路径有调整macOS 版本14.x13.x 上部分命令行工具的签名校验行为不一致部署目标iOS 13.0 及以上低于 12.5 会撞上静态运行库缺失的问题13.0 更保险真机系统iOS 15 及以上过老的系统对新签名策略支持不好调试反而更麻烦工具链版本与工程安装目录保持一致多版本共存时务必确认脚本调用的是哪一个架构仅 arm64真机 arm64模拟器按需额外处理关于部署目标那一条值得多说两句。Xcode 从 14.3 开始当部署目标低于 12.5 时链接阶段会去找一个早已不在工具链里的静态运行库报的错误是「file not found」加一长串路径。这个错看起来像缺文件很多人第一反应是去装库、去改搜索路径其实只要你把部署目标提到 12.5 以上问题自动消失。我在老工程上就是先被这个错误带偏了半小时最后发现改一个数字就完事。另外要提醒的是工具链版本。如果你机器上装了多个 Xcodexcode-select -p指向的和你在图形界面里打开的未必是同一个而 Run Script 里的命令行工具是按绝对路径调用的。我见过一个案例图形界面用的是新版本脚本里写死了老版本的路径结果编译用的是旧工具链、签名用的是新工具链两边的签名策略不一致产物在安装时被判为无效。适配期一定要把所有路径统一别留这种隐形不一致。2.2 工程骨架选型dylib target 还是 framework target老模板默认产出的是 dylib新版本 Xcode 对 framework 的支持更完整比如资源打包、模块导出、rpath的处理都更规范。但改骨架的代价不小你要重做 target、改注入脚本、调整加载指令的指向还要处理 framework 内嵌签名的问题。我的建议是除非你有明确的资源打包需求否则不要换骨架。dylib 的路径简单、签名逻辑少、注入脚本好写适配成本明显更低。只有当你的插件需要带图片、带 xib、带独立配置文件时framework 才值得考虑因为 framework 内嵌资源是官方支持的标准做法而 dylib 想带资源得自己想办法塞进主 bundle很容易和宿主 App 的资源冲突。如果确实要走 framework 路线两个额外的注意点一是INSTALL_PATH要设成executable_path/Frameworks二是宿主 target 的LD_RUNPATH_SEARCH_PATHS要包含executable_path/Frameworks否则运行时找不到。这两个键名字很像但作用完全不同前者决定「装到哪」后者决定「去哪找」写反了就是典型的「dyld: Library not loaded」。2.3 一份可以直接粘的 xcconfig与其在图形界面里一个个点不如抽出公共配置做成 xcconfig多人协作和重建工程时都省事。下面这份是我在 dylib target 上验证过的注释里写了每个键的作用// 关掉脚本沙盒允许访问工程外路径 ENABLE_USER_SCRIPT_SANDBOXING NO // 架构只保留 arm64去掉所有遗留架构 ARCHS arm64 VALID_ARCHS // 部署目标抬高绕开静态运行库缺失 IPHONEOS_DEPLOYMENT_TARGET 13.0 // 关闭模块校验的严格模式避免第三方头文件报 module not found CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES YES CLANG_ENABLE_MODULES NO // 新链接器的重复库只警告不中断构建 OTHER_LDFLAGS $(inherited) -Wl,-no_warn_duplicate_libraries // 签名方式统一为手动避免自动签名把 dylib 漏掉 CODE_SIGN_STYLE Manual CODE_SIGN_IDENTITY Apple Development // 关闭严格警告老代码的告警太多会淹没真正的错误 GCC_TREAT_WARNINGS_AS_ERRORS NO // 库的安装路径 INSTALL_PATH executable_path/这份配置里最容易被忽略的是CODE_SIGN_STYLE Manual。自动签名在 Xcode 15 下会尝试给 bundle 内的所有 Mach-O 重签但它对 dylib 的处理逻辑和主程序不同经常出现「主程序签了、dylib 没签」或者「签名用的证书不一致」的情况最终表现是安装成功但一启动就退出。手动签名虽然要多写几行脚本但每一步在不在你的掌控之中排查起来心里有底。3. 逐项改造Build Settings 里的十几处硬修改3.1 沙盒、位码、架构三件套这三个是最先要处理的因为它们决定了构建能不能走完。沙盒这一项在 target 的 Build Settings 里搜sandbox就能找到把User Script Sandboxing改成NO。改完之后要清理一次派生数据再构建因为 Xcode 会缓存脚本的执行策略不清理的话改了也不生效你会以为没改对。清理的方式是Product → Clean Build Folder或者直接删掉派生数据目录里对应的工程文件夹。位码这一项除了在 Build Settings 里删掉Enable Bitcode还要检查工程的 project.pbxproj 文件里有没有残留。有些从老版本迁移过来的工程图形界面里看不到这个键了但文件里还留着构建时依然会读。稳妥的做法是用文本编辑器搜一遍ENABLE_BITCODE把整行删掉。顺带一提.pbxproj文件在改之前一定要备份或者提交一次这个文件一旦改坏工程打不开就麻烦了。架构这一项除了ARCHS设成arm64还要看一眼EXCLUDED_ARCHS。有些人为了在模拟器上跑会在这里加一堆排除规则比如EXCLUDED_ARCHS[sdkiphonesimulator*] arm64这在 Intel 机器上是合理的但在 Apple Silicon 机器上就会导致模拟器跑不起来。判断标准很简单你的机器是什么芯片模拟器就用什么架构别硬套别人的配置。真机调试不受这一项影响如果只是在真机上跑直接把模拟器相关的排除规则都删干净最省事。3.2 链接器与模块校验的两处关键开关新链接器带来的第一个麻烦是重复库。老工程里经常出现同一个库被间接链接多次的情况比如-lz在多个依赖里都写了老链接器默默去重新链接器会给你一条ignoring duplicate libraries的警告。这条警告本身不致命但如果你的工程开了「警告视为错误」构建就断了。所以要么加-Wl,-no_warn_duplicate_libraries抑制它要么关掉「警告视为错误」我倾向于后者因为老代码里的警告本来就不可能清干净。第二个麻烦是模块校验。老工程里的第三方头文件大多不是模块化的Xcode 15 对import和#import的混用更敏感经常报module 某库 not found或者include of non-modular header inside framework module。这类错误的根源不是头文件真的找不到而是编译器试图把非模块化头文件塞进模块化上下文。解法就是把CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES打开同时把CLANG_ENABLE_MODULES关掉——后者会让编译器完全走传统的#import路径虽然编译速度慢一点但兼容性最好。注入调试工程的代码量通常不大这点编译时间可以忽略。还有一个容易被忽略的键是ENABLE_TESTABILITY。Xcode 15 在 Debug 配置下默认把它打开而它会让编译产物带上一些额外的符号信息某些 hook 框架在解析符号表时会因为多了这些信息而行为异常。如果你的插件在 hook 之后出现莫名其妙的方法找不到可以试着把它关掉看看。3.3 签名参数与 Info.plist 的配套改动签名这块的核心原则是「一处配置全链路一致」。证书名、描述文件、签名方式三个地方必须完全对齐主程序、dylib、内嵌的其他 Mach-O 都要签同一个证书。CODE_SIGN_IDENTITY的值在 Xcode 15 里叫Apple Development老工程里写的iPhone Developer虽然在部分场景下还能识别但建议统一改过来。PROVISIONING_PROFILE_SPECIFIER要填描述文件的名称注意是名称不是 UUID填 UUID 在命令行构建时可能不生效。另外CODE_SIGN_ENTITLEMENTS要指向正确的文件如果你没有特殊需求主程序和 dylib 都要在这个文件里声明同样的能力尤其是涉及调试相关的能力dylib 缺了会直接导致附加进程失败。Info.plist 这边有两个键需要确认CFBundleExecutable必须和实际的可执行文件名一致MinimumOSVersion要和部署目标对齐。我见过一个案例工程的部署目标改成了 13.0但 Info.plist 里还写着 11.0结果在较新的系统上安装时被拒报的错误信息完全不指向这个字段纯靠猜。还有一个跟签名相关但和 Build Settings 无关的设置——设备的开发者模式。较新版本的系统在首次连接调试时需要手动在设备上开启开发者模式不开启的话Xcode 会显示「已连接」但安装阶段总是失败而且错误提示非常笼统。这个不是 Xcode 15 引入的但适配期很容易撞上先确认一下能省很多时间。4. Run Script 重写注入流程的核心4.1 脚本执行时机与沙盒的正确处理脚本的执行时机决定了它能看到什么产物。Xcode 的构建分阶段Run Script 如果放在Compile Sources之前dylib 还没编译出来脚本自然拷不到东西放在Copy Bundle Resources之后资源已经打包完再改产物就需要重新签名整个 bundle。正确的位置是所有编译和资源阶段之后、签名阶段之前。老模板一般放在最后但 Xcode 15 的签名阶段被提前了需要手动把脚本往前挪一格。脚本内容的开头建议加三行#!/bin/bash set -e set -o pipefailset -e让脚本在任何一步失败时立即退出而不是继续往下跑出一个半成品。这一点很重要因为注入流程有严格的先后依赖前一步失败后一步继续跑产出的东西必然不可用但构建日志里可能只有一个不起眼的 warning你会在真机上排查半天才发现源头在构建阶段。脚本里用的路径变量Xcode 15 下有两个推荐用$TARGET_BUILD_DIR指向最终产物目录$WRAPPER_NAME是.app的目录名拼起来就是完整的 App 路径。$BUILT_PRODUCTS_DIR也可以但它指向的是构建中间目录在多配置构建设置下可能和最终产物不在同一个地方。老脚本里经常写死Debug-iphoneos这种写法在新构建系统下有时会失效因为 DerivedData 的目录结构变了。沙盒关闭之后脚本就可以正常访问安装目录了。但有个细节脚本里调用的命令行工具最好写绝对路径或者在最前面export PATH$PATH:/your/tool/path。因为 Xcode 执行脚本时的 PATH 是一个精简版本不包含你自己安装目录下的可执行文件直接写工具名会报command not found。4.2 注入路径与加载指令的处理dylib 拷贝进.app之后还要让主程序知道「启动时该加载它」。这一步靠往 Mach-O 头里插入一条LC_LOAD_DYLIB指令完成路径一般写成executable_path/你的库名.dylib表示相对主程序所在目录查找。这里有个实操细节拷贝的时候要确认拷贝的是正确架构的切片。如果你的 dylib target 同时产出了 arm64 和模拟器切片直接cp整个 fat 文件过去也能用但体积会大一倍而且可能夹带不该有的切片。稳妥做法是用lipo -thin arm64抽出来再拷。同理往主程序里插指令的工具对 fat 二进制的支持程度不一老工具处理 fat 文件时可能只改第一个切片导致真机架构上根本没有加载指令运行时看起来就是「代码完全不生效」。插入指令之前最好做个备份或者至少在脚本里判断一下指令是否已经存在避免重复执行时插进去两条。有些工具自带/D之类的去重参数有些没有没有的话就自己用otool -l检查一下再决定要不要插。这个判断逻辑写起来不复杂但能省掉「为什么我的库被加载了两次」这种玄学问题。4.3 重签名顺序不能乱签名顺序是整条链路里最容易出错的一环而且错误的表现极具迷惑性。正确的顺序是从内到外先签 dylib再签 bundle 内的其他 Mach-O然后是整个.app目录最后如果有外层容器再签外层。顺序反过来的话签外层的时候会把内层的签名状态一起固化进去之后再改内层外层就失效了系统加载时直接报签名无效。脚本里的签名命令大致长这样codesign --force --sign $EXPANDED_CODE_SIGN_IDENTITY \ --timestampnone \ $APP_PATH/你的库名.dylib codesign --force --sign $EXPANDED_CODE_SIGN_IDENTITY \ --timestampnone \ $APP_PATH--timestampnone是必须的加时间戳会去联网请求构建时网络不通就卡住而且调试用途也不需要时间戳。--force允许覆盖已有签名。$EXPANDED_CODE_SIGN_IDENTITY是 Xcode 提供的环境变量值就是你在 Build Settings 里配的证书用它比自己写死字符串更安全。还有一个坑如果你的.app里有Frameworks目录或者PlugIns目录里面的内容必须先单独签完再签整个 bundle。脚本里用一个find加循环处理就行注意跳过.dSYM和隐藏文件。签完可以用codesign -dv --verbose4检查一下每个 Mach-O 的签名信息确认Authority字段一致、Identifier没有重复。5. 验证与排错产物到底对不对5.1 otool 三连自查构建完之后别急着装先对产物做三项检查能提前发现八成问题。第一项看加载指令otool -L 主程序路径输出里应该能看到你的 dylib 并且路径是executable_path/...。如果看不到说明插入失败或者插到了别的切片上如果路径是绝对路径说明插入时写错了装到设备上一定找不到。第二项看架构lipo -info分别看主程序和 dylib确认两边都只包含 arm64。如果一边有 arm64 另一边只有 x86_64那就是架构没对齐链接阶段会出问题。第三项看签名codesign -dv --verbose4 路径重点看Authority、TeamIdentifier、Identifier三个字段。主程序和 dylib 的TeamIdentifier必须一致Identifier必须不同否则系统会认为是同一个 bundle 的重复实例。这三项检查加起来不到一分钟但能把后面的排查时间压缩十倍。我现在的习惯是把它写成一个 shell 脚本构建完成后自动跑一遍有问题直接报出来不用等到真机闪退才回头翻。5.2 启动即崩的六种典型与定位方法第一种dyld 报Library not loaded。原因通常是 dylib 没拷进包里或者加载路径写错。定位方法是在设备上用log stream抓系统日志崩溃瞬间会打出完整的查找路径照着路径去find一下就知道文件到底在不在。第二种code signature invalid。签名顺序错了或者某个 Mach-O 漏签。用codesign -v全量校验一遍它会告诉你是哪个文件、哪一项校验没过。第三种进程被Killed: 9。这种情况多见于证书问题比如用了免费开发者证书且已经过期有效期七天或者描述文件和设备不匹配。表现是完全没有崩溃日志进程直接消失。第四种走到 hook 点就EXC_BAD_ACCESS。类名或方法名拼错是最常见的原因其次是参数类型对不上。这类问题要把日志级别调高让 hook 框架把每次替换的类、方法都打出来逐条比对。第五种objc_msgSend附近崩溃。通常是 hook 框架没有正确加载或者加载了但初始化没跑。检查一下库的入口函数有没有被执行可以在入口打一行日志验证。第六种界面起来了但代码完全不生效。这种最隐蔽往往是因为 dylib 被加载了但加载的是错误架构的切片或者加载指令插在了错误的 Mach-O 头里。回到上一节的三项检查基本能定位。6. 高频报错速查表与几条血泪经验6.1 报错对照表报错关键词真实原因处理方式Sandbox: bash deny file-read-data脚本沙盒没关把 User Script Sandboxing 设为 NO清派生数据building for armv7 is no longer supported架构里有遗留值ARCHS 只留 arm64清掉 VALID_ARCHSfile not found: ...libarclite 相关部署目标过低IPHONEOS_DEPLOYMENT_TARGET 提到 13.0module 某库 not found模块化头文件混用关 CLANG_ENABLE_MODULES开非模块化包含ignoring duplicate libraries新链接器重复库警告加 -Wl,-no_warn_duplicate_librariescommand not found: 某工具脚本 PATH 精简脚本里写绝对路径或前置 export PATHdyld: Library not loaded拷贝或路径错误otool -L 看指令检查包内文件是否存在code signature invalid签名顺序或漏签从内到外重签codesign -v 全量校验进程 Killed: 9证书或描述文件问题检查证书有效期与设备绑定关系加载了但代码不生效架构切片不对lipo -info 核对两边架构这张表建议直接贴到工程 README 里团队里谁再撞上对着查就行。里面每一条我都是真机踩出来的尤其最后一条当时反复确认代码没错、签名没错、包内文件也在最后发现是插入指令的工具只处理了 fat 文件的第一个切片。6.2 几条踩出来的经验第一改造之前先把老工程的.pbxproj完整备份一份。这个文件是纯文本但结构复杂改错一个括号工程就打不开。我吃过一次亏删ENABLE_BITCODE那一行的时候不小心删多了工程直接无法解析只能从 git 里回滚重来。第二别相信「构建成功」这四个字。Xcode 15 的构建日志里脚本阶段的失败很多时候只报 warning界面照样显示绿色的成功提示。真正要确认的是脚本有没有跑完——可以在脚本最后加一句echo inject done日志里没看到这句话就说明没走到最后。第三真机调试第一次跑通之后立刻把整套配置提交并打一个 tag。注入调试工程的配置太脆弱任何一个 Xcode 小版本更新都可能打破平衡有个可回退的基线非常重要。我现在的做法是给每个能跑通的组合打一个 tag名字里带上 Xcode 版本和系统版本出问题直接切回去对比。第四尽量把改动收敛到 xcconfig少在图形界面里点。图形界面的改动藏在.pbxproj里diff 出来全是一堆十六进制看不懂的东西xcconfig 是纯文本改了什么一目了然多人协作时冲突也好处理。第五也是我个人体会最深的一点这类工具链适配最大的时间消耗不在「改」而在「确认改对了没有」。所以每一步改完都要立刻做一次最小验证——改完架构就lipo -info看一眼改完脚本就单独跑一次脚本改完签名就codesign -v校验一次。别想着全部改完再一起跑那样一旦失败你不知道是哪一处引起的只能从头二分。我这两天真正编码的时间加起来不到一小时剩下的全在验证和确认上这个比例在工具链适配里是正常的心里有数就不会急躁。如果你后续还想继续深挖其实这套流程可以往上抽象成一层自动化把构建、注入、签名、校验写成一条流水线每次改配置自动跑一遍输出报告比手动点快得多。我打算下次有空把这部分整理出来到时候再说。