序:原文 Dancing in the Debugger — A Waltz with LLDB
声明:译文有一部分参考自:与调试器共舞 - LLDB 的华尔兹
前言
你是否曾经为试图理解尼的代码和打印一个变量的值而感到苦恼?
NSLog(@"%@", whatIsInsideThisThing);
或者跳过一个函数调用来简化程序的行为?
NSNumber *n = @7; // theFunctionThatShouldReallyBeCalled();
或短路一个逻辑检查?
if (1 || theBooleanAtStake) { ... }
抑或伪造一个函数的实现?
int calculateTheTrickyValue {
return 9;
/*
Figure this out later.
...
}
并且每次都必须重新编译,重新运行吗?
构建软件是复杂的,并且bug总是会出现。常见的修复周期是修改代码,编译,再次运行,并希望变的最好。
其实并不需要这样。您可以使用调试器哈!即使你已经知道如何使用调试器来检查一个变量,可是调试器可以做的远不止这些。
本文打算挑战你对调试的认知,更详细地解释了一些你可能不知道的基本原理,然后给你展示一些有趣的例子。让我们随着舞曲开始旋转起来,看看我们会以何种水平结束。
LLDB
LLDB 是一个有着 REPL 特性,并内置 C++ 和 Python 插件的开源调试器。该调试器捆绑在Xcode内部,并内置于Xcode窗口底部的控制台面板里。调试器允许您在程序运行的特定时刻暂停程序,来检查变量的值,执行自定义的指令,然后按照你所认为合适的步骤来操作程序的进展。(这里 是调试器如何工作的大体介绍。)
之前尼很有可能使用过调试器,即使只是在Xcode窗口页面添加断点。但是有一些小窍门,可以使你有一些很漂亮而又酷爽的事情去做。GDB to LLDB 参考的是一个伟大的可用命令的鸟瞰图,你还有可能想要安装 Chisel ,一个可以使调试器更加有趣的 LLDB 插件的开源合辑。
与此同时,让我们以 在调试器中如何打印一个变量的值 来开始这场华尔兹的旅程吧。
基础知识
这里是一段打印一个字符串的简单示例小程序。注意,在第16行添加了一个断点:

程序会在第16行被暂停运行,并且控制台会被打开,允许我们和调试器交互。那我们应该输入些什么呢?
help
最简单的命令是 help ,这一指令将会列出所有的命令。如果你忘记某条命令是做什么的或者想了解更多的命令,你可以使用 help 命令查看更多的细节,例如 help print 或 help thread 。如果你忘记了 help 命令是做什么的?你可以使用 help help 命令,但如果你知道的足够多,也许你还没有完全忘记命令是做什么的。
使用 print 命令打印一个值是很简单的。

LLDB 实际上会做前缀匹配,因此尼也可以使用
prin 、 pri 、 p 命令,但是不可以使用 pr ,因为 LLDB 不能把它和 process 命令做区分(幸运的是,p 并没有歧义)。你可能还会注意到,结果中有个
$0。实际上你可以使用它来代指这个结果。试试 print $0 + 7,你会看到 45。任何以美元符开头的东西都是存在于 LLDB 的命名空间的,它们是为了帮助你进行调试而存在的。
expression
如果想修改一个值怎么办?这里使用 expression 命令。

这一命令不仅仅修改了调试器中的值,实际上还修改了程序中的值。如果你在这个基础上继续执行程序,将会打印
83 huang qimeng,神奇吧!从现在开始,我们下面会使用这两个命令的简化形式
p、 e。
什么是 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 @"huang qimeng"
如果我们尝试打印结构更复杂的对象,结果甚至会更糟:
(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"
打印变量
可以为print指定不同的打印格式。他们都以print/<fmt>或者简化的p/<fmt>格式书写。例子如下:
默认格式:
(lldb) p 16
16
十六进制:
(lldb) p/x 16
0x10
二进制(t代表two):
(lldb) p/t 16
0b00000000000000000000000000010000
(lldb) p/t (char)16
0b00010000
你也可以使用p/c打印字符,或者p/s打印以空终止的字符串*char **,这里是输出格式的完整说明。
变量
现在你已经可以打印对象和简单类型的变量了,以及如何使用expression命令在调试器中修改他们了。~~此处废话不翻译~~,不过为了能使用声明的变量,变量必须以美元符号$开头。
(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 [[$array objectAtIndex:$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)[[$array objectAtIndex:$a] characterAtIndex:0]
'M'
(lldb) p/d (char)[[$array objectAtIndex:$a] characterAtIndex:0]
77
变量使调试器的使用变得更容易了,你想不到吧?😄