天天看點

XCode下的GDB指令執行個體

對于大多數Cocoa程 序員來說,最常用的debugger莫過于Xcode自帶的調試工具了。而實際上,它正是gdb的一個圖形化包裝。相對于gdb,圖形化帶來了很多便利, 但同時也缺少了一些重要功能。而且在某些情況下,gdb反而更加友善。是以,學習gdb,了解一下幕後的實質,也是有必要的。

gdb可以通過終端運作,也可以在Xcode的控制台調用指令。本文将通過終端講述一些gdb的基本指令和技巧。

首先,我們來看一個例子:

    #import <Foundation/Foundation.h> 

    int main(int argc, char **argv) 

    { 

        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 

        NSLog(@"Hello, world!"); 

        [pool release]; 

        return 0; 

    }

我們把檔案命名為test.m,然後編譯:

    gcc -g -framework Foundation test.m

準備工作已經完成。現在我們可以開始調試了。隻要把要調試的檔案名作為參數,啟動gdb:

    gdb a.out

gdb啟動後會輸出很多法律聲明之類的資訊。無視它們,最後我們看到一個提示:

    (gdb)

成功!現在debugger和剛才編譯好的程式都被裝載了。不過,現在程式還沒有開始運作。因為gdb在程式開始前把它暫停了,好讓我們有機會設定調試參數。這次我們不需要做特别設定,是以馬上開始運作吧:

    (gdb) run 

    Starting program: /Users/mikeash/shell/a.out 

    Reading symbols for shared libraries .++++....................... done 

    2011-06-16 20:28:53.658 a.out[2946:a0f] Hello, world! 

    Program exited normally. 

    (gdb)

糟糕,程式竟然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 

    9       NSLog("Hello, world! x = %@", x);

這回不僅是函數名,連出錯的那行代碼也列印出來了。但是,我們還可以使用list(簡寫為l)指令,列印出更多資訊:

ps: 如果需要回到棧清單。可以使用down指令。

    (gdb) l 

    5   

    6   int main(int argc, char **argv) 

    7   { 

    8       NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 

    9       int x = 42; 

    10      NSLog("Hello, world! x = %@", x); 

    11      [pool release]; 

    12      

    13      return 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 

    8       NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

debugger在在我們期望的地方停下了。現在我們使用next(簡寫n)指令單步調試程式,看看它到底是在哪一行崩潰的:

    (gdb) n 

    9       int x = 42; 

    (gdb) 

    10      NSLog(@"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 

    <NSAutoreleasePool: 0x10010e820>

這個指令不僅僅可以輸出顯式定義的對象,也可以輸出表達式的結果。這次我們測試一下nsobject中debugDescription的方法簽名:

    (gdb) po [NSObject instanceMethodSignatureForSelector: @selector(debugDescription)] 

    <NSMethodSignature: 0x10010f320> 

        number of arguments = 2 

        frame size = 224 

        is special struct return? NO 

        return value: -------- -------- -------- -------- 

            type encoding (@) '@' 

            flags {isObject} 

            modifiers {} 

            frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0} 

            memory {offset = 0, size = 8} 

        argument 0: -------- -------- -------- -------- 

            type encoding (@) '@' 

            flags {isObject} 

            modifiers {} 

            frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0} 

            memory {offset = 0, size = 8} 

        argument 1: -------- -------- -------- -------- 

            type encoding (:) ':' 

            flags {} 

            modifiers {} 

            frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0} 

            memory {offset = 0, size = 8}

是不是很友善。但是要注意,gdb也許會不能識别NSObject這樣的類名。這時我們就要使用一些小技巧,比如說用NSClassFromString來獲得類名:

    (gdb) po [NSClassFromString(@"NSObject") instanceMethodSignatureForSelector: @selector(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 

    <NSObject: 0x100105950> 

    (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 <OBJC_CLASS_$_NSObject>: 0x00007fff70adb440  0x0000000000000000 

    0x7fff70adb478 <OBJC_CLASS_$_NSObject+16>:  0x0000000100105ac0  0x0000000100104ac0

接下來再看看一個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

這很好了解,隻有一點需要提一下:如果commands指令是作用在剛設定的斷點上的話,那麼就可以省略斷點序号。

有些時候,我們希望調試器輸出一些資訊,但是并不想中斷程式運作。這實際上也可以通過追加指令實作。我們隻需要在指令的最後增加continue指令就行了。在下面的例子裡,我們在斷點中斷後列印棧資訊和參數資訊,随後繼續運作:

    (gdb) b -[Class myMethod:] 

    Breakpoint 5 at 0x7fff864f1404 

    (gdb) commands 

    Type commands for when breakpoint 5 is hit, one per line. 

    End with a line saying just "end". 

    >bt 

    >p parameter 

    >continue 

    >end

最後一個奇特的運用是return指令。它和c中的同名指令一樣,都用來跳出目前函數。如果設定了參數,這參數會作為函數的傳回值。

比如說,我們可以用這個技巧屏蔽掉NSLog函數:

    (gdb) commands 

    Type commands for when breakpoint 6 is hit, one per line. 

    End with a line saying just "end". 

    >return 

    >continue 

    >end

有一點需要提醒:雖然上述的技巧很有用,但同時它會帶來副作用。例如上面屏蔽NSLog的技巧會嚴重拖慢程式的運作速度。因為每次斷點中斷,都會使控制權轉移到debugger一邊,然後運作指令。這些跨程序的操作很耗時間。

有時候也許看不出來,但當執行的斷點變多,或是你在諸如objc_msgSend這樣的方法上添加了條件斷點,那麼也許你的程式會一直運作到天荒地老。

無源碼時的參數

有時我們需要在沒有代碼的地方調試。比如說,我們在用xcode調試時,經常會發現程式在Cocoa架構裡的某個地方崩潰了。我們需要找到到底是在哪裡出錯了。這種時候,一個可行的方法就是檢視崩潰處的參數,看看到底發生了什麼。

這是一篇很好的文章,它講解了在不同的體系結構下,參數是如何存儲的。不過它并沒有講到ARM(= =)。所幸ARM的存儲很簡單,參數隻是按順序被存儲在$r0, $r1, $r2, $r3寄存器裡。記住,在所有通過寄存器傳遞參數的體系結構裡(i386不是),隻有在函數開頭的一小段裡,寄存器裡存的才是參數。因為在程式進行的過程中,它們随時都可能被其他變量替換掉。

舉例來說,我們可以列印出傳給NSLog的參數:

    Breakpoint 2, 0x00007fff87beaa62 in NSLog () 

    (gdb) po $rdi 

    Hello, world!

這裡有個很常見的技巧:如果我們想給NSLog添加斷點來巡查崩潰,就可以根據輸出内容設定一下判斷,讓debugger不至于在每次NSLog時都中斷:

    (gdb) break NSLog if (char)[$rdi hasPrefix: @"crashing"]

記住,方法的前兩個參數是self和_cmd。是以我們的參數應該從$rdx(x86_64)或$rd2(ARM)開始計算。

異常

異常會被運作時方法objc_exception_throw抛出。在這個方法裡設定斷點是很重要的。原因有兩點:

1. 抛出異常,通常是程式出現嚴重錯誤的信号。

2. 被抛出的異常通常會被對應的代碼捕獲。如果你不在這裡設定斷點的話,就隻能獲得異常被捕獲之後的資訊,而不知道它到底是在哪裡被抛出的。

如果你設定了斷點,程式就會在異常被抛出的時候停止。這樣你就有機會檢視棧資訊,知道具體是哪裡抛出了異常。

為異常設定斷點的方法也很簡單,因為要抛出的異常是objc_exception_throw方法的唯一一個參數,是以我們可以用上一小節提到的方法來完成它。

線程

現在,多線程代碼随處可見。知道如何調試多線程程式也越來越重要。以下一段代碼啟動了幾個背景運作的線程:

    dispatch_apply(3, dispatch_get_global_queue(0, 0), ^(size_t x){ 

        sleep(100); 

    });

運作debugger,在程式睡眠的時候用Control-C殺掉它:

    (gdb) run 

    Starting program: /Users/mikeash/shell/a.out 

    Reading symbols for shared libraries .+++........................ done 

    ^C 

    Program received signal SIGINT, Interrupt. 

    0x00007fff88c6ff8a in __semwait_signal () 

    (gdb) bt 

    #0 0x00007fff88c6ff8a in __semwait_signal () 

    #1 0x00007fff88c6fe19 in nanosleep () 

    #2 0x00007fff88cbcdf0 in sleep () 

    #3 0x0000000100000ea7 in __main_block_invoke_1 (.block_descriptor=0x1000010a0, x=0) at test.m:12 

    #4 0x00007fff88cbbbc8 in _dispatch_apply2 () 

    #5 0x00007fff88cb31e5 in dispatch_apply_f () 

    #6 0x0000000100000e6a in main (argc=1, argv=0x7fff5fbff628) at test.m:11

和我們想的一樣,我們輸出了一個線程的資訊。但是,另外兩個背景運作的線程在哪裡?我們可以用info threads指令擷取所有線程的清單:

    (gdb) info threads 

      3 "com.apple.root.default-priorit" 0x00007fff88c6ff8a in __semwait_signal () 

      2 "com.apple.root.default-priorit" 0x00007fff88c6ff8a in __semwait_signal () 

    * 1 "com.apple.root.default-priorit" 0x00007fff88c6ff8a in __semwait_signal ()

線程1前面有個星号,這表示它是現在活動中的線程。現在我們切換到線程2:

    (gdb) thread 2 

    [Switching to thread 2 (process 4794), "com.apple.root.default-priority"] 

    0x00007fff88c6ff8a in __semwait_signal () 

    (gdb) bt 

    #0 0x00007fff88c6ff8a in __semwait_signal () 

    #1 0x00007fff88c6fe19 in nanosleep () 

    #2 0x00007fff88cbcdf0 in sleep () 

    #3 0x0000000100000ea7 in __main_block_invoke_1 (.block_descriptor=0x1000010a0, x=1) at test.m:12 

    #4 0x00007fff88cbbbc8 in _dispatch_apply2 () 

    #5 0x00007fff88c4f7f1 in _dispatch_worker_thread2 () 

    #6 0x00007fff88c4f128 in _pthread_wqthread () 

    #7 0x00007fff88c4efc5 in start_wqthread ()

現在我們輸出了線程2的資訊。然後時線程3……是不是覺得這種方法效率太低了?我們隻有3個線程,但如果有300個呢?幸好,gdb提供了thread apply all backtrace指令(簡寫為t a a bt),用來列出所有線程的詳細資訊。

Thread 3 (process 4794):

#0 0x00007fff88c6ff8a in __semwait_signal ()

#1 0x00007fff88c6fe19 in nanosleep ()

#2 0x00007fff88cbcdf0 in sleep ()

#3 0x0000000100000ea7 in __main_block_invoke_1 (.block_descriptor=0x1000010a0, x=2) at test.m:12

#4 0x00007fff88cbbbc8 in _dispatch_apply2 ()

#5 0x00007fff88c4f7f1 in _dispatch_worker_thread2 ()

#6 0x00007fff88c4f128 in _pthread_wqthread ()

#7 0x00007fff88c4efc5 in start_wqthread ()

Thread 2 (process 4794):

#0 0x00007fff88c6ff8a in __semwait_signal ()

#1 0x00007fff88c6fe19 in nanosleep ()

#2 0x00007fff88cbcdf0 in sleep ()

#3 0x0000000100000ea7 in __main_block_invoke_1 (.block_descriptor=0x1000010a0, x=1) at test.m:12

#4 0x00007fff88cbbbc8 in _dispatch_apply2 ()

#5 0x00007fff88c4f7f1 in _dispatch_worker_thread2 ()

#6 0x00007fff88c4f128 in _pthread_wqthread ()

#7 0x00007fff88c4efc5 in start_wqthread ()

Thread 1 (process 4794):

#0 0x00007fff88c6ff8a in __semwait_signal ()

#1 0x00007fff88c6fe19 in nanosleep ()

#2 0x00007fff88cbcdf0 in sleep ()

#3 0x0000000100000ea7 in __main_block_invoke_1 (.block_descriptor=0x1000010a0, x=0) at test.m:12

#4 0x00007fff88cbbbc8 in _dispatch_apply2 ()

#5 0x00007fff88cb31e5 in dispatch_apply_f ()

#6 0x0000000100000e6a in main (argc=1, argv=0x7fff5fbff628) at test.m:11

現在我們可以友善地檢視整個程式中的線程了。如果想要更徹底地觀察某個線程,隻需要用thread指令切換到該線程,然後使用各種已經學過的gdb指令。

控制台參數和環境變量

在用gdb調試帶參數的程式時會遇到一個疑惑,即程式的參數究竟怎麼輸入:

    $ gdb /bin/echo hello world 

    Excess command line arguments ignored. (world) 

    [...] 

    /Users/mikeash/shell/hello: No such file or directory

如上,把參數直接綴在後面顯然是不對的。因為這樣它們會被解釋成gdb的參數,而不是要調試程式的參數。運作結果也證明了這一點,gdb把hello和world都解釋成了要運作的程式名。

解決方法也很簡單,即,在gdb啟動之後,執行run指令的同時輸入參數:

    (gdb) run hello world 

    Starting program: /bin/echo hello world 

    Reading symbols for shared libraries +. done 

    hello world

環境變量可以在啟動gdb之前預先在shell中載入,通常情況下這麼做也沒有問題。但是,如果你操縱的環境變量會對每個程式都造成嚴重影響的話,這就不是一個好主意了。在這種情況下,我們用set env指令,做針對于目标程式的修改:

    (gdb) set env DYLD_INSERT_LIBRARIES /gdb/crashes/if/this/is/inserted.dylib

繼續閱讀