ENGINEERING NOTE

云端 Mac 的 LLDB 附加与符号加载排查

云端 Mac 的 LLDB 附加与符号加载排查

Xcode 已经显示“正在附加”,云端 Mac 的 CPU 占用却接近空闲,断点从蓝色变成灰色,变量窗口也没有结果。此时连续点击停止、重启或删除 DerivedData,往往只会抹掉现场。更有效的做法是把问题拆成三层:目标进程是否可调试、LLDB 与目标之间的会话是否建立、当前产物能否找到匹配的符号。

先判断卡在哪一层

先记录发生时间、工程提交号、Scheme、Configuration、目标设备和启动方式,再观察 Xcode 状态。不要把“附加慢”“应用启动慢”和“符号解析慢”混为一类。

现象 优先检查 常见结论
应用未启动,LLDB 一直等待 目标进程、启动参数 进程退出或启动到了另一份产物
已显示进程 PID,界面无响应 进程状态、调试服务 目标暂停、阻塞或调试握手未完成
断点为空心或灰色 模块与符号 模块未加载、路径变化或 UUID 不匹配
能停住但看不到局部变量 优化级别、调试信息 Release 优化导致变量被合并或消除

在另一个终端保存进程快照:

mkdir -p "$HOME/lldb-case"
date -u > "$HOME/lldb-case/time.txt"
ps -axo pid,ppid,state,%cpu,etime,command \
  > "$HOME/lldb-case/processes.txt"
xcrun simctl list devices \
  > "$HOME/lldb-case/simulators.txt"

state 中长期出现 U 可能意味着进程处于不可中断等待;持续为 T 则说明进程已暂停。单次快照不能证明趋势,间隔十秒再采一份更有价值。

先保留证据,再做清理。重启成功只能恢复工作,不能说明根因已经消失。

验证进程和调试会话

确认附加的是正确产物

同名应用可能同时存在于多个模拟器、测试目录或旧构建目录。先在 LLDB 中确认进程和已加载镜像:

(lldb) process status
(lldb) target list
(lldb) image list -o -f

target list 应指向本次构建的可执行文件;image list 中目标模块的路径应落在预期的 Build Products 目录。若路径来自另一套 DerivedData,不要立刻全量删除,先记下旧路径并检查 Scheme 是否复用了错误的构建产物。

若 LLDB 命令行本身仍可响应,可执行:

(lldb) thread list
(lldb) thread backtrace all

如果所有线程都有回溯,调试通道通常已经建立,问题更可能位于应用自身等待或符号解析。若命令持续无返回,再从系统侧采样目标进程:

sample <PID> 10 1 -file "$HOME/lldb-case/app-sample.txt"

不要连续运行多个 sample。十秒样本足以判断主线程是在锁、文件 I/O、网络调用还是系统框架中等待。

用 UUID 核对 dSYM

文件名相同不代表符号匹配。每次链接都可能产生不同 UUID,LLDB 只会采用与 Mach-O 对应的 dSYM。

dwarfdump --uuid "/path/to/MyApp.app/MyApp"
dwarfdump --uuid "/path/to/MyApp.app.dSYM"

两边相同架构的 UUID 必须一致。若应用包含 arm64,至少要对照 arm64 那一行;不要拿另一轮 Archive、另一提交或重新链接后的 dSYM 顶替。

检查模块是否已加载

在 LLDB 中查询目标模块:

(lldb) image lookup -n AppDelegate
(lldb) image lookup -r -n 'YourModule\..*'
(lldb) breakpoint list

image lookup 找不到符号,但 image list 已出现模块时,优先检查调试信息格式。开发构建通常应生成 DWARF 或 DWARF with dSYM;如果使用了高优化配置,函数可能被内联,局部变量也可能不可见。此时应建立一个专用诊断 Configuration,而不是临时修改团队共用的 Release 配置。

断点显示 pending 不一定是错误。动态框架尚未加载时,断点会等待对应镜像进入进程。应把断点设在确定已加载的入口,再观察后续模块,而不是反复删除同一个断点。

从系统日志找握手失败

Xcode 界面经常只显示“附加失败”,系统日志会保留更具体的进程退出、权限拒绝或连接中止信息。复现前开启一段受限日志采集:

log stream --style compact --info \
  --predicate 'process == "debugserver" OR process == "lldb-rpc-server"' \
  > "$HOME/lldb-case/debug-session.log"

复现一次后用 Control-C 停止。日志可能包含用户名、工程路径和设备标识,提交工单或共享给团队前应脱敏。不要用无谓的全系统 --debug 长时间采集,它会制造大量噪声,也会增加筛选成本。

若日志显示目标进程在附加瞬间退出,应先脱离 LLDB 单独启动应用,确认它能稳定存活。若只有测试进程失败,则分别检查测试宿主、测试 Bundle 和被测应用,不要只盯主应用 PID。

建立可重复的恢复顺序

恢复动作应从影响最小的步骤开始:

  1. 结束当前调试会话,但保留目标应用和日志。
  2. 核对 Scheme、Configuration、运行目标及可执行文件路径。
  3. 比较 Mach-O 与 dSYM UUID。
  4. 重新构建当前 Target,不执行全工程清理。
  5. 仅在确认旧产物被复用后,删除对应工程的 DerivedData 目录。
  6. 仍可复现时,保存线程回溯、进程采样、系统日志和最小复现步骤。

可用下面的命令先列出目录及更新时间,避免误删其他任务的数据:

find "$HOME/Library/Developer/Xcode/DerivedData" \
  -maxdepth 1 -mindepth 1 -type d -print

云端 Mac 上可能同时运行构建、测试和图形调试任务。清理前应确认没有其他流水线正在使用同一目录;更稳妥的方案是让不同任务使用独立的 -derivedDataPath,把调试现场与自动构建缓存分开。

把排查结果变成验收项

一次修复结束后,至少完成四项验收:冷启动可附加、已有进程可附加、源码断点能命中、异常暂停时能查看关键变量。随后退出并重新进入远程会话再测一次,排除结果依赖当前图形会话或临时环境变量。

团队可以把 UUID 核对、产物路径和 Configuration 写入构建清单,但不要把个人目录写死进脚本。真正稳定的调试流程并非“清理后再试”,而是任何成员都能根据同一组证据判断故障位于进程、会话还是符号层,并只修复发生偏差的那一层。

常见问题

LLDB 能附加进程但断点始终不命中,应该先查什么?

先用 image list 确认目标模块已经加载,再核对可执行文件与 dSYM 的 UUID。模块未加载时断点会保持 pending,UUID 不一致时则无法获得正确的源码和行号。

调试卡住时可以立即清理 DerivedData 吗?

不建议。先保存进程列表、LLDB 输出、系统日志和当前产物 UUID;直接清理会破坏可复现现场。确认问题来自旧构建目录后,再只删除对应工程的目录。

远程调试需要把调试端口公开到互联网吗?

通常不需要。优先在云端 Mac 内部运行 Xcode 与 LLDB,并通过受控远程桌面或 SSH 操作;如必须转发端口,应使用受控隧道并限制监听地址。

VMDebug 云端 Mac

需要一台由单一订单独享的物理 Mac mini?

核对 M4 与 M4 Pro 两档配置、四种租用周期和 5 个可选节点,再按工作流选择设备。

选择配置并订购