LLDB:lowerlevel debugge/底层调试器。
本节要介绍的所有的内容几乎都是针对LLDB的,因为苹果已将GDB替换成LLDB。Xcode4.0开始Xcode4.2,他们默认的编译器都是LLVM3.0,使用Clang作为编译器前端,取代了GCC作为编译器前端会有很多优势;到了Xcode4.5(同iOS6同时发布)默认的编译器就是LLVM4.0。LLVM搭配Clang,可以提供更快更好地编译过程,更好地支持代码补全。
1.
实用LLDB命令
命令名
1.1
他会在每次构建工程时自动创建:
有两类重要的调试器:
让符号调试器工作起来,需要一个编译过的代码和你编写的源代码之前的链接或映射。这正是调试信息文件中所包含的内容。
调试器使用这个调试信息文件将编译过的代码---不管是中间代码还是机器码----映射回源代码。可以将调试信息文件当做游客浏览陌生城市时参考的地图。调试器能参考调试信息文件,根据你再源代码中放置的断电让应用停在正确的位置。
他有什么作用? 当release的版本crash的时候,会有一个日志文件,包含出错的内存地址,使用symbolicatecrash工具能够把日志和dSYM文件转换成可以阅读的log信息,也就是将内存地址,转换成程序里的函数或变量和所属于的文件
1.2
2.
2.1. 异常断点
在代码有问题导致抛出异常时,异常断点会停止程序的执行。Foundation.framework的NSArray、NSDictionary或UIKit类(比如UITableView方法)中的一些方法会在不能满足特定条件的情况下抛出异常。这些场景包括尝试改变NSArray或是尝试访问越界的数组元素。UITableView会在将行数声明为“n”而没有给每行都提供一个单元格时抛出异常。调试异常在理论上比较容易,但理解造成异常的源相当复杂。应用在崩溃时可能只会在日志中显示造成崩溃的那条异常。这些Foundation.framework方法会在整个工程中都用到,不设置异常断点,即使看了日志也不知道究竟发生什么了。设置了异常断点后,调试器会在异常抛出的瞬间暂停程序的执行,但在捕获异常之前,你需要在断点导航面板中查看崩溃了的那个线程的栈轨迹。
为了方便理解,我们比较一下使用和不使用异常断点调试应用的不同。
在Xcode中创建一个空应用(任何模板都能工作)。在应用委托中,添加以下行:
NSLog(@"%@", [@[]objectAtIndex:100]); |
它会创建一个空数组,然后访问第一百个元素,并记录它。由于这种用法并不符合规范,执行该程序时它会崩溃,控制台会有如下输出,Xcode会跳转到main.m:
2012-08-27 15:25:23.040Test[31224:c07] (null) libc++abi.dylib: terminate calledthrowing an exception (lldb) 2013-07-24 09:39:08.776testDemo[961:c07] *** Terminating app due to uncaught exception'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]:index 100 beyond bounds for empty array' *** First throw callstack: (0x1c90012 0x10cde7e 0x1c45b440x1ec3 0xf157 0xf747 0x1094b 0x21cb5 0x22beb 0x14698 0x1bebdf90x1bebad0 0x1c05bf5 0x1c05962 0x1c36bb6 0x1c35f44 0x1c35e1b 0x1017a0x11ffc 0x1bd2 0x1b05) libc++abi.dylib: terminate calledthrowing an exception |
但看看这难懂的日志消息,没人晓得背后发生了什么。要调试这样的异常,需要设置一个异常断点。
可以在断点导航面板中设置一个异常断点。打开断点导航面板,点击左下角的+按钮,选择AddException Breakpoint,接受默认设置,新加一个断点,如图19-2所示。
Exception:可选all 所有语言引起的异常,objective-c语言和c++语言引起的异常。
Break:可选onThrow和onCatch。
Action:可在程序断点执行后增加额外动作(Applescript,捕捉动画帧速,调试器命令(lldb),输入log记录,终端命令(shell),播放声音)
例如:DebuggerCommond中可填入
po item 输出 item变量的值
bt 表示输出 方法调用堆栈信息
图19-2 增加一个异常断点
再次运行该工程。你应该能看到调试器暂停了应用的执行,程序正好停在抛出异常的那行,如图19-3所示。
图19-3Xcode在设置断点的位置停止执行应用
异常断点能帮你理解异常的起因。我在新建工程时,要做的第一件事就是设置一个异常断点。我强烈推荐这么做。
如果想快速运行应用而不想在任何断点处停留,那么可以在键盘上用快捷键Cmd+Y来禁用所有断点。
2. 符号断点
符号断点会在执行到特定符号时暂停程序。符号可以是一个方法名、类中的一个方法或者任何C方法(objc_msgSend)。
可以在断点导航面板中设置符号断点,跟设置异常断点差不多,不过要选择符号断点而不是异常断点。现在,对话框中输入了你关注的符号,如图19-4所示。
图 增加一个符号断点
Symbol:填入你想检测消息发送实体的方法
(例如:-[NSExceptionraise],-号是实例方法,+号是类方法)。
你也可以输入:
objc_exception_throw
malloc_error_break
Module:填入是否在一个dylib中,默认不用填。
Conditon:填入条件,例如:
(BOOL)[itemisEqualToString:@"test"]
前面的(BOOL)是必须的。否则console会提示类型不符号,导致条件不能生效。
意思是item(NSString)是test时停下。
同样可以写一下判断的方法比如用来确定类类型的isKindOfClass:,确定对象在继承体系中的位置的isMemberOfClass:,判断一个对象是否能接收某个特定消息的respondsToSelector:,判断一个对象是否遵循某个协议的conformsToProtocol:,以及提供方法实现地址的methodForSelector:。
Ignore:忽略几次。
Action:同上表示在执行后附加动作。
现在键入application:didFinishLaunchingWithOp
你查看的符号除了在application:didFinishLaunchingWithOp
-[NSException raise] malloc_error_break -[NSObjectdoesNotRecognizeSelector |
事实上,前一节创建的第一个异常断点与指向[NSException raise]的符号断点的意思是一样的。
malloc_error_break和[NSObjectdoesNotRecognizeSelector
3. 编辑断点
创建的每个断点都可以在断点导航面板中修改。按住Ctrl键并点击断点,然后从菜单中选择Edit
Breakpoint的方式来编辑断点。你会看到一个断点编辑页,如图19-5所示。
图19-5 编辑断点
通常,断点会在每次执行到该行时停止程序的执行。你可以编辑断点来设置一个条件,从而创建一个条件断点,只在满足设定的条件时该断点才会执行。为什么这种断点会有用呢?假设你在遍历一个大型数组(*n*>10000),很确定5500之后的对象都有问题,你想知道为什么会出问题。常见的做法是,(在应用的代码中)编写额外的代码检查5500之后的索引值,然后在调试环节结束后删除这段代码。
举个例子,你可能会写出如下代码:
for(int i = 0 ; i <10000; i ++) { |
并在NSLog处设置一个断点。更简洁的做法是向断点增加这个条件。在图19-5中,文本框是用来添加条件的。将这个条件设为i>5500,然后运行应用。现在,断点只会在满足这个条件时停止应用的执行,而不是每次循环都停下来。
你可以定制断点来打印一个值、播放音频文件,或是执行一段动作脚本(添加了动作脚本的话)。举个例子,如果你正在遍历的对象是一些用户,想知道某个用户是否在这个列表中,这时可以编辑断点使其在运行到你关注的对象时再停下来。除此之外,在这个动作中,还可以选择一些音频片段来播放,执行一段AppleScript或其他功能。点击Action按钮(参考图19-5),选择自定义动作Sound。现在,在断点处Xcode会播放你选择的音乐片段,而不是停下来。如果你是一名游戏开发人员,你感兴趣的可能是在特定条件发生时捕捉一个OpenGLES帧,这个选项在Action按钮中也可以找到。
4. 共享断点
断点现在与要保存到版本控制系统中的代码(或者只是代码片段)关联了起来。Xcode 4(及以上版本)允许将断点提交到版本控制系统,从而与合作者共享它们。你所要做的就是按住Ctrl键并点击一个断点,然后点击Share。你的断点现在已经保存到了工程文件包的xcshareddata目录中。将该目录提交到版本控制系统中,就可以跟团队中的所有其他程序员共享你的断点了。
3.
利用断点,能够在执行到特定行时暂停程序的执行。利用观察点,可以在某个变量中保存的值发生变化时暂停程序的执行。段差点可以帮助解决与全局变量有关的问题,追踪具体是哪个方法改变了特定的全局变量。观察点和断点很像,当不是在执行到某段代码时停止执行,而是在数据被修改时停止执行。
4.
Xcode的调试控制台窗口是一个功能完备的LLDB调试控制台。当(在断点处)暂停应用时,调试控制台会显示LLDB命令行提示符。你可以在该控制台上输入任何LLDB调试器命令来帮助调试,包括加载额外的Python脚本。
最常用的命令是po,意为打印对象(printobject)。当应用在调试器中暂停时,可以打印当前作用域内的任何变量。这包括所有的栈变量、类变量、属性、ivar以及全局变量。总之,在断点处你的应用能访问的所有变量也都能通过调试控制台访问。
1. 打印标量变量
处理整型或结构体型(CGRect、CGPoint等)标量时,要用p,而不是po,后跟结构体的类型,例如:
p (int) self.myAge
p (CGPoint)self.view.center
2. 打印寄存器
为什么需要打印寄存器中的值呢?你不会直接在CPU的寄存器上存储变量,对吗?是的,但寄存器中保存了跟程序状态有关的大量信息。这些信息与给定处理器架构上的子函数调用规范有关。了解这些信息能够大大地减少你的调试周期时间,让你的编程功力炉火纯青。
CPU的寄存器用来存储常用的变量。编译器会对循环变量、方法参数及返回值等常用变量进行优化,将其放到寄存器中。当应用崩溃了但没有明显的原因时(应用经常会莫名其妙地崩溃,直到你找到问题所在,不是吗?),查看寄存器中保存的那些导致应用崩溃的方法名或选择器名会很有用。
C99语言标准定义了关键字register,指导编译器将变量存储在CPU的寄存器中。举个例子,用for(register int i = 0 ; i < n ; i++)这样的方式声明一个for循环时,它会将变量i保存到CPU的寄存器中。注意,这个声明并不能保证变量一定保存到寄存器中,如果没有可用的空闲寄存器,编译器也可以将变量保存到内存中。
可以在LLDB控制台上用registerread命令来打印寄存器。现在,创建一个应用,添加一个会造成应用崩溃的代码片段。
int *a = nil;
NSLog(@"%d", *a);
你创建了一个nil指针,并尝试访问该地址处的值。显然,这会抛出EXC_BAD_ACCESS异常。将前面的代码添加到application:didFinishLaunchingWithOp
registerread
你的控制台应该显示类似下面这样的输出:
寄存器内容(模拟器)
General PurposeRegisters:
设备(ARM处理器)上等价的输出如下所示:
寄存器内容(设备)
|
你的输出可能会不同,要密切注意模拟器中的eax、ecx和esi,或者设备上的r0~r4寄存器。这些寄存器都保存了一些你感兴趣的值。在模拟器中(运行在Mac的Intel处理器上),ecx寄存器保存的是程序崩溃时调用的选择器名称。可以用如下方式通过指定寄存器名称将单独某个寄存器打印到控制台上:
register read ecx.
也可以指定多个寄存器:
register read eax ecx.
Intel体系结构上的ecx寄存器和ARM体系结构上的r15寄存器保存的都是程序计数器。打印程序计数器的地址会显示最后执行的指令。类似地,eax(ARM上是r0)保存的是接收者的地址,而ecx(ARM上是r4)保存的是最后调用的选择器(本例中,就是application:didFinishLaunchingWithOp
因此,*sp和*sp+4包含的是第四个和第五个参数的地址,以此类推。在Intel体系结构上,这些参数是以寄存器ebp中保存的地址开始的。
从iTunesConnect上下载了一份崩溃报告时,它通常含有寄存器的状态。因此,了解ARM体系结构上的寄存器分布能够帮助你更好地分析崩溃报告。以下就是一份崩溃报告中的寄存器状态。
崩溃报告中的寄存器状态
|
通过otool,就能打印出应用中使用的方法。用grep命令找出程序计数器中保存的地址,你就能发现应用崩溃时执行到哪个方法了。
|
这里,要将替换为崩溃的应用图片(可以将它提交到代码仓库中,或者保存到Xcode的应用归档中)。
注意,你在本节中学到的内容都跟处理器体系结构紧密相关。如果苹果将来改变了iOS适用的CPU规格(从ARM变成其他的),那么这部分内容也可能要改变。不过,只要你掌握了基础知识,应该能将它应用到任何新的处理器上。
3. 调试器脚本编程
LLDB调试器的设计由底至上都支持API和插件接口。针对LLDB的Python脚本编程就受益于这些插件接口。如果你是一名Python程序员,可能会惊喜地发现LLDB支持导入Python脚本来帮助调试;也就是说,可以用Python写个脚本,将它导入到LLDB中,然后用这个脚本查看变量。如果你不是Python程序员,那么可以直接跳过本节内容。
假设你要从包含10
000个对象的大数组中查找一个元素。针对该数组的一条简单的po命令会列出所有的10
000个对象,仅凭肉眼观察很难找到这个元素。如果你有一个脚本,可以将这个数组作为参数接收,然后自动找到要查看的对象,那就可以将这个脚本导入到LLDB中,用来调试。
可以在LLDB提示符中键入script来启动Python
shell。命令行提示符会由(lldb)变为>>>。在脚本编辑器中,可以用Python变量lldb.frame来访问LLDB的调用栈帧。所以lldb.frame.FindVariable("a")会从当前LLDB调用栈帧中得到变量a的值。如果你正通过遍历数组查找一个特定值,可以将lldb.frame.FindVariable("myArray")赋给一个变量,并将它传给Python脚本。
下面的代码说明了具体的做法。
调用Python脚本搜索一个对象
>>> importmypython_script
>>> array =lldb.frame.FindVariable ("myArray")
>>> yesOrNo =mypython_script.SearchObject (array, "")
这段代码假设你在mypython_script文件中写了一个`SearchObject`函数。本书不会介绍Python脚本的具体实现机制。
5.
NSZombieEnabled变量用来调试与内存相关的问题,跟踪对象的释放过程。启动了它的话,他会用一个僵尸实现来替换默认的dealloc实现,也就是在引用计数降到0时,该僵尸实现回将该对象转换成僵尸对象。
6.
崩溃通常是指操作系统向正在运行的程序发送的信号。
6.1
6.2
6.3
6.4
6.5
6.6
执行非法指令:将函数指针传给另外一个函数式,该函数指针由于某种原因是坏的,指向了一段已经释放了的内存或是一个数据段。
6.7
6.8
7.
苹果文档:1.
还应该读读下面这些头文件的头部文档: