作为工程师,我们几乎 70% 的时间都花在了调试上。剩下的 20% 用于思考架构方法和与队友交流,只有 10% 用于编写代码。
调试就像是犯罪电影中的侦探,而你同时也是凶手。
- Filipe Fortes 通过 Twitter
因此,让我们这 70% 的时间尽可能愉快地度过极为重要。LLDB 来拯救您了。花哨的 Xcode 调试器 UI 无需键入任何 LLDB 命令即可显示所有可用信息。然而,控制台仍然是我们工作流程的重要组成部分。让我们来分析一些最有用的 LLDB 技巧。我个人每天都会使用它们进行调试。
我们应该先去哪里? LLDB 是一个庞大的工具,里面有很多有用的命令。我就不一一介绍了。我只想带领大家学习最有用的命令。下面是我们的计划:
- 获取变量值:expression、e、print、po、p
- 获取应用程序的整体状态 + 语言特定命令:bugreport、frame、language
- 控制应用程序的执行流程:process, breakpoint, thread, watchpoint
- 荣誉奖:command, platform, gui
我还准备了附有说明和示例的 LLDB 实用命令图。如果需要,你可以把它挂在 Mac 的上方,以便记住这些命令 🙂。
- 获取变量的值和状态
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 在expression 后使用了双斜线符号--如下所示:
(lldb) expression <some flags> -- <variable>
expression 有近 30 种不同的flags。我鼓励你去探索它们。在终端中编写下面的命令,获取完整文档:
> lldb
> (lldb) help # To explore all available commands
> (lldb) help expression # To explore all expressions related sub-commands
我想停止使用以下 expression's flags:
-D <count>(--depth <count>) — 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>(--format <format>) –– Specify a format to be used for display.-i <boolean>(--ignore-breakpoints <boolean>) — 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 ...}属性。只有这样,阿宝才会返回可读的描述。
在本节开头,我还提到了 print 命令。
基本上,print <expression/variable> 和 expression -- <expression/variable> 是一样的。除了 print 命令不使用任何标志或附加参数。
- 获取整个应用程序的状态 + 语言特定命令
bugreport,frame,language
您有多少次将崩溃日志复制粘贴到任务管理器中,以便日后探讨问题?LLDB 有一个很棒的小命令,叫做 bugreport,它可以生成当前应用程序状态的完整报告。如果您遇到了某些问题,但又想稍后再解决,那么它可能真的很有帮助。为了恢复您对应用程序状态的了解,您可以使用 bugreport 生成的报告。
(lldb) bugreport unwind --outfile <path to output file>
最终报告将如下截图所示:
比方说,你想快速查看当前线程中的堆栈帧,frame 命令就能帮你做到这一点:
使用下面的片段可快速了解您目前所处的位置和周围的情况:
(lldb) frame infoframe #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 和 refcount。
demangle的名称是 “demangle mangled Swift type name”(Swift会在编译时生成该名称,以避免命名空间问题)。如果您想了解更多,我建议您观看 WWDC14 会议--“LLDB 中的 Swift 高级调试”。
refcount 也是一个非常简单的命令。它会显示特定对象的引用计数。让我们用上一节中使用的对象--日志记录器--来看看输出示例:
(lldb) language swift refcount loggerrefcount data: (strong = 4, weak = 0)
当然,如果要调试一些内存泄漏问题,这可能会非常有用。
3. 控制应用程序的执行流程process, breakpoint, thread
这部分是我的最爱。因为使用 LLDB 中的这些命令(尤其是断点命令),你可以在调试过程中自动执行很多例行工作。这最终会大大加快调试过程。
使用process,您基本上可以控制调试进程,并将其附加到特定目标或从中分离调试器。不过,由于 Xcode 会自动为我们完成进程附加(每次运行目标时,Xcode 都会附加 LLDB),所以我就不多说了。您可以阅读 Apple 指南中的 “将 LLDB 用作独立调试器”,了解如何使用终端附加到目标。
使用进程状态,您可以探索调试器正在等待您的当前位置:
使用 process status 您可以探索调试器正在等待您的当前位置:
(lldb) process statusProcess 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
它相当于 Xcode 调试器工具栏中的 “继续 ”按钮:
breakpoint 命令允许你以任何可能的方式操作断点。让我们跳过最显而易见的命令,如breakpoint enable、breakpoint disable和breakpoint delete。
首先,要查看所有断点,请使用list子命令,如下图所示:
(lldb) breakpoint listCurrent 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 = 11.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 = 12: file = '/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift', line = 60, exact_match = 0, locations = 1, resolved = 1, hit count = 12.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 96Breakpoint 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
您还可以使用下面的命令使用特定的 regex(例如函数名)设置断点:
(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() 来查看你感兴趣的值以进行调试吗?请不要这样做,还有更好的办法。
使用断点命令,您可以设置在断点被击中时立即执行的命令。你甚至可以设置不会中断执行的 “隐形 ”断点。从技术上讲,这些 “隐形 ”断点会中断执行,但如果在命令链末尾添加 continue 命令,就不会被发现了。
(lldb) b ViewController.swift:96 // Let's add a breakpoint firstBreakpoint 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 2Breakpoint 2:
Breakpoint commands:
p sum
p a + b
下一次,当这个断点被击中时,我们将在控制台中得到以下输出:
Process 36612 resuming
p sum
(Int) $R0 = 6p a + b
(Int) $R1 = 4
好极了!这正是我们想要的。在命令链的末尾添加继续命令,可以让运行更加流畅。这样,你甚至不会在这个断点上停止。
(lldb) breakpoint command add 2 // Setup some commandsEnter 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
So the result would be:
p sum
(Int) $R0 = 6p a + b
(Int) $R1 = 4continue
Process 36863 resuming
Command #3 'continue' continued the target.
通过线程命令及其子命令,您可以完全控制执行流程:step-over、step-in、step-out和continue。这些直接等同于 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 listProcess 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'
Honorable mentionscommand, platform, gui
在 LLDB 中,您可以找到用于管理其他命令的命令。听起来很奇怪,但实际上,这是一个相当有用的小工具。首先,它允许您从文件中直接执行一些 LLDB 命令。因此,您可以创建一个包含一些有用命令的文件,然后像执行单个 LLDB 命令一样一次性执行这些命令。下面是一个简单的文件示例:
thread info // Show current thread info
br list // Show all breakpoints
下面是实际命令的样子:
(lldb) command source /Users/Ahmed/Desktop/lldb-test-scriptExecuting 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 inbr 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 Debugger!”:
然后,我们需要导入一个 Python 模块,并开始正常使用我们的脚本命令:
(lldb) command import ~/Desktop/script.pyThe "print_hello" python command has been installed and is ready for use.(lldb) print_helloHello Debugger!
您可以使用 status 子命令快速查看当前平台信息: SDK 路径、处理器架构、操作系统版本,甚至该 SDK 的可用设备列表。
(lldb) platform statusPlatform: 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.
总结: 在本文中,我仅仅触及了 LLDB 真正强大的表面。尽管 LLDB 已经存在了很长时间,但仍有很多人没有充分发挥它的潜力。我对 LLDB 的基本功能以及 LLDB 如何实现调试过程自动化进行了快速概述。希望对大家有所帮助。
很多 LLDB 函数都被遗忘了。还有一些视图调试技巧我甚至都没有提及。如果你对此类话题感兴趣,请在下方留言。我将非常乐意为您撰写相关文章。
我强烈建议你打开终端,启用 LLDB 并输入 help。这将为您显示完整的文档。你可以花几个小时来阅读它。但我保证,这将是一笔合理的时间投资。因为了解您的工具是工程师真正提高工作效率的唯一途径。