与调试器共舞 - LLDB 的华尔兹

LLDB调试

help

最简单命令是help,它会列举出所有的命令。如果你忘记了一个命令是做什么的,或者想知道更多的话,你可以通过help 来了解更多细节,例如help print或者help thread。如果你甚至忘记了help命令是做什么的,你可以试试help help。不过你如果知道这么做,那就说明你大概还没有忘光这个命令。

print

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

与调试器共舞 - LLDB 的华尔兹_第1张图片

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

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

expression

如果想改变一个值怎么办?你或许会猜modify。其实这时候我们要用到的是expression这个方便的命令。

与调试器共舞 - LLDB 的华尔兹_第2张图片

这不仅会改变调试器中的值,实际上它改变了程序中的值。这时候继续执行程序,将会打印42 red balloons。神奇吧。

注意,从现在开始,我们将会偷懒分别以p和e来代替print和expression。

什么是print命令

考虑一个有意思的表达式:p count = 18。如果我们运行这条命令,然后打印count的内容。我们将看到它的结果与expression count = 18一样。

和expression不同的是,print命令不需要参数。比如e -h +17中,你很难区分到底是以-h为标识,仅仅执行+17呢,还是要计算17和h的差值。连字符号确实很让人困惑,你或许得不到自己想要的结果。

幸运的是,解决方案很简单。用--来表征标识的结束,以及输入的开始。如果想要-h作为标识,就用e -h -- +17,如果想计算它们的差值,就使用e -- -h +17。因为一般来说不使用标识的情况比较多,所以e --就有了一个简写的方式,那就是print。

输入help print,然后向下滚动,你会发现:

'print'is an abbreviationfor'expression --'.  (print是`expression --`的缩写)

打印对象

尝试输入

p objects

输出会有点啰嗦

(NSString *) $7 =0x0000000104da4040@"red balloons"

如果我们尝试打印结构更复杂的对象,结果甚至会更糟

(lldb) p @[ @"foo", @"bar"](NSArray *) $8 =0x00007fdb9b71b3e0@"2 objects"

实际上,我们想看的是对象的description方法的结果。我么需要使用-O(字母 O,而不是数字 0) 标志告诉expression命令以对象(Object) 的方式来打印结果。

(lldb) e -O -- $8<__NSArrayI0x7fdb9b71b3e0>(foo,bar)

幸运的是,e -o --有也有个别名,那就是po(printobject 的缩写),我们可以使用它来进行简化:

(lldb) po $8<__NSArrayI0x7fdb9b71b3e0>(foo,bar)

(lldb) po @"lunar"lunar

(lldb) p @"lunar"(NSString *) $13 =0x00007fdb9d0003b0@"lunar"

打印变量

可以给print指定不同的打印格式。它们都是以print/或者简化的p/格式书写。下面是一些例子:

默认的格式

(lldb) p 16

16

十六进制:

(lldb) p/x 16

0x10

二进制 (t代表two):

(lldb) p/t160b00000000000000000000000000010000(lldb) p/t (char)160b00010000

你也可以使用p/c打印字符,或者p/s打印以空终止的字符串 (译者注:以 '\0' 结尾的字符串)。

这里是格式的完整清单。

变量

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

(lldb) e int $a =2(lldb) p $a *1938(lldb) e NSArray *$array = @[ @"Saturday", @"Sunday", @"Monday"](lldb) p [$array count]2

(lldb) po [[$arrayobjectAtIndex:0] uppercaseString]SATURDAY

(lldb) p [[$arrayobjectAtIndex:$a]characterAtIndex:0]

error:no known method'-characterAtIndex:'; cast the message send to the method's return type

error: 1 errors parsing expression

悲剧了,LLDB 无法确定涉及的类型 (译者注:返回的类型)。这种事情常常发生,给个说明就好了:

(lldb) p (char)[[$arrayobjectAtIndex:$a]characterAtIndex:0]'M'

(lldb) p/d (char)[[$arrayobjectAtIndex:$a]characterAtIndex:0]77

变量使调试器变的容易使用得多,想不到吧?

不用断点调试

程序运行时,Xcode 的调试条上会出现暂停按钮,而不是继续按钮:

点击按钮会暂停 app (这会运行process interrupt命令,因为 LLDB 总是在背后运行)。这会让你可以访问调试器,但看起来可以做的事情不多,因为在当前作用域没有变量,也没有特定的代码让你看。

这就是有意思的地方。如果你正在运行 iOS app,你可以试试这个: (因为全局变量是可访问的)

(lldb) po [[[UIApplicationsharedApplication] keyWindow] recursiveDescription]; layer = >  | >

更新UI

有了上面的输出,我们可以获取这个 view:

(lldb) eid$myView = (id)0x7f82b1d01fd0

然后在调试器中改变它的背景色:

(lldb) e (void)[$myView setBackgroundColor:[UIColorblueColor]]

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

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

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

(lldb) e (void)[CATransactionflush]

即使你仍然在调试器中,UI 也会在模拟器或者真机上实时更新。Chisel为此提供了一个别名叫做caflush,这个命令被用来实现其他的快捷命令,例如hide ,show 以及其他很多命令。所有Chisel的命令都有文档,所以安装后随意运行help show来看更多信息。

Push 一个 View Controller

想象一个以UINavigationController为 root ViewController 的应用。你可以通过下面的命令,轻松地获取它:

(lldb) eid$nvc = [[[UIApplicationsharedApplication] keyWindow] rootViewController]

然后 push 一个 child view controller:

(lldb) eid$vc = [UIViewControllernew](lldb) e (void)[[$vc view] setBackgroundColor:[UIColoryellowColor]](lldb) e (void)[$vc setTitle:@"Yay!"](lldb) e (void)[$nvc pushViewContoller:$vc animated:YES]

最后运行下面的命令:

(lldb) caflush// e (void)[CATransaction flush]

navigation Controller 就会立刻就被 push 到你眼前。

查找按钮的 target

想象你在调试器中有一个$myButton的变量,可以是创建出来的,也可以是从 UI 上抓取出来的,或者是你停止在断点时的一个局部变量。你想知道,按钮按下的时候谁会接收到按钮发出的 action。非常简单:

(lldb) po [$myButtonallTargets]{(    )}(lldb) po [$myButtonactionsForTarget:(id)0x7fb58bd2e240forControlEvent:0]<__NSArrayM 0x7fb58bd2aa40>(_handleTap:)

现在你或许想在它发生的时候加一个断点。在-[MagicEventListener _handleTap:]设置一个符号断点就可以了,在 Xcode 和 LLDB 中都可以,然后你就可以点击按钮并停在你所希望的地方了。

观察实例变量的变化

假设你有一个UIView,不知道为什么它的_layer实例变量被重写了 (糟糕)。因为有可能并不涉及到方法,我们不能使用符号断点。相反的,我们想监视什么时候这个地址被写入。

首先,我们需要找到_layer这个变量在对象上的相对位置:

(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyViewclass], "_layer"))(ptrdiff_t) $0 =8

现在我们知道($myView + 8)是被写入的内存地址:

(lldb) watchpointsetexpression -- (int *)$myView+ 8Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabledtype= w    new value: 0x0000000000000000

这被以wivar $myView _layer加入到Chisel中。

非重写方法的符号断点

假设你想知道-[MyViewController viewDidAppear:]什么时候被调用。如果这个方法并没有在MyViewController中实现,而是在其父类中实现的,该怎么办呢?试着设置一个断点,会出现以下结果:

(lldb) b -[MyViewController viewDidAppear:]

Breakpoint 1: no locations (pending).

WARNING:  Unable to resolve breakpoint to any actual locations.

因为 LLDB 会查找一个符号,但是实际在这个类上却找不到,所以断点也永远不会触发。你需要做的是为断点设置一个条件[self isKindOfClass:[MyViewController class]],然后把断点放在UIViewController上。正常情况下这样设置一个条件可以正常工作。但是这里不会,因为我们没有父类的实现。

viewDidAppear:是苹果实现的方法,因此没有它的符号;在方法内没有self。如果想在符号断点上使用self,你必须知道它在哪里 (它可能在寄存器上,也可能在栈上;在 x86 上,你可以在$esp+4找到它)。但是这是很痛苦的,因为现在你必须至少知道四种体系结构 (x86,x86-64,armv7,armv64)。想象你需要花多少时间去学习命令集以及它们每一个的调用约定,然后正确的写一个在你的超类上设置断点并且条件正确的命令。幸运的是,这个在Chisel被解决了。这被成为bmessage:

(lldb) bmessage -[MyViewController viewDidAppear:]Setting a breakpoint at -[UIViewControllerviewDidAppear:] with condition (void*)object_getClass((id)$rdi) ==0x000000010e2f4d28Breakpoint1: where =UIKit`-[UIViewControllerviewDidAppear:], address =0x000000010e11533c

LLDB 和 Python

LLDB 有内建的,完整的Python支持。在LLDB中输入script,会打开一个 Python REPL。你也可以输入一行 python 语句作为script 命令的参数,这可以运行 python 语句而不进入REPL:

(lldb) scriptimportos(lldb) script os.system("open http://www.objc.io/")

这样就允许你创造各种酷的命令。把下面的语句放到文件~/myCommands.py中:

defcaflushCommand(debugger, command, result, internal_dict):  debugger.HandleCommand("e (void)[CATransaction flush]")

然后再 LLDB 中运行:

command scriptimport~/myCommands.py

或者把这行命令放在/.lldbinit里,这样每次进入 LLDB 时都会自动运行。Chisel其实就是一个 Python 脚本的集合,这些脚本拼接 (命令) 字符串 ,然后让 LLDB 执行。很简单,不是吗?

紧握调试器这一武器

LLDB 可以做的事情很多。大多数人习惯于使用p,po,n,s和c,但实际上除此之外,LLDB 可以做的还有很多。掌握所有的命令 (实际上并不是很多),会让你在揭示代码运行时的运行状态,寻找 bug,强制执行特定的运行路径时获得更大的能力。你甚至可以构建简单的交互原型 - 比如要是现在以 modal 方式弹出一个 View Controller 会怎么样?使用调试器,一试便知。

这篇文章是为了想你展示 LLDB 的强大之处,并且鼓励你多去探索在控制台输入命令。

打开 LLDB,输入help,看一看列举的命令。你尝试过多少?用了多少?

但愿NSLog看起来不再那么吸引你去用,每次编辑再运行并不有趣而且耗时。

调试愉快!

你可能感兴趣的:(与调试器共舞 - LLDB 的华尔兹)