有些时候我们会遇到 Release 包有 Bug,Debug 包正常,或者是本地构建没问题,上传到 App Store 或者 TestFlight 版本就有 Bug。 能有个方式调试一下就好了。
这个可以有,解决的方式也很简单,我主要的灵感来自 MonkeyDev。如果你用过 MonkeyDev,会感觉使用这个工具逆向非常方便,我们可以直接在 Xcode 上像普通的 App 一样构建调试。
这个思路也可以用在我们调试一些“不能调试”的 ipa。
所以我们先来看一看,MonkeyDev 是如何将已经构建好的 ipa 直接放到 Xcode 运行起来的。
# 借助 Xcode 重签名运行 App
事实上这一过程很简单,基本上我们直接将原本构建的流程改成直接将从 ipa 解压的 app 拷贝过去即可。
为了了解 MonkeyDev 在这里做了什么,我创建了一个新的工程,这个工程只有三个文件 Info.plist
、*.xcodeproj
、copy_target_app.sh
。
这个工程没有需要编译的文件,也没有需要处理的资源。只有一个 Copy Target App
的脚本。
脚本执行关键步骤如下:
- 拷贝要调试的 .app 中的内容到 Xcode 构建产物路径
- 设置产物的 Bundle ID 和工程配置的
PRODUCT_BUNDLE_IDENTIFIER
一致 - 使用 Xcode 提供的环境变量
EXPANDED_CODE_SIGN_IDENTITY
给动态库签名
对应脚本内容也非常少:
# Xcode 生成 .app 产物的路径
BUILD_APP_PATH="${BUILT_PRODUCTS_DIR}/${TARGET_NAME}.app"
# 待拷贝的 .app 路径
TARGET_APP_PUT_PATH="${SRCROOT}/${TARGET_NAME}/TargetApp"
# 当前工程的 Info.plist 路径
TARGET_INFO_PLIST="${SRCROOT}/${INFOPLIST_FILE}"
# 拷贝 .app 中所有文件
COPY_APP_PATH=$(find "${TARGET_APP_PUT_PATH}" -type d | grep "\.app$" | head -n 1)
cp -rf "${COPY_APP_PATH}/" "${BUILD_APP_PATH}/"
# 移除不考虑支持的 PlugIns 和 Watch
rm -rf "${BUILD_APP_PATH}/PlugIns" "${BUILD_APP_PATH}/Watch" || true
# 工程和产物的 Bundle ID 应当保持一致
cp -rf "${COPY_APP_PATH}/Info.plist" "${TARGET_INFO_PLIST}"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER}" "${TARGET_INFO_PLIST}"
cp -rf "${TARGET_INFO_PLIST}" "${BUILD_APP_PATH}/Info.plist"
# 给所有的动态库签名
for library in "${BUILD_APP_PATH}/Frameworks"/*; do
/usr/bin/codesign --force --sign "${EXPANDED_CODE_SIGN_IDENTITY}" "${library}"
done
Xcode 会帮我们完成剩下的事情,比如创建 .app 目录、给 .app 进行签名:
以上的这些工作,足以使用 Xcode 将一个已经构建好的 app 运行到手机中,并使用 lldb 进行调试。
那 ipa/app 从哪里获得?我们自己开发的 app,手里当然有未加密的 ipa(这可以从 .xcarchive 获得),完全不需要砸壳的参与,自然也用不到越狱的设备。
一般传到 App Store 的 app 我们都会使用 CI 进行构建,相关的产物也可以保留下来。只有找到需要调试那次的 ipa,直接使用上述流程处理即可。
# 完成源码调试
这一步的关键是完成符号表恢复和源码路径映射。
为 App Store 构建的 app 没有调试信息,不过既然是我们自己的 app,每次构建应当留下 dSYM
文件供跟踪线上崩溃,这当然也可以用来进行本地调试。
lldb
提供了一个 add-dsym
参数,我们可以在 app 启动时(可以使用 LLDB Init File)添加 dSYM。
这里我随便挑选了一个 Demo app 进行尝试,这个工程来自 Adopting Menus and UIActions in your User Interface (opens new window)。
在 CI 上打包上传后,可以得到 ControlMenus.xcarchive
:
既有 app 又有 dSYM。
将 app 按照上一小节进行构建后,使用 (lldb) add-dsym {dsym-path}
即可添加符号信息。
如下图所示,我们可以看到调用栈和一些符号信息,同时还可以使用符号断点:
符号信息恢复完毕,为了查看源码,我们先来使用 source info
查看看断点所在的源码位置:
(lldb) source info
Lines found in module `ControlMenus
[0x00000001025f7cdc-0x00000001025f7cf4): /Users/yahaha/Downloads/AdoptingMenusAndUIActionsInYourUserInterface/ControlMenus/ViewController.swift:13:15
(Downloads 只是举个例子)这个路径是 CI 上打包的路径,我们可能很难在本地相同路径放一份代码。
不过我们可以使用 lldb 的源码路径映射解决。这个文件对应到我设备上的本地路径为:
/Users/yahaha/Desktop/AdoptingMenusAndUIActionsInYourUserInterface/ControlMenus/ViewController.swift
只是举例说明,CI 环境放到了
Downloads
目录下,本地放在了Desktop
目录下。
此时我们使用一个 lldb settings append target.source-map {old-path} {new path}
命令,即可完成目录映射,具体到这个场景命令如下:
(lldb) settings append target.source-map /Users/yahaha/Downloads /Users/yahaha/Desktop
此时在调试到指令属于某一行代码时,Xcode 将显示具体的代码信息,同时还可以查看到各个变量信息:
此外还可以从上图中看到 source info
中路径从 Downloads
变成了 Desktop
。
以上实践,我在 https://github.com/DianQK/debug-ipa (opens new window) 中提供了所有的内容,这包括一个我生成的 ControlMenus.xcarchive
以及相关源码。
甚至我们可以借助这个工程进行改造,完成一个专属的 App Store 调试套装。
需要额外补充的一点是,线上包一般会有一些反调试的手段,我们可以参考 MonkeyDev 增加动态库的注入,去掉这些反调试,也可以直接使用 MonkeyDev,本文主要是理解实现的过程。