mcpField
A
axiom-lldb

axiom-lldb

github.com/charleswiltgen开发1.2k108 次安装今天更新
代码仓库

关于 axiom-lldb

axiom-lldb 是面向 Swift 应用开发者的 LLDB 调试技能,提供崩溃和卡顿诊断、Swift 值检查、断点设置及运行时表达式求值的操作指南。

主要功能

  1. 01根据停止原因和回溯排查崩溃
  2. 02使用 v、p 和 po 检查 Swift 值
  3. 03通过线程状态诊断卡顿
  4. 04设置条件断点和日志断点
  5. 05求值表达式并修改运行时状态

适用场景

  1. 01查找可复现应用崩溃的原因
  2. 02检查运行时出现异常的 Swift 值
  3. 03卡顿或死锁时追踪线程状态
  4. 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. 即时诊断(1-3 条要点,标注置信度:HIGH/MEDIUM/LOW)
  2. 要运行的命令(编号排列,可直接复制粘贴,并带有 (lldb) 前缀)
  3. 需要关注的内容(命令 → 预期输出 → 解读)
  4. 可能的根本原因(按概率排序)
  5. 下一步断点计划(下次更早捕获问题)
  6. 未附加调试器时(仅依赖崩溃日志的备用路径)

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_BREAKPOINTSwift 运行时陷阱(越界、解包、前置条件)修复被违反的前置条件
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 类型。以下是实际可用的方法。

四种打印命令
命令完整形式功能最适用场景
vframe variable直接读取内存,无需编译Swift 结构体、枚举、局部变量——首选
pexpression(带格式化程序)编译表达式并显示格式化结果计算属性、函数调用
poexpression --object-description调用 debugDescription实现了 CustomDebugStringConvertible 的类
exprexpression求值任意代码调用方法、修改状态
何时使用各命令

从 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 nameSwift 表达式解析器无法解析该类型对于 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 中重现崩溃。

正确做法:

  1. 使用 Debug 配置构建,但采用类似 Release 的设置:
    • 优化级别:-O(而非 -Onone)
    • 仍需包含调试符号(DEBUG_INFORMATION_FORMAT = dwarf-with-dsym)
  2. 启用 Address Sanitizer(-fsanitize=address)——以 2-3 倍开销捕获内存错误
  3. 根据崩溃报告,在崩溃位置设置断点
  4. 设置异常断点,在崩溃前捕获错误:
    (lldb) breakpoint set -E swift
    (lldb) breakpoint set -E objc
    
  5. 如果变量显示 <optimized out>,请降低该文件的优化级别:
    • Build Settings → Per-file flags → 为特定文件设置 -Onone
  6. 最后的办法——直接读取寄存器值(变量被优化掉之前会存放在寄存器中):
    (lldb) register read
    (lldb) register read x0 x1 x2
    
    在 ARM64 上:x0 = self,x1-x7 = 前 7 个参数。详情请参阅 /skill axiom-lldb-ref 第 1 部分。
场景 2:“直接加几条打印语句”

**情况:**开发者添加 print() 调用进行调试,重新构建、运行、查看控制台,然后重复此过程。

**为何行不通:**每轮打印调试都要花 3-5 分钟(编辑 → 构建 → 运行 → 导航到状态 → 查看输出)。而 LLDB 断点只需 30 秒。

正确做法:

  1. 在你准备添加 print() 的代码行设置断点:
    (lldb) b MyFile.swift:42
    
  2. 添加日志断点,无需重新构建即可实现类似打印的效果:
    • 编辑断点 → Add Action → Log Message → 勾选“Auto continue”
  3. 直接检查变量:v self.someValue
  4. 在运行时修改变量以验证猜想:expr self.debugMode = true
  5. 一次断点调试即可替代 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 结构体,由于缺少类型元数据、泛型或模块解析问题,这种编译经常会失败。

正确做法:

  1. 使用 v 而不是 po——直接读取内存,无需编译:
    (lldb) v myStruct
    (lldb) v myStruct.propertyName
    
  2. 使用 p 查看计算属性:
    (lldb) p myStruct.computedValue
    
  3. 仅对实现了 CustomDebugStringConvertible 的类使用 po
  4. 如果 p 也失败,尝试指定语言:
    (lldb) expr -l objc -- (id)0x12345
    
  5. 如果所有方法都失败,在方法内部使用 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

评论

支持 Markdown