---------------------
关于调试异常崩溃:
一般崩溃是由内存使用错误导致的,要么多了,要么少了。
用xcode的调试提示可以知道是什么原因导致的崩溃。
在xcode中product àedit scheme à diagnostics 将enable Zombie objects 和 Malloc Stack 选中, 如果是内存释放错误,则gdb会提示release dealloc object。
然后可以用断点缩小错误范围, 在可能出现错误的地方用单步调试, 当执行到有错误代码时, gdb会再次提示release dealloc object。
其实XCODE内嵌GDB,那个 lldb就是gdb!
-----------------------
关于GDB对于大多数Cocoa程序员来说,最常用的debugger莫过于Xcode自带的调试工具了。而实际上,它正是gdb的一个图形化包装。相对于gdb,图形化带来了很多便利,但同时也缺少了一些重要功能。而且在某些情况下,gdb反而更加方便。因此,学习gdb,了解一下幕后的实质,也是有必要的。
gdb可以通过终端运行,也可以在Xcode的控制台调用命令。本文将通过终端讲述一些gdb的基本命令和技巧。
首先,我们来看一个例子:
#import
int main(int argc, char **argv)
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSLog(@"Hello, world!");
[pool release];
return 0;
}
糟糕,程序竟然exited normally了(==|||)。这可不行,我们得让他崩溃才行。所以我们给这个小程序添加一个bug:
int x = 42;
NSLog("Hello, world! x = %@", x);
nice。这样一来程序就会漂亮地崩溃了:
(gdb) run
Starting program: /Users/mikeash/shell/a.out
Reading symbols for shared libraries .++++....................... done
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: 13 at address: 0x0000000000000000
0x00007fff84f1011c in objc_msgSend ()
(gdb)
如果我们是在shell中直接运行的程序,在崩溃后就会回到shell。不过现在我们是通过gdb运行的,所以现在并没有跳出。gdb暂停了我们的程序,但依然使之驻留在内存中,让我们有机会做调试。
首先,我们想知道具体是哪里导致了程序崩溃。gdb已经通过刚才的输出告知了我们: 函数objc_msgSend就是祸之根源。但是这个信息并不足够,因为这个objc_msgSend是objc运行时库中的函数。我们并不知道它是怎么调用的。我们关注的是我们自己的代码。
要知道这一点,我们需要得到当前进程的函数调用栈的情况,以此回溯找到我们自己的方法。这时我们需要用到backtrace命令,一般简写为bt:
(gdb) bt
#0 0x00007fff84f1011c in objc_msgSend ()
#1 0x00007fff864ff30b in _CFStringAppendFormatAndArgumentsAux ()
#2 0x00007fff864ff27d in _CFStringCreateWithFormatAndArgumentsAux ()
#3 0x00007fff8657e9ef in _CFLogvEx ()
#4 0x00007fff87beab3e in NSLogv ()
#5 0x00007fff87beaad6 in NSLog ()
#6 0x0000000100000ed7 in main () at test.m:10
现在我们可以看到,程序在test.m的第10行,调用NSLog方法时崩溃了。接下来我们想看一下这次调用的详细信息。这时我们要用到up命令。up命令可以在栈的各层之间跳转。本例中,我们的代码main是#6:
(gdb) up 6
#6 0x0000000100000ed7 in main () at test.m:10
9NSLog("Hello, world! x = %@", x);
这回不仅是函数名,连出错的那行代码也打印出来了。但是,我们还可以使用list(简写为l)命令,打印出更多信息:
ps: 如果需要回到栈列表。可以使用down命令。
(gdb) l
5
6int main(int argc, char **argv)
7{
8NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
9int x = 42;
10NSLog("Hello, world! x = %@", x);
11[pool release];
12
13return 0;
14}
啊,整个代码都被列出来了。虽然我们用编辑器打开test.m文件然后找到第10行也可以打到同样效果,但显然没有上面的方法更有效率。(当然没有Xcode自带的那个快就是了)
好了,现在我们再来看看这个bug(虽然是我们自己弄出来的)。很明显,在格式化字符串前少加了一个@。我们改正它,并重新运行一遍程序:
(gdb) run
Starting program: /Users/mikeash/shell/a.out
Reading symbols for shared libraries .++++....................... done
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0x000000000000002a
0x00007fff84f102b3 in objc_msgSend_fixup ()
(gdb) bt
#0 0x00007fff84f102b3 in objc_msgSend_fixup ()
#1 0x0000000000000000 in ?? ()
啊咧,程序还是崩溃了。更杯具的是,栈信息没有显示出这个objc_msgSend_fixup方法是从哪里调用的。这样我们就没法用上面的方法找到目标代码了。这时,我们只好请出一个debugger最常用的功能:断点。
在gdb中,设置断点通过break命令实现。它可以简写为b。有两种方法可以确定断点的位置:传入一个已定义的符号,或是直接地通过一个file:line对设置位置。
现在让我们在main函数的开始处设置一个断点:
(gdb) b test.m:8
Breakpoint 1 at 0x100000e8f: file test.m, line 8.
debugger给了我们一个回应,告诉我们断点设置成功了,而且这个断点的标号是1。断点的标号很有用,可以用来给断点排序&停用&启用&删除等。不过我们现在不需要理会,我们只是接着运行程序:
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /Users/mikeash/shell/a.out
Breakpoint 1, main (argc=1, argv=0x7fff5fbff628) at test.m:8
8NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
debugger在在我们期望的地方停下了。现在我们使用next(简写n)命令单步调试程序,看看它到底是在哪一行崩溃的:
(gdb) n
9int x = 42;
(gdb)
10NSLog(@"Hello, world! x = %@", x);
(gdb)
Program received signal EXC_BAD_ACCESS, Could not access memory.
Reason: KERN_INVALID_ADDRESS at address: 0x000000000000002a
0x00007fff84f102b3 in objc_msgSend_fixup ()
值得注意的是,我只键入了一次n命令,随后直接敲了2次回车。这样做的原因是gdb把任何空输入当作最近一次输入命令的重复。所以这里相当于输入了3次n。
现在我们可以看到,崩溃之处依然是NSLog。原因嘛,当然是在格式化输出的地方用%@表示int型变量x了。我们仔细看一下输出信息:崩溃原因是错误地访问了0x000000000000002a这个地址。而2a的十进制表示正是42--我们为x赋的值。编译器把它当作地址了。
输出数值
一个很重要的调试方法是输出表达式和变量的值。在gdb中,这是通过print命令完成的。
(gdb) p x
$1 = 42
在print命令后追加/format可以格式化输出。/format是一个gdb的格式化字符串,比较有用的格式化字符有 x:十进制数; c:字符; a:地址等。
(gdb) p/x x
$2 = 0x2a
print-object方法(简写为po)用来输出obj-c中的对象。它的工作原理是,向被调用的对象发送名为debugDescription的消息。它和常见的description消息很像。
举例来说,让我们输出一下autorelease pool:
(gdb) po pool
这个命令不仅仅可以输出显式定义的对象,也可以输出表达式的结果。这次我们测试一下nsobject中debugDescription的方法签名:
返回值是对象的表达式可以用po命令输出结果,那么返回值是基本类型的方法又怎样呢?显然,它们是可以用p命令输出的。但是要小心,因为gdb并不能自动识别出返回值的类型。所以我们在输出前要显式地转换一下:
(gdb) p [NSObject instancesRespondToSelector: @selector(doesNotExist)]
Unable to call function "objc_msgSend" at 0x7fff84f100f4: no return type information available.
To call this function anyway, you can cast the return type explicitly (e.g. ‘print (float) fabs (3.0)‘)
(gdb) p (char)[NSObject instancesRespondToSelector: @selector(doesNotExist)]
$5 = 0 ‘00‘
你也许发现了,doesNotExist方法的返回值是BOOL,而我们做的转换却是char。这是因为gdb也不能识别那些用typedef定义的类型。不仅仅是你定义的,即使是Cocoa框架里定义的也不行。
你也许已经注意到,在用p进行输出的时侯,输出值前面会有一个类似"$1="的前缀。它们是gdb变量。它们可以在后面的表达式中使用,来指代它后面的值。在下面的例子里,我们开辟了一块内存,将其置零,然后释放。在这个过程中,我们使用了gdb变量,这样就不用一遍遍地复制粘贴地址了。
(gdb) p (int *)malloc(4)
$6 = (int *) 0x100105ab0
(gdb) p (void)bzero($6, 4)
$7 = void
(gdb) p *$6
$8 = 0
(gdb) p (void)free($6)
$9 = void
我们也想把这个技巧用到对象上,但不幸的是po命令并不会把它的返回值存储到变量里。所以我们在得到一个新的对象时必须先使用p命令:
(gdb) p (void *)[[NSObject alloc] init]
$10 = (void *) 0x100105950
(gdb) po $10
(gdb) p (long)[$10 retainCount]
$11 = 1
(gdb) p (void)[$10 release]
$12 = void
检查内存
有些时候,仅仅输出一个数值还不能帮助我们查找出错误。我们需要一次性地打印出一整块内存来窥视全局。这时候我们就需要使用x命令。
x命令的格式是x/format address。其中address很简单,它通常是指向一块内存的表达式。但是format的语法就有点复杂了。它由三个部分组成:
第一个是要显示的块的数量;第二个是显示格式(如x代表16进制,d代表十进制,c代表字符);第三个是每个块的大小。值得注意的是第三部分,即块大小是用字符对应的。用b, h, w,g 分别表示1, 2, 4, 8 bytes。举例来说,用十六进制方式,打印从ptr开始的4个4-byte块应该这样写:
(gdb) x/4xw ptr
接下来举一个比较实际的例子。我们看一下NSObject类的内容:
(gdb) x/4xg (void *)[NSObject class]
0x7fff70adb468 : 0x00007fff70adb4400x0000000000000000
0x7fff70adb478 :0x0000000100105ac00x0000000100104ac0
接下来再看看一个NSObject实例的内容:
(gdb) x/1xg (void *)[NSObject new]
0x100105ba0:0x00007fff70adb468
现在我们看到,在实例开头引用了类的地址。
设置变量
有时,查看数值程度的能力还是稍弱了一点,我们还想能够修改变量。这也很简单,只需要使用set命令:
(gdb) set x = 43
我们可以用任意表达式给一个变量赋值。比如说新创建一个对象然后赋值:
(gdb) set obj = (void *)[[NSObject alloc] init]
断点
我们可以在程序的某个位置设置断点,这样当程序运行到那里的时候就会暂停,而把控制权转移给调试器。就像之前提到的,我们用break命令来设置断点。下面详细地列出了如何设置断点的目标:
SymbolName: 为断点指定一个函数名。这样断点就会设置在该函数上。
file.c:1234: 把断点设置在指定文件的一行。
-[ClassName method:name:]: 把断点设置在objc的方法上。用+代表类方法。
*0xdeadbeef: 在内存的指定位置设置断点。这不是很常用,一般在没有源码的调试时使用。
断点可以用enable命令和disable命令来切换到使用和停用状态,也可以通过delete命令彻底删除。想要查看现有断点的话,使用info breakpoints命令(可以简写成info b,或是i b)。
另外,我们也可以用if命令,把断点升级成条件断点。顾名思义,条件断点只会在设定的条件成真时起作用。举例来说,下面的语句为MyMethod添加了一个条件断点,它只在参数等于5的时候有效:
(gdb) b -[Class myMethod:] if parameter == 5
最后,在断点上可以附加gdb命令。这样,当断点中断时,附带的命令会自动执行。附加命令使用commands breakpointnumber。这时gdb就会进入断点指令输入状态。
断点指令就是一个以end结尾的标准gdb指令序列。举个例子,我们想在每次NSLog被调用时输出栈信息:
(gdb) b NSLog
Breakpoint 4 at 0x7fff87beaa62
(gdb) commands
Type commands for when breakpoint 4 is hit, one per line.
End with a line saying just "end".
>bt
>end