iOS 客户端最头疼的一类 Crash:栈里全是系统库,无现成源码可参考。静态反汇编工具多数时候只能推导不能证实,且丢失运行时信息这样的关键线索,面对有些疑难杂症,运行时二进制分析在所难免。
而这样的分析需要环境构造、GUI 操作、业务复现等,AI 难以完全承担,更合理的是工程师与 AI 协同分析。
IDA 虽然可调试 iOS,但操作复杂体验差,适配落后;lldb-mcp 没有 GUI 更适合自动化;新版 Xcode 的 mcpbridge 提供 LLDB 调用通道,似乎可以做到无缝协同。
头皮发麻的系统栈
举一个 Crash 报告例子:
objectivec
EXC_BAD_ACCESS(SIGSEGV)
Trapped mach exception code 0x1, subcode 0x1d
0 libdispatch.dylib dispatch_source_set_timer + 36
1 CFNetwork URLConnectionLoader::loadWithWhatToDo(NSURLRequest*, _CFCachedURLResponse const*, long, URLConnectionLoader::WhatToDo) + 959
2 CFNetwork URLConnectionLoader::loadWithWhatToDo(NSURLRequest*, _CFCachedURLResponse const*, long, URLConnectionLoader::WhatToDo) + 959
3 CFNetwork invocation function for block in URLConnectionLoader::continueWithCacheLookupResult(NSURLRequest*, _CFCachedURLResponse const*, NSURLRequestCachePolicy) + 99
4 CFNetwork invocation function for block in URLConnectionLoader::withExistingProtocolAsync(void (URLProtocol*) block_pointer) const + 39
5 CFNetwork invocation function for block in QCoreSchedulingSet::performAsync(void () block_pointer) const + 51
6 libdispatch.dylib _dispatch_call_block_and_release + 31
7 libdispatch.dylib _dispatch_client_callout + 15
8 libdispatch.dylib _dispatch_lane_serial_drain + 739
9 libdispatch.dylib _dispatch_lane_invoke + 439
10 libdispatch.dylib _dispatch_workloop_invoke + 1611
11 libdispatch.dylib _dispatch_root_queue_drain_deferred_wlh + 291
12 libdispatch.dylib _dispatch_workloop_worker_thread + 691
13 libsystem_pthread.dylib _pthread_wqthread + 326
Thread 65 crashed with ARM Thread State (64-bit):
x0:0x0000000121be6f80 x1:0x0000019d410181e8 x2:0xffffffffffffffff x3:0x000000003b9aca00
x4:000000000000000000 x5:0x0000000000000020 x6:000000000000000000 x7:000000000000000000
...
fp:0x000000016dfa2690 lr:0x00000001b5ad9968 sp:0x000000016dfa2650
pc:0x00000001d9455fe0 cpsr:0x0000000020000000
全是系统库,没有一帧是业务代码,线索只有寄存器现场和 subcode。
古法时代我们怎么做
首要目标:确认 Crash 的对象类型,以及其来源。
在古法时代,我们可能尝试做的事情:
- 静态反汇编:没有 dSYM,先拿 crash report 里的二进制 UUID 找到对应版本的系统库,拖进 Hopper / Ghidra 跳到偏移处读指令或伪代码,由于缺失运行时信息,很多场景只能做大量猜测无法直接证实
- 寻找源代码:少量基础库能在 Apple 开源站找到,版本还未必对得上;更多核心组件(比如 CFNetwork 内部那批 C++ 类)闭源,公网根本没有,只能对着汇编硬啃
- 运行时汇编分析 :起 LLDB、attach、下断点、触发复现,之后全靠人:
- 读寄存器推参数语义,比如 x0 是指针还是常量,是 OC 对象还是 C++ 对象
- 读内存推对象布局,比如 +0x58 偏移是什么成员,每个数据类型占用多少字节
- 读汇编分支推测走向,写寄存器、写内存改变指令执行路径
- OC 对象要顺着 isa 扒 class 和方法列表,block 对象要挖出引用表分析内存计数
- 循环推测验证:每一步都是猜测。库版本对不对、对象类型是什么、偏移含义、故障机制(初始化未完成还是释放后清零)------猜错任何一环,整条推理链推倒重来
问题复杂程度不同,短则几十分钟,长则数天。而其中有大量复杂的机械劳动 ------------ 这正是 AI 该接管的部分。
如何使用 Xcode MCP
接入很简单:Xcode > Settings > Intelligence 打开「Allow external agents to use Xcode tools」,然后在 Agent 客户端注册 Xcode 自带的 mcpbridge:
json
{
"mcpServers": {
"xcode": {
"command": "xcrun",
"args": ["mcpbridge"]
}
}
}
接上之后,里面 InvokeDebuggerCommand 就是 lldb 操作通道,AI 就能在活动调试会话里下断点、读寄存器、读内存、改执行路径------操作实时同步在 Xcode 界面上,人随时可以观察和接管。分工自然形成:人负责触发复现、收敛假设、拍板结论;AI 负责断点取证、读汇编、批量验证。
但直接让 AI 干会踩坑:continue 是异步的,返回 resuming 不代表已暂停,必须轮询 process status 等停止;lldb 命令可换行串联批量执行,省掉大半往返......这些纪律沉淀为一个简单的 skill。
skill 不写任何指令含义、系统类内存布局等通识性东西,仅写:
- 通道纪律:轮询、调用形态、批量命令,通道特性,模型不知道,漏写链路走不通
- 场景经验 :无符号库的 slide 偏移换算、
objc_msgSend之后参数从 x2 开始------「场景 × 动作」的经验,模型有通识但可能不会组合
工程师与 AI 协同分析
Xcode Run 起来后,结合 Agent 的观感是这样的(图中 Agent 通过 MCP 调用 Xcode LLDB 进程去执行指令并回执):

Agent 执行了半小时的样子(模型拥挤时段),这期间需要人工做一些 GUI 交互,简要分析流程如下:
perl
> 线索只有崩溃报告:`subcode 0x1d`、`dispatch_source_set_timer + 36`、寄存器现场,栈内无应用帧。
1. **授权与会话探测**|`XcodeOpenWorkspace` 触发授权,从两个 workspace id 里挑出真正带活动调试会话的那个,`process status` 确认 PID 63623 在跑|执行 11 条命令、写 0 B|读 1 个空文件、0 B 内存
2. **定位触发入口**|在 27 个 `.xcodeproj` 里定位 demo,读 `ViewController.m`、`ScenarioRunner.m` 确认「案例2」按钮发网络请求|执行 4 条命令、写 0 B|读 2 个源码文件约 90 行、0 B 内存
3. **模块与符号定位**|`image list` + `image lookup -n/-rn`:本机没有独立的 C 符号 `dispatch_source_set_timer`,实现是 `_dispatch_source_set_runloop_timer_4CF`|执行 6 条命令(含 2 次失败)、写 0 B|读 约 250 条符号、0 B 内存
4. **反汇编定位故障指令**|`disassemble -n ... -c 16` 拿到 `+32 mov x19,x0` → `+36 ldr x25,[x0,#0x58]` → `+40 ldrb w8,[x25,#0x1d]`,`0x1d` 正是这条指令的立即数|执行 1 条命令、写 0 B|读 32 条反汇编、0 B 内存
5. **溯源:对象由谁创建**|反汇编 `QCoreSchedulingSet::createDispatchTimer`,见其尾调用 `dispatch_source_create` 创建 TIMER 源|执行 2 条命令、写 0 B|读 24 条反汇编、0 B 内存
6. **溯源:谁在调 set_timer**|反汇编 `URLConnectionLoader::scheduleTimeoutTimer`,见 `+92` 从 loader 取源、`mov x2,#-1`、组装 `0x3b9aca00`,与报告 x2/x3 逐位一致|执行 2 条命令、写 0 B|读 40 条反汇编、0 B 内存
7. **确认导入桩**|读 `__auth_stub` 的 `adrp/add/ldr/braa`,确认 CFNetwork 的 set_timer 调用最终跳进 4CF|执行 3 条命令(含 1 次失败)、写 0 B|读 10 条反汇编、0 B 内存
8. **断点一轮:4CF 函数入口**|命中 UIKit/BaseBoard 键盘定时器,读栈判定为共享 API 噪音;顺带 `po` + 内存读首次确认 `OS_dispatch_source` 与 `[x0+0x58]=ds_refs`,随后禁用|执行 5 条命令、写 0 B|读 8 帧栈、9 个寄存器、2 处内存 64 B
9. **断点二轮:set_timer 导入桩**|仍命中 BaseBoard,判定它是共享缓存全局桩、非 CFNetwork 独占,禁用|执行 4 条命令、写 0 B|读 8 帧栈、0 B 内存
10. **断点三轮:CFNetwork 唯一调用点**|断在 `scheduleTimeoutTimer + 152`,放行后两个 `NSURLSession-work` 线程同时命中|执行 6 条命令、写 0 B|读 8 帧栈、0 B 内存
11. **正常态现场取证**|`bt`/`register read`/`po $x0`/`memory read` 坐实对象是 timer 源、`[x0+0x58]` 有效、`ds_refs+0x1d = 0x02`|执行 9 条命令、写 0 B|读 21 个寄存器、2 个对象描述、2 处内存 48 B
12. **注入复现**|不改 x0(保住「x0 有效」这一报告特征),只清 `[x0+0x58]`,回验后继续 → `EXC_BAD_ACCESS (address=0x1d)`|执行 5 条命令、写 1 处 8 B|读 1 处 8 B
13. **崩溃现场记录与清理**|读 `bt 8`、12 个寄存器、PC 附近 6 条指令,与报告逐项对拍后删除 3 个断点|执行 4 条命令、写 0 B|读 6 条反汇编、12 个寄存器、8 帧栈
14. **栈差异复核**|`thread list` + 切线程 `bt 12/20` 补全 11 帧,反汇编 `continueWithCacheLookupResult` block 证明报告那条链本机存在|执行 8 条命令(含 1 次失败)、写 0 B|读 26 条反汇编、21 帧栈
## 结论
对象:`OS_dispatch_source` 的 timer 源 ------ CFNetwork 的连接超时定时器;崩点在读它内部 `ds_refs + 0x1d` 的标志字节。
来源:CFNetwork 内部创建、loader 持有。不可用不是传参坏,是对象内部 `[x0+0x58]` 被清空(框架内 UAF / 生命周期竞态或内存踩踏);本机清掉这 8 字节,崩出同样的 `subcode 0x1d`,复现成立。
单从这一次任务来说,人工参与度低,因为整体上下文紧凑,业务没有割裂,但要把根因查到,还需要工程师结合业务和场景做更多分析。拿到这个结论,就可以继续下一步的排查,看 loader 对于 source 的生命周期管理具体是怎样的,是底层本身的 BUG 还是上层调用 API 干预到了,后续过程很复杂这里不展开了。
可以看到,Agent 全程下发了约 70 条调试命令、阅读约 140 条反汇编指令和 250 条符号,往进程里写了 8 个字节,期间做了大量"经验老到"的决策 ------------ 这些是资深工程师也要折腾很久的机械环节。
skill 提供的流程与纪律让分析没有走弯路:定位 → 触发 → 读现场 → 语义化,四步各自命中;70 条命令零抢跑、零状态错乱。
总结
AI 完成的是动态二进制分析这个环节 ------------ 对象是什么、来源在哪、如何复现。再往上的业务归因、修复决策,仍要工程师拿着这个结论回到业务里继续查。这套工具也不止于崩溃分析:三方 SDK 的黑盒行为确认、系统 API 的运行时验证、偶现问题的现场取证,都是同一套工作流。
在这样的场景,工程师可能不再需要记忆大量的汇编和操作系统固化知识,而是理解原理和方法论。工程师来做核心决策,AI 作为方案贡献者和重复劳动执行者。