2. LLDB控制台
Xcode的调试控制台窗口是一个功能完备的LLDB调试控制台。当(在断点处)暂停应用时,调试控制台会显示LLDB命令行提示符。你可以在该控制台上输入任何LLDB调试器命令来帮助调试,包括加载额外的Python脚本。
最常用的命令是po,意为打印对象(print object)。当应用在调试器中暂停时,可以打印当前作用域内的任何变量。这包括所有的栈变量、类变量、属性、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控制台上用register read命令来打印寄存器。现在,创建一个应用,添加一个会造成应用崩溃的代码片段。
int *a = nil;
NSLog(@"%d", *a);
你创建了一个nil指针,并尝试访问该地址处的值。显然,这会抛出EXC_BAD_ACCESS异常。将前面的代码添加到application:didFinishLaunchingWithOptions:方法中,在**模拟器**上运行该应用。是的,我说的是在**模拟器**上。当应用崩溃时,打开LLDB控制台,输入以下命令来打印寄存器的值:
register read
你的控制台应该显示类似下面这样的输出:
寄存器内容(模拟器)
General Purpose Registers:
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 at AppDelegate.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.
也可以指定多个寄存器:
registerread eax ecx.
Intel体系结构上的ecx寄存器和ARM体系结构上的r15寄存器保存的都是程序计数器。打印程序计数器的地址会显示最后执行的指令。类似地,eax(ARM上是r0)保存的是接收者的地址,而ecx(ARM上是r4)保存的是最后调用的选择器(本例中,就是application:didFinishLaunchingWithOptions:方法)。这些方法的参数都会保存到寄存器r1~r3中。如果你的选择器参数多于3个,那么它们会被保存到栈中,通过栈指针(r13)可以访问。sp、lr和pc实际上是寄存器r13、r14和r15的别名。因此,register read r13跟register read sp是一回事。
因此,*sp和*sp+4包含的是第四个和第五个参数的地址,以此类推。在Intel体系结构上,这些参数是以寄存器ebp中保存的地址开始的。
从iTunes Connect上下载了一份崩溃报告时,它通常含有寄存器的状态。因此,了解ARM体系结构上的寄存器分布能够帮助你更好地分析崩溃报告。以下就是一份崩溃报告中的寄存器状态。
崩溃报告中的寄存器状态
Thread 0 crashed with 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 <your image> | grep 3238b32c |
这里,要将<your image>替换为崩溃的应用图片(可以将它提交到代码仓库中,或者保存到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脚本搜索一个对象
>>> import mypython_script
>>> array = lldb.frame.FindVariable ("myArray")
>>> yesOrNo = mypython_script.SearchObject (array,"<search element>")
>>> print yesOrNo
这段代码假设你在mypython_script文件中写了一个`SearchObject`函数。本书不会介绍Python脚本的具体实现机制。
3. NSZombieEnabled
NSZombieEnabled变量用来调试与内存相关的问题,跟踪对象的释放过程。启动了它的话,他会用一个僵尸实现来替换默认的dealloc实现,也就是在引用计数降到0时,该僵尸实现回将该对象转换成僵尸对象。
启动他之后,当一个错误的内存访问就会变成一条无法是别的消息发送给僵尸对象。僵尸对象会显示接受到得消息然后跳入调试器,这样你就可以查看到底是哪里出了问题。
4. 不同的崩溃类型
崩溃通常是指操作系统向正在运行的程序发送的信号。
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 is undefined
6.6 SIGILL
SIGNALILLEGAL INSTRUCTION (非法指令信号)。当在处理器商之行非法指令时。他就会发生。
执行非法指令:将函数指针传给另外一个函数式,该函数指针由于某种原因是坏的,指向了一段已经释放了的内存或是一个数据段。
6.7 SIGABRT
SIGNALABORT中止信号。当操作系统发现不安全的情况时,它能够对这种情况进行跟多的控制;必要时他能要求进程进行清理工作。
当它出现时,控制台会输出大量信息,说明具体哪里出错了。可以在LLDB控制台输入bt命令打印处回溯信息。
6.8 看门狗
0x8badf00d;固定的错误编码。他经常出现在执行一个同步网络调用而阻塞了主线程时。
5. 收集崩溃报告
http://www.raywenderlich.com/zh-hans/30818/ios应用崩溃日志揭秘
苹果文档:1. Xcode 4 User Guide “Debug Your App”
2.Develop Tools Overiew
3.LLVM Compoler Overview
还应该读读下面这些头文件的头部文档:
exception_types.h signal.h