以下内容整理自:<ADVANCED APPLE DEBUGGING & REVERSE ENGINEERING> 如有出入望指正
本节主要讲通过 expression 命令对代码进行调试
Formatting p & po
po 经常用于 Swift & Objective-C 的一些输出操作。
如果你使用 help po 查看的话,你会发现 po 是 expression -O – 的缩写,O 用于输出对象的描述信息。
po 的一个经常被忽略的姐妹,p,是一个省去了__O__ 的表达式(expression -O –)。p 输出格式更多的依赖于 LLDB type system。LLDB 的值类型格式有助于确定其输出,并可以完全自定义(像你将在下面看到的那样)。
现在是时候学习 p&po 命令如何获取内容的时候了。这此节将继续使用 Signals 项目。
打开 MasterViewController.swift ,添加如下代码:
override var description: String {
return "Yay! debugging " + super.description
}
在 viewDidLoad 方法中,添加代码到 super.viewDidLoad() 下面:
print("\(self)")
添加断点到 print 方法下边,像下面这样,编译运行:
停到 viewDidLoad() 之后,输入:
(lldb) po self
Yay! debugging <Signals.MasterViewController: 0x7f8335f0db10>
记下 print 语句的输出以及它如何匹配的你刚刚在调试器中执行的 po self。
你也可以进一步,NSObject 有另一种方法没描述用于调试称为 debugDescription。现在试着实现。在刚添加 description 变量下面添加:
override var debugDescription: String {
return "debugDescription: " + super.debugDescription
}
编译运行,这时在断点处停下后输入:
(lldb) po self
控制台输出与下面类似:
debugDescription: Yay! debugging <Signals.MasterViewController: 0x7f9b27c0df80>
注意在你执行了 debugDescription 之后,po self 和 print 输出的 self 的不同。使用 LLDB 打印对象的时候,调用 debugDescription 方法,而不是 description。
你可以看到,在使用 NSObject 类或者子类的时,description
或者 debugDescription
将影响 po
的输出。
那么哪些对象重写了这些描述方法?可以使用 image lookup
查看。
举例来说,如果你想知道所有重写 debugDescription
的 Objective-C 的类,可以使用:
(lldb) image lookup -rn '\ debugDescription\]'
基于输出,Foundation 框架的开发者将 debugDescription
添加到了很多基础类中(i.e. NSArray),这样方面于我们调试。此外,也有私有类重写了 debugDescription
方法。
在输出中可以看到有 CALayer。让我们看看 description
和 debugDescription
在 CALayer 中的不同。
输入:
(lldb) po self.view.layer.description
输出类似信息:
"<CALayer: 0x600000034f60>"
信息太少了,换个方式:
(lldb) po self.view.layer
得到类似以下输出:
<CALayer:0x600000034f60; position = CGPoint (187.5 333.5); bounds = CGRect (0 0; 375 667); delegate = <UITableView: 0x7fc2d501a000; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x600000244b90>; layer = <CALayer: 0x600000034f60>; contentOffset: {0, 0}; contentSize: {600, 0}; adjustedContentInset: {0, 0, 0, 0}>; sublayers = (<CALayer: 0x600000035480>, <CALayer: 0x600000035520>); masksToBounds = YES; allowsGroupOpacity = YES; backgroundColor = <CGColor 0x6000000b8d80> [<CGColorSpace 0x6000000b8780> (kCGColorSpaceICCBased; kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 0.980392 0.980392 0.980392 1 )>
输出很多很有用的信息,很显然, Core Animation 的开发人员决定 description
应该只是对象引用,但如果你在调试器中,你可以看到更多的信息。不清楚为什么要造成这种差异。可能是为了节省资源。
下一步,如果你仍停在调试器(没有的话,回到 viewDidLoad() 断点),尝试在 self
上运行 p
命令
(lldb) p self
得到类似以下输出:
(Signals.MasterViewController) $R3 = 0x00007fc2d4c0b080 {
UIKit.UITableViewController = {
baseUIViewController@0 = <extracting data from value failed>
_tableViewStyle = 0
_keyboardSupport = nil
_staticDataSource = nil
_filteredDataSource = 0x0000608000247d70
_filteredDataType = 0
}
detailViewController = nil
}
看起来有点恐怖,让我们慢慢讲。
首先,LLDB 输出 self 的类名。类名:Signals.MasterViewController
然后是一个引用,你可以在 LLDB 会话中引用这个对象。栗子中的是 $R0
,这个数字会在你使用 LLDB 的时候增加。
如果你想在之后的会话中使用这个对象,或许当你处于不同的范围并且 self
不再是同一个对象,这个引用将非常有用。这种情况下,你可以使用 $R0
使用这个对象。使用方法:
(lldb) p $R0
你可以看到同样信息又输出了一边。
在 LLDB 变量名称之后是该对象的地址,后面是一些特定于此类类型的输出。在这种情况下,它显示 MasterViewController 的父类 UITableViewController 的相关信息,后跟 detailViewController 实例变量。
p 的输出取决于 type formatting,LLDB 开发人员已经在内部数据结构中添加了所有的(受关注的)数据结构,Objective-C、 Swift 还有一些其他的语言。尤其要注意的是, Swift 处在开发阶段,所以 MasterViewController 输出可以有所不同。
由于这些类型类型格式化程序是由 LLDB 持有的,所以如果愿意,可以直接更改的。在 LLDB 会话中,输入:
(lldb) type summary add Signals.MasterViewController --summary-string "哇喔"
你告诉 LLDB 当输出 MasterViewController 实例的时候你只想输出字符串 “哇喔”。输出 self:
(lldb) p self
(Signals.MasterViewController) $R0 = 0x00007fdd377029e0 哇喔
这个样式会被 LLDB 记录,下次启动的时候还存在,需要在使用结束之后清除掉。
(lldb) type summary clear
现在输入 p self
就回到默认输出样式了。
Swift vs Objective-C debugging contexts
需要注意的是调试程序时有两个调试上下文:非 Swift 调试上下文和 Swift 上下文。默认情况下,当停在 Objective-C 代码时,LLDB 使用非 Swift(Objective-C) 调试上下文,而在 Swift 代码中停止时, LLDB 将使用 Swift上下文。听起来很合逻辑,对吧?
如果你意外的停止,LLDB 将默认选择 Objective-C 上下文。
确保刚才的蓝色(GUI 下设置)断点还在,编译运行。停在断点处后,在LLDB 会话中输入:
(lldb) po [UIApplication sharedApplication]
LLDB 会抛出一个奇怪的错误:
error: <EXPR>:3:16: error: expected ',' separator
[UIApplication sharedApplication]
^
,
你这时是停在 Swift 代码中,所以是 Swift 上下文,而且你执行的是 Objective-C 代码。因此行不通。同样的,在 Objective-C 上下文 po Swift 多象也是不可以的。
在 Objective-C 上下文时可以在 expression 中使用 -l
来选择语言。然而,由于 po
是 expression -O --
的映射,所以你不能直接在 po
后面直接跟 --
,这意味着需要使用 expression
。在 LLDB,输入:
(lldb) expression -l objc -O -- [UIApplication sharedApplication]
这时你告诉 LLDB 为 Objective-C 使用 objc
,如果有需要,还可以为 Objective-C++ 使用 objc++
。
使用 Swift 做同样的事情:
(lldb) po UIApplication.shared
和使用 Objective-C 输出的相同,然后 continue,之后再突然停止。
按 ↑
,看到:
(lldb) po UIApplication.shared
这时看输出:
error: property 'shared' not found on object of type 'UIApplication'
请记住,LLDB 意外停止时使用的是 Objective-C 上下文,这就是为什么执行 Swift 方法出现错误的原因。
你应该始终了解当前在调试器中暂停的位置使用的语言。
User defined variables
如之前看到的,当打印对象时,LLDB 将自动创建本地变量。你也可以创建自己的变量。
移除所有断点,运行应用。在调试器意外的停止,默认是 Objective-C 上下文。输入:
(lldb) po id test = [NSObject new]
LLDB 将会执行这个代码,该代码创建一个新的 NSObject 并将其存储到 test 变量中。现在打印出来:
(lldb) po $test
出现一个错误:
error: use of undeclared identifier 'test'
这是因为你需要使用 $
预先添加要使用 LLDB 的变量。
重新输入:
(lldb) po id $test = [NSObject new]
(lldb) po $test
<NSObject: 0x604000014eb0>
此变量在 Objective-C 对象中创建。但是,如果你尝试从 Swift 上下文访问会发生什么?试一下:
(lldb) expression -l swift -O -- $test
发现输出还是正常的。现在尝试使用 Swift 代码风格在这个 Objective-C 上执行。
(lldb) expression -l swift -O -- $test.description
输出错误:
error: <EXPR>:3:1: error: use of unresolved identifier '$test'
$test.description
^~~~~
如果在 Objective-C 上下文中创建一个 LLDB 变量,然后使用 Swift 上下文,不要指望一切可以“正常运行”。目前这块在积极开发,Objective-C 和 Swift 桥接可能会随着时间的推移而改善。
那么如何在 LLDB 中创建引用可以在实际情况下使用?你可以获取对象的引用,并执行(以及调试)任意选择的方法。要查看此操作,创建一个符号断点在 MasterViewController 的父控制器 __MasterContainerViewController__上,为 MasterContainerViewController 的 viewDidLoad 添加符号断点。
在 Symbol 行输入:
Signals.MasterContainerViewController.viewDidLoad() -> ()
输完之后如下:
编译运行,Xcode 将停在 MasterContainerViewController.viewDidLoad()。输入:
(lldb) p self
由于这是在 Swift 上下文中执行的第一个参数,所以 LLDB 将会创建变量 $R0
。通过在 LLDB 中执行 continue 恢复程序的执行。
现在你没有通过使用 self 来引用 MasterContainerViewController 的实例,因为执行已经不在 viewDidLoad(),并移动到 bigger and better run loop events。
你这时候还有 $R0
变量,你可以引用 MasterContainerViewController,甚至是执行任意方法来帮助调试代码。
手动暂定调试的应用,然后输入:
(lldb) po $R0.title
很不幸,报错了:
error: use of undeclared identifier '$R0'
因为这时候调试器是意外停止的。记住,LLDB 默认的是 Objective-C;你需要使用 -l
来保证使用的是 Swift 上下文;
(lldb) expression -l swift -- $R0.title
这时候看下输出:
(String?) $R1 = "Quarterback"
当然,这是 view Controller 的标题,如导航栏显示。
现在输入:
(lldb) expression -l swift -- $R0.title = "😛😛😛😛"
然后 continue
恢复运行。
Note: 在 macOS 上使用
⌘ + ⌃(control) + 空格
可以进行符号和表情的选择。
此外,你也可以在代码中创建断点,执行代码,命中断点。如果你正在调试并希望通过某些输入来查看其操作方式,这很可能会有用。
例如:你符号断点仍然在 viewDidLoad() 方法上,请尝试执行该方法来检查代码。暂停执行程序,然后输入:
(lldb) expression -l swift -O -- $R0.viewDidLoad()
什么也没有发生,断点也没有出现。是什么原因呢?实际上,MasterContainViewController 已经执行了这个方法。但是默认情况下,LLDB 会在执行命令是忽略任何断点。你可以使用 -i
禁用此选项。
在 LLDB 会话输入:
(lldb) expression -l swift -O -i 0 -- $R0.viewDidLoad()
LLDB 现在在之前创建的 viewDidLoad() 符号断点中断。这种策略是测试逻辑的好方法。例如,你可以通过给出不同参数的函数来实现 test-driven 调试。以了解如何处理不同的输入。
Type formatting
编译运行,然后暂停,确保你停在 Objective-C 上下文。
(lldb) expression -G x -- 10
-G
告诉 LLDB 输出想要使用的格式。G 表示 GDB。GDB 是 LLDB 之前的调试器。所以,这时你指定了使用 GDB 格式。在这种情况下, x
表示十六进制。
输出结果:
(int) $0 = 0x0000000a
这是十进制10的十六进制。
还有更多!LLDB 允许你使用简洁语法格式。输入:
(lldb) p/x 10
(int) $1 = 0x0000000a
你会发现一样的输出。但是输入少了很多。
这对学习 C 数据类型很有帮助。例如,10的二进制数据?
(lldb) p/t 10
(int) $2 = 0b00000000000000000000000000001010
/t
指定了二进制格式。你可以看到10的二进制显示。例如:当处理一个位字段是很有用,以便自习检查给定数字将设置哪些字段。
-10 的二进制是多少?
(lldb) p/t -10
(int) $3 = 0b11111111111111111111111111110110
10.0的二进制呢?
(lldb) p/t 10.0
(double) $4 = 0b0100000000100100000000000000000000000000000000000000000000000000
‘D’ 的 ASCII 是多少?
(lldb) p/d 'D'
(char) $5 = 68
/d
指定的是十进制格式。
最后,下面这个整数隐藏的缩写是什么?
(lldb) p/c 1430672467
(int) $7 = STFU
/c
指定的是字符串格式。它采用二进制数,分为8位(1 字节),并将每个块转换为 ASCII 字符。在这种情况下,它是一个4字符的代码(FourCC),结果是 SDFU
。
完整的输出格式列表(https://sourceware.org/gdb/ onlinedocs/gdb/Output-Formats.html)
- x: 十六进制(hexadecimal)
- d: 十进制(decimal)
- u: 无符号十进制(unsigned decimal)
- o: 八进制(octal)
- t: 二进制(binary)
- a: 地址(address)
- c: 字符常数(character constant)
- f: 浮点数(float)
- s: 字符串(string)
如果这些格式不够用,还可以使用 LLDB 的额外的格式化方式,尽管将不能使用 GDB 格式化语法。
LLDB 的格式化方法可以像以下方式使用:
(lldb) expression -f Y -- 1430672467
(int) $0 = 53 54 46 55 STFU
这解释了之前的 FourCC 代码。
LLDB 有以下格式化程序(http://lldb.llvm.org/ varformats.html):
- B: boolean
- b: binary
- y: bytes
- Y: bytes with ASCII
- c: character
- C: printable character
- F: complex float
- s: c-string
- i: decimal
- E: enumeration
- x: hex
- f: float
- o: octal
- O: OSType
- U: unicode16
- u: unsigned decimal
- p: pointer
Where to go from here?
思考一下可以用表达式命令做什么。尝试通过执行 help expression
自己探索一些其他的表达式选项,并查看是否可以弄清楚它们在做什么。