与调试器共舞 - LLDB 的华尔兹
- 你是否曾经苦恼于理解你的代码,而去尝试打印一个变量的值?
NSLog(@"%@", whatIsInsideThisThing);
- 或者跳过一个函数调用来简化程序的行为?
NSNumber *n = @7; // 实际应该调用这个函数:Foo();
- 或者短路一个逻辑检查?
if (1 || theBooleanAtStake) { ... }
- 或者伪造一个函数实现?
int calculateTheTrickyValue { return 9; /* 先这么着 ... */}
并且每次必须重新编译,从头开始?
构建软件是复杂的,并且 Bug 总会出现。一个常见的修复周期就是修改代码,编译,重新运行,并且祈祷出现最好的结果。
但是不一定要这么做。你可以使用调试器。而且即使你已经知道如何使用调试器检查变量,它可以做的还有很多。
这篇文章将试图挑战你对调试的认知,并详细地解释一些你可能还不了解的基本原理,然后展示一系列有趣的例子。现在就让我们开始与调试器共舞一曲华尔兹,看看最后能达到怎样的高度。
LLDB
是一个有着 REPL 的特性和 C++ ,Python 插件的调试器。LLDB 绑定在 Xcode 内部,存在于主窗口底部的控制台中。调试器允许你在程序运行的特定时暂停它,你可以查看变量的值,执行自定的指令,并且按照你所认为合适的步骤来操作程序的进展。(有一个关于调试器如何工作的总体的解释。)
你以前有可能已经使用过调试器,即使只是在 Xcode 的界面上加一些断点。但是通过一些小的技巧,你就可以做一些非常酷的事情。GDB to LLDB 参考是一个非常好的调试器可用命令的总览。你也可以安装 Chisel,它是一个开源的 LLDB 插件合辑,这会使调试变得更加有趣。
与此同时,让我们以在调试器中打印变量来开始我们的旅程吧
基础
这里有一个简单的小程序,它会打印一个字符串。注意断点已经被加在第 8 行。断点可以通过点击 Xcode 的源码窗口的侧边槽进行创建。
- 程序会在这一行停止运行,并且控制台会被打开,允许我们和调试器交互。那我们应该打些什么呢?
help
- 最简单命令是 help,它会列举出所有的命令。如果你忘记了一个命令是做什么的,或者想知道更多的话,你可以通过 help command 来了解更多细节,例如 help print 或者 help thread。如果你甚至忘记了 help 命令是做什么的,你可以试试 help help。不过你如果知道这么做,那就说明你大概还没有忘光这个命令。
打印值很简单;只要试试 print 命令:
LLDB 实际上会作前缀匹配。所以你也可以使用 prin,pri,或者 p。但你不能使用 pr,因为 LLDB 不能消除和 process 的歧义 (幸运的是 p 并没有歧义)。
你可能还注意到了,结果中有个 $0。实际上你可以使用它来指向这个结果。试试 print $0 + 7,你会看到 106。任何以美元符开头的东西都是存在于 LLDB 的命名空间的,它们是为了帮助你进行调试而存在的。
expression
如果想改变一个值怎么办?你或许会猜 modify。其实这时候我们要用到的是 expression 这个方便的命令。
这不仅会改变调试器中的值,实际上它改变了程序中的值。这时候继续执行程序,将会打印 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 abbreviation for '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<__NSArrayI 0x7fdb9b71b3e0>( foo, bar)
- 幸运的是,e -o -- 有也有个别名,那就是 po (print object 的缩写),我们可以使用它来进行简化:
(lldb) po $8<__NSArrayI 0x7fdb9b71b3e0>( foo, bar)(lldb) po @"lunar"lunar(lldb) p @"lunar"(NSString *) $13 = 0x00007fdb9d0003b0 @"lunar"
变量
- 现在你已经可以打印对象和简单类型,并且知道如何使用 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 [[$array objectAtIndex:0] uppercaseString]SATURDAY(lldb) p [[$array objectAtIndex:$a] characterAtIndex:0]error: no known method '-characterAtIndex:'; cast the message send to the method's return typeerror: 1 errors parsing expression// 悲剧了,LLDB 无法确定涉及的类型 (译者注:返回的类型)。这种事情常常发生,给个说明就好了:(lldb) p (char)[[$array objectAtIndex:$a] characterAtIndex:0]'M'(lldb) p/d (char)[[$array objectAtIndex:$a] characterAtIndex:0]77