序:原文 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
变量使调试器的使用变得更容易了,你想不到吧?😄