关于 axiom-lldb
axiom-lldb 是面向 Swift 应用开发者的 LLDB 调试技能,提供崩溃和卡顿诊断、Swift 值检查、断点设置及运行时表达式求值的操作指南。
主要功能
- 01根据停止原因和回溯排查崩溃
- 02使用 v、p 和 po 检查 Swift 值
- 03通过线程状态诊断卡顿
- 04设置条件断点和日志断点
- 05求值表达式并修改运行时状态
适用场景
- 01查找可复现应用崩溃的原因
- 02检查运行时出现异常的 Swift 值
- 03卡顿或死锁时追踪线程状态
- 04无需重新构建即可验证调试假设
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-lldb安装后在 Claude Code 中输入/axiom-lldb确认已加载。
详细介绍
LLDB 调试
使用 LLDB 进行交互式调试。调试器会冻结时间,让你能够检查正在运行的应用——查看变量、计算表达式、浏览线程,并准确了解问题出在哪里。
核心要点:“LLDB 毫无用处”真正的意思是“我不知道该对 Swift 类型使用哪个命令”。这是知识缺口问题,而不是工具问题。
危险信号——遇到以下情况时查看本技能
| 症状 | 本技能是否适用 |
|---|---|
| 需要在运行时检查变量 | 适用——断点 + 检查 |
| 可以在本地复现的崩溃 | 适用——在崩溃位置之前设置断点 |
| 运行时值不正确,但代码看起来没问题 | 适用——单步执行并检查 |
| 需要了解卡顿期间的线程状态 | 适用——暂停 + 线程回溯 |
po 无法运行 / 显示乱码 | 适用——Playbook 3 提供替代方法 |
| 已分析崩溃日志,需要复现 | 适用——根据崩溃上下文设置断点 |
| 需要在不重新构建的情况下测试修复 | 适用——计算表达式 |
| 想在所有异常处中断 | 适用——异常断点 |
| 应用感觉很慢,但仍有响应 | 不适用——使用 axiom-performance-profiling |
| 内存随时间增长 | 不适用——先使用 axiom-memory-debugging |
| 应用完全冻结 | 可能适用——先使用 axiom-hang-diagnostics,再用 LLDB 检查线程 |
| 生产环境崩溃,无法在本地复现 | 不适用——先使用 axiom-testflight-triage |
LLDB 与其他工具的区别
digraph tool_selection {
"What do you need?" [shape=diamond];
"axiom-testflight-triage" [shape=box];
"axiom-hang-diagnostics" [shape=box];
"axiom-memory-debugging" [shape=box];
"axiom-performance-profiling" [shape=box];
"LLDB (this skill)" [shape=box, style=bold];
"What do you need?" -> "axiom-testflight-triage" [label="Crash log from field,\ncan't reproduce locally"];
"What do you need?" -> "axiom-hang-diagnostics" [label="App frozen,\nneed diagnosis approach"];
"What do you need?" -> "axiom-memory-debugging" [label="Memory growing,\nneed leak pattern"];
"What do you need?" -> "axiom-performance-profiling" [label="Need to measure\nCPU/memory over time"];
"What do you need?" -> "LLDB (this skill)" [label="Need to inspect state\nat a specific moment"];
}
**经验法则:**Instruments 用于测量。LLDB 用于检查。如果你需要了解某个特定时刻发生了什么,请使用 LLDB。如果你需要了解一段时间内的趋势,请使用 Instruments。
回复格式
协助进行 LLDB 调试时,请按以下结构组织输出:
- 即时诊断(1-3 条要点,标注置信度:HIGH/MEDIUM/LOW)
- 要运行的命令(编号排列,可直接复制粘贴,并带有
(lldb)前缀) - 需要关注的内容(命令 → 预期输出 → 解读)
- 可能的根本原因(按概率排序)
- 下一步断点计划(下次更早捕获问题)
- 未附加调试器时(仅依赖崩溃日志的备用路径)
Playbook 1:崩溃排查
**目标:**从停止位置开始,了解应用崩溃的原因。
步骤 1:读取停止原因
调试器停止时,首先检查:
(lldb) thread info
这会显示停止原因。常见停止原因:
| 停止原因 | 含义 | 下一步 |
|---|---|---|
EXC_BAD_ACCESS (SIGSEGV) | 访问了无效内存(空指针、悬空引用) | 检查地址——0x0 到 0x10 表示发生了 nil 解引用 |
EXC_BAD_ACCESS (SIGBUS) | 地址未对齐或无效 | 通常是 C 互操作或不安全指针问题 |
EXC_BREAKPOINT (SIGTRAP) | 触发了陷阱——Swift 运行时检查失败 | 检查是否有 fatalError()、preconditionFailure()、对 nil 强制解包或数组越界 |
EXC_CRASH (SIGABRT) | 主动中止——断言失败或出现未捕获的异常 | 查看“Application Specific Information”中的消息 |
breakpoint | 触发了你设置的断点 | 正常——检查状态 |
步骤 2:获取回溯
(lldb) bt
从上到下阅读。找到你的代码中的第一个栈帧(不是系统框架中的栈帧)。从这里开始调查。
(lldb) bt 10
如果完整回溯信息太多,可限制为 10 个栈帧。
步骤 3:跳转到你的栈帧
(lldb) frame select 3
跳转到栈帧 3(或你的代码所在的栈帧)。
步骤 4:检查状态
(lldb) v
(lldb) v self.someProperty
(lldb) v localVariable
使用 v(而不是 po)可靠地检查 Swift 值。详情请参阅 Playbook 3。
步骤 5:分类并修复
| 异常类型 | 典型原因 | 修复模式 |
|---|---|---|
低地址处的 EXC_BAD_ACCESS | 对 nil 可选值强制解包 | guard let / if let |
高地址处的 EXC_BAD_ACCESS | 使用已释放对象 / 悬空指针 | 检查对象生命周期,使用 [weak self] |
EXC_BREAKPOINT | Swift 运行时陷阱(越界、解包、前置条件) | 修复被违反的前置条件 |
SIGABRT | 未捕获的 ObjC 异常或 fatalError() | 阅读异常消息,修复根本原因 |
步骤 6:设置条件断点以便更早捕获问题
(lldb) breakpoint set -f MyFile.swift -l 42 -c "value == nil"
只有当第 42 行的 value 为 nil 时才会触发断点——在崩溃前捕获问题。
操作手册 2:卡顿/死锁诊断
目标: 检查所有线程状态,了解应用为何冻结。
步骤 1:暂停应用
如果应用卡住了,请按 Xcode 中的暂停按钮(⌃⌘Y),或使用:
(lldb) process interrupt
步骤 2:获取所有线程的回溯
(lldb) thread backtrace all
或使用简写:
(lldb) bt all
步骤 3:对线程状态进行分类
查看 Thread 0(主线程)——它负责处理所有 UI 事件。如果它被阻塞,应用就会冻结。
主线程被同步等待阻塞:
frame #0: libsystem_kernel.dylib`__psynch_mutexwait
frame #1: libsystem_pthread.dylib`_pthread_mutex_firstfit_lock_wait
...
frame #5: MyApp`ViewController.viewDidLoad()
说明:主线程正在等待互斥锁。另一个线程持有该锁。
主线程被 dispatch_sync 阻塞:
frame #0: libdispatch.dylib`_dispatch_sync_f_slow
...
frame #3: MyApp`DataManager.fetchData()
说明:从后台线程调用了 DispatchQueue.main.sync → 经典死锁。
主线程正忙(受 CPU 限制):
frame #0: MyApp`ImageProcessor.processAllImages()
frame #1: MyApp`ViewController.viewDidLoad()
说明:主线程上有耗时操作。将其移至后台线程。
步骤 4:检查死锁
如果两个线程都在等待对方持有的资源:
(lldb) thread list
查找多个状态为 waiting、且相互引用对方锁的线程。
步骤 5:检查特定线程
(lldb) thread select 3
(lldb) bt
(lldb) v
切换到另一个线程以检查其状态。
交叉参考: 确认卡顿原因后,查看修复模式 → /skill axiom-hang-diagnostics
操作手册 3:Swift 值检查
这是此技能的核心价值。 大多数开发者都会放弃使用 LLDB,因为 po 无法可靠地处理 Swift 类型。以下是实际可用的方法。
四种打印命令
| 命令 | 完整形式 | 功能 | 最适用场景 |
|---|---|---|---|
v | frame variable | 直接读取内存,无需编译 | Swift 结构体、枚举、局部变量——首选 |
p | expression(带格式化程序) | 编译表达式并显示格式化结果 | 计算属性、函数调用 |
po | expression --object-description | 调用 debugDescription | 实现了 CustomDebugStringConvertible 的类 |
expr | expression | 求值任意代码 | 调用方法、修改状态 |
何时使用各命令
从 v 开始——它读取存储属性的速度最快,也最可靠:
(lldb) v self.userName
(lldb) v self.items[0]
(lldb) v localStruct
v 通过直接读取内存来工作。它不会编译任何内容,因此不会因表达式编译错误而失败。
v 的限制: 它只能读取存储属性——计算属性、lazy var(首次访问之前)以及属性包装器的投影值($binding)都无法显示有意义的值。如果使用 v 时字段的值看起来不对或未显示,请改用 p。
当 v 无法访问时使用 p:
(lldb) p self.computedProperty
(lldb) p self.items.count
(lldb) p someFunction()
p 会编译并执行表达式。计算属性和函数调用需要用它。
使用 po 查看类的描述:
(lldb) po myObject
(lldb) po error
(lldb) po notification
po 会对结果调用 debugDescription。最适用于具有有意义描述的对象(NSError、Notification 等)。
“LLDB 出问题了”的时刻
| 你看到的内容 | 原因 | 修复方法 |
|---|---|---|
<uninitialized> | po 失败;优化器尚未填充变量 | 改用 v |
expression failed to parse, unknown type name | Swift 表达式解析器无法解析该类型 | 对于 ObjC 对象,尝试 expr -l objc -- (id)0x12345,或使用 v |
<variable not available> | 编译器将其优化掉了(Release 构建) | 使用 Debug 重新构建、为对应文件设置 -Onone,或将 register read 作为最后手段 |
error: Couldn't apply expression side effects | 表达式包含 LLDB 无法还原的副作用 | 尝试更简单的表达式;避免修改状态 |
po 显示的是内存地址,而不是值 | 对象未遵循 CustomDebugStringConvertible | 使用 v 获取原始值,或实现该协议 |
cannot find 'self' in scope | 断点位于没有 self 的上下文中(静态上下文、闭包) | 使用 v 和显式变量名 |
p 显示 $R0 = ...,但 po 崩溃 | 编译路径不同 | 能用时使用 p;po 会额外执行描述步骤,该步骤可能失败 |
检查可选值
(lldb) v optionalValue
显示:(String?) some = "hello" 或 (String?) none
不要使用 po optionalValue——它可能只显示 Optional("hello"),不太有用。
检查集合
(lldb) v myArray
(lldb) v myArray[2]
(lldb) v myDict
对于大型集合,可限制输出:
(lldb) p Array(myArray.prefix(5))
检查 SwiftUI 状态
SwiftUI 的 @State 由带下划线前缀的存储属性支持:
(lldb) v self._isPresented
(lldb) v self._items
对于 @Observable 模型:
(lldb) v self.viewModel.propertyName
诊断“视图没有更新”: 如果属性发生了变化(已通过 v 确认),但 SwiftUI 视图没有重新渲染,请用 bt 检查执行变更的线程。@Observable 的变更必须发生在 @MainActor 上,SwiftUI 才能观察到它们——在后台 Actor 上进行的变更不会触发视图更新。在视图 body 中使用 Self._printChanges(),查看是哪个属性触发了(或没有触发)重新渲染:
(lldb) expr Self._printChanges()
完整的观察诊断流程 → /skill axiom-swiftui-debugging
检查 Actor
使用 v 检查 Actor 状态效果最好,因为它可以直接读取内存,不受隔离限制:
(lldb) v actor
显示所有存储属性。之所以可行,是因为 LLDB 会暂停整个进程——无论 Actor 隔离状态如何,你都可以读取任何内存(Actor 隔离是编译时概念)。
在运行时修改值
(lldb) expr self.debugFlag = true
(lldb) expr myArray.append("test")
(lldb) expr self.view.backgroundColor = UIColor.red
无需重新构建即可修改值。适合用于验证推测。
引用先前的结果
LLDB 会为结果分配变量($R0、$R1 等):
(lldb) p someValue
$R0 = 42
(lldb) p $R0 + 10
$R1 = 52
操作手册 4:断点策略
源代码断点(基础)
(lldb) breakpoint set -f ViewController.swift -l 42
(lldb) b ViewController.swift:42
简单情况下可使用简写 b。
条件断点
仅在条件为真时中断:
(lldb) breakpoint set -f MyFile.swift -l 42 -c "index > 100"
(lldb) breakpoint set -f MyFile.swift -l 42 -c "name == \"test\""
基于迭代次数: 在命中 N 次后中断:
(lldb) breakpoint set -f MyFile.swift -l 42 -i 50
忽略前 50 次命中,然后中断。
日志断点(执行操作并自动继续)
记录日志但不中断——类似打印语句,但无需重新构建:
(lldb) breakpoint set -f MyFile.swift -l 42
(lldb) breakpoint command add 1
> v self.value
> continue
> DONE
或者在 Xcode 中:编辑断点 → 添加操作 → “Log Message” → 使用 @self.value@ 标记语法 → 勾选“Automatically continue”
符号断点
在任何对指定名称方法的调用处中断:
(lldb) breakpoint set -n viewDidLoad
(lldb) breakpoint set -n "MyClass.myMethod"
在所有向某个选择器发送的 ObjC 消息处中断:
(lldb) breakpoint set -S "layoutSubviews"
异常断点
Swift 错误(在抛出时中断):
(lldb) breakpoint set -E swift
Objective-C 异常(在抛出时中断):
(lldb) breakpoint set -E objc
在 Xcode 中: 断点导航器 → + → Swift 错误断点 / 异常断点
这是调试崩溃时最有用的断点。它会在抛出位置停止,而不是在 catch 位置或崩溃位置停止。
监视点
在变量值发生变化时中断:
(lldb) watchpoint set variable self.count
(lldb) watchpoint set variable -w read_write myGlobal
监视点由硬件支持——每个进程最多约 4 个,但速度很快。
一次性断点
中断一次后自动删除:
(lldb) breakpoint set -f MyFile.swift -l 42 -o
管理断点
(lldb) breakpoint list
(lldb) breakpoint disable 3
(lldb) breakpoint enable 3
(lldb) breakpoint delete 3
(lldb) breakpoint delete
操作手册 5:异步/并发调试
识别异步帧
Swift 并发回溯信息很杂乱——你会看到 swift_task_switch、_dispatch_call_block_and_release 和执行器内部信息夹杂在自己的代码中。不要被 40 多个运行时噪声帧吓退。请关注来自你自己模块的帧。
在 Swift 并发回溯信息中,查找 swift-task 帧:
Thread 3:
frame #0: MyApp`MyActor.doWork()
frame #1: swift_task_switch
frame #2: MyApp`closure #1 in ViewController.loadData()
swift_task_switch 帧表示一个异步挂起点。你的代码帧是那些带有你模块名称前缀的帧(上例中的 MyApp)。
检查任务状态
(lldb) thread backtrace all
查找帧中包含 swift_task 的线程。每个线程都代表一个活动的 Swift 任务。
Actor 隔离代码
在 actor 内部暂停时:
(lldb) v self
显示所有 actor 状态。这是因为 LLDB 会暂停整个进程——actor 隔离是一个编译时概念,而非运行时锁(对于默认 actor 而言)。
检查任务组
调试任务组时,在组闭包内部设置断点并检查:
(lldb) v
(lldb) bt
每个子任务都在自己的线程上运行。使用 bt all 查看这些线程。
**交叉参考:**有关 Swift 并发模式和修复策略 → /skill axiom-swift-concurrency。有关异步性能分析 → /skill axiom-concurrency-profiling
压力场景
场景 1:“仅在 Release 中崩溃——LLDB 在 Release 中毫无用处”
**情况:**崩溃发生在 Release 构建中,但 Debug 中不会。团队表示“我们没法调试”。
**为何行不通:**Release 优化会改变时序和内存布局,还可能消除变量,因此无法在 Debug 中重现崩溃。
正确做法:
- 使用 Debug 配置构建,但采用类似 Release 的设置:
- 优化级别:
-O(而非-Onone) - 仍需包含调试符号(
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym)
- 优化级别:
- 启用 Address Sanitizer(
-fsanitize=address)——以 2-3 倍开销捕获内存错误 - 根据崩溃报告,在崩溃位置设置断点
- 设置异常断点,在崩溃前捕获错误:
(lldb) breakpoint set -E swift (lldb) breakpoint set -E objc - 如果变量显示
<optimized out>,请降低该文件的优化级别:- Build Settings → Per-file flags → 为特定文件设置
-Onone
- Build Settings → Per-file flags → 为特定文件设置
- 最后的办法——直接读取寄存器值(变量被优化掉之前会存放在寄存器中):
在 ARM64 上:(lldb) register read (lldb) register read x0 x1 x2x0= self,x1-x7= 前 7 个参数。详情请参阅/skill axiom-lldb-ref第 1 部分。
场景 2:“直接加几条打印语句”
**情况:**开发者添加 print() 调用进行调试,重新构建、运行、查看控制台,然后重复此过程。
**为何行不通:**每轮打印调试都要花 3-5 分钟(编辑 → 构建 → 运行 → 导航到状态 → 查看输出)。而 LLDB 断点只需 30 秒。
正确做法:
- 在你准备添加
print()的代码行设置断点:(lldb) b MyFile.swift:42 - 添加日志断点,无需重新构建即可实现类似打印的效果:
- 编辑断点 → Add Action → Log Message → 勾选“Auto continue”
- 直接检查变量:
v self.someValue - 在运行时修改变量以验证猜想:
expr self.debugMode = true - 一次断点调试即可替代 5-10 轮打印调试。
时间对比(典型控制流调试):
| 方法 | 每次调查 | 5 个变量 |
|---|---|---|
print() 语句 | 3-5 分钟(构建 + 运行) | 15-25 分钟 |
| LLDB 断点 | 30 秒(设置 + 检查) | 2.5 分钟 |
**例外:**在紧密循环中(每秒命中数千次),日志断点会带来每次命中的额外开销。使用 -i 跳到你关心的迭代,或者在该特定循环中临时使用 print()。
场景 3:“po 不起作用,所以 LLDB 坏了”
**情况:**开发者输入 po myStruct,结果得到一堆乱码。于是认定 LLDB 不适用于 Swift,转而使用打印调试。
这是开发者放弃使用 LLDB 的首要原因。
po 对 Swift 结构体失效的原因:po 会调用 debugDescription,这需要在调试器上下文中编译表达式。对于 Swift 结构体,由于缺少类型元数据、泛型或模块解析问题,这种编译经常会失败。
正确做法:
- 使用
v而不是po——直接读取内存,无需编译:(lldb) v myStruct (lldb) v myStruct.propertyName - 使用
p查看计算属性:(lldb) p myStruct.computedValue - 仅对实现了
CustomDebugStringConvertible的类使用po - 如果
p也失败,尝试指定语言:(lldb) expr -l objc -- (id)0x12345 - 如果所有方法都失败,在方法内部使用
v self始终有效。
反模式
| 反模式 | 错误原因 | 更好的替代方案 |
|---|---|---|
对所有内容都使用 po | 对 Swift 结构体、枚举、可选值无效 | 对值使用 v,仅对类使用 po |
| 打印调试循环 | 每轮耗时 3–5 分钟,而断点只需 30 秒 | 使用带有日志点操作的断点 |
| “LLDB 不适用于 Swift” | LLDB 可以——只是命令选择不当 | v 专为 Swift 值设计 |
| 忽略回溯 | 不看跟踪信息就直接猜测 | 先运行 bt,再切换栈帧 |
| 每次命中都使用条件断点 | 条件开销较大时会拖慢执行 | 尽可能使用 -i(忽略计数) |
| 调试优化后的(Release)构建 | 变量缺失,代码顺序被重排 | 使用 Debug 配置,或按文件设置 -Onone |
| 强行继续执行,跳过异常 | 隐藏真正的错误 | 修复异常,不要压制它 |
| 未设置异常断点 | 崩溃会停在系统代码,而不是抛出位置 | 始终添加 Swift Error 和 ObjC Exception 断点 |
调试检查清单
开始调试会话前:
- 使用 Debug 构建配置(而不是 Release)
- 启用异常断点(Swift Error 和 ObjC Exception)
- 在疑似问题区域之前设置断点
- 确认要使用的命令:对值使用
v,对计算属性使用p,对描述使用po
调试会话期间:
- 先读取停止原因(
thread info) - 获取回溯(
bt)——找到你的栈帧 - 切换到你的栈帧(
frame select N) - 检查相关状态(
v self、v localVar) - 弄清原因后再编写修复
找到问题后:
- 设置条件断点,以便问题再次出现时捕获
- 考虑针对此情况添加断言或前置条件
- 移除临时断点
资源
**WWDC:**2019-429、2018-412、2022-110370
文档:/xcode/stepping-through-code-and-inspecting-variables-to-isolate-bugs、/xcode/setting-breakpoints-to-pause-your-running-app、/xcode/diagnosing-memory-thread-and-crash-issues-early
**技能:**axiom-lldb-ref、axiom-testflight-triage、axiom-hang-diagnostics、axiom-memory-debugging、axiom-swift-concurrency、axiom-concurrency-profiling