现在,你在调试方面已经有了坚实的基础。你可以找到并附加到感兴趣的进程,有效地创建正则表达式断点以覆盖广泛的罪魁祸首,浏览堆栈框架并使用表达式命令调整变量。
然而,现在是时候通过LLDB的力量来探索寻找感兴趣的代码的最佳工具之一了。在本章中,你将深入了解image命令。
图像命令是目标模块子命令的一个别名。image命令专门用于查询模块的信息;也就是说,在一个进程中加载和执行的代码。模块可以由许多东西组成,包括主可执行文件、框架或插件。然而,这些模块中的大多数通常以动态库的形式出现。动态库的例子包括iOS的UIKit或MacOS的AppKit。
图像命令非常适用于查询任何私有框架的信息以及这些头文件中未公开的类或方法。
你将继续使用Signals项目。启动该项目,在iPhone X模拟器上构建并运行。
暂停调试器,在LLDB控制台输入以下内容。
(lldb) image list
这个命令将列出当前加载的所有模块。你会看到很多! 列表的开头应该是下面这样的。
第一个模块是应用程序的主要二进制文件,Signals。第二个和第三个模块与动态链接编辑器(dyld)有关。这些模块允许你的程序将动态库和你进程中的主要可执行文件加载到内存中。
但是,这个列表中还有很多东西!你可以只选择其中一个模块。你可以只筛选出你感兴趣的那些。在LLDB中键入以下内容。
(lldb) image list Foundation
输出结果将类似于以下内容。
这是一个有用的方法,可以找到你想要的模块的信息。
让我们探讨一下这个输出。这里面有几个有趣的地方。
模块的UUID首先被打印出来(D153C8B2-743C-36E2-84CD-C476A5D33C72)。UUID对查找符号信息很重要,它能唯一地识别出基金会模块的版本。
UUID之后是加载地址(0x000000010eb0c000)。这确定了Foundation模块被加载到Signals可执行文件的进程空间的位置。
最后,你可以得到模块在磁盘上的完整路径。
让我们更深入地了解另一个常见的模块,UIKit。在LLDB中键入以下内容。
(lldb) image dump symtab UIKitCore -s address
这将转储所有UIKitCore可用的符号表信息。它的输出量比你能摇动的棍子还要多! 由于有了-s地址参数,该命令按照函数在私有UIKitCore模块中实现的地址对输出进行排序。
这里面有很多有用的信息,但你不能去读所有的信息,现在你可以吗?你需要一种方法来有效地查询UIKitCore模块,用一种灵活的方式来搜索感兴趣的代码。
图像查询命令非常适合于筛选出所有的数据。在LLDB中键入以下内容。
(lldb) image lookup -n "-[UIViewController viewDidLoad]"
这将倾倒出与UIViewController的viewDidLoad实例方法有关的信息。你会看到与该方法相关的符号名称,以及该方法的代码在UIKitCore框架中的实现位置。这很好,但打字有点繁琐,而且这只能倾倒出非常具体的实例。
这就是正则表达式发挥作用的地方。-r选项可以让你做一个正则表达式查询。在LLDB中键入以下内容。
(lldb) image lookup -rn UIViewController
这不仅会倾倒出所有的UIViewController方法,也会吐出UIViewControllerBuiltinTransitionViewAnimator这样的结果,因为它包含了名字
UIViewController。你可以聪明地使用正则表达式查询,只吐出UIViewController方法。在LLDB中键入以下内容。
(lldb) image lookup -rn '\[UIViewController\ '
另外,你可以使用 \s 元字符来表示一个空格,这样你就不必转义一个实际的空格并用引号包围它。下面的表达式是等价的。
(lldb) image lookup -rn \[UIViewController\s
这很好,但是类别呢?它们是以UIViewController(CategoryName)的形式出现的。搜索所有的UIViewController类别。
(lldb) image lookup -rn '\[UIViewController\(\w+\)\ '
.这开始变得复杂了。开头的反斜杠表示你要的是"["的字面字符,然后是UIViewController。最后是"("的字面意义,然后是一个或多个字母数字或下划线字符(用\w+表示),然后是")",后面是一个空格。
正则表达式的工作知识将帮助你创造性地查询加载到你的二进制中的任何模块中的任何公共或私有代码。
这不仅可以打印出公有和私有的代码,还可以给你提示UIViewController类从其父类重写的方法。
不管你是在寻找公共代码还是私有代码,有时候,试图弄清楚编译器是如何为一个特定的方法创建函数名的,也是很有意思的。你曾用上面的图像查询命令来寻找UIViewController方法。在第4章 "代码中的停顿 "中,你也用它来寻找Swift属性设置器和获取器的命名方式。
然而,还有很多情况下,了解代码是如何生成的会让你更好地理解在哪里以及如何为你感兴趣的代码创建断点。一个特别有趣的例子是Objective-C的块的方法签名。
那么,搜索Objective-C块的方法签名的最好方法是什么?由于你对从哪里开始搜索块的命名方式没有任何头绪,一个好的方法是在块内设置一个断点,然后从那里进行检查。
打开UnixSignalHandler.m,然后找到shareHandler这个单子方法。在该函数中,寻找以下代码。
dispatch_once(&onceToken, ^{
sharedSignalHandler = [[UnixSignalHandler alloc] initPrivate];
});
使用Xcode GUI在以sharedSignalHandler开头的那一行设置一个断点。
然后构建并运行。Xcode现在会在你刚刚设置断点的那行代码上暂停。在调试窗口中查看顶部的堆栈框架。
你可以通过Xcode的GUI找到你所在的函数的名称。在Debug Navigator中,你会看到你的堆栈跟踪,你可以看一下第0帧。这有点难以复制和粘贴(实际上是不可能的)。相反,在LLDB中输入以下内容。
(lldb) frame info
你会得到与下面类似的输出。
正如你所看到的,完整的函数名是__34+[UnixSignalHandler
sharedHandler]_block_invoke。
这个函数名有一个有趣的部分,_block_invoke。这可能是你需要的模式,以帮助在Objective-C中唯一地识别块。在LLDB中键入以下内容。
(lldb) image lookup -rn _block_invoke
这将对_block_invoke这个词做一个正则表达式搜索。它将把该短语前后的所有内容作为通配符。
但是,等等! 你不小心打印出了所有加载到程序中的Objective-C块。这个搜索包括来自UIKit、Foundation、iPhoneSimulator SDK等的任何东西。你应该限制你的搜索,只搜索Signals模块。
在LLDB中键入以下内容。
(lldb) image lookup -rn _block_invoke Signals
没有任何东西被打印出来。是什么原因呢?打开右侧的Xcode面板,点击文件检查器。或者,如果你有默认的Xcode键盘图,可以按⌘ + Option + 1。
如果你看一下UnixSignalHandler.m的编译位置,你会发现它实际上被编译到Commons框架中。所以,重做那个搜索,在Commons模块中寻找Objective-C块。在LLDB中键入以下内容。
(lldb) image lookup -rn _block_invoke Commons
最后,你会得到一些输出!
你现在会看到你在Commons框架中搜索到的所有Objective-C块。
现在,让我们创建一个断点,在你找到的这些块的一个子集上停下来。在LLDB中键入以下内容。
(lldb) rb appendSignal.*_block_invoke -s Commons
注意:搜索模块中的代码与破解模块中的代码之间有细微的区别。以上面的命令为例。当你想搜索Commons框架中的所有块时,你使用image lookup -rn _block_invoke Commons。当你想为Commons框架中的块做断点时,你用rb appendSignal.*block_invoke -s Commons。请注意-s参数与空格的关系。
我们的想法是,这个断点会在appendSignal方法中的任何块上命中。
通过点击播放按钮或在LLDB中输入继续,恢复程序。跳到终端,输入以下内容。
pkill -SIGIO Signals
你发送给程序的信号将被处理。然而,在信号被直观地更新到表视图之前,你的重码断点会被击中。
你将碰到的第一个断点是在。
__38-[UnixSignalHandler appendSignal:sig:]_block_invoke
继续调试器就可以过去了。
接下来你会遇到一个断点。
__38-[UnixSignalHandler appendSignal:sig:]_block_invoke_2
与第一个函数相比,这个函数名称有一个有趣的地方;注意方法名称中的数字2。编译器使用<FUNCTION_NAME>_block_invoke作为基础,用于在名为<FUNCTION_NAME>的函数中定义的块。然而,当函数中存在多个块时,会在末尾附加一个数字来表示。
正如你在上一章中所学到的,框架变量命令将打印所有已知的局部变量实例到一个特定的函数中。现在执行该命令可以看到在这个特定块中发现的引用。在LLDB中键入以下内容。
(lldb) frame variable
输出结果将类似于下面的内容。
这些读取内存的失败看起来不妙! 踏过一次,可以使用Xcode GUI或者在LLDB中输入next。接下来,在LLDB中再次执行框架变量。这一次你会看到类似于下面的内容。
(__block_literal_5 *) = 0x0000608000275e80
(int) sig = 23
(siginfo_t *) siginfo = 0x00007fff587525e8
(UnixSignalHandler *) self = 0x000061800007d440
(UnixSignal *) unixSignal = 0x000000010bd9eebe
你需要跨过一条语句,所以该块执行了一些初始逻辑来设置函数,也被称为函数序言。函数序幕是一个与汇编有关的话题,你将在第二节中学习。
这实际上是很有趣的。首先你看到一个对象,它引用了正在被调用的块。在这个例子中,它是__block_literal_5的类型。然后是sig和siginfo参数,它们被传递到调用该块的Objective-C方法中。这些参数是如何被传递到这个块中的呢?
好吧,当一个程序块被创建时,编译器很聪明地猜测出哪些参数会被它使用。然后,它创建了一个函数,将这些参数作为参数。当程序块被调用时,这个函数就会被调用,同时传入相关的参数。
在LLDB中键入以下内容。
(lldb) image lookup -t __block_literal_5
你会得到与下面类似的东西。
这就是定义块的对象! 很好!
正如你所看到的,这几乎和头文件一样好,可以告诉你如何在块中浏览内存。只要你把内存中的引用转换为__block_literal_5类型,你就可以很容易地打印出该块所引用的所有变量。
首先,通过输入以下内容再次获得堆栈框架的变量信息。
(lldb) frame variable
接下来,找到__block_literal_5对象的地址,像这样打印出来。
(lldb) po ((__block_literal_5 *)0x0000618000070200)
你应该看到与下面类似的东西。
<__NSMallocBlock__: 0x0000618000070200>
如果你没有,请确保你投递到__block_literal_5的地址是你的块的地址,因为每次项目运行时它都会不同。
注意:lldb-900.0.57中的错误提示,在执行框架变量命令时,LLDB将错误地解除对__block_literal_5指针的引用。这意味着(__block_literal_5 *)的指针输出将给出类NSMallocBlock而不是NSMallocBlock的实例。如果你得到的是类的描述而不是实例的描述,你可以通过在函数开始时立即引用RDI寄存器来解决这个问题,或者通过x/gx '$rbp - 32'获得__NSMallocBlock__的实例,如果你在函数中更进一步的话。
现在你可以查询__block_literal_5结构的成员。在LLDB中键入以下内容。
(lldb) p/x ((__block_literal_5 *)0x0000618000070200)->__FuncPtr
这将转储该块的函数指针的位置。输出将看起来像下面这样。
(void (*)()) $1 = 0x000000010756d8a0 (Commons`__38-[UnixSignalHandler
appendSignal:sig:] _block_invoke_2 at UnixSignalHandler.m:123)
块的函数指针指向了块被调用时运行的函数。这和现在正在执行的地址是一样的。你可以通过输入以下内容来确认这一点,用你上次执行的命令中打印的函数指针的地址来代替地址。
(lldb) image lookup -a 0x000000010756d8a0
这是用图像查找的-a(地址)选项来确定一个给定的地址与哪个符号有关。
跳回块结构的成员,你也可以打印出传递给块的所有参数。键入以下内容,同样用你的块的地址来替换地址。
(lldb) po ((__block_literal_5 *)0x0000618000070200)->sig
这将输出作为参数送入块的父函数的信号号码。
在结构的一个叫self的成员中也有一个对UnixSignalHandler的引用。这是为什么呢?看看这个块,找找这行代码。
[(NSMutableArray *)self.signals addObject:unixSignal];
这是代码块捕获的对自我的引用,并用来确定信号阵列的偏移量。因此,该块需要知道自己是什么。很酷,是吗?
顺便说一下,你可以用p命令转储出完整的结构,并像这样取消引用指针。
(lldb) p *(__block_literal_5 *)0x0000618000070200
结合模块使用image dump symfile命令是学习某种未知数据类型如何工作的好方法。它也是了解编译器如何为你的源码生成代码的好工具。
此外,你可以检查块如何持有对块外指针的引用--在调试内存保留周期问题时,这是一个非常有用的工具。
好吧,你已经发现了如何以静态的方式检查一个私有类的实例变量,但那个块的内存地址太诱人了,不能放过。试着把它打印出来,用动态分析法来探索它。键入以下内容,将地址替换为你的块的地址。
po 0x0000618000070200
LLDB会转储出一个类,表明它是一个Objective-C类。
<__NSMallocBlock__: 0x618000070200>
这很有意思。这个类是 NSMallocBlock。现在你已经学会了如何为私有类和公有类转储方法,现在是时候探索 NSMallocBlock 实现了哪些方法。在 LLDB 中,键入。
(lldb) image lookup -rn __NSMallocBlock__
什么都没有。嗯。这意味着 NSMallocBlock 没有覆盖任何由其超类实现的方法。在 LLDB 中键入以下内容,以找出 NSMallocBlock 的父类。
(lldb) po [__NSMallocBlock__ superclass]
这将产生一个类似的名为__NSMallocBlock的类--注意没有尾部的下划线。关于这个类,你能发现什么?这个类是否实现或覆盖了任何方法?在LLDB中键入以下内容。
(lldb) image lookup -rn __NSMallocBlock
这个命令转储的方法似乎表明 __NSMallocBlock 是负责内存管理的,因为它实现了 retain 和 release 这样的方法。__NSMallocBlock 的父类是什么?在LLDB中键入以下内容。
(lldb) po [__NSMallocBlock superclass]
你会得到另一个名为 NSBlock 的类。这个类怎么样?它是否实现了任何方法?在LLDB中键入以下内容。
(lldb) image lookup -rn 'NSBlock\ '
注意结尾处的反斜杠和空格。这可以确保没有其他的类可以匹配这个查询--记住,如果没有它,可能会返回一个包含NSBlock这个名字的不同的类。还有几个方法会被吐出来。其中一个,invoke,看起来非常有趣。
Address: CoreFoundation[0x000000000018fd80] (CoreFoundation.__TEXT.__text +
1629760)
Summary: CoreFoundation`-[NSBlock invoke]
你现在要尝试在块上调用这个方法。然而,当保留这个块的引用释放它们的控制权时,你不希望这个块消失,从而降低 retainCount,并有可能将这个块去分配。
有一个简单的方法来保管这个区块--只要保留它就可以了 在LLDB中键入以下内容,用你的块的地址替换地址。
(lldb) po id $block = (id)0x0000618000070200
(lldb) po [$block retain]
(lldb) po [$block invoke]
最后一行,你会看到以下输出。
Appending new signal: SIGIO
nil
这表明该块已经被再次调用了! 很好!
这只是因为一切都已经以正确的方式设置好了块的调用,因为你目前就在块的开始处暂停了。
这种探索公有类和私有类的方法,然后探索它们实现的方法,是了解程序内部情况的一个好方法。以后你会对方法使用同样的发现过程,然后分析这些方法执行的汇编,给你一个非常接近原始方法的源代码。
图像查找命令在搜索私有方法以及你在整个苹果开发生涯中见过的公共方法方面做得很好。然而,有一些隐藏的方法在调试你自己的代码时相当有用。
例如,一个以_开头的方法通常表示自己是一个私有的(而且可能是重要的!)方法。
让我们试着在所有的模块中搜索任何以下划线字符开头的Objective-C方法,并在其中包含 "描述 "一词。
再次构建并运行该项目。当你在sharedHandler中的断点被击中时,在LLDB中键入以下内容。
(lldb) image lookup -rn (?i)\ _\w+description\]
这个正则表达式有点复杂,让我们把它分解一下。
该表达式搜索了一个空格(\),后面跟着一个下划线(_)。接下来,该表达式搜索一个或多个字母数字或下划线字符(\w+),然后是单词description,最后是]字符。
正则表达式的开头有一组有趣的字符,(?i)。这说明你希望这是一个不区分大小写的搜索。
这个正则表达式有反斜线作为字符的前缀。这意味着你要的是字面字符,而不是其正则表达式的含义。这被称为 "转义"。例如,在正则表达式中,]字符是有意义的,所以要匹配字面意义的"]"字符,你需要使用]。
在上面的正则表达式中,例外的情况是\w字符。这是一个特殊的搜索项,返回一个字母数字字符或下划线(即_, a-z, AZ, 0-9)。
如果你在阅读这行代码时有小鹿乱撞的表情,强烈建议你仔细扫描https://docs.python.org/2/library/re.html,温习一下你的正则表达式查询;从这里开始,它只会变得更加复杂。
仔细地扫描图像查找的输出。正是这种繁琐的扫描给了你最好的答案,所以请确保你浏览了所有的输出。
你会注意到一连串有趣的方法,这些方法属于UIKit中一个名为IvarDescription的NSObject类别。
重新进行搜索,这样就只有这个类别的内容被打印出来。在LLDB中键入以下内容。
(lldb) image lookup -rn NSObject\(IvarDescription\)
控制台将倾倒出这个类别实现的所有方法。在这组方法中,有几个非常有趣的方法引人注目。
_ivarDescription
_propertyDescription
_methodDescription
_shortMethodDescription
由于这个类别是在NSObject上,所以NSObject的任何子类都可以使用这些方法。当然,这几乎是所有的东西!
在UIApplication Objective-C类上执行_ivarDescription。在LLDB中键入以下内容。
(lldb) po [[UIApplication sharedApplication] _ivarDescription]
你会得到一系列的输出,因为UIApplication在幕后持有许多实例变量。仔细扫描并找到你感兴趣的东西。在找到你感兴趣的东西之前,不要再来读这个东西。这很重要。
仔细扫描输出结果后,你可以看到一个对私有类UIStatusBar的引用。我听到你问,UIStatusBar有哪些Objective-C的setter方法?让我们来看看! 在LLDB中键入以下内容。
(lldb) image lookup -rn '\[UIStatusBar\ set'
这将跳出所有UIStatusBar可用的setter方法。除了在UIStatusBar中声明和重写的方法外,你还可以访问其父类的所有方法。检查UIStatusBar是否是UIView类的一个子类
(lldb) po (BOOL)[[UIStatusBar class] isSubclassOfClass:[UIView class]]
另外,你可以重复使用superclass方法来向上跳转类的层次结构。正如你所看到的,看起来这个类是UIView的一个子类,所以背景颜色属性在这个类中对你是可用的。让我们来玩玩它。
首先,在LLDB中键入以下内容。
(lldb) po [[UIApplication sharedApplication] statusBar]
你会看到与下面类似的东西。
<UIStatusBar_Modern: 0x7fdcf3c0f090; frame = (0 0; 375 44); autoresize = W+BM; layer = <CALayer: 0x60c000036640>>
这就为你的应用程序打印出了UIStatusBar实例。接下来,使用状态栏的地址,在LLDB中键入以下内容。
(lldb) po [0x7fdcf3c0f090 setBackgroundColor:[UIColor purpleColor]]
在LLDB中,删除你之前创建的任何一个断点。
(lldb) breakpoint delete
继续应用,看看你通过你的指尖向世界释放出的美感吧!
现在不是最漂亮的应用程序,但至少你已经成功地检查了一个私人方法,并使用它来做一些有趣的事情!
作为一个挑战,你可以尝试用图像查找的方式来找出信号模块中所有的Swift闭包。一旦你找到了,就在信号模块的每个Swift闭包上建立一个断点。如果这太容易了,可以试着看看那些可以在didSet/willSet属性助手上停止的代码,或者do/try/catch块。
另外,试着寻找隐藏在Foundation和UIKit中的更多私有方法。玩得开心点!
需要另一个挑战吗?
使用私有的UIKitCore NSObject类别方法_shortMethodDescription以及你的图像查询-rn命令,搜索负责在状态栏左上角显示时间的类,并把它改为更有趣的东西。钻进子视图,看看你是否能用到目前为止给出的工具找到它。
上一章 | 目录 | 下一章 |
---|