此文下半部分爲轉載:在此感謝原創者。html
---------------------shell
關於調試異常崩潰:小程序
通常崩潰是由內存使用錯誤致使的,要麼多了,要麼少了。xcode
用xcode的調試提示能夠知道是什麼緣由致使的崩潰。app
在xcode中product àedit scheme à diagnostics 將enable Zombie objects 和 Malloc Stack 選中, 若是是內存釋放錯誤,則gdb會提示release dealloc object。框架
而後能夠用斷點縮小錯誤範圍, 在可能出現錯誤的地方用單步調試, 當執行到有錯誤代碼時, gdb會再次提示 release dealloc object。編輯器
其實XCODE內嵌GDB,那個 lldb就是gdb!函數
-----------------------工具
http://blog.csdn.net/ch_soft/article/details/7005998學習
關於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
2)其餘
XCode 內置GDB,咱們能夠在命令行中使用 GDB 命令來調試咱們的程序。下面將介紹一些經常使用的命令以及調試技巧。
po 命令:爲 print object 的縮寫,顯示對象的文本描述(顯示從對象的 description 消息得到的字符串信息)。
好比:
上圖中,我使用 po 命令顯示一個 NSDictionary 的內容。注意在左側咱們能夠看到 dict 的一些信息:3 key/value pairs,顯示該 dict 包含的數據量,而展開的信息顯示 isa 層次體系(即class 和 metaclass結構關係)。咱們能夠右擊左側的 dict,選中「Print Description of "dict"」,則能夠在控制檯輸出 dict 的詳細信息:
[cpp] view plain copy
print 命令:有點相似於格式化輸出,能夠輸出對象的不一樣信息:
如:
[cpp] view plain copy
注:4是 NSUTF8StringEncoding 的值。
info 命令:咱們能夠查看內存地址所在信息
好比 "info symbol 內存地址" 能夠獲取內存地址所在的 symbol 相關信息:
[cpp] view plain copy
好比 "info line *內存地址" 能夠獲取內存地址所在的代碼行相關信息:
[cpp] view plain copy
show 命令:顯示 GDB 相關的信息。如:show version 顯示GDB版本信息
[cpp] view plain copy
help 命令:若是忘記某條命令的語法了,可使用 help 命令名 來獲取幫助信息。如:help info 顯示 info 命令的用法。
[cpp] view plain copy
在系統拋出異常處設置斷點
有時候咱們的程序不知道跑到哪一個地方就 crash 了,而 crash 又很難重現。保守的作法是在系統拋出異常以前設置斷點,具體來講是在 objc_exception_throw處設置斷點。設置步驟爲:首先在 XCode 按 CMD + 6,進入斷點管理窗口;而後點擊右下方的 +,增長新的 Symbolic Breakpoint,在 Symbol 一欄輸入:objc_exception_throw,而後點擊 done,完成。 這樣在 Debug 模式下,若是程序即將拋出異常,就能在拋出異常處中斷了。好比在前面的代碼中,我讓 [firstObjctcrashTest]; 拋出異常。在 objc_exception_throw 處設置斷點以後,程序就能在該代碼處中斷了,咱們從而知道代碼在什麼地方出問題了。