现在, 你已经有了坚实的调试基础.你可以找到并附加到你感兴趣的程序上, 高效的创建正则表达式断点来覆盖一个宽泛的范围, 在栈帧中导航并且使用expression命令查看变量. 然而, 是时候通过强大的LLDB来查看感兴趣的代码了.在本章中, 你会深入的学习image命令. image命令是target modules命令的别名. image是专门用来查询模块(modules)相关信息的; 更确切的说, 代码被加载到一个线程里面执行.模块可以包含许多事情, 包含主要的执行代码, 框架或者插件.然而, 大多数的模块都来自动态库.比如iOS的UIKit和macOS的AppKit都是常见的动态库. image命令用来查询任何私有框架的信息和它里面没有在头文件里公开的类和方法都是非常有用的.
等一下...模块?
你将继续用到Signals项目.打开这个项目, 用iPhone7模拟器构建并运行. 暂停调试器并在LLDB控制台输入下面的命令:
(lldb) image list 这条命令将会列出当前加载的所有的模块. 你会看到许多!这个列表的开头看起来应该是下面这个样子:
[ 0] 13A9466A-2576-3ABB-AD9D-D6BC16439B8F 0x00000001013aa000 /usr/lib/ dyld [ 1] 493D07DF-3F9F-30E0-96EF-4A398E59EC4A 0x000000010118e000 / Applications/Xcode.app/Contents/Developer/Platforms/ iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/ dyld_sim [ 2] 4969A6DB-CE85-3051-9FB2-7D7B2424F235 0x000000010115c000 /Users/ derekselander/Library/Developer/Xcode/DerivedData/Signals- bqrjxlceauwfuihjesxmgfodimef/Build/Products/Debug-iphonesimulator/ Signals.app/Signals 头两个是动态加载器:一个是基于系统的另一个是专门为模拟器添加的. 这些都是必要的代码, 它们允许你的程序将动态库加载到内存中用于程序的执行.第三个是APP的主二进制文件, Signals. 但是在这个列表中还有其他更多的内容!你可以只过滤出那些你感兴趣的内容.在LLDB中输入下面命令:
(lldb) image list Foundation 你会得到类似于下面的输出:
[ 0] 4212F72C-2A19-323A-84A3-91FEABA7F900 0x0000000101435000 / Applications/Xcode.app/Contents/Developer/Platforms/ iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//System/ Library/Frameworks/Foundation.framework/Foundation 这对于只显示你想要查看的模块的信息是非常有用的. 让我们看一下这些输出. 这里有一些有趣的内容:
首先打印出来的是模块的UUID(4212F72C-2A19-323A-84A3-91FEABA7F900).这个UUID对于捕获符号信息和唯一标示Foundation模块是非常重要的. 2.紧接着UUID的是加载地址(0x0000000101435000).这标明了Foundation模块加载到Signals可执行进程空间后的地址. 3.最后, 你会得到这个模块的二进制文件在本地的全路径. 让我们深入到另一个常用的模块UIKit中看一下.在LLDB中输入下面的命令: (lldb) image dump symtab UIKit -s address 这条命令会提取出UIKit中所有可用的符号表信息. 这条命令输出的内容是按照函数在UIKit中实现的顺序排列的, 这是-s address的作用. 这里有很多有用的信息, 但是你不可能把所有的内容都读一遍. 你需要一个高效的方法在UIKit中查询你感兴趣的代码. image lookup命令可以完美的过滤出所有的数据.输入下面的命令:
(lldb) image lookup -n "-[UIViewController viewDidLoad]" 这回提取出只与UIViewController的viewDidLoad实例方法相关的内容.你会看到与这个方法相关的符号的名字, 还有那个方法在UIKit框架中实现的代码. 这很好并且很全, 但是输入这些文字有些乏味并且这只能提取出指定的实例. 然而这正是正则表达式要做的事情.-r选项将能够让你使用正则表达式查询. 在LLDB中输入下面的命令:
(lldb) image lookup -rn UIViewController 这条指令不仅仅会提取出所有的UIViewController方法, 而且会输类似UIViewControllerBuiltinTransitionViewAnimator这样包含有UIViewController字样的方法.你也可以使用这则表达式只输出UIViewController的方法.在LLDB中输入下面的内容:
(lldb) image lookup -rn '[UIViewController\ ' 这当然很好, 但是怎么处理分类呢?他们是UIViewController(CategoryName)的形式.尝试搜索UIViewController的所有分类:
(lldb) image lookup -rn '[UIViewController(\w+)\ ' 现在指令开始变得复杂了. 开头的反斜杠表明你是想要使用[的字面意, 然后是UIViewController. 最后是(的字面意, 然后是一个或多个文字数字或下划线字符(w+的含义)., 然后是), 最后跟着一个空格. 正则表达式的知识将会帮助你创造性的查询加载到二进制文件中的任何模块的公有或者私有的代码. 这不仅会打印出公有和私有的方法, 而且会给出UIViewController类覆盖的父类的方法.
捕获代码
无论你捕获的是公有的代码还是私有代码, 有时只想弄明白编译器是如何生成一个特定函数的函数名的.你已经简单的使用上面的image lookup命令找到了UIViewController的方法.你也在第四章中用它找到了swift属性的setters 和 getter方法. 然而, 这里还有许多例子可以帮助你更好的理解代码是如何生成的以及如何在你感兴趣的地方设置断点. 一个特殊的例子就是查看 Objective-C代码块的方法声明. 那么搜索Objective-C代码块方法声明的最好的方法是什么呢?鉴于你没有任何线索来寻找代码块是在那里被命名的, 所以一个好的方法就是在代码块的内部创建一个断点然后从那里开始检查. 打开UnixSignalHandler.m, 然后找到单例方法sharedHandler.在这个函数中找到下面的代码:
dispatch_once(&onceToken, ^{ sharedSignalHandler = [[UnixSignalHandler alloc] initPrivate]; }); 在Xcode中在sharedSignalHandler的起始位置设置一个断点. 然后构建并运行. Xcode现在将会停在你设置断点的地方.在调试窗口中查看栈帧的顶部.
page82image1080.png
你可以找到你在Xcode中的函数的名字. 在调试栏中你将会看到栈追踪并且可以看到frame 0. 要复制粘贴的话有点小难. 我们可以用下面的命令替代:
(lldb) frame info 你将会得到类似下面的输出:
frame #0: 0x0000000100cb20a0 Commons`__34+[UnixSignalHandler sharedHandler]_block_invoke((null)=0x0000000100cb7210) + 16 at UnixSignalHandler.m:68 正如你看到的, 函数的全名是__34+[UnixSignalHandler sharedHandler]_block_invoke. 函数名中有一个很有趣的部分, _block_invoke. 这也许就是在Objective-C中帮助你标识一个代码块的标志.在LLDB中输入下面的命令:
(lldb) image lookup -rn _block_invoke 这条命令用正则表达式搜索关键词_block_invoke. 它会将_block_invoke作为通配符处理包含_block_invoke的内容. 但是等一下!实际上你打印出所有加载到程序中的Objective-C代码快.这个搜索包含UIKit,Foundation,iPhoneSimulator SDK等等中的所有代码块.你应该将你的搜索范围限定在Signals模块中.在LLDB在中输入下面的命令:
(lldb) image lookup -rn _block_invoke Signals 什么都没有打印出来.发生了什么事呢?打开Xcode右侧的File Inspector面板. 或者按下⌘ + Option + 1.
page83image8096.png
如果你看到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中输入continue继续运行程序.跳进终端并输入下面的命令:
pkill -SIGIO Signals 你发送给程序的信号将会被处理.然而, 在这个信号被更新到tableview之前, 你的正则断点就会被触发. 触发的第一个断点应该是:
__38-[UnixSignalHandler appendSignal:sig:]_block_invoke 继续运行调试器并跳过这一步. 接下来你会触发另一个断点:
__38-[UnixSignalHandler appendSignal:sig:]_block_invoke_2 这个函数名与第一个函数名相比有一个有趣的地方; 注意到数字2.编译器使用<FUNCTION_NAME>_block_invoke的格式定义叫做<FUNCTION_NAME>的blocks.然而, 在函数中不只有一个block时, 就会在尾部添加一个数字. 正如你在前面学到的内容, frame variable命令将会打印出指定函数中所有已知的实例变量. 现在尝试执行一下这个命令来看一下这个block的引用.输入下面的命令:
(lldb) frame variable 输出的内容应该像下面这个样子:
(__block_literal_5 *) = 0x0000608000275e80 (int) sig = <read memory from 0x41 failed (0 of 4 bytes read)> (siginfo_t *) siginfo = <read memory from 0x39 failed (0 of 8 bytes read)> (UnixSignalHandler *const) self = <read memory from 0x31 failed (0 of 8 bytes read)> 这些读内存时的故障看起来很不友好!步过一次, 即可以使用Xcode也可以在LLDB 中输入next.接下来, 再次在LLDB中执行frame variable. 这一次你会看到类似下面的输出:
(__block_literal_5 *) = 0x0000608000275e80 (int) sig = 23 (siginfo_t *) siginfo = 0x00007fff587525e8 (UnixSignalHandler *) self = 0x000061800007d440 (UnixSignal *) unixSignal = 0x000000010bd9eebe 你需要步过函数的声明, 以便代码块可以执行一些初始化逻辑来设置这个函数. 函数声明是与汇编相关的内容, 你会在第二部分学到.
这实际上非常有趣. 首先你看到了一个引用着这个代码块的对象, 那是被调用的地方.在这里是__block_literal_5. 然后是一些传到调用这个代码块的Objective-C方法里的sig 和 siginfo 参数. 这些是如何传到代码块里的呢? 好, 当一个代码块创建的时候, 编译器聪明到足以弄明白它会用到哪些参数.然后它会创建一个函数并把这些参数带进去. 当代码快被调用的时候, 就是这个函数被调用, 并将相关的参数穿进去. 在LLDB中输入下面命令:
(lldb) image dump symfile Commons 你将会看到许多输出. 使用⌘ + F 通过编译器搜索block类型的声明:__block_literal_5. 有一个比较重要的东西要提醒你的是当LLVM更新的时候你得到的类型可能有细微的不同, 所以请确保你从frame variable 命令的输出中得到了正确的类型. 在搜索block类型的声明时, 会有几种不同情形. 搜索与block的行号相匹配的结构体的声明. 例如, 你最初创建的123行的断点, 同样也可以在声明中搜索. 最终你会得到一些类似下面的输出:
0x7fefe24bcf90: Type{0x100000e06} , name = "__block_literal_5", size = 52, decl = UnixSignalHandler.m:123, compiler_type = 0x00007fefd86d0410 struct __block_literal_5 { void __isa; int __flags; int __reserved; void (__FuncPtr)(); __block_descriptor_withcopydispose *__descriptor; UnixSignalHandler *const self; siginfo_t *siginfo; int sig; } 这就是定义代码块的那个对象! 正如你看到的, 这就如同有一个头文件在告诉你如何在代码块的内存中自由的找到你想要的东西. 只要提供你找到的__block_literal_5在内存中的应用, 你就可以轻松的打印出这个block引用的所有变量.通过输入下面的命令再次获取栈帧的变量信息:
(lldb) frame variable 接下来, 找到__block_literal_5 对象的内存地址并用下面的方式打印出来:
(lldb) po ((__block_literal_5 *)0x0000618000070200) 你将会看到类似下面的输出:
<NSMallocBlock: 0x0000618000070200> 如果你的输出与上面的不一样, 确保你使用的__block_literal_5 的内存地址是你的block的地址,每一次运行的时候内存地址都会有些许不同. 现在你可以查询__block_literal_5的内存结构了. 在LLDB中输入:
(lldb) p/x ((__block_literal_5 *)0x0000618000070200)->__FuncPtr 这条指令会提取出这个block的函数指针的位置.输出的内容看起来应该是下面这个样子:
(void (*)()) $1 = 0x000000010756d8a0 (Commons`__38-[UnixSignalHandler appendSignal:sig:]_block_invoke_2 at UnixSignalHandler.m:123) block的函数指针指向, 运行时block被调用时的函数. 现在被执行的时候他们是同样的地址! 你可以输入下面的命令进行确认, 用你最近一次的命令打印出来的函数指针的地址替换下面的地址:
(lldb) image lookup -a 0x000000010756d8a0 这里在image lookup后面用了-a(address)选项来查看给定地址相关的符号.回到block结构体的成员变量, 你依然可以打印出传递给block的所有的参数. 输入下面的命令, 再次用你的block的地址替换下面的地址:
(lldb) po ((__block_literal_5 *)0x0000618000070200)->sig 这将会输出作为block的父函数的参数的signal的序号.在结构体的成员变量里还有一个UnixSignalHandler的引用叫做self. 为什么会那样呢? 看一下这个 block 并且 捕获下面这行代码:
[(NSMutableArray *)self.signals addObject:unixSignal]; 它是block捕获的self的引用., 是用来找到signals数组的偏移的.因此block需要知道self 是什么. 很酷, 对吧? 用image dump symfile命令与module联合起来是用来学习某种未知数据类型的好方法. 它还是用来学编译器是如何用你的源代码生成代码的好工具. 此外, 你可以检查blocks是如何持有指向外部的block的引用的-当出现运行循环的时候会是一个非常有用的工具.
窥探
你已经知道了如何用静态的方式检查一个私有类的实例变量, 但是把block的内存地址单独留下来是在太折磨人了. 尝试着把他打印出来并用动态分析的方法查看它.输入下面的内容, 用你block的地址替换下面的地址:
po 0x0000618000070200 LLDB会提取出一个表明自己是Objective-C类的类:
<NSMallocBlock: 0x618000070200> 这很有趣. 这是一个__NSMallocBlock__类.现在你已经学习了如何提取出一个类的共有方法和私有方法, 现在是时候查看一下__NSMallocBlock__实现的方法了.在LLDB中输入:
(lldb) image lookup -rn NSMallocBlock 什么都没有发生. 这说明__NSMallocBlock__没有覆盖它的父类的任何方法. 输入下面的命令来查看__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
-
Summary: CoreFoundation`-[NSBlock invoke]
现在你将会尝试着在block中调用这个方法.然而, 你并不想让持有这个block的的引用在release的时候消失, release减少它的retainCount, 所以block有被释放的风险. 有一个非常简单的方法可以保留这个block-只需要retain一下!输入下面的命令, 用你的block的地址替换下面代码中的地址:
(lldb) po id $block = (id)0x0000618000070200 (lldb) po [$block retain] (lldb) po [$block invoke] 在最后一行你将会看到下面这些输出:
Appending new signal: SIGIO nil 这表明你的block已经被调用了一次. 干净漂亮! 它之所以会生效是应为当block被调用的时候所有设置都已经准备就绪了, 因为你当前已经正确的停在了block开始的位置. 这种类型的方法来查看公有和私有类的, 然后查看它们实现的方法, 是一种学习程序底层实现的好方法.后面你会用同样的过程来查找方法并分析这些方法执行时的汇编代码, 会给到你一个非常接近源始方法的源代码.
调试私有方法
image lookup命令在寻找私有方法以及公有方法上面做的很漂亮, 他会贯穿你的整个apple开发生涯. 然而, 这里还有一些隐藏的方法在调试你自己的代码的时候非常有用.例如, 以_开头的方法通常都表明它是一个私有(并且是潜在的很重要的)方法. 让我们搜索一下所有模块中的所有包含下划线字符并包含description关键字的的Objective-C的方法. 再次构建并运行项目. 当sharedHandler处的断点被触发的时候, 在LLDB中输入下面的内容:
(lldb) image lookup -rn (?i)\ _\w+description] 这个表达式有点复杂所以让我们解析一下. 这个表达式会搜索空格(前面需要有一个)后面跟着下划线, 接下来是, 一个或多个字母或数字后面跟着description单词, 最后跟着]字符的方法. 在这个正则表达式开始的地方有一个有趣的字符集(?i). 这表明在搜索的时候不区分大小写. 这个表达式有一个反斜杠前缀符. 这表明你想使用字符的字面量而不是字符在正则表达式中的含义.这叫做'escaping'.例如, 在一个正则表达式中, ]字符是有特定含义的, 所以你需要是使用]. 在上面的正则表达式中\w字符是个例外. 它指定的搜索内容是下划线, 字母或数字(例如, _, a- z, A-Z, 0-9). 如果你在阅读这行代码的时候依然有不理解的地方, 强烈推荐你看一下https://docs.python.org/2/library/re.html深入了理解正则表达式查询.往后去正则表达式只会变得更加复杂. 仔细查看image lookup的输出. 在找到最佳答案之前查找是很乏味的, 因此确保你查看了所有的输出. 你可能会注意到几个UIKit中的一个NSObject的分类IvarDescription中几个比较有趣的方法. 尝试只将这个分类的内容打印出来.在LLDB中输入下面的内容:
(lldb) image lookup -rn NSObject(IvarDescription) 控制台将会输出这个分类实现的所有方法. 在打印出来的这些方法中, 有几个比较有趣的方法:
_ivarDescription _propertyDescription _methodDescription 因为这是NSObject的分类, 所以所有NSObject的子类都能调用这些方法. 这让一切都变得更完美, 当然! 尝试在UIApplication上调用这些方法. 在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方法这继承树上往上爬.正如你看到了, UIStatusBar看起来是UIView的子类, 因此在这个类中backgroundColor属性是可以用的. 让我们练习一下. 首先, 在LLDB中输入下面的指令:
(lldb) po [[UIApplication sharedApplication] statusBar] 你将会看到一些类似下面的输出;
<UIStatusBar: 0x7fb8d400d200; frame = (0 0; 375 20); opaque = NO; autoresize = W+BM; layer = <CALayer: 0x61800003aec0>> 这回打印出你APP的UIStatusBar实例.接下来使用, status bar的地址, 在LLDB中输出下面的命令:
(lldb) po [0x7fb8d400d200 setBackgroundColor:[UIColor purpleColor]] 在LLDB中, 删除你之前创建的所有断点:
(lldb) breakpoint delete 继续运行APP并看看你用指尖创造出来的美好世界!
page91image1048.png 现在还不是最漂亮的APP, 但是至少你已经找到了一个私有方法并用它做了一些有趣的事情!
我们为什么要学习这些呢?
作为一个挑战, 试着用image lookup命令找出Signals模块中所有的闭包. 一旦你做到了, 在每一个Signals模块的每一个Swift闭包中创建一个断点. 如果它对你来说太简单了, 尝试着找到可以在didSet/willSet属性时停下来的代码, 或者做一些/try/catch``blocks的操作. 也可以, 找出更多隐藏在Foundation或者UIKit中的私有方法. 学习愉快!