【译】使用 LLDB 调试 Swift 代码



原文地址

作为工程师,我们会花费 70% 的时间来调试。剩下的 20% 用于思考架构和与同事交流,最后的 10% 才是真正用于编码的时间。

调试就像是在犯罪电影中同时扮演侦探和嫌疑人。

Filipe Fortes via Twitter

因此,如何让 70% 的调试时间尽可能的愉快,就显得及其重要。LLDB 因此应运而生。Xcode 的 UI 调试工具展示了所有有用的调试信息,而不用敲任何 LLDB 指令。然而,控制台在我们日常工作中仍然扮演了重要的角色,下面介绍一些笔者在日常调试工作中经常使用的 LLDB 小技巧。

从哪里开始?

LLDB 是一款包含了许多有用指令的强大工具。笔者不可能详细的介绍每一条指令,所以这里会列举一些最常用的命令。下面是本文的计划

  1. 查看变量值:expressioneprintpop

  2. 获取 App 整体状态与特定语言指令:bugreportframelanguage

  3. 控制 App 的执行流程:processbreakpointthreadwatchpoint
  4. 其他:commandplatformgui

原作者也准备了一份包含描述和例子的 LLDB 指令图。按需自取

Download full size version with this link — https://www.dropbox.com/s/9sv67e7f2repbpb/lldb-commands-map.png?dl=0

1. 查看变量值和状态

指令: expressioneprintpop

调试器最基本的功能就是查看和修改变量的值,这也是 expressione 的功能。

假设你正在调试一个 valueOfLifeWithoutSumOf() 函数,函数把两个数值相加再与 42 做差。

也假设你一直得到错误的结果并且无从得知原因何在。因此为了找到问题所在,你尝试了如下的方法:

最佳的方式,其实是使用 LLDB 而不是在代码中直接修改值。首先,在你觉得有问题的代码处,设置一个断点,接着运行 app。

在 LLDB 中,通过调用如下指令,来打印特定的变量值

1
(lldb) e <variable>

另外,用来执行表达式计算的指令也及其相似

1
(lldb) e <expression>

lldb

1
2
3
4
5
(lldb) e sum 
(Int) $R0 = 6 // 可以使用 $R0 在当前调试会话中调用这个变量
(lldb) e sum = 4 // 修改 sum 变量
(lldb) e sum
(Int) $R2 = 4 // sum 变量的值在当前的调试中都会是 '4'

expression 指令也提供了一些标记。为了区分标记和表达式,LLDB 使用 -- 来做区分:

1
(lldb) expression <标记> -- <variable>

expression 指令有接近 30 种不同的标记。在终端中可以通过如下指令来获取全部的介绍文档

1
2
3
> lldb
> (lldb) help # 查阅所有指令文档
> (lldb) help expression # 查阅所有 expression 的子指令用法

下面介绍一些常用的 expression 标记:

  • -D <count> (--depth <count>) 设置集合类型的最大递归深度 (默认为无穷大)
  • -O (--object-description) 如果可能的话,使用显示特定语言描述的 API
  • -T (--show-types) 显示打印的值的类型
  • -f <format> (--format <format>) 指定显示的格式
  • -i <boolean> (--ignore-breakpoints <boolean>) 是否在运行 expression 时忽略断点

这里假设有个名为 logger 的对象。该对象包含一些 string 和结构体作为属性。例如,你只想查看最上层的属性。只要是有 -D 标签指定遍历深度

1
2
3
4
5
6
(lldb) e -D 1 -- logger

(LLDB_Debugger_Exploration.Logger) $R5 = 0x0000608000087e90 {
currentClassName = "ViewController"
debuggerStruct ={...}
}

默认的 LLDB 会无限递归的查看对象内部所有对象的描述:

1
2
3
4
5
6
7
(lldb) e -- logger


(LLDB_Debugger_Exploration.Logger) $R6 = 0x0000608000087e90 {
currentClassName = "ViewController"
debuggerStruct = (methodName = "name", lineNumber = 2, commandCounter = 23)
}

也可以通过 e -O -- 或者简单的 po 来查看对象的描述

1
2
(lldb) po logger
<Logger: 0x608000087e90>

为了便于阅读,需要为自定义的类遵循 CustomStringConvertible 并实现 var description: String { return ...} 属性。po 才会返回便于阅读的描述信息。

print

print <expression/variable>expression -- <expression/variable> 功能一致。区别在于 print 指令不接受任何标签或额外的参数。

2. 获取 App 整体状态与特定语言指令

bugreportframelanguage

bugreport

LLDB 提供了 bugreport 来生成当前 app 状态的完整日志。当你遇到问题,并且想要稍后处理的时候,这个指令相当有用。为了存储发生错误时的 app 状态,可以使用 bugreport 生成日志。

1
(lldb) bugreport unwind --outfile <path to output file>

最终的日志如下截图

bugreport 的输出范例

frame

frame 可以用于快速查看当前线程中的堆栈信息

例子

用如下指令快速获取堆栈信息

1
2
3
(lldb) frame info

frame #0: 0x000000010bbe4b4d LLDB-Debugger-Exploration`ViewController.valueOfLifeWithoutSumOf(a=2, b=2, self=0x00007fa0c1406900) -> Int at ViewController.swift:96

下文中的断点管理部分,这部分信息将十分有用。

language

LLDB 有用于 C++,Objective-C,Swift 以及 RenderScript 的指令。这里只讨论 Swift 的相关指令 demanglerefcount

Swift 在编译时,为了避免 namespace 的问题,会将类型名做特殊处理,demangle 的作用就是恢复这一问题。如果想要深入了解这一部分,可以查阅 WWDC14 的 session – “Advanced Swift Debugging in LLDB”

refcount 用于显示指定对象的引用计数。

1
2
3
(lldb) language swift refcount logger

refcount data: (strong = 4, weak = 0)

在调试一些内存泄漏问题时,将会十分有用。

控制 App 的执行流程

precessbreakpointthread

接下来介绍的 LLDB 指令能够自动完成许多日常重复的 Debug 工作,最终大大提高我们的 debug 的流程。

process

通过 process 指令,能够控制 debug 流程,不过由于 Xcode 会在断点处自动停下,这里就不再赘述,如果需要了解如何在终端中到达指定的调试点,可以查阅 Apple 官方文档

通过 process status 能够查看当前调试器等待的位置

1
2
3
4
5
6
7
8
9
10
11
12
(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

通过如下命令可以继续项目运行,直到下一个断点出现

1
2
3
(lldb) process continue

(lldb) c // Or just type "c" which is the same as previous command

在 Xcode 调试器的工具栏里也提供了按钮来继续运行

Xcode continue

breakpoint

breakpoint 指令允许我们在任意可能的地方操作断点。这里跳过见文知意的指令,例如:breakpoint enablebreakpoint disablebreakpoint delete

首先,查看所有的断点,可以使用 list 指令

1
2
3
4
5
6
7
8
9
10
(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

开始的数字代表了断点的唯一标识符,可以用来快速查找任何指定的断点。通过如下指令,可以设置新的断点

1
2
3
(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 指令能够简化设置断点的指令:

1
(lldb) b ViewController.swift:96

也可以通过特定的正则表达式来设置断点(例如函数名)。

1
2
3
(lldb) breakpoint set --func-regex valueOfLifeWithoutSumOf

(lldb) b -r valueOfLifeWithoutSumOf // 简单版本

有时需要设置一个只响应一次的断点,在响应之后自动删除该断点。下面的指令实现了这样的功能

1
2
3
(lldb) breakpoint set --one-shot true -f ViewController.swift -l 90

(lldb) br s -o true -f ViewController.swift -l 91 // Shorter version of the command above

接下来是最有趣的部分–自动化断点。可以在断点响应之后可以立即执行一些特定的动作。你是否在调试时使用 print 打印你想要查看的值?下面有更好的方式。

利用 breakpoint command,你可以设置在断点命中时立即执行一些设定的指令。甚至可以设置不中断程序执行的“不可见”断点。当然,这些“不可见”断点也会中断程序运行但无法注意到,其实不过是在指令集之后添加了 continue 指令。

1
2
3
4
5
6
7
8
9
10
(lldb) b ViewController.swift:96 // 首先添加一个断点

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 // 设置一些指令

Enter your debugger command(s). Type 'DONE' to end.
> p sum // 打印 "sum" 的值
> p a + b // 计算 a + b
> DONE

可以使用下面的指令来查看添加的指令是否正确

1
2
3
4
5
6
(lldb) breakpoint command list 2

Breakpoint 2:
Breakpoint commands:
p sum
p a + b

当这个断点再次被命中时,就可以在控制台得到如下输出

1
2
3
4
5
6
Process 36612 resuming
p sum
(Int) $R0 = 6

p a + b
(Int) $R1 = 4

这正是我们想要的。可以在指令最后添加 continue 指令,这样就不会在这个断点处停下来。

1
2
3
4
5
6
7
(lldb) breakpoint command add 2 // 设置一些指令 

Enter your debugger command(s). Type 'DONE' to end.
> p sum // 打印 "sum" 的值
> p a + b // 计算 a + b
> continue // 在断点命中后继续执行
> DONE

因此结果应该是:

1
2
3
4
5
6
7
8
9
p sum
(Int) $R0 = 6

p a + b
(Int) $R1 = 4

continue
Process 36863 resuming
Command #3 'continue' continued the target.

thread

利用 thread 以及它的子命令,可以完全控制执行流程:step-overstep-instep-out 以及 continue。这些指令也和 Xcode 调试器的工具栏上的流程控制按钮功能一致。

flow control buttons

上述的指令也有简写版本

1
2
3
4
5
6
7
(lldb) thread step-over
(lldb) next // 与 "thread step-over" 相同
(lldb) n // 与 "next" 相同

(lldb) thread step-in
(lldb) step // 与 "thread step-in" 相同
(lldb) s // 与 "step" 相同

可以通过 info 指令来获取当前线程的信息

1
2
3
(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 子命令,可以列举所有当前激活的线程信息

1
2
3
4
5
6
7
8
9
10
11
(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'

其他

commandplatformgui

command

在 LLDB 中你可以发现一个用于管理其他命令的指令。听起来很诡异,但事实上缺是一个很有用的小工具。首先,它允许你从文件中执行一些 LLDB 命令。因此可以创建一个包含一些命令的文件,之后可以通过一条命令执行他们。下面是写入文件的命令:

1
2
thread info // 显示当前线程信息
br list // 列举所有断点

执行命令后,结果如下:

1
2
3
4
5
6
7
8
9
10
11
(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

缺点就是无法传递参数到源文件中。

进阶的做法是使用 script 子命令。script 允许使用自定义的 Python 脚本(adddeleteimportlist)。script 让真正意义上的自动化成为可能。可以参阅指南。简单的例子,创建一个脚本文件 script.py 并且通过 print_hello() 打印 “Hello Debugger!” 到控制台:

1
2
3
4
5
6
7
8
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

之后需要导入 Python 模块,执行 script 命令:

1
2
3
4
5
6
7
(lldb) command script import ~/Desktop/script.py

The "print_hello" python command has been installed and is ready for use.

(lldb) print_hello

Hello Debugger!

platform

可以通过 status 子命令来检查当前平台信息。status 会显示:SDK 路径,处理器架构,操作系统版本以及该 SDK 所有支持的设备列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(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
...

gui

虽然无法在 Xcode 中使用 LLDB 的 GUI 模式,不过可以在终端中使用。

1
2
3
(lldb) gui
// 如果在 Xcode 中尝试执行 gui 命令,会得到如下错误
error: the gui command requires an interactive terminal.

LLDB GUI 样式

Swift 5.0 语言新特性 【译】Swift 算法 - 哈希表

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×