LLDB教程-命令篇

你是否曾经苦恼于理解项目的代码,而去尝试打印一个变量的值?然后使用NSLog 并且每次必须重新编译,从头开始?但是不一定要这么做。你可以使用调试器。而且即使你已经知道如何使用调试器检查变量,它可以做的还有很多。

LLDB 是什么?

LLDB是Mac OS X上Xcode的默认调试器,支持再桌面和iOS设备和模拟器上调试C ,Objective-C和C++。它是新一代高性能调试器,它可以高效利用LLVM项目中的现有库,例如Clang表达式解析器和LLVM反汇编程序。

随着Xcode 5的发布,LLDB调试器已经取代了GDB,成为了Xcode工程中默认的调试器。它与LLVM编译器一起,带给我们更丰富的流程控制和数据检测的调试功能。LLDB为Xcode提供了底层调试环境,其中包括内嵌在Xcode IDE中的位于调试区域的控制面板。

Chisel 是facebook下一个开源LLDB命令集合。

与此同时,让我们以在调试器中打印变量来开始我们的旅程吧。

基础命令

这是一个简单加了断点的程序,程序会在这一行停止运行,并且控制台会被打开,允许我们和调试器交互。这时候我们应该打些什么命令呢?

帮助 help

最简单命令是 help,它会列举出所有的命令。如果你忘记了一个命令是做什么的,或者想知道更多的话,你可以通过 help <command-name> 来了解更多细节,例如 help print 或者 help thread。只需要在控制台 上图lldb字样的地方键入 help即可。

打印对象 print
打印值很简单;只要试试 print 命令:

LLDB 实际上会作前缀匹配。所以你也可以使用 prin,pri,或者 p。但你不能使用 pr,因为 LLDB 不能消除和 process 的歧义 (幸运的是 p 并没有歧义)。而print 则是expression --的简写方式。

你可能还注意到了,结果中有个 $0。实际上你可以使用它来指向这个结果。试试 print $0 + 7,你会看到 130。任何以$符开头的东西都是存在于 LLDB 的命名空间的,它们是为了帮助你进行调试而存在的。

打印复杂对象时,print可能显得力不从心 ,我们想看的是对象的 description 方法的结果,这时可以使用 po ,po 其实是 e -o --的别名。

甚至可以给print 指定不同的打印格式。它们都是以 print/<fmt> 或者简化的 p/<fmt> 格式书写。

1
2
3
4
5
6
7
8
9
10
11
//默认的格式
(lldb) p 16
16
//十六进制:
(lldb) p/x 16
0x10
//二进制 (t 代表 two):
(lldb) p/t 16
0b00000000000000000000000000010000
(lldb) p/t (char)16
0b00010000

这里是格式的完整清单

修改对象 expression

如果想改变一个值怎么办?我们要用到的是 expression 这个方便的命令。

上图中修改了num的值,断点步进后可以看到NSLog的对应值已经发生了变化。

变量

现在你已经可以打印对象和简单类型,并且知道如何使用 expression 命令在调试器中修改它们了。现在让我们使用一些变量来减少输入量。就像你可以在 C 语言中用 int a = 0 来声明一个变量一样,你也可以在 LLDB 中做同样的事情。不过为了能使用声明的变量,变量必须以$符开头。

1
2
3
4
5
6
7
8
9
10
11
12
(lldb) e int $a = 2
(lldb) p $a * 19
38
(lldb) e NSArray *$array = @[ @"Saturday", @"Sunday", @"Monday" ]
(lldb) p [$array count]
2
(lldb) po [[$array objectAtIndex:0] uppercaseString]
SATURDAY
(lldb) p (char)[[$array objectAtIndex:$a] characterAtIndex:0]
'M'
(lldb) p/d (char)[[$array objectAtIndex:$a] characterAtIndex:0]
77

UI调试

因为全局变量是可访问的,可以像这样打印整个视图层级:

1
2
3
(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
<UIWindow: 0x7fe5ac70c6a0; frame = (0 0; 375 812); gestureRecognizers = <NSArray: 0x6000034cb780>; layer = <UIWindowLayer: 0x600003a84680>>
| <UIView: 0x7fe5ac5069b0; frame = (0 0; 375 812); autoresize = W+H; layer = <CALayer: 0x600003ad3620>>

1、更新UI

就像上文变量中提到那样,我们可以拿到这个view:

(lldb) expression id $myView = (id)0x7fe5ac5069b0

尝试做一些修改:

(lldb) expression (void)[$myView setBackgroundColor:[UIColor redColor]]

但是只有程序继续运行之后才会看到界面的变化。因为改变的内容必须被发送到渲染服务中,然后显示才会被更新。

渲染服务实际上是一个另外的进程 (被称作 backboardd)。这就是说即使我们正在调试的内容所在的进程被打断了,backboardd 也还是继续运行着的。

这意味着你可以运行下面的命令,而不用继续运行程序:

(lldb) expression (void)[CATransaction flush]

这个时候就能看到背景颜色的改变了。

流程控制

通过xcode加断点调试时,调试条上回出现四个可以控制程序执行流程的按钮:

从左到右分别是 continue program execution 、 step over 、 step into 和 step out。

1、 continue program execution 按钮,会取消程序的暂停,允许程序正常执行 (要么一直执行下去,要么到达下一个断点)。
在 LLDB 中,你可以使用 process continue 或者 thread continue 命令来达到同样的效果。

2、 step over 按钮,会以黑盒的方式执行一行代码。如果所在这行代码是一个函数调用,那么就不会跳进这个函数,而是会执行这个函数,然后继续。
LLDB 则可以使用 thread step-overnext,或者 n 命令。

3、 step in按钮,可以跳进一个函数调用来调试或者检查程序的执行情况。
在LLDB中使用 thread step-instep,或者 s 命令。注意,当前行不是函数调用时,next 和 step 效果是一样的。

4、step out按钮 ,如果你曾经不小心跳进一个函数,但实际上你想跳过它,常见的反应是重复的运行 n 直到函数返回。其实这种情况,step out 按钮是你的救世主。它会继续执行到下一个返回语句 (直到一个堆栈帧结束) 然后再次停止。
在LLDB中使用 thread step-out

thread return 使用help thread可以看到这个比较实用的函数。它有一个可选参数,在执行时它会把可选参数加载进返回寄存器里,然后立刻执行返回命令,跳出当前栈帧。这意味这函数剩余的部分不会被执行。这会给 ARC 的引用计数造成一些问题,或者会使函数内的清理部分失效。但是在函数的开头执行这个命令,是个非常好的隔离这个函数,伪造返回值的方式 。

断点

Xcode在断点导航中提供了一系列工具创建和管理断点,我们可以来看LLDB中等价的命令,主要是breakpoint命令。

1、查看 启用/禁用

上图是xcode查看断点的地方,点击断点会开启或关闭断点。对应的LLDB如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//查看断点 命令输出列表显示每个逻辑断点都有一个整数标识
//输出列表中另一个信息是断点位置是否是已解析的(resolved)。这个标识表示当与之相关的文件地址被加载到程序进行调试时,其位置是已解析的。
//例如,如果在共享库中设置的断点之后被卸载了,则断点的位置还会保留,但其不能再被解析。
(lldb) breakpoint list
Current breakpoints:
1: file = '/Users/suntongsheng/Desktop/tmp/asdasd/asdasd/main.m', line = 23, exact_match = 0, locations = 1, resolved = 1, hit count = 1

1.1: where = asdasd`main + 51 at main.m:23, address = 0x00000001070195d3, resolved, hit count = 1
//禁用断点
(lldb) breakpoint disable 1
1 breakpoints disabled.
//启用断点
(lldb) breakpoint enable 1
1 breakpoints enabled.

2、 创建/删除

在Xcode创建断点的方式一种是 直接在代码左边的行数出点击 即可创建断点。对应的LLDB如下:

1
2
3
4
5
6
//在main.m的第24行创建断点
(lldb) breakpoint set -f main.m -l 24
Breakpoint 3: where = asdasd`main + 59 at main.m:24, address = 0x00000001070195db
//删除刚才的断点
(lldb) breakpoint delete 3
1 breakpoints deleted; 0 breakpoint locations disabled.

还有一种是在断点导航,点击左下角的加号按钮,选择Symbolic BreakPoint会出现:

对应的LLDB如下:

1
2
3
4
5
6
//在一个符号 (C 语言函数) 上创建断点,而完全不用指定哪一行
(lldb) breakpoint set -F isEven
Breakpoint 6: where = asdasd`isEven + 16 at main.m:13, address = 0x0000000107019750
//Objective-C 的方法也完全可以
(lldb) breakpoint set -F "-[NSArray objectAtIndex:]"
Breakpoint 5: where = CoreFoundation`-[NSArray objectAtIndex:], address = 0x000000010ac7a950

[NSArray objectAtIndex:]这个断点上,我们怎么能知道设置了什么呢?接下来我们可以用$arg1、$arg2等命令来打印出我们想要的信息。
在这里$arg1是指对象本身,$arg2是对象被调用的函数,po命令无法直接输出函数名,需要加上(SEL),$arg3是被赋给函数的参数。

3、 断点行为

在Xcode中邮件断点可以编辑添加action信息,你可以添加多个行为:

对应的LLDB如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(lldb) breakpoint set -f main.m -l 31
Breakpoint 2: where = asdasd`main + 205 at main.m:31, address = 0x000000010ab2166d
//添加条件
(lldb) breakpoint modify -c 'i == 80' 2
//添加行为
(lldb) breakpoint command add 2
Enter your debugger command(s). Type 'DONE' to end.
> po i
> DONE
//显示刚才的断点
(lldb) breakpoint list 2
2: file = 'main.m', line = 31, exact_match = 0, locations = 1, resolved = 1, hit count = 0
Breakpoint commands:
po i

Condition: i == 80

2.1: where = asdasd`main + 205 at main.m:31, address = 0x000000010ab2166d, resolved, hit count = 0

执行断点后自动继续运行,允许你完全通过断点来修改程序!你可以在某一行停止,运行一个 expression 命令来改变变量,然后继续运行。

查看 线程/调用栈 状态

在进程停止后,LLDB会选择一个当前线程和线程中当前帧(frame)。很多检测状态的命令可以用于这个线程或帧。

为了检测进程的当前状态,可以从以下命令thread list开始:

1
2
3
4
(lldb) thread list
Process 48541 stopped
* thread #1: tid = 0x3bd1d2, 0x000000010eae0b66 libsystem_kernel.dylib`__pthread_kill + 10, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
thread #2: tid = 0x3bd24c, 0x000000010eae128a libsystem_kernel.dylib`__workq_kernreturn + 10

星号(*)表示thread #1为当前线程。为了获取线程的跟踪栈,可以使用以下命令thread backtrace

1
2
3
4
5
//默认为当前线程 也可以指定线程 : thread backtrace 2
(lldb) thread backtrace
thread #1: tid = 0x2c03, stop reason = breakpoint 1.1, queue = com.apple.main-thread
frame #0: 0x0000000100010d5b, where = Sketch`-[SKTGraphicView alignLeftEdges:] + 33 at /Projects/Sketch/SKTGraphicView.m:1405
...

如果想查看所有线程的调用栈,则可以使用以下命令:(lldb) thread backtrace all

检查帧参数和本地变量的最简便的方式是使用frame variable命令:

1
2
3
4
5
6
7
8
(int) argc = 1
(char **) argv = 0x00007ffee4150ff0
(NSUInteger) num = 123
(__NSCFConstantString *) str = 0x000000010bab00d8 @"learning LLDB"
(__NSArrayI *) arr = 0x0000600002982fa0 @"2 elements"
(int) i = 80
(BOOL) result0 = YES
(BOOL) result1 = NO

如果没有指定任何变量名,则会显示所有参数和本地变量。如果指定参数名或变量名,则只打印指定的值。如:

1
2
(lldb) frame variable self
(SKTGraphicView *) self = 0x0000000100208b40

如果想查看另外一帧,可以使用frame select命令,如下所示:

1
2
(lldb) frame select 2
frame #2: 0x000000010e88dc45 libsystem_c.dylib`abort + 127

image

image指令是target module指令的缩写,借助它我们能够查看当前的Binary Images相关的信息。日常开发我们主要利用它寻址。image命令的用法也挺多,首先可以用它来查看工程中使用的库,如下所示:

1
2
3
(lldb) image list
[ 0] 9E11F0C7-9AB1-36A8-8FE4-8821DBA05A2F 0x000000010baad000 /Users/suntongsheng/Library/Developer/Xcode/DerivedData/asdasd-dufvhftdhjomdkcnrilgxlwykarv/Build/Products/Debug-iphonesimulator/asdasd.app/asdasd
[ 1] 8A72DE9C-A136-3506-AA02-4BA2B82DCAF3 0x0000000115aa8000 /usr/lib/dyld

我们还可以用它来查找可执行文件或共享库的原始地址,这一点还是很有用的,当我们的程序崩溃时,我们可以使用这条命令来查找崩溃所在的具体位置,如下所示:

1
2
3
4
5
6
7
8
9
10
11
NSArray *array = @[@1, @2];
NSLog(@"array 3: %@", array[2]);
//这段代码会抛出
2019-03-19 16:10:32.841204+0800 asdasd[49351:4010863] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndexedSubscript:]: index 2 beyond bounds [0 .. 1]'
*** First throw call stack:
(
0 CoreFoundation 0x0000000102c7f1bb __exceptionPreprocess + 331
1 libobjc.A.dylib 0x000000010221d735 objc_exception_throw + 48
2 CoreFoundation 0x0000000102bcb4ec _CFThrowFormattedException + 194
3 CoreFoundation 0x0000000102d01b00 +[__NSArrayI allocWithZone:] + 0
4 asdasd 0x00000001019004ff -[ViewController viewDidLoad] + 287

根据以上信息,我们可以判断崩溃位置是在ViewController中,要想知道具体在哪一行,可以使用以下命令image lookup --address

1
2
3
(lldb) image lookup --address 0x00000001019004ff
Address: asdasd[0x00000001000014ff] (asdasd.__TEXT.__text + 287)
Summary: asdasd`-[ViewController viewDidLoad] + 287 at ViewController.m:23

可以看到,最后定位到了ViewController.m:23行,正是我们代码所在的位置。

image更多用法可以参考: Executable and Shared Library Query Commands。