大多数浏览器和
Developer App 均支持流媒体播放。
-
使用 LLDB 对 Swift 进行调试
学习如何设置复杂的 Swift 项目以进行调试。我们将带您深入了解 LLDB 的内部知识和调试信息。我们还将分享复杂情景的最佳实践,例如,针对构建在构建服务器上的代码或自定义构建系统的代码进行调试。
资源
相关视频
WWDC21
WWDC19
WWDC18
-
下载
♪ ♪
Adrian: 大家好 我是 Adrian 我将和大家谈谈 如何设置您的项目 以获得优秀的 LLDB 调试体验 LLDB 是 Xcode 附带的 基础调试技术 LLDB 允许您在 App 中设置断点 暂停执行 检查变量和对象的状态 探索代码等等 LLDB 可以帮助您理解代码正在做什么 并使您能够找到代码行为 偏离预期的地方 它是理解和探索代码的强大工具 如果您想了解更多关于 LLDB 的信息 请查看早期的视频 如 WWDC21 的 “Discover breakpoint improvements” 今天我们要来看一些高级工作流 它们在调试 Swift 代码时 具有独特的意义 也许您正在将一个第三方框架 集成到您的 App 中 也许您的 App 和您的团队 已经发展到了大部分代码 都由一个持续集成系统构建的地步 也许您正在使用自定义 构建系统来与您公司的 基础结构集成 也许您正在为其他软件开发者 构建软件 或者只是想更多地了解 LLDB 我的目标是让您更好地理解 LLDB 是如何工作的 以及它需要从构建系统中 获得什么信息才能正常工作 我这里有一个小项目 我们将用它作为我们的运行例子 我是一名编译工程师 我喜欢游戏 所以我在业余时间 为文本冒险游戏编写了解析器 这个是我最近完全从 Swift 开始的 让我给您展示一下我目前的成果 游戏使用文本界面 所以我要在 Terminal 中运行它 就像在每一次冒险中 我们都会从检查库存开始
这个游戏的背景设置在当代 我看到我有一部 iPhone 接下来 让我们看看周围的环境
嗯 这个传感器看起来很有意思 也许我们可以在传感器上 使用 iPhone
我把 iPhone 掉了 哦 这不是我想给您展示的 我觉得我的游戏有 bug 还好这是关于调试器的演讲 让我们在解析器中设置一个断点 并再次运行我们的命令
我们首先应该验证 该命令是否被正确读取 words 变量包含标记化的命令
啊 这跟预期的不太一样 我不知道这是怎么回事 昨天我在使用调试器时没有任何问题 然后我昨天晚上集成了这个 UI framework 从而在终端上对文本进行样式化 该框架的开发者 有一个持续集成系统 该系统每晚都在构建开发框架 我将直接链接到最新的一个 我想知道这个框架 是否与我的调试问题有关 这个例子正合适 我已经注意到 我不能进入框架的源代码 即使我明确下载了调试版本 看看这个
我只能看到反汇编
让我们试着理解一下这里发生了什么 让我们从弄清楚我为什么 看不到任何源代码开始 为了显示源代码 LLDB 需要什么 当编译器编译一个函数时 它会生成机器码
并且为调试器留下回溯线索 因此可执行文件中的地址 就可以映射到源文件 和行号 反之亦然 这些线索被称为调试信息 在 Apple 平台上 调试信息存储在目标文件中 为了存档和发布 调试信息可以链接到 .dSYM 包中 调试信息链接器被称为 dsymutil LLDB 使用 Spotlight 来定位 .dSYM 包 因此它们在磁盘上的位置可以非常灵活 现在我们知道了调试信息 是如何工作的 让我们回到示例 首先 让我们验证 LLDB 是否已经为框架找到了dSYM 我们可以使用 image list 命令来完成 UI framework 被称为 “TerminalInterface”
是的 LLDB 找到了框架的 dSYM 这意味着它可以访问调试信息 我们可以使用 image lookup 来获取更多关于当前地址的信息
顺便说一下 如果您想了解 更多关于各种选项的信息 LLDB 有很棒的内置帮助
啊 我想我知道为什么没有源代码了 这里的源代码路径指向了 构建服务器上的源代码 而不是它们在我的本地机器上的位置 我们可以解决这个问题 LLDB 有一个内置的源码映射 我们可以用它重定向这些路径
我们可以现在就输入命令 但我更希望让这个更改更持久 您可以通过依次打开 Product Scheme Edit scheme 或者选择单击播放按钮 来打开 Scheme 编辑器 在该编辑器中 您可以定义 每个项目的 LLDB init 文件 我已经为这个项目添加了一个
现在我们设置好了 LLDB 让我们再次运行项目吧
我们有了源代码
LLDB 可以使用“settings set target.source-map” 重新映射源路径 您可以将此命令 放入项目的 .lldbinit 文件中 以使其自动运行 或者 每个 .dSYM 包 包含一个 XML .plist 文件 您可以在其中放置一个路径前缀重映射字典 如果您有一个从服务器 获取最新版本的下载脚本 您可以修改该脚本 自动将适当的 重映射字典注入下载的 .dSYM 中 您可以在 LLDB 网站上 了解更多关于这个过程的信息
源码路径完全不是针对特定语言的 所以这种方法同样适用于 Swift C++ 和 Objective-C 项目等 要了解更多 关于 Apple 平台上的符号的信息 请观看 WWDC21 的 “Symbolication: beyond basics” 在构建服务器场上编译源代码时 不同机器到源文件的 远程路径可能不同 为了避免必须在每台机器上 定义一个映射前缀 我们可以指示编译器 在将源路径放入调试信息之前 对它们进行规范化 这可以使用 -debug-prefix-map 选项完成 通过这种方式 特定于机器的路径前缀就可以 被一个唯一 规范的占位符名称替换 然后该占位符名称就可以 重新映射到 LLDB 中的本地路径 在我们开始切入源码之前 我试图打印 words 的对象描述
但这没有奏效 事实上 即使只是 为表达式 words 求值也不起作用
至少我们可以在变量视图中看到变量
与 Xcode 变量视图等价的控制台是 frame variable 或 v 命令
如果您想进一步了解 这些命令之间的细微差别 请查看 WWDC19 的 “LLDB: beyond 'po” 那么 po 又是什么 为什么它仍然不能运作呢 要理解这意味着什么 我们需要进一步了解 LLDB 提醒一下 LLDB 是一个调试器 但 LLDB 又不仅仅是调试器 它也是一个编译器 除了调试器的功能 LLDB 还包括完整的 Swift 和 Clang 编译器的副本 这些编译器支持 LLDB 的 表达式求值器 您可能通过它的 p 和 po 命令别名 了解到它 使用表达式求值器 我们不仅可以查看变量 还可以执行计算 调用函数 甚至改变程序的状态 请查看 WWDC18 的 “Advanced Debugging with Xcode and LLDB” 大概了解一下 通过这些命令能做到什么 调试器如何格式化局部变量呢 编译器提供的调试信息 会告诉调试器 变量在内存中的位置 但仅凭这些信息 LLDB 只能向我们展示 原始字节的随机组合 那么 LLDB 要如何将其转换成 格式良好的输出呢 答案是类型 类型信息允许 LLDB 了解 源变量的结构 和内存布局 通过类型信息 LLDB 能知道 聚合类型有哪些字段 并且类型会允许 LLDB 使用适当的数据格式化器 将它们漂亮地打印出来 现在 让我们看看类型信息从何而来吧 在调试器端 使用 frame variable 和 v 命令的地方 LLDB 会从调试信息中获取类型信息 LLDB 也会从 Swift 反射元数据中 获取类型 在编译器端 使用表达式求值器和 po 的地方 LLDB 会从模块中获取类型信息 这种清晰的分离在 Xcode 14 中 是全新的 它解释了变量视图为什么可以 在表达式求值器不能执行的情况下 完全发挥作用 模块是编译器组织类型声明的方式 Swift 编译器知道许多 导入模块的方法 但在深入研究之前 我想向您展示一个便捷的新功能
我们如何开始诊断 在编译器端发生的问题呢 今年 LLDB 增加了一个 新的 swift-healthcheck 命令 它是判断模块导入 是否失败的第一站 让我来告诉您这是怎么回事 通过在问题发生后运行 swift-healthcheck 我们可以访问 Swift 表达式求值器配置的日志
在日志的末尾 我们看到 LLDB 在导入 TerminalUI Swift 模块时 遇到了麻烦 根据名称 我假定这是 TerminalInterface framework 的 实现细节 这个缺失的模块是个问题 因为 self 的类型在 UI 实现中 是泛型的 如果没有包含该类型的模块 表达式求值器 就无法了解 self 的动态类型 我正在给框架的开发者发送消息 请他们进行调查 根据我的经验 他们总是积极响应 谁知道呢 也许我们可以 在这个视频结束前找到解决方案 同时 让我们看看 LLDB 的编译器 是如何查找 Swift 模块的
我的 App 有自己的 Swift 模块 它可以导入一个系统框架 比如 Foundation 系统框架是文本稳定的 Swift 接口文件 存在于 SDK 中 任何 Swift 模块都可能 导入 Clang 模块 Clang 模块只是一个或多个 header 文件的时髦名称 这些 header 文件会在 模块映射文件的帮助下组合在一起 Clang 模块可以 依赖于其他 Clang 模块
我的 App 也可能导入属于本地构建框架 的 Swift 模块 它还可以导入不属于 SDK 的 文本的 Swift 接口文件 如果您想了解该如何做 请查看 WWDC19 的“Binary Frameworks in Swift” 我的 App 也可能链接到一个 包含 Swift 代码的静态库 然后 它也附带一个 Swift 模块 嗯 不过我们还没结束 我应该提一下还有桥接 header 它也可以导入 Clang 模块 最后 作为 LLDB 特有的一个功能 部分模块内容可以仅从调试信息重构 源可真多 LLDB 是如何找到它们的呢
构建系统的工作是将模块打包 以便 LLDB 可以找到它们 来自系统框架的模块 会留在 SDK 中 当 LLDB 连接到您的程序时 它会找到匹配的 SDK 读取它们 当直接从目标文件进行调试时 LLDB 将找到所有非 SDK 模块 在构建时所在的位置 Dsymutil 可以为每个动态库框架 或 dylib 和可执行文件 打包一个名为 .DSYM 包的 调试信息归档文件
每个 .dSYM 包都可以包含 二进制 Swift 模块 其中可能包含桥接 header 文本的 Swift 接口文件 以及最重要的调试信息 它包含一切 一切吗 除了属于静态归档的 Swift 模块
为了让一个 Swift 模块 被 dsymutil 接收 它需要被链接器注册 对于动态库和可执行文件 构建系统将自动为您完成此操作 但静态档案不是由链接器生成的 它们就像 zip 文件一样 只是目标文件的集合 这意味着 向链接器 注册任何 Swift 模块的责任 落在了连接静态存档的 每个可执行文件或动态库上 在很多情况下 Xcode 的构建系统会帮您完成这些 但是 如果您正在维护自己的 自定义构建系统 或者您已经定义了自定义构建规则 则需要注意这一点
当使用 Apple 链接器时 Swift 模块需要使用 -add-ast-path 选项注册 请检查构建日志 以验证是否存在这种情况 您还可以使用 dsymutil 来输出可执行文件的符号表 并使用 grep 查看 swiftmodule 来验证是否奏效
在 Linux 等其他平台上 swift 驱动程序支持 -modulewrap 操作 该操作会将二进制 Swift 模块文件 转换为您可以 和其他调试信息一起 链接到您的二进制文件中的对象 LLDB 将在那里找到它 框架开发者的响应速度非常快 正如我们所怀疑的 作为框架构建系统的一部分 使用了静态归档 而属于静态存档的 Swift 模块 在 dSYM 包中丢失了 我现在已经安装了一个 修正版本的框架 它已经用链接器 注册了丢失的静态模块 所以 dsymutil 能够收集它
现在 self 可以解析了
我们可以打印 words 的对象描述了
因为我们刚好在使用控制台 所以我使用 s 别名 进入 parseFrom 函数
现在我们也可以轻易地找到 bug 这里只是一个复制粘贴的错误
这样 我们不仅解决了 Swift 模块缺失的难题 也解决了游戏的第一个难题
在我们准备结束之前 我还要讲一个需要注意的细节 Swift 编译器会将 Clang header 搜索路径和其他相关选项 序列化到二进制的 .swiftmodule 文件中 这非常棒 因为它使 导入它们的 Clang 模块依赖 在构建期间刚好可以工作 但是 当在不同的机器上构建时 这些本地路径可能是有害的 因此 在将二进制 .swiftmodule 发送到另一台机器之前 请考虑使用 -no-serialize-debugging-options 编译器标记进行构建 在 Xcode 中控制的设置是 SWIFT_SERIALIZE_DEBUGGING_OPTIONS
您可以使用以下设置之一 在 LLDB 中重新引入这些搜索路径 让我们回顾一下所学的内容 如果您想将代码从一台机器 传送到另一台机器 您应该问问自己希望进行 何种级别的调试 例如 如果您将一个二进制框架 交付给另一个开发者 并且您不希望他们在调试器中 进入您的代码 那么最好只将 Swift 模块以 文本的 .swiftinterface 文件形式交付 但是 如果您正在设置一个 构建服务器或持续集成系统 开发者需要在其中 调试下载的构建工件 您将需要确保构建了一个 二进制 Swift 模块 并考虑关闭搜索路径序列化 您还可以使用 -debug-prefix-map 选项 在调试信息中规范化服务器上的源路径 这就是我要为大家讲的全部内容 今天我们了解了 LLDB 作为调试器和编译器的双重性质 调试器需要 调试信息和反射元数据来运行 并提供 Xcode 变量视图 和 v 命令 编译器需要 Module 并且对搜索路径很敏感 它位于 expr p 和 po 命令之后 获取编译器诊断的一个好方法 是在 LLDB 中 使用新的 swift-healthcheck 命令 感谢收看
-
-
5:04 - Show info about all loaded dylibs
image list
-
5:24 - Show debug info for a code address
image lookup -va $pc
-
5:58 - Show help for target.source-map
settings list target.source-map
-
6:37 - Remap source paths in LLDB
settings set target.source-map /Volumes/BUILD_SERVER/projects /Users/demo/Desktop/Adventure/3rdparty
-
7:02 - Source path remapping
settings set target.source-map prefix new
-
8:13 - Debug prefix map
-debug-prefix-map $PWD=/BUILDROOT
-
8:32 - Print object description of "words"
po words expr -O -- words
-
8:40 - Evaluate the expression "words"
p words expr words
-
8:58 - Display the variable "words"
v words frame variable words
-
10:10 - Raw memory of a Swift variable
mem read UnsafePointer<Items>(self.inventory)
-
11:59 - See diagnostics from LLDB's embedded Swift compiler
swift-healthcheck
-
15:47 - Register Swift modules with the Linker
ld … -add_ast_path /path/to/My.swiftmodule
-
16:05 - Verify Swift modules were registered in binary
dsymutil -s MyApp | grep .swiftmodule
-
16:12 - Wrapping Swift modules in object files on Linux
swiftc -modulewrap My.swiftmodule -o My.swiftmodule.o
-
16:52 - Evaluate the expression "self"
p self
-
16:58 - Print object description of "words"
po words expr -O -- words
-
17:08 - Step into function call
s thread step-in
-
17:10 - Step over instruction
n thread step-over
-
18:23 - Avoiding serialized search paths in Swift modules (command line)
-no-serialize-debugging-options
-
18:24 - Avoiding serialized search paths in Swift modules (Xcode)
SWIFT_SERIALIZE_DEBUGGING_OPTIONS=NO
-
18:32 - Reintroducing search paths in LLDB
settings set target.swift-extra-clang-flags … settings set target.swift-framework-search-paths … settings set target.swift-module-search-paths …
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。