在Xcode中调试程序

作为一个合格的iOS程序猿,掌握并熟练使用Debugger的相关技能也是十分必要的,由于从Xcode5以后就全面使用了LLDB来做为程序的调试器,所以我们就来总结一下LLDB在Xcode调试中的使用吧,如下图所示
Image

在开始了解LLDB的常用调试语句之前,让我们来大致了解一下LLDB中的一些基础知识,更多LLDB的相关知识,请访问LLDB Debugger.

首先是LLDB命令结构的组成
<noun> <verb> [-options [option-value]] [argument [argument...]]
其中:
<command>(命令)和<subcommand>(子命令):LLDB调试命令的名称。命令和子命令按层级结构来排列:一个命令对象为跟随其的子命令对象创建一个上下文,子命令又为其子命令创建一个上下文,依此类推。
<action>:我们想在前面的命令序列的上下文中执行的一些操作。
<options>:行为修改器(action modifiers)。通常带有一些值。
<argument>:根据使用的命令的上下文来表示各种不同的东西。

例如:
(lldb) breakpoint set --file foo.c --line 12


设置断点

我们可以直接在代码的左侧直接单击形成一个断点,我们也可以使用breakpoint命令,例如
如果想在某个文件中的某行设置一个断点,可使用以下命令:

(lldb) breakpoint set --file foo.c --line 12
如果想给某个函数设置断点,可使用以下命令:

(lldb) breakpoint set --name foo

当然也可以x86_64寄存器,再额外添加条件判断
x86_64下,函数通过寄存器进行参数传递而不是传统的堆栈,下面是与函数调用相关的寄存器说明

寄存器 作用 Objective-C函数调用 C函数调用
$rsp 返回地址 返回地址 返回地址
$rdi 参数一 id receiver 参数一
$rsi 参数二 SEL op 参数二
$rdx 参数三 参数一 参数三
$rcx 参数四 参数二 参数四
$r8 参数五 参数三 参数五
$r9 参数六 参数四 参数六

例如可以通过如下命令可以在NSString 调用substringWithRange:时打上断点,同时条件是NSRange.location为18446744073709551615。

(lldb) breakpoint set --name "-[__NSCFString substringWithRange:]" --condition '$rdx == 18446744073709551615' #设置条件断点

如果想给C++中所有命名为foo的方法设置断点,可以使用以下命令:

(lldb) breakpoint set --method foo
如果想给Objective-C中所有命名为alignLeftEdges:的选择器设置断点,则可以使用以下命令:

(lldb) breakpoint set --selector alignLeftEdges:
此处文章直接转载至工具篇:LLDB调试
这里的breakpoint也可以直接简写成br.
这里的话,我们来分析一下,这些命令的使用场景,例如,我们想在调试一个函数方法,但是这个方法在很多文件里面都有引用,而我们不知道它的具体调用者,于是我们就可以通过breakpoint set --selector来把所有的调用地方都打上断点,不留死角。

另外,我们可以通过breakpoint list命令来打印当前的所有断点,这个命令也是非常有用的,因为当我们知道的数点的序号时,我们可以调用如下命令,来增加打印一些额外的信息

(lldb) breakpoint command add 1.1
Enter your debugger command(s). Type 'DONE' to end.
> bt
> DONE

如上面的话就是在断点1.1这里追加打印堆栈。记得以DONE结束。

另外一个,我们也可以通过为断点添加额外的限制条件和触发事件。
Image
这里的话,请直接参考与调试器共舞 - LLDB 的华尔兹,这里我就不再深入探讨了。


设置观察者

作为断点的补充,LLDB支持观察点以在不中断程序运行的情况下监测一些变量。例如,我们可以使用以下命令来监测名为global的变量的写操作,并在(global==5)为真时停止监测:

(lldb) watch set var global
Watchpoint created: Watchpoint 1: addr = 0x100001018 size = 4 state = enabled type = w
   declare @ '/Volumes/data/lldb/svn/ToT/test/functionalities/watchpoint/watchpoint_commands/condition/main.cpp:12'
(lldb) watch modify -c '(global==5)'
(lldb) watch list
Current watchpoints:
Watchpoint 1: addr = 0x100001018 size = 4 state = enabled type = w
    declare @ '/Volumes/data/lldb/svn/ToT/test/functionalities/watchpoint/watchpoint_commands/condition/main.cpp:12'
    condition = '(global==5)'
(lldb) c
Process 15562 resuming
(lldb) about to write to 'global'...
Process 15562 stopped and was programmatically restarted.
Process 15562 stopped and was programmatically restarted.
Process 15562 stopped and was programmatically restarted.
Process 15562 stopped and was programmatically restarted.
Process 15562 stopped

* thread #1: tid = 0x1c03, 0x0000000100000ef5 a.out`modify + 21 at main.cpp:16, stop reason = watchpoint 1
    frame #0: 0x0000000100000ef5 a.out`modify + 21 at main.cpp:16
   13
   14      static void modify(int32_t &var) {
   15          ++var;
-> 16      }
   17
   18      int main(int argc, char** argv) {
   19          int local = 0;
(lldb) bt

* thread #1: tid = 0x1c03, 0x0000000100000ef5 a.out`modify + 21 at main.cpp:16, stop reason = watchpoint 1
    frame #0: 0x0000000100000ef5 a.out`modify + 21 at main.cpp:16
    frame #1: 0x0000000100000eac a.out`main + 108 at main.cpp:25
    frame #2: 0x00007fff8ac9c7e1 libdyld.dylib`start + 1
(lldb) frame var global
(int32_t) global = 5
(lldb) watch list -v
Current watchpoints:
Watchpoint 1: addr = 0x100001018 size = 4 state = enabled type = w
    declare @ '/Volumes/data/lldb/svn/ToT/test/functionalities/watchpoint/watchpoint_commands/condition/main.cpp:12'
    condition = '(global==5)'
    hw_index = 0  hit_count = 5     ignore_count = 0
(lldb)

可以使用help watchpoint来查看该命令的使用。


打印

(lldb) print a
(NSInteger) $0 = 0
(lldb) print b
(NSInteger) $1 = 0
(lldb) print str
(NSString *) $2 = 0x0000000100001048 @"abc"
(lldb) print url
(NSURL *) $3 = 0x0000000100206cc0 @"abc"

我们使用print命令就可以打印一些常用的变量值,其实print可以直接简写成p.但如果要打印一个object-C对象的话,我们可以使用po命令。
在我们进行断点调试的时候,我们也可以用frame info命令来打印当前的一些调试信息。
recursiveDescription
我们可以通过这个命令字来打印当前的视图堆栈,例如:

po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]

_printHierarchy
我们可以用这个命令来检查视图控制器。这是一个苹果默默在 iOS 8 中为 UIViewController 添加的私有辅助方法。

 po [[[UIWindow keyWindow] rootViewController] _printHierarchy]

流程控制

Image
从左到右,四个按钮分别是:continuestep overstep intostep out
其在LLDB中前面三个按钮所对应的命令分别是cns,但是第四个的话,大家可以直接点击图片上的按钮,或者调用finish命令来结束本次断点调试。
流程控制中另外一个非常重要的命令字是thread return。我们可以通过这个命令来直接跳出某个函数,后面的都不执行了。thread return 也可以带返回值,如thread return YES,返回一个BOOL值。


expression

如果想改变一个值怎么办?你或许会猜 modify。其实这时候我们要用到的是 expression 这个方便的命令。
Image


image

当我们的程序崩溃时,我们可以使用这条命令来查找崩溃所在的具体位置,我们可以调用如下命令
image lookup --address $x8+0x0000000100000de0


call

call即是调用的意思。其实上述的po和p也有调用的功能。因此一般只在不需要显示输出,或是方法无返回值时使用call。 和上面的命令一样,我们依然在viewDidLoad:里面设置断点,然后在程序中断的时候输入下面的命令:

call [self.view setBackgroundColor:[UIColor redColor]]
继续运行程序,看看view的背景颜色是不是变成红色的了!在调试的时候灵活运用call命令可以起到事半功倍的作用。
有时候调用这个call会报错,这个时候,我们需要在函数的前面加上返回类型,例如

call (void)[self.view setBackgroundColor:[UIColor redColor]]

另外一个使用场景是call (void)instrumentObjcMessageSends(YES),其只适用于模拟器,它会把runtime调用的过程打印在/tmp/msgSend-xxxx里面。


命令别名

我们可以使用LLDB的别名机制来为常用的命令创建一个别名,以方便我们的使用,如下命令:

(lldb) breakpoint set --file foo.c --line 12

如果在我们的调试中需要经常用到这条命令,则每次输入这么一长串的字符一定会很让人抓狂。此时,我们就可以为这条命令创建一个别名,如下所示:

(lldb) command alias bfl breakpoint set -f %1 -l %2

这样,我们只需要按如下方式来使用它即可:

(lldb) bfl foo.c 12

help

LLDB帮助系统让我们可以了解LLDB提供了哪些功能,并可以查看LLDB命令结构的详细信息。熟悉帮助系统可以让我们访问帮助系统中中命令文档.
如果你对某个关键字的使用不是很清楚,那么你可以使用这个命令字来进行查询。其实上面我们所讲到的这些命令字都只是举了一个简单的例子,其实他们各自还有许多使用的场景。如果有兴趣的话,大家可以再仔细深入的研究。


Xcode调试的一些其它的黑科技

1、例如我们在调试程序的时候出现了crash。而显示在控制台上的堆栈信息又无法直接看出问题的话,我们可以尝试如下做法
Image
这样就有可能把出错的堆栈翻译成有效的函数名称。
2、环境变量
Image
通过僵尸信息的处理,我们有可能捕获到一些关键信息来定位程序出现问题的地方。

2015-08-15 18:13239