LLDB:lowerlevel debugge/底层调试器。
本节要介绍的所有的内容几乎都是针对LLDB的,因为苹果已将GDB替换成LLDB。Xcode4.0开始Xcode4.2,他们默认的编译器都是LLVM3.0,使用Clang作为编译器前端,取代了GCC作为编译器前端会有很多优势;到了Xcode4.5(同iOS6同时发布)默认的编译器就是LLVM4.0。LLVM搭配Clang,可以提供更快更好地编译过程,更好地支持代码补全。
1. LLDB
LLDB是用LLVM中可以重用组件构建的额下一代高性能调试器,包括完整的LLVM编译器,其中就有LLVM的Clang表达式解析器和反汇编程序。对于开发者而言,这意味着LLDB能理解你的编译器所能理解的语法,包括OC字面量,OC属性的点标记法。
前一版调试器GDB性能并不好,像poself.view.frame 他是不能理解的,你需要输入po [[self view]frame]。当替换编译器时,APPLE就需要改进调试器,由于GDB是一个整体,所以没办法解决。开发者需要重新编写一个调试器。LLDB是模块化的,而为调试器提供API支持和脚本编程接口是设计目标之一。LLDB命令行调试器会通过这个API链接到LLDB库。
实用LLDB命令
命令名 用法 说明
1.1 dSYM文件
Xcode的调试信息文件称为dSYM文件(因为扩展名叫为.dSYM),又叫调试信息文件,它存储着与目标相关的调试信息。
他会在每次构建工程时自动创建:
用任何一种编程语言编写的代码都需要一个编译器,将这些代码翻译成可以被IDE理解的某种中间语言,或者是可在机器的体系结构上直接运行的原生机器码。调试器通常会集成在开发环境中。Xcode支持放置断点使应用停止运行,从而查看代码中变量的值。也就是说,调试器能够实时的使应用停止运行,这样就可以查看变量和寄存器。
有两类重要的调试器:
a. 符号调试器 :能够在调试代码时显示应用中使用的符号或变量。跟机器语言不同,符号调试器容许你观察代码中得符号,而不是寄存器和地址
b. 机器语言调试器:能够在运行到断电时显示你想过来的汇编代码,容许你观察寄存器的值和内存地址。
让符号调试器工作起来,需要一个编译过的代码和你编写的源代码之前的链接或映射。这正是调试信息文件中所包含的内容。
调试器使用这个调试信息文件将编译过的代码---不管是中间代码还是机器码----映射回源代码。可以将调试信息文件当做游客浏览陌生城市时参考的地图。调试器能参考调试信息文件,根据你再源代码中放置的断电让应用停在正确的位置。
在XCODE编译项目之后,会在app旁看见一个同名的dSYM文件。他是一个编译的中转文件,简单说就是debug的symbols包含在这个文件中。
他有什么作用? 当release的版本crash的时候,会有一个日志文件,包含出错的内存地址,使用symbolicatecrash工具能够把日志和dSYM文件转换成可以阅读的log信息,也就是将内存地址,转换成程序里的函数或变量和所属于的文件
1.2 符号化
包括LLVM在内的编译器都是用来将源代码转换成汇编代码的。所有汇编代码都有一个基地址,。你定义的变量,用到的栈和堆都会依赖这个基地址。每次运行应用时,这个基地址都会改变,尤其是在iOS4.3及以上版本的操作系统中,这些操作系统都采用了地址空间布局随机化的机制。符号化使用方法名和变量名来替换基地址的过程。基地址是应用的入口地址,通常就是main方法,除非你是在写一个静态库。可以符号化其他符号,方法是计算他们相对基地址的偏移,然后将他们映射到dSYM文件中。符号化过程再用Xcode调试应用时才会进行,或者在用Instruments做性能计数分析时进行。
2. 断点
添加到工程中的断点会自动在断点导航面板中列出。可以使用快捷键组合Cmd+6来访问断点导航面板。断点导航面板还支持为异常和符号设置断点。
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 //跟踪调试释放了2次的对象
-[NSObjectdoesNotRecognizeSelector:] //向某个object发送没有的方法
Module:填入是否在一个dylib中,默认不用填。
Conditon:填入条件,例如:
(BOOL)[itemisEqualToString:@"test"]
前面的(BOOL)是必须的。否则console会提示类型不符号,导致条件不能生效。
意思是item(NSString)是test时停下。
同样可以写一下判断的方法比如用来确定类类型的isKindOfClass:,确定对象在继承体系中的位置的isMemberOfClass:,判断一个对象是否能接收某个特定消息的respondsToSelector:,判断一个对象是否遵循某个协议的conformsToProtocol:,以及提供方法实现地址的methodForSelector:。
Ignore:忽略几次。
Action:同上表示在执行后附加动作。
现在键入application:didFinishLaunchingWithOptions:,然后按下回车键。构建并运行应用。你应该看到调试器会在程序刚开始运行时就停止执行应用,并显示栈轨迹。
你查看的符号除了在application:didFinishLaunchingWithOptions:中放置了一个断点,再没有其他好处。符号断点通常用来观察你要关注的方法,比如:
-[NSException raise] malloc_error_break -[NSObjectdoesNotRecognizeSelector:] |
事实上,前一节创建的第一个异常断点与指向[NSException raise]的符号断点的意思是一样的。
malloc_error_break和[NSObjectdoesNotRecognizeSelector:]对调试与内存相关的崩溃非常有帮助。如果应用崩溃了并抛出EXC_BAD_ACCESS,那么在其中一个或全部两个符号上设置断点能够帮助你定位问题。
3. 编辑断点
创建的每个断点都可以在断点导航面板中修改。按住Ctrl键并点击断点,然后从菜单中选择Edit
Breakpoint的方式来编辑断点。你会看到一个断点编辑页,如图19-5所示。
图19-5 编辑断点
通常,断点会在每次执行到该行时停止程序的执行。你可以编辑断点来设置一个条件,从而创建一个条件断点,只在满足设定的条件时该断点才会执行。为什么这种断点会有用呢?假设你在遍历一个大型数组(*n*>10000),很确定5500之后的对象都有问题,你想知道为什么会出问题。常见的做法是,(在应用的代码中)编写额外的代码检查5500之后的索引值,然后在调试环节结束后删除这段代码。
举个例子,你可能会写出如下代码:
for(int i = 0 ; i <10000; i ++) { if(i>5500) { NSLog(@"%@", [self.dataArray objectAtIndex:i]); } } |
并在NSLog处设置一个断点。更简洁的做法是向断点增加这个条件。在图19-5中,文本框是用来添加条件的。将这个条件设为i>5500,然后运行应用。现在,断点只会在满足这个条件时停止应用的执行,而不是每次循环都停下来。
你可以定制断点来打印一个值、播放音频文件,或是执行一段动作脚本(添加了动作脚本的话)。举个例子,如果你正在遍历的对象是一些用户,想知道某个用户是否在这个列表中,这时可以编辑断点使其在运行到你关注的对象时再停下来。除此之外,在这个动作中,还可以选择一些音频片段来播放,执行一段AppleScript或其他功能。点击Action按钮(参考图19-5),选择自定义动作Sound。现在,在断点处Xcode会播放你选择的音乐片段,而不是停下来。如果你是一名游戏开发人员,你感兴趣的可能是在特定条件发生时捕捉一个OpenGLES帧,这个选项在Action按钮中也可以找到。
4. 共享断点
断点现在与要保存到版本控制系统中的代码(或者只是代码片段)关联了起来。Xcode 4(及以上版本)允许将断点提交到版本控制系统,从而与合作者共享它们。你所要做的就是按住Ctrl键并点击一个断点,然后点击Share。你的断点现在已经保存到了工程文件包的xcshareddata目录中。将该目录提交到版本控制系统中,就可以跟团队中的所有其他程序员共享你的断点了。
3. 观察点(没用过)
利用断点,能够在执行到特定行时暂停程序的执行。利用观察点,可以在某个变量中保存的值发生变化时暂停程序的执行。段差点可以帮助解决与全局变量有关的问题,追踪具体是哪个方法改变了特定的全局变量。观察点和断点很像,当不是在执行到某段代码时停止执行,而是在数据被修改时停止执行。
观察点可能不常用,不过,用它来跟踪单例,或者其他全局变量时会很有用。
默认情况下,观察窗口会列出局部作用域内的变量,在观察窗口中按下Ctrl键并点击一个变量。再点击Watch菜单,就在哪个变量上添加了一个观察点。观察点会在断点导航面板中列出。
4. LLDB控制台
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:didFinishLaunchingWithOptions:方法中,在**模拟器**上运行该应用。是的,我说的是在**模拟器**上。当应用崩溃时,打开LLDB控制台,输入以下命令来打印寄存器的值:
registerread
你的控制台应该显示类似下面这样的输出:
寄存器内容(模拟器)
General PurposeRegisters:
eax = 0x00000000
ebx = 0x07f359c0
ecx = 0x00000024
edx = 0x0300078c CoreAudio`HP_Object::GetObjectByID(unsigned long) + 42
edi = 0x08d19470
esi = 0x08d19470
ebp = 0xbfffdce8
esp = 0xbfffdc70
ss = 0x00000023
eflags = 0x00010282 YuWan`-[AppDelegateappLaunchProcess] + 194 at AppDelegate.m:59
eip = 0x00010d18 YuWan`-[AppDelegateapplication:didFinishLaunchingWithOptions:] + 408 atAppDelegate.m:122
cs = 0x0000001b
ds = 0x00000023
es = 0x00000023
fs = 0x00000000
gs = 0x0000000f
设备(ARM处理器)上等价的输出如下所示:
寄存器内容(设备)
(lldb) register read General Purpose Registers: r0 = 0x00000000 r1 = 0x00000000 r2 = 0x2fdc676c r3 = 0x00000040 r4 = 0x39958f43 "application:didFinishLaunchingWithOptions:" r5 = 0x1ed7f390 r6 = 0x00000001 r7 = 0x2fdc67b0 r8 = 0x3c8de07d r9 = 0x0000007f r10 = 0x00000058 r11 = 0x00000004 r12 = 0x3cdf87f4 (void *)0x33d3eb09:OSSpinLockUnlock$VARIANT$mp + 1 sp = 0x2fdc6794 lr = 0x0003a2f3 Test`-[MKAppDelegate application:didFinishLaunchingWithOptions:] + 27 at MKAppDelegate.m:13 pc = 0x0003a2fe Test`-[MKAppDelegate application:didFinishLaunchingWithOptions:] + 38 at MKAppDelegate.m:18 cpsr = 0x40000030 (lldb) |
你的输出可能会不同,要密切注意模拟器中的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:didFinishLaunchingWithOptions:方法)。这些方法的参数都会保存到寄存器r1~r3中。如果你的选择器参数多于3个,那么它们会被保存到栈中,通过栈指针(r13)可以访问。sp、lr和pc实际上是寄存器r13、r14和r15的别名。因此,registerread r13跟registerread sp是一回事。
因此,*sp和*sp+4包含的是第四个和第五个参数的地址,以此类推。在Intel体系结构上,这些参数是以寄存器ebp中保存的地址开始的。
从iTunesConnect上下载了一份崩溃报告时,它通常含有寄存器的状态。因此,了解ARM体系结构上的寄存器分布能够帮助你更好地分析崩溃报告。以下就是一份崩溃报告中的寄存器状态。
崩溃报告中的寄存器状态
Thread 0 crashedwith ARM Thread State: r0:0x00000000 r1:0x00000000 r2:0x00000001 r3: 0x00000000 r4:0x00000006 r5:0x3f871ce8 r6:0x00000002 r7: 0x2fdffa68 r8:0x0029c740 r9:0x31d44a4a r10:0x3fe339b4 r11: 0x00000000 ip:0x00000148 sp:0x2fdffa5c lr:0x36881f5b pc: 0x3238b32c cpsr: 0x00070010 |
通过otool,就能打印出应用中使用的方法。用grep命令找出程序计数器中保存的地址,你就能发现应用崩溃时执行到哪个方法了。
otool -v -arch armv7 -s __TEXT __cstring | grep 3238b32c |
这里,要将替换为崩溃的应用图片(可以将它提交到代码仓库中,或者保存到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, "")
>>> print yesOrNo
这段代码假设你在mypython_script文件中写了一个`SearchObject`函数。本书不会介绍Python脚本的具体实现机制。
5. NSZombieEnabled
NSZombieEnabled变量用来调试与内存相关的问题,跟踪对象的释放过程。启动了它的话,他会用一个僵尸实现来替换默认的dealloc实现,也就是在引用计数降到0时,该僵尸实现回将该对象转换成僵尸对象。
启动他之后,当一个错误的内存访问就会变成一条无法是别的消息发送给僵尸对象。僵尸对象会显示接受到得消息然后跳入调试器,这样你就可以查看到底是哪里出了问题。
6. 不同的崩溃类型
崩溃通常是指操作系统向正在运行的程序发送的信号。
6.1 EXC_BAD_ACCESS
在访问一个已经释放的对象,或者向他发送消息时,它就会出现。造成EXC_BAD_ACCESS最常见的原因是,在初始化方法中初始化变量时用错了修饰符,这会导致对象被释放。
6.2 SIGSEGV
段错误信息,是操作系统产生的一个更严重的错误。当硬件出现错误;访问不可读的是内存地址;或者向受保护的内存地址写入数据时,就会发生这个错误。尽管他并不常见。
导致它的最常见的原因是不正确的类型转换。要避免过度使用指针,或尝试手动修改指针来读取私有数据结构。如果你做了,而在修改指针式没有注意内存对齐和填充问题,就会收到SIGSEGV
6.3 SIGBUS
总线错误信号,代表无效内存访问,即访问的内存是一个无效的内存地址。
SIGBUS 和SIGSEGV 都属于EXC_BAD_ACCESS它的子类型。
6.4 ·
陷阱信号,他并不是一个真正的崩溃信号。他会在处理器执行trap指令时发送。LLDB调试器通常会处理此信号,并在指定的断点处停止运行,如果你收到原因不明的SIGTRAP,clean一下,然后重新构建通常能解决这个问题。
6.5 EXC_ARITHMETIC
当要除零时,应用会受到此信号。解决比较容易。现在用最新的Xcode会提示Devision by zero isundefined
6.6 SIGILL
SIGNAL ILLEGAL INSTRUCTION (非法指令信号)。当在处理器商之行非法指令时。他就会发生。
执行非法指令:将函数指针传给另外一个函数式,该函数指针由于某种原因是坏的,指向了一段已经释放了的内存或是一个数据段。
6.7 SIGABRT
SIGNAL ABORT中止信号。当操作系统发现不安全的情况时,它能够对这种情况进行跟多的控制;必要时他能要求进程进行清理工作。
当它出现时,控制台会输出大量信息,说明具体哪里出错了。可以在LLDB控制台输入bt命令打印处回溯信息。
6.8 看门狗
0x8badf00d;固定的错误编码。他经常出现在执行一个同步网络调用而阻塞了主线程时。
7. 收集崩溃报告
http://www.raywenderlich.com/zh-hans/30818/ios应用崩溃日志揭秘
苹果文档:1. Xcode 4 User Guide “Debug YourApp”
2.DevelopTools Overiew
3.LLVMCompoler Overview
还应该读读下面这些头文件的头部文档:
exception_types.h signal.h