相信大家肯定都有过为了调试而添加打印变量,或者使用直接常量代替函数调用结果,或者更改判断条件以进入某特定分支的调试经历,但每次更改代码都需要重新编译,重新来过一遍,但其实本可以不用这样,因为我们有调试器,而且除了监视变量的值,还有很多它可以做的。
LLDB
LLDB是一个开源的以REPL为特性,并可以配置C++和python插件的调试器。它集成在xcode中,并在窗口底部的控制台中运行。调试器可以暂停程序执行,观察变量,执行自定义指令,并掌控程序的执行。如果对GDB比较熟悉的话,GDB-TO-LLDB 这个份指引应该对你了解LLDB的指令有极大助益,如果安装了Chisel这个LLDB插件的话,调试会更有趣。
基础操作
程序在断点处暂停的时候,console会打开供输入命令:
help
最简单的命令即是help,它会列出所有命令,如果忘记了help本身,help help试试
用来打印值,由于 LLDB的前缀匹配,所以也可以使用prin,pri,但不可以用pr,因为还有一个命令是process,但可以用p代表print。
注意到结果里面会有$n的字样,带$前缀的标识属于LLDB命名空间,我们可以利用这个特性为自己服务
expression
修改变量值
注:有一个需要注意的是,如果使用中文字符串常量的时候,由于LLDB解析器的bug,会报错An Objective-C constant string's string initializer is not an array,需要使用[NSString stringWithUTF8String:]
list命令
list 行号 显示行号开始的数行代码(默认10行)
list 函数名 显示函数名为中心的前后十行代码
list不带参数,接着上一次list命令
print 命令
如果执行 p count = 18,会发现除了打印18之后,count的值也会变成18.它与expression count = 18执行结果相同。不同的地方在于print 命令没有参数。
想想 e -h +17 这行命令,如果将-h理解为flag的话, +17看起来并不像输入。如果理解为计算17与h的差值,那这个连字符看起来很让人困惑。
幸运的是,使用--来分隔flag与其后的输入,实际上e -- 的缩写是print。
打印对象
如果使用print objects,则输入看起来十分冗长:(NSString *) $7 = 0x0000000104da4040 @"red balloons",如果打印更复杂的结构 p @[ @"foo", @"bar" ],输出可能像这样(NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects",而实际上我们想看对象的description,所以需要将对象按对象输出,使用-O 选项。
同样幸运的是,e -O --的别名是po (print object)
打印变量
可以在print命令中指定很多种格式,其形式像这样print/<fmt>,或者p/<fmt>,默认格式是这样,p 16 > 16,十六进制形式输出 p/x 16 > 0x10, 二进制 (t代表two) p/t 16 > 0b10000
还可以用p/c输出字符,p/s输出c字符串,这里是完整的输出格式
变量
在lldb中可以使用变量以减少过多的typing,但在LLDB中定义的变量必须以$开头,比如e int $i = 1,e NSArray* $a = @[@"Saturday",@"Sunday"], p [$a count], po [[$a objectAtIndex:0] uppercaseString]
而如果输入这条命令p [[$a objectAtIndex:$i] characterAtIndex:0],得到的结果是
error: no known method '-characterAtIndex:'; cast the message send to the method's return type
error: 1 errors parsing expression
因为LLDB无法识别涉及到的类型,这种情况有时候会发生,可以这样处理
p (char)[[$a objectAtIndex:$i] characterAtIndex:0] > 'M'
或者p/d (char)[[$a objectAtIndex:$i] characterAtIndex:0] > 77
执行流控制
命中断点时,调试条上的4个按钮可供控制程序的执行流

其意义从左到右分别是继续,越过,进入函数体,退出函数体
继续按钮的作用同LLDB的process continue,简写为continue,或者c
越过按钮的作用相当于执行完当前行的指令,就算当前当为函数调用,也不进入函数体,对应LLDB中的thread step-over,next 或者n
进入函数体按钮的作用相当于LLDB中的 thread step-in,step和s,而如果当前行非函数调用,则其与thread step-over 表现是相同的
退出函数体的作用在于能够一直执行到当前函数return之后再中断到调试器,即将当前栈帧弹出之后再停止。对应LLDB中的命令finish
frame info
输出当前代码行及其所在在源代码文件
线程return
thread return是控制执行流的另一利器,其可携带选项参数,其中将参数加载进返回寄存器,并立即执行return命令,并跳出当前栈帧,而这也意味着当前函数中剩余的语句不会被执行。这样做导致ARC的引用计数和追踪方面的问题或者在函数尾所做的清理等被跳过,但如果在进入函数体之后马上执行,则其在假装函数已经执行方面的作用还是很大的。
断点篇

LLDB列举断点: breakpoint list/br li
enable/disable 断点: breakpoint enable <breakpointID>/breakpoint disable <breakpointID>
设置断点:(在Xcode中可以在编辑区的代码行首点击添加断点,以及鼠标拖拽断点到代码行首区释放以移除断点)
breakpoint set: 比如breakpoint set -f main.m -l 16
由于b是_regexp-break的简写,所以breakpoint的缩写为br
但其实使用b设置断点,LLDB在通常情况下也是可以识别的,比如b main.m:17同样可以设置断点成功
其实也可以使用符号(C函数来设置断点,不用指定行号),比如
(lldb) b isEven
(lldb) br s -F isEven
这样可以使得调用此函数时会在其入口暂停,也可以使用oc 方法设置断点
(lldb) breakpoint set -F "-[NSArray objectAtIndex:]"
(lldb) b -[NSArray objectAtIndex:]
(lldb) breakpoint set -F "+[NSSet setWithObject:]"
(lldb) b +[NSSet setWithObject:]
如果想创建一个符号断点,可以在断点导航页中点击“+”

同时在右键每个现有断点时都会出现包含edit breakpoint项的菜单,点击之后

Add Action的动作在如下详细介绍
Breakpoint Actions
这个功能很有用,可以在断点发生之后,马上执行你所定义的动作之后再将控制权交给你,即lldb命令行。action支持多行Debugger command,shell command,log message,使用lldb而非UI操作的方法为

Continuing after Evaluation
在编辑断点的UI中,options项Automatically continue after evaluation actions勾选可以使得在断点处执行完预设action之后马上恢复运行,就像没断点一样。
Full Execution in the Debugger
还有一个特性是可以在debugger中运行任何C/Objective-C/C++/Swift命令,一点不足之外是不能创建新的函数,即不能创建新类,block,函数,带虚方法的C++类等,除此之外都可以。
比如可以分配数字节的空间

不过要注意,分配空间的时候,其作用域是与当前栈帧中当前代码行所在的作用域相同的,所以要尽量避免因作用域问题引起的执行异常
也可以使用x命令查看新数组的4个字节
(lldb) x/4c $str
0x7fd04a900040: monk
也可以查看数组第3个字节开始的内容(x命令需要反引号,需要地址作为参数):
(lldb) x/1w `$str + 3`
0x7fd04a900043: keys
但所有这些操作结束之后,确保释放这些内存,以免引起内存泄漏(在调试器中):
(lldb) e (void)free($str)
根据以上内容我们可以做的
在调试条中的暂停按钮可以暂停当前app,实际是其是执行了process interrupt,因为调试器其实一直都在执行场景之中。虽然此时的中断可能并没有暂停在你熟悉的代码上下文中,但其实可以试试这个:
(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
更新UI
基于上述输出,可以从UI层级中选择一个来操作,假定其地址为0xadd2e55
(lldb) e id $myView = (id)0xadd2e55
(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]
走到恢复app的执行才会看到UI的变化,因为需要将这个信息通知给渲染Server显示才会更新。
render server实际是另一个进程backboardd,但在我们在调试当前进程的时候,backboardd是没有暂停的,所以其实我们可以执行(lldb) e (void)[CATransaction flush],这样就可以在不恢复执行的情况下看到UI更新。chisel提供了caflush来完成这个更新的功能(chisel还有更多方便的功能)
Pushing a View Controller
假设当前app的root vc为UINavigationController,则
(lldb) e id $nvc = [[[UIApplication sharedApplication] keyWindow] rootViewController](lldb) e id $vc = [UIViewController new]
(lldb) e (void)[[$vc view] setBackgroundColor:[UIColor yellowColor]]
(lldb) e (void)[$vc setTitle:@"Yay!"]
(lldb) e (void)[$nvc pushViewContoller:$vc animated:YES]
即可以添加一个子view controller,然后执行(lldb) caflush // e (void)[CATransaction flush],就可以看见一个view controller被push进当前View Controller层级
笔者在使用presentViewController测试的时候,执行flush之后未见到UI有更新,然后执行continue才看到vc被present进来,xcode 6.4,iOS 8.3,真机和模拟器均是这样
Finding the Target of a Button
如果想知道$myButton相应的监听action有哪些,可以这样
(lldb) po [$myButton allTargets]{()}
(lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0]
<__NSArrayM 0x7fb58bd2aa40>(
_handleTap:
)
这个时候,对_handleTap就想怎样就怎样设置断点
观察实例变量的更改
假定UIView的_layer被覆写,由于不涉及到方法的调用,无法设置断点。于是我们可以设置对某地址的写入,先找下_layer 变量在对象中的所在
(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer"))
(ptrdiff_t) $0 = 8
于是我们知道$myView+8即是我们关心的地址,于是
(lldb) watchpoint set expression -- (int *)$myView + 8
Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabled type = w
new value: 0x0000000000000000
在Chisel中,上述功能被精简为命令wivar $myView _layer
未被重载的方法上的符号化断点
假定想对-[MyViewController viewDidAppear:]的调用时机做监听,但vc本身未重载此方法,如果这样设置断点
(lldb) b -[MyViewController viewDidAppear:]
Breakpoint 1: no locations (pending).
WARNING: Unable to resolve breakpoint to any actual locations.
但由于未重载此方法,所以不会有viewDidAppear的符号,故断点不会命中。这种情况下需要设置条件 [self isKindOfClass:[MyViewController class]],并将此条件置于UIViewController之上。但由于我们并未拥有UIViewController viewDidAppear的实现,其为apple所编写,故没有符号 ;且在此方法之内并没有self可用。由于如果在符号化断点中想用self, 那首先要知道self的位置(可能在寄存器中也可能在栈上,x86中可以在$esp+4中找到,但最少有4种硬件架构啊,亲)。难以想象了解每种硬件架构的指令集和调用规则,然后再编写在正确的父类上使用正确的条件设置断点的命令。幸运的是,使用Chisel可以很轻松地做到:
(lldb) bmessage -[MyViewController viewDidAppear:]
Setting a breakpoint at -[UIViewController viewDidAppear:] with condition (void*)object_getClass((id)$rdi) == 0x000000010e2f4d28
Breakpoint 1: where = UIKit`-[UIViewController viewDidAppear:], address = 0x000000010e11533c
这篇文章的作者为了推广Chisel也是够拼的⊙﹏⊙
LLDB and Python
LLDB拥有全面而且是内置的Python支持,如果在LLDB中输入script,会打开Python REPL(Read-Eval-Print Loop),相当于python命令行。当然也可以使用script command执行python命令。
(lldb) script import os
(lldb) script os.system("open http://www.objc.io/")
接下来的事情就多了,可以使用py脚本文件,比如~/myCommands.py
def caflushCommand(debugger, command, result, internal_dict):
debugger.HandleCommand("e (void)[CATransaction flush]")
然后在lldb中执行
(lldb) script import ~/myCommands.py
而且,也可以将上述导入放入/.lldbinit以在每次LLDB启动时都执行,Chisel所做的也都是拼接字符串并交给LLDB执行而已。
错误处理
1 退出lldb repl的方法,使用 : 命令
2 一个需要注意的问题是在lldb中创建对象的时候,从init方法返回时,可能会报错 cast the message send to the methods return type,这时候不要慌,将对象显式转换为相应的对象类型,比如 (CGRect)[[UIView alloc] init],另外也需要 (CGRect)[view frame]
因为lldb并不支持.语法方法调用,同时还需要显式转换以及必要时候的括号将整个对象包裹起来,以告诉lldb,我们的输入表示的绝对是一个对象
3 如果在lldb中输入expr -- content.text = (NSString*)[NSString stringWithFormat:@"e"]
网友评论