1. Debugging Swift code with LLDB
作为工程师,我们几乎70%的时间都花在调试上。剩下的20%继续思考架构方法+与团队成员的交流,只有10%真正在写代码。
调试就像是犯罪电影中的侦探,你同时也是凶手。
— Filipe Fortes via Twitter
所以让这70%的时间尽可能愉快是非常重要的。LLDB来救我们了。奇特的Xcode调试器UI显示了所有可用的信息,而无需键入一个LLDB命令。 然而,控制台仍然是我们工作流程的重要组成部分。让我们来分析一些最有用的LLDB技巧。我个人每天都用它们来调试。
1.1. Where should we go first?
LLDB是一个巨大的工具,里面有很多有用的命令。我就不一一描述了。相反,我想带您浏览最有用的命令。这就是我们的计划:
- 探索变量值: expression, e, print, po, p
- 获取整个应用的状态和语言的特定命令: bugreport, frame, language
- 控件APP的执行流程: process, breakpoint, thread, watchpoint
- Honorable mentions: command, platform, gui
我还准备了有用的LLDB命令的映射以及描述和示例。如果你需要,你可以把它挂在你的Mac上面来记住这些命令 🙂

1.2. Explore variables value and state
Commands: expression, e, print, po, p

调试器的基本功能是查看和修改变量的值。这就是expression 或 e的含义(实际上更确切地说)。您基本上可以在运行时计算任何表达式或命令。
让我们假设您正在调试某个函数valueOfLifeWithoutSumOf(),该函数对两个数字求和并从42中提取结果。

让我们假设你一直得到错误的答案,你不知道为什么。所以要找到一个问题,你可以这样做:

或者,最好使用LLDB表达式来代替在运行时更改值。找出问题出在哪里。首先,将断点设置为感兴趣的位置。然后运行应用程序。
要以LLDB格式打印特定变量的值,你应该调用:
(lldb) e <variable>
同样的命令用于计算某些表达式:
(lldb) e <expression>

(lldb) e sum
(Int) $R0 = 6 // You can also use $R0 to refer to this variable in the future (during current debug session)
(lldb) e sum = 4 // Change value of sum variable
(lldb) e sum
(Int) $R2 = 4 // sum variable will be "4" till the end of debugging session
expression命令也有一些标志。对于不同的标志和实际表达式,LLDB使用双破折号——在表达式命令之后,像这样:
(lldb) expression <some flags> -- <variable>
expression几乎有30种不同的flag。我鼓励你们去探索它们。在终端中编写下面的命令以获得完整的文档:
> lldb
> (lldb) help # To explore all available commands
> (lldb) help expression # To explore all expressions related sub-commands
下面表达式的flags我想停下来:
- -D
(--depth ) — Set the max recurse depth when dumping aggregate types (default is infinity). - -O (--object-description) — Display using a language-specific description API, if possible.
- -T (--show-types) — Show variable types when dumping values.
- -f
(--format ) –– Specify a format to be used for display. - -i
(--ignore-breakpoints ) — Ignore breakpoint hits while running expressions
假设我们有一个名为logger的对象。这个对象包含一些字符串和结构作为属性。例如,您只想浏览一级属性。只需使用-D标志和适当的深度级别来做到这一点:
(lldb) e -D 1 -- logger
(LLDB_Debugger_Exploration.Logger) $R5 = 0x0000608000087e90 {
currentClassName = "ViewController"
debuggerStruct ={...}
}
默认情况下,LLDB会无限地查看对象,并向你展示每个嵌套对象的完整描述:
(lldb) e -- logger
(LLDB_Debugger_Exploration.Logger) $R6 = 0x0000608000087e90 {
currentClassName = "ViewController"
debuggerStruct = (methodName = "name", lineNumber = 2, commandCounter = 23)
}
你也可以使用e -O来查看对象描述——或者简单地使用别名po,如下面的例子所示
(lldb) po logger
<Logger: 0x608000087e90>
不是描述性的,不是吗?要获得人类可读的描述,必须将自定义类应用于CustomStringConvertible协议并实现 var description: String { return ...} property. 只有po返回你可读的描述。

在本节的开头,我还提到了print命令。 基本上print <expression/variable>和expression - <expression/variable>是一样的。但print命令不接受任何flags或附加arguements。
1.3. Get overall app’s state + language specific commands
bugreport, frame, language

您是否经常将崩溃日志复制、粘贴到任务管理器中,以便稍后研究该问题?LLDB有一个叫做bugreport的命令,它会生成一个完整的当前应用状态报告。如果你遇到了一些问题,但想稍后再解决它,这将非常有帮助。为了恢复你对应用程序状态的理解,你可以使用bugreport生成的报告。
(lldb) bugreport unwind --outfile <path to output file>
最终的报告如下图所示:


假设您想快速了解当前线程中的当前堆栈帧。Frame命令可以帮助你:

使用下面的代码片段来快速了解你所在的位置和周围的情况:
(lldb) frame info
frame #0: 0x000000010bbe4b4d LLDB-Debugger-Exploration`ViewController.valueOfLifeWithoutSumOf(a=2, b=2, self=0x00007fa0c1406900) -> Int at ViewController.swift:96
这些信息将在本文后面的断点管理中很有用。

LLDB有一些针对特定语言的命令。这里有c++, Objective-C, Swift和RenderScript的命令。 在这种情况下,我们对Swift感兴趣。这里有两个命令: demangle and refcount.
demangle就像它的名字中写的那样,只是demangle混乱的Swift类型名称(这是Swift在编译期间生成的,以避免命名空间问题)。 如果你想了解更多,我建议你观看这次WWDC14会议 — “Advanced Swift Debugging in LLDB”.
refcount也是一个非常简单的命令。它显示了特定对象的引用计数。让我们看一下上一节中使用的对象logger的输出示例:
(lldb) language swift refcount logger
refcount data: (strong = 4, weak = 0)
当然,如果您正在调试一些内存泄漏,这可能非常有用。
1.4. Control app’s execution flow
process, breakpoint, thread
这部分是我最喜欢的。因为使用这些来自LLDB的命令(特别是breakpoint),您可以在调试过程中自动化许多例行程序。这最终会大大加快您的调试过程。

使用process你基本上可以控制调试进程,并附加到一个特定的目标或从它分离调试器。但是由于Xcode为我们自动执行进程附件(LLDB是由Xcode每次运行目标附加),我不会停止在那。 你可以阅读如何使用终端连接到目标在这个苹果指南 — “Using LLDB as a Standalone Debugger”.
使用progress status,你可以查看调试器正在等待你的当前位置:
(lldb) process status
Process 27408 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
frame #0: 0x000000010bbe4889 LLDB-Debugger-Exploration`ViewController.viewDidLoad(self=0x00007fa0c1406900) -> () at ViewController.swift:69
66
67 let a = 2, b = 2
68 let result = valueOfLifeWithoutSumOf(a, and: b)
-> 69 print(result)
70
71
72
为了继续执行目标,直到出现下一个断点,运行以下命令:
(lldb) process continue
(lldb) c // Or just type "c" which is the same as previous command


breakpoint命令允许您以任何可能的方式操作断点。让我们跳过最明显的命令,如:breakpoint enable, breakpoint disable和breakpoint delete.
首先,让我们使用如下示例中的list子命令来研究所有断点:
(lldb) breakpoint list
Current breakpoints:
1: file = '/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift', line = 95, exact_match = 0, locations = 1, resolved = 1, hit count = 1
1.1: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.valueOfLifeWithoutSumOf (Swift.Int, and : Swift.Int) -> Swift.Int + 27 at ViewController.swift:95, address = 0x0000000107f3eb3b, resolved, hit count = 1
2: file = '/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift', line = 60, exact_match = 0, locations = 1, resolved = 1, hit count = 1
2.1: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.viewDidLoad () -> () + 521 at ViewController.swift:60, address = 0x0000000107f3e609, resolved, hit count = 1
列表中的第一个数字是断点ID,您可以使用它来引用任何特定的断点。让我们直接从控制台设置一些新的断点
(lldb) breakpoint set -f ViewController.swift -l 96
Breakpoint 3: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.valueOfLifeWithoutSumOf (Swift.Int, and : Swift.Int) -> Swift.Int + 45 at ViewController.swift:96, address = 0x0000000107f3eb4d
在本例中-f是要放置断点的文件的名称。-l是新断点的行号。有一个更短的方法来设置相同的断点用 b 快捷方式:
(lldb) b ViewController.swift:96
你也可以使用下面的命令用一个特定的正则表达式(例如函数名)来设置断点:
(lldb) breakpoint set --func-regex valueOfLifeWithoutSumOf
(lldb) b -r valueOfLifeWithoutSumOf // Short version of the command above
有时,为只命中一次而设置断点是有用的。然后指示断点立即删除自己。当然,这是一个标志:
(lldb) breakpoint set --one-shot -f ViewController.swift -l 90
(lldb) br s -o -f ViewController.swift -l 91 // Shorter version of the command above
现在让我们处理最有趣的部分-断点自动化。您知道您可以设置一个特定的操作,该操作将在断点发生时立即执行吗? 是的,你可以!您是否在代码中使用print()来探索您感兴趣的用于调试的值?别这么做,有更好的办法。🙂
使用breakpoint命令,您可以设置命令,这些命令将在碰到断点时立即执行。您甚至可以创建“不可见”的断点,它不会中断执行。 从技术上讲,这些“看不见的”断点会中断执行,但是如果在命令链的末尾添加continue命令,您就不会注意到它。
(lldb) b ViewController.swift:96 // Let's add a breakpoint first
Breakpoint 2: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.valueOfLifeWithoutSumOf (Swift.Int, and : Swift.Int) -> Swift.Int + 45 at ViewController.swift:96, address = 0x000000010c555b4d
(lldb) breakpoint command add 2 // Setup some commands
Enter your debugger command(s). Type 'DONE' to end.
> p sum // Print value of "sum" variable
> p a + b // Evaluate a + b
> DONE
要确保您添加了正确的命令,请使用断点命令列表
(lldb) breakpoint command list 2
Breakpoint 2:
Breakpoint commands:
p sum
p a + b
下次当这个断点到达时,我们将在控制台中得到以下输出:
Process 36612 resuming
p sum
(Int) $R0 = 6
p a + b
(Int) $R1 = 4
太棒了!这正是我们要找的。通过在命令链的末尾添加continue命令,可以使它更加流畅。所以你甚至不会在这个断点上停下来。
(lldb) breakpoint command add 2 // Setup some commands
Enter your debugger command(s). Type 'DONE' to end.
> p sum // Print value of "sum" variable
> p a + b // Evaluate a + b
> continue // Resume right after first hit
> DONE
所以结果是:
p sum
(Int) $R0 = 6
p a + b
(Int) $R1 = 4
continue
Process 36863 resuming
Command #3 'continue' continued the target.

通过线程命令及其子命令,您可以完全控制执行流程:单步、步进、步出和继续。这相当于Xcode调试器工具栏上的流程控制按钮。
对于这些特定的命令,还有一个预定义的LLDB快捷方式:
(lldb) thread step-over
(lldb) next // The same as "thread step-over" command
(lldb) n // The same as "next" command
(lldb) thread step-in
(lldb) step // The same as "thread step-in"
(lldb) s // The same as "step"
为了获得更多关于当前线程的信息,只需调用info子命令:
(lldb) thread info
thread #1: tid = 0x17de17, 0x0000000109429a90 LLDB-Debugger-Exploration`ViewController.sumOf(a=2, b=2, self=0x00007fe775507390) -> Int at ViewController.swift:90, queue = 'com.apple.main-thread', stop reason = step in
要查看当前所有活动线程的列表,请使用list子命令:
(lldb) thread list
Process 50693 stopped
* thread #1: tid = 0x17de17, 0x0000000109429a90 LLDB-Debugger-Exploration`ViewController.sumOf(a=2, b=2, self=0x00007fe775507390) -> Int at ViewController.swift:90, queue = 'com.apple.main-thread', stop reason = step in
thread #2: tid = 0x17df4a, 0x000000010daa4dc6 libsystem_kernel.dylib`kevent_qos + 10, queue = 'com.apple.libdispatch-manager'
thread #3: tid = 0x17df4b, 0x000000010daa444e libsystem_kernel.dylib`__workq_kernreturn + 10
thread #5: tid = 0x17df4e, 0x000000010da9c34a libsystem_kernel.dylib`mach_msg_trap + 10, name = 'com.apple.uikit.eventfetch-thread'
1.5. Honorable mentions
command, platform, gui

在LLDB中可以找到管理其他命令的命令。听起来很奇怪,但实际上,这是非常有用的小工具。 首先,它允许您直接从文件中执行一些LLDB命令。因此,您可以使用一些有用的命令创建一个文件,并立即执行它们,就像它是一个单一的LLDB命令一样。 下面是一个简单的文件示例:
thread info // Show current thread info
br list // Show all breakpoints
下面是实际命令的样子:
(lldb) command source /Users/Ahmed/Desktop/lldb-test-script
Executing commands in '/Users/Ahmed/Desktop/lldb-test-script'.
thread info
thread #1: tid = 0x17de17, 0x0000000109429a90 LLDB-Debugger-Exploration`ViewController.sumOf(a=2, b=2, self=0x00007fe775507390) -> Int at ViewController.swift:90, queue = 'com.apple.main-thread', stop reason = step in
br list
Current breakpoints:
1: file = '/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift', line = 60, exact_match = 0, locations = 1, resolved = 1, hit count = 0
1.1: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.viewDidLoad () -> () + 521 at ViewController.swift:60, address = 0x0000000109429609, resolved, hit count = 0
不幸的是,也有一个缺点,您不能向源文件传递任何参数(除非您将在脚本文件本身中创建一个有效的变量)。
如果你需要更高级的东西,你总是可以使用脚本子命令。这将允许您管理(添加、删除、导入和列出)自定义Python脚本。有了这个脚本,真正的自动化就成为可能。 请查看这个关于LLDB的Python脚本的很好的指南。为了演示,让我们创建一个脚本文件script.py,并编写一个简单的命令print_hello(),它将打印“Hello调试器!”"在控制台中:
import lldb
def print_hello(debugger, command, result, internal_dict):
print "Hello Debugger!"
def __lldb_init_module(debugger, internal_dict):
debugger.HandleCommand('command script add -f script.print_hello print_hello') // Handle script initialization and add command from this module
print 'The "print_hello" python command has been installed and is ready for use.' // Print confirmation that everything works
view raw
然后我们需要导入一个Python模块,并开始正常使用script命令:
(lldb) command import ~/Desktop/script.py
The "print_hello" python command has been installed and is ready for use.
(lldb) print_hello
Hello Debugger!

您可以使用状态子命令快速检查当前平台信息。状态会告诉你:SDK路径,处理器架构,操作系统版本,甚至是这个SDK可用设备的列表。
(lldb) platform status
Platform: ios-simulator
Triple: x86_64-apple-macosx
OS Version: 10.12.5 (16F73)
Kernel: Darwin Kernel Version 16.6.0: Fri Apr 14 16:21:16 PDT 2017; root:xnu-3789.60.24~6/RELEASE_X86_64
Hostname: 127.0.0.1
WorkingDir: /
SDK Path: "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk"
Available devices:
614F8701-3D93-4B43-AE86-46A42FEB905A: iPhone 4s
CD516CF7-2AE7-4127-92DF-F536FE56BA22: iPhone 5
0D76F30F-2332-4E0C-9F00-B86F009D59A3: iPhone 5s
3084003F-7626-462A-825B-193E6E5B9AA7: iPhone 6
...

你不能在Xcode中使用LLDB GUI模式,但你总是可以在终端上做。
(lldb) gui
// You'll see this error if you try to execute gui command in Xcode
error: the gui command requires an interactive terminal.

Conclusion:
在本文中,我只是触及了LLDB真正威力的皮毛。虽然LLDB已经存在了很长时间,但仍然有很多人没有充分利用它的潜力。我简要介绍了基本功能,以及LLDB如何实现自动调试过程。我希望它有用。
如此多的LLDB函数被抛在了后面。还有一些视图调试技术,我甚至没有提到。如果你对这个话题感兴趣,请在下面留言。我很乐意把它写下来。
我强烈建议您打开一个终端,启用LLDB并输入help。这将向您展示完整的文档。你可以花几个小时阅读它。 但我保证这是一个合理的时间投资。因为了解你的工具是工程师真正变得高效的唯一途径。
有关LLDB的参考资料及有用文章
-
Official LLDB site — 你会在这里找到所有可能与LLDB有关的材料。文档、指南、教程、资源等等。
-
LLDB Quick Start Guide by Apple — 和往常一样,苹果有很好的文档。本指南将帮助您快速地开始使用LLDB。此外,他们还描述了如何在没有Xcode的情况下使用LLDB进行调试。
-
How debuggers work: Part 1 — 基础——我非常喜欢这个系列的文章。这只是调试器如何工作的奇妙概述。这篇文章描述了使用用c编写的手工调试器代码的所有基本原则。我强烈建议您阅读这些伟大系列的所有部分(第2部分、第3部分)。
-
WWDC14 Advanced Swift Debugging in LLDB — 很好的概述了在Swift调试方面LLDB的新特性。以及LLDB如何通过使用内置函数和特性的整体调试过程帮助您提高生产率。
-
Introduction To LLDB Python Scripting — LLDB的Python脚本指南,它允许你快速入门。
-
Dancing in the Debugger. A Waltz with LLDB — 对一些LLDB基础知识的巧妙介绍。有些信息有点过时(例如(lldb)线程返回命令)。不幸的是,它不能与Swift正常工作,因为它可能会给引用计数带来一些潜在的损害)。 尽管如此,这是一篇开启LLDB之旅的好文章。