iOS包含許多“秘密”調試工具,包括環境變量、偏好、GCB的常規調用,等等。本技術說明描述了這些工具。如果你開發iOS,你應該通過這個列表查看你錯過的可以使你更輕松的工具。
簡介
所有蘋果系統包括蘋果工程團隊添加的調試工具都是用來幫助開發和調試特定子系統。這些工具屬于已發布系統軟件,可以使用它們來調試代碼。本技術手冊描述一些廣泛使用的工具。調試工具記錄在另一個地方,有個簡短的工具概述并鏈接到現有文檔。
重要:這不是一個詳盡的列表:并沒有記錄所有的調試工具。
本技術說明中的許多細節在不同的平臺和版本不一樣。因此,你可能遇到平臺間的小差異以及新舊系統的差異。已知的重大變化在文章中列出。
本技術說明是參照iOS4.1寫的。
警告:本技術說明中描述的調試工具是無根據的。蘋果有權依據iOS的演變改變或消除每個工具;這在過去已經發生,并且在未來也有可能發生。這些工具只用于調試:不能發布一個依賴本技術說明中存在或描述工具功能的產品
除了兼容二進制這個技術問題,記住,iOS應用程序必須遵守各種法律協議和應用商店審查指南。
注意:如果你也開發Mac OS X,你可能希望閱讀Technical Note TN2124, 'Mac OS X Debugging Magic'.
本技術說明包括先進的調試技術。如果你是剛剛開始,你應該先閱讀如下材料“
GDB是系統的主要調試工具。查看GDB完整描述,可參閱GDB調試(Debugging with GDB)。
Xcode是平跟的集成開發環境(IDE)。它包括一個復雜的圖形調試器,作為GDB的包裝。關于Xcode的更多信息,可參閱蘋果開發者參考庫中的“語言和工具”章節。
本技術說明不包括性能調試。如果你想調試性能問下,最好從開始使用性能(Getting Started with Performance )文檔開始
基礎知識
本技術說明中后面的部分將詳細描述調試工具。許多工具使用類似的技術來啟用或禁用工具,并看到輸出。本章節描述這些常見的技術。
啟用調試設備
一些調試工具默認是啟用的。然而,大多數工具必須使用下面章節描述的技術來啟用。
環境變量
在許多情況下,你可以通過設置一個特定的環境變量來啟用調試工具。你可以通過Xcode中的可執行檢查器來實現,如圖1所示。
偏好設置
一些調試工具可以通過設置特殊的偏好來啟用。你可以通過配置Xcode中命令行參數來設置調試偏好,如圖2所示。
可調用的程序
許多系統框架包括打印調試信息到stderr
的程序。專門設計這些程序用來調用GDB,或者他們可能是調試時非常有用的API程序。列表1展示了如何調用GDB調試程序的例子,特別的,它調用CFBundleGetAllBundles
獲取應用中加載的所有bundles 的列表,通過調用CFShow
程序來打印列表。
列表1 調用GDB調試程序
<pre><code>
(gdb) call (void)CFShow((void *)CFBundleGetAllBundles())
<CFArray 0x10025c010 [0x7fff701faf20]>{type = mutable-small, count = 59, values = (
0 : CFBundle 0x100234d00 </System/Library/Frameworks/CoreData.framework> …
[…]
12 : CFBundle 0x100237790 </System/Library/Frameworks/Security.framework> …
[…]
23 : CFBundle 0x100194eb0 </System/Library/Frameworks/CoreFoundation.framework> …
[…]
)}
</pre></code>
注意:當調用像這樣的程序,GDB必須知道程序的返回類型(因為一些返回類型可以影響參數傳遞的方式)。如果調用一個沒有調試標識的程序,你必須告訴GDB添加一個返回類型。例如,列表1傳遞
CFBundleGetAllBundles
返回類型為(void \*)
和CFShow
返回類型為(void)
。
類似的,如果你調用一個采用非標準參數的程序,你需要傳遞參數確保GDB能正確的傳遞。
如果你看不到程序的輸出,你可能需要查看控制臺日志,如調試輸出章節所述。
重要:如果在代碼中使用這種技術,它并不總是為靜態程序公正。編譯器程序間的優化坑內會引起一個靜態程序偏離標準函數調用ABI。這種情況下,通過GDB調用不可靠。
在實踐中,這只會影響使用英特爾32位代碼的iPhone模擬器。
配置概要文件
一些iOS子系統可以通過安裝一個特殊的配置概要文件,啟用調試工具。查看推送通知可查看這樣的例子。通常以這種方式安裝配置概要文件:
把它放到一個web服務器上,然后使用設備上的Safari下載
附加到email,寄到設備上的賬戶,然后運行mail并打開配置文件的附近
了解更多關于配置概要文件,可閱讀iPhone商業網址上的各種文檔。
查看調試輸出
生成調試輸出的程序通常使用下列機制:
NSLog
打印到
stderr
系統日志
NSLog是一個高級的API用于日志,在Objective-C代碼中廣泛使用。NSLog確切行為非常復雜,并且隨著時間的推移在不斷的改變,這已超出了本文的范圍。然而,我們可以了解nslog打印到stderr或者記錄到系統日志或者兩者。因此,如果你了解這兩種機制,可以通過NSLog查看任何記錄。
打印到stderr是最常用的輸出機制。鑒于這個主題的重要性,在下一節將深度覆蓋。
查看系統日志最簡單的方法是在Xcode的Organizer 窗口中的控制臺tab。如果用戶不行安裝Xcode,可以使用iPhone配置utility(iPhone Configuration Utility)查看系統日志。
控制臺輸出
許多程序,甚至許多系統框架,打印調試信息到stderr。輸出的目的地最終是程序控制:它可以將stderr重定向到任何選擇的目的地。然而,在大多數情況下,程序一般不重定向stderr,所以輸出到默認的目的地,該目的地繼承自啟動環境程序。通常是下列情況之一:
如果你啟動一個GUI應用程序,同時這個應用程序也可能被普通用戶啟動,系統將任何打印到stderr的信息重定向到系統日志。你可以使用前面描述的技術來查看這些信息。
如果你在Xcode中運行一個程序,你可以在Xcode的調試器控制臺窗口(在運行菜單中選擇控制臺菜單項便可以看到這個菜單)看到stderr輸出.
附加到運行程序(使用Xcode中的附加到程序菜單或者GDB中附加命令)不會自動將程序的stderr連接到你的GDB窗口。你可以使用Technical Note TN2030, 'GDB for MacsBug Veterans'中“附加后查看stdout和stderr”章節中描述的技巧來完成。
一些匯編要求
現在不太可能需要編寫大量的匯編語言代碼,但對這有個基本的了解很有用。尤其是當你在調試,特別是當你調試庫或框架崩潰,并且沒有源代碼。本節討論一些基本的技巧有利于匯編級調試程序。具體的說,它描述了如何在所有支持的架構中設置斷點,訪問參數以及訪問返回地址。
重要的區別是在本技術說明中匯編級別例子來自運行在iPhone 3GS的armv7。然而,在大多數情況下差異并不顯著,例子可能來自不同的架構,甚至來自Mac。很容易改編這樣的例子到其他架構。最顯著的差異是:
訪問參數
獲取返回地址
這些將在以下章節討論。
重要提示:以下特定架構章節包含經驗法則。如果程序有任何非標準參數或非標準函數結果,這些法則并不適用,你應閱讀文檔了解相關細節。
在這種情況下,標準參數是interger(一個寄存器),枚舉和指針(包括數組指針和函數指針)。非標準參數是浮點數,向量,結構,比一個寄存器大的interger以及在程序最后固定參數后的任何參數,該程序參數數量可變。
了解所有iOS設備的調用約定詳情,可查看iOS ABI函數調用指南(iOS ABI Function Call Guide.)。使用英特爾32位架構的iPhone模擬器在Max OS X ABI函數調用指南(Mac OS X ABI Function Call Guide.)。
在你閱讀以下章節前,了解GDB的精妙處是非常重要的。因為GDB本質上是一個源代碼級別的調試器,當你在程序設置一個斷點,GDB不會在程序的第一個指令處設置斷點;相反,它在程序prologue后第一個指令處設置斷點。從源代碼級別調試來看,這樣非常完美。在源代碼級別調試器中,你不會想要單步調試程序prologue。然而,當在匯編級別調試中,非常容易在prologue運行前訪問參數。這是因為程序第一條指令的參數的位置是由調用ABI函數決定的,但運行prologue自行決定攪亂周圍的事情。此外,每個prologue可以以稍微不同的方式在做這件事。所以唯一訪問已執行prologue后參數的方法是反編譯prologue,搞清楚一切。這是典型但不總是很簡單,但它仍然是額外的工作。
告訴GDB在程序的第一個指令處設置斷點最好的辦法是在程序名字上價格前綴“*”。列表2展示了這樣的例子。
列表2 prologue前后
<pre><code>
(gdb) b CFStringCreateWithFormat
Breakpoint 1 at 0x34427ff8
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep n 0x34427ff8 <CFStringCreateWithFormat+8>
-- The breakpoint is not at the first instruction.
-- Disassembling the routine shows that GDB has skipped the prologue.
(gdb) x/5i CFStringCreateWithFormat
0x34427ff0 <CFStringCreateWithFormat>: push {r2, r3}
0x34427ff2 <CFStringCreateWithFormat+2>: push {r7, lr}
0x34427ff4 <CFStringCreateWithFormat+4>: add r7, sp, #0
0x34427ff6 <CFStringCreateWithFormat+6>: sub sp, #4
0x34427ff8 <CFStringCreateWithFormat+8>: add r3, sp, #12
-- So we use a "*" prefix to disable GDB's 'smarts'.
(gdb) b *CFStringCreateWithFormat
Breakpoint 2 at 0x34427ff0
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep n 0x34427ff8 <CFStringCreateWithFormat+8>
2 breakpoint keep n 0x34427ff0 <CFStringCreateWithFormat>
</pre></code>
重要:因為iOS無法從命令行運行程序,像列表2中的iOS特殊列表來自于Xcode控制臺窗口。由于控制臺窗口不支持“#”字符注釋,所以注釋用‘--’來表示。如果你想自己重復這些例子,你不能在控制臺窗口輸入--。
相比之下,在本文檔中其他列表是在Mac OS X上創建的,使用傳統GDB注釋。
最后,如果你正在尋找關于具體說明信息,Shark(包括Xcode開發工具)中的幫助菜單中有一個ARM,英特爾和PowerPC 架構的指令集引用。
ARM
在ARM程序中,前面四個參數放在寄存器中。返回地址在寄存器LR中。表1展示了當你停在函數的第一個指令時,如何從GDB訪問這些值。
表1 ARM的訪問參數 | |
---|---|
What | GDB Syntax |
return address | $lr |
first parameter | $r0 |
second parameter | $r1 |
third parameter | $r2 |
fourth parameter | $r3 |
函數返回的結果在R0 ($r0
)寄存器中。
因為參數傳遞到寄存器中,在prologue之后沒有簡單的方法來訪問參數。
列表3展示了如何使用這些信息訪問GDB參數。
<pre><code>
-- We have to start the program from Xcode. Before we do that, we go to
-- Xcode's Breakpoints window and set a symbolic breakpoint on
-- CFStringCreateWithFormat.
GNU gdb 6.3.50-20050815 […]
-- We've stopped after the prologue.
(gdb) p/a $pc
$1 = 0x34427ff8 <CFStringCreateWithFormat+8>
-- Let's see what the prologue has done to the registers
-- holding our parameters.
(gdb) x/5i $pc-8
0x34427ff0 <CFStringCreateWithFormat>: push {r2, r3}
0x34427ff2 <CFStringCreateWithFormat+2>: push {r7, lr}
0x34427ff4 <CFStringCreateWithFormat+4>: add r7, sp, #0
0x34427ff6 <CFStringCreateWithFormat+6>: sub sp, #4
0x34427ff8 <CFStringCreateWithFormat+8>: add r3, sp, #12
-- Hey, the prologue hasn't modified R0..R3, so we're OK.
--
-- first parameter is "alloc"
(gdb) p/a $r0
$2 = 0x3e801d60 <__kCFAllocatorSystemDefault>
-- second parameter is "formatOptions"
(gdb) p/a $r1
$3 = 0x0
-- third parameter is "format"
(gdb) call (void)CFShow($r2)
managed/%@/%@
-- It turns out the prologue has left LR intact as well.
-- So we can get our return address.
--
-- IMPORTANT: Bit zero of the return address indicates that it lies
-- in a Thumb routine. So when using a return address in LR or on
-- the stack, always mask off bit zero.
(gdb) p/a $lr & 0xfffffffe
$4 = 0x34427e18 <__CFXPreferencesGetManagedSourceForBundleIDAndUser+48>
-- Now clear the breakpoint and set a new one before the prologue.
(gdb) del 1
(gdb) b *CFStringCreateWithFormat
Breakpoint 2 at 0x34427ff0
(gdb) c
Continuing.
Breakpoint 2, 0x34427ff0 in CFStringCreateWithFormat ()
-- We're at the first instruction. The parameters are guaranteed
-- to be in the right registers.
(gdb) p/a $pc
$5 = 0x34427ff0 <CFStringCreateWithFormat>
-- first parameter is "alloc"
(gdb) p/a $r0
$6 = 0x3e801d60 <__kCFAllocatorSystemDefault>
-- second parameter is "formatOptions"
(gdb) p/a $r1
$7 = 0x0
-- third parameter is "format"
(gdb) call (void)CFShow($r2)
%@/%@.plist
-- return address is in LR; again, we mask off bit zero
(gdb) p/a $lr & 0xfffffffe
$8 = 0x34427e7c <__CFXPreferencesGetManagedSourceForBundleIDAndUser+148>
(gdb) b *0x34427e7c
Breakpoint 3 at 0x34427e7c
-- Delete the other breakpoint to avoid thread-based confusion.
(gdb) del 2
-- Continue to the return address.
(gdb) c
Continuing.
Breakpoint 3, 0x34427e7c in __CFXPreferencesGetManagedSourceForBundleIDAndUser ()
-- function result
(gdb) p/a $r0
$9 = 0x1062d0
(gdb) call (void)CFShow($r0)
mobile/.GlobalPreferences.plist
</pre></code>
列表3 ARM參數
英特爾32位
iPhone模擬器使用的是英特爾32位架構。
在英特爾32位程序中,參數傳遞到堆棧中。程序的第一個指令在棧頂,包含返回地址,下個字節包含第一個(最左邊)參數,下個字節包含第二個參數,等等。表2展示了如何訪問GDB的這些值。
表2 英特爾32位的可訪問參數 | |
---|---|
What | GDB Syntax |
return address | (int)$esp |
first parameter | (int)($esp+4) |
second parameter | (int)($esp+8) |
... and so on |
在程序prologue之后,可以訪問幀指針參數(寄存器EBP)。表3展示了語法。
表3 prologue之后可訪問參數 | |
---|---|
What | GDB Syntax |
previous frame | (int)$ebp |
return address | (int)($ebp+4) |
first parameter | (int)($ebp+8) |
second parameter | (int)($ebp+12) |
... and so on |
函數返回的結果在寄存器EAX ($eax
)。
列表4展示了如何使用信息訪問GDB參數。
<pre><code>
$ # Use the -arch i386 argument to GDB to get it to run the
$ # 32-bit Intel binary.
$ gdb -arch i386 /Applications/TextEdit.app
GNU gdb 6.3.50-20050815 (Apple version gdb-1346) […]
(gdb) fb CFStringCreateWithFormat
Breakpoint 1 at 0x31ec6d6
(gdb) r
Starting program: /Applications/TextEdit.app/Contents/MacOS/TextEdit
Reading symbols for shared libraries […]
Breakpoint 1, 0x940e36d6 in CFStringCreateWithFormat ()
(gdb) # We've stopped after the prologue.
(gdb) p/a $pc
$1 = 0x940e36d6 <CFStringCreateWithFormat+6>
(gdb) # However, for 32-bit Intel we don't need to inspect
(gdb) # the prologue because the parameters are on the stack.
(gdb) # We can access them relative to EBP.
(gdb) #
(gdb) # first parameter is "alloc"
(gdb) p/a (int)($ebp+8)
$2 = 0xa0473ee0 <__kCFAllocatorSystemDefault>
(gdb) # second parameter is "formatOptions"
(gdb) p/a (int)($ebp+12)
$3 = 0x0
(gdb) # third parameter is "format"
(gdb) call (void)CFShow((int)($ebp+16))
%@
(gdb) # return address is at EBP+4
(gdb) p/a (int)($ebp+4)
$4 = 0x940f59fb <__CFXPreferencesGetNamedVolatileSourceForBundleID+59>
(gdb) # Now clear the breakpoint and set a new one before the prologue.
(gdb) del 1
(gdb) b *CFStringCreateWithFormat
Breakpoint 2 at 0x940e36d0
(gdb) c
Continuing.
Breakpoint 2, 0x940e36d0 in CFStringCreateWithFormat ()
(gdb) # We're at the first instruction. We must access
(gdb) # the parameters relative to ESP.
(gdb) p/a $pc
$6 = 0x940e36d0 <CFStringCreateWithFormat>
(gdb) # first parameter is "alloc"
(gdb) p/a (int)($esp+4)
$7 = 0xa0473ee0 <__kCFAllocatorSystemDefault>
(gdb) # second parameter is "formatOptions"
(gdb) p/a (int)($esp+8)
$8 = 0x0
(gdb) # third parameter is "format"
(gdb) call (void)CFShow((int)($esp+12))
managed/%@/%@
(gdb) # return address is on the top of the stack
(gdb) p/a (int)$esp
$9 = 0x940f52cc <__CFXPreferencesGetManagedSourceForBundleIDAndUser+76>
(gdb) # Set a breakpoint on the return address.
(gdb) b *0x940f52cc
Breakpoint 3 at 0x940f52cc
(gdb) c
Continuing.
Breakpoint 3, 0x940f52cc in __CFXPreferencesGetManagedSourceForBundleIDAndUser ()
(gdb) # function result
(gdb) p/a $eax
$10 = 0x1079d0
(gdb) call (void)CFShow($eax)
managed/com.apple.TextEdit/kCFPreferencesCurrentUser
</pre></code>
列表4 英特爾32位參數
架構陷阱
下面章節描述在匯編級調試時坑內個遇到的幾個陷阱
額外參數
在查看匯編級參數時,要記住以下幾點:
如果程序是C++成員函數,第一個隱式參數是this。
如果程序是Objective-C方法,有兩個隱式參數(有關詳細信息,請參閱Objective-C)。
如果編譯器可以找到函數(通常是聲明為static的函數)的所有調用者,它可以以非標準方式選擇傳遞參數給函數。很少有高效基于寄存器ABI的架構,但是對于英特爾32位程序是非常常見的。因此,如果你在一個英特爾32位程序的靜態函數上設置斷點,小心這令人費解的行為。
字節順序和單位大小
當檢查GDB內存事,如果使用正確的單位大小,事情會更加順利。表4是GDB支持的單位大小的總結。
表4 GDB單位大小 | |||
---|---|---|---|
Size | C Type | GDB Unit | Mnemonic |
1 byte | char | b | byte |
2 bytes | short | h | half word |
4 bytes | int | w | word |
8 bytes | long or long long | g | giant |
當在低位優先系統(所有iOS設備和模擬器)上調試時這非常重要,如果你使用了錯誤的單位大小,你會得到一個令人困惑的結果。列表5展示了一個例子(來自Max,但在iOS設備上結果是相同的)。CFStringCreateWithCharacters``
的第二個和第三個參數指定Unicode字符數組。每個元素都是一個UniChar,是本地優先格式的16位數字。當在低位優先系統運行時,必須使用正確的單位大小轉儲數組,否則看起來一團糟。
列表5 使用正確的單位大小
<pre><code>
$ gdb
GNU gdb 6.3.50-20050815 (Apple version gdb-1346) […]
(gdb) attach Finder
Attaching to process 4732.
Reading symbols for shared libraries . done
Reading symbols for shared libraries […]
0x00007fff81963e3a in mach_msg_trap ()
(gdb) b *CFStringCreateWithCharacters
Breakpoint 1 at 0x7fff86070520
(gdb) c
Continuing.
Breakpoint 1, 0x00007fff86070520 in CFStringCreateWithCharacters ()
(gdb) # The third parameter is the number of UniChars
(gdb) # in the buffer pointed to by the first parameter.
(gdb) p (int)$rdx
$1 = 18
(gdb) # Dump the buffer as shorts. Everything makes sense.
(gdb) # This is the string "Auto-Save Recovery".
(gdb) x/18xh $rsi
0x10b7df292: 0x0041 0x0075 0x0074 0x006f 0x002d 0x0053 0x0061 0x0076
0x10b7df2a2: 0x0065 0x0020 0x0052 0x0065 0x0063 0x006f 0x0076 0x0065
0x10b7df2b2: 0x0072 0x0079
(gdb) # Now dump the buffer as words. Most confusing.
(gdb) # It looks like "uAotS-va eeRocevyr"!
(gdb) x/9xw $rsi
0x10b7df292: 0x00750041 0x006f0074 0x0053002d 0x00760061
0x10b7df2a2: 0x00200065 0x00650052 0x006f0063 0x00650076
0x10b7df2b2: 0x00790072
(gdb) # Now dump the buffer as bytes. This is a little less
(gdb) # confusing, but you still have to remember that it's big
(gdb) # endian data.
(gdb) x/36xb $rsi
0x10b7df292: 0x41 0x00 0x75 0x00 0x74 0x00 0x6f 0x00
0x10b7df29a: 0x2d 0x00 0x53 0x00 0x61 0x00 0x76 0x00
0x10b7df2a2: 0x65 0x00 0x20 0x00 0x52 0x00 0x65 0x00
0x10b7df2aa: 0x63 0x00 0x6f 0x00 0x76 0x00 0x65 0x00
0x10b7df2b2: 0x72 0x00 0x79 0x00
</pre></code>
控制的崩潰
在某些情況下,以可控的方式控制程序崩潰。一個常見的方法是終止調用。另一個選擇是使用__builtin_trap
內在函數,產生特定指令。列表6展示了如何實現。
列表6 通過__builtin_trap
崩潰
<pre><code>
int main(int argc, char **argv) {
__builtin_trap();
return 1;
}
</pre></code>
注意:
__builtin_trap
內在函數在PowerPC 和ARM上生成一個trap指令,在英特爾生成一個ud2a指令。
如果你在這個調試器上運行程序,必須在調用__builtin_trap
之前停止。否則程序將崩潰并生成崩潰報告。
警告:我們建議你在調試版本使用這個技術,發布版本應該使用
abort``。
需注意:iOS應用程序聲明周期由用戶控制,這意味著iOS應用程序不應該退出。只有當程序崩潰的情況下,你發布的版本需調用abort,調用abort可以防止破壞用戶數據或者讓你診斷問題更容易。
工具
工具是一個動態跟蹤和分析代碼的應用程序。它運行在Mac OS X 上,允許你的目標程序運行在Mac OS X,iOS設備,iPhone模擬器。
而工具主要集中在性能調試,你也可以使用它來調試錯誤。例如,ObjectAlloc 工具可以幫助你追蹤bug。
工具的一個特別好的功能是讓你可以訪問死機的電腦,有關詳情和其他工具功能,請參閱工具用戶指南( Instruments User Guide)。
崩潰報告
崩潰報告是一個非常重要的調試功能,可以記錄程序崩潰的信息。它一直處于啟用狀態,你只需要查看它的輸出。
崩潰報告的詳情在“理解和分析iPhone OS應用程序崩潰報告”(Technical Note TN2151, 'Understanding and Analyzing iPhone OS Application Crash Reports')。
BSD
BSD子系統實現過程、內存、文件和網絡基礎結構,從而對系統上所有應用程序至關重要。BSD實現一些方便的調試工具,這些工具你都可以使用。
內存分配器
默認的內存分配器中包含大量的調試工具,你可以通過環境變量啟用。這些都記錄在指南頁面(manual page)。表5列出了一些更有用的環境變量。
表5 一些有用的內存分配器環境變量 | |
---|---|
變量 | 概要 |
MallocScribble | 先填充到分配的內存0xAA ,釋放內存到0x55 |
MallocGuardEdges | 在大內存分配之前或之后添加保護頁面 |
MallocStackLogging | 為每個內存block記錄回溯以協助內存調試工具;如果block被分配并立即釋放,兩條記錄將從日志中刪除,這有助于減小日志大小。 |
MallocStackLoggingNoCompact | 和MallocStackLogging 一樣,但是保存所有日志記錄 |
如果默認的內存分配器檢查到某些常見的編程問題,將記錄日志。例如,如果你兩次釋放block的內存或者你釋放未分配的內存,free將打印列表7中列出的消息。括號里的數字是進程id
列表7 free打印的公共消息
<pre><code>
DummyPhone(1839) malloc:
*** error for object 0x826600:
double free *** set a breakpoint in malloc_error_break to debug
</pre></code>
你可以在GDB上運行你的程序并在malloc_error_break
設置一個斷點,來調試這類問題。一旦你點擊斷點,你可以使用GDB的 backtrace 命令決定調用者。
最后,你可以使用malloc_zone_check
(來自``)以編程的方式檢查堆的一致性。
標準C++庫
標準C++庫支持大量的調試特性:
- 設置
_GLIBCXX_DEBUG
編譯時變量啟用標準C++庫中的調試模式。
GCC4.2及更高版本不支持,在這種情況下不能使用。
- GCC4.0之前的版本,將環境變量
GLIBCPP_FORCE_NEW
設置為1并在標準C++庫中禁用內存緩存。運行你使用其他內存調試工具調試C++內存分配。
在GCC4.0 及之后版本這是默認的。
動態鏈接(dyld)
動態鏈接(dyld) 支持大量的調試工具,你可以通過環境變量啟用。這些都記錄在指南頁面(manual page)。表6列出了一些更有用的環境變量。
表6 動態鏈接環境變量 | |
---|---|
變量 | 概述 |
DYLD_IMAGE_SUFFIX | 先用后綴搜索庫 |
DYLD_PRINT_LIBRARIES | 加載日志庫 |
DYLD_PRINT_LIBRARIES_POST_LAUNCH | 同上,但只在main運行后 |
DYLD_PRINT_OPTS [1] | 打印啟動命令參數 |
DYLD_PRINT_ENV [1] | 打印啟動環境變量 |
DYLD_PRINT_APIS [1] | 記錄dyld API調用(例如,dlopen) |
DYLD_PRINT_BINDINGS [1] | 記錄符號綁定 |
DYLD_PRINT_INITIALIZERS [1] | 記錄圖像初始化打印 |
DYLD_PRINT_SEGMENTS [1] | 記錄分段映射 |
DYLD_PRINT_STATISTICS [1] | 打印開始性能統計數據 |
注意:只支持Mac OS X 10.4及之后版本。不過,支持所有版本的iOS。
雖然這些環境變量在iOS上實現,因為受限于系統環境,其中只有有限的用處。
核心服務
核心基礎
核心基礎(CF)框架導出CFShow程序,該程序輸出任何CF對象的描述到stderr。你可以在自己的代碼中調用,然而,從GDB中調用時特別有用。列表8中展示了這樣的一個例子。
列表8 從GDB調用CFShow
<pre><code>
$ gdb /Applications/TextEdit.app
GNU gdb 6.3.50-20050815 (Apple version gdb-1346) […]
(gdb) fb CFRunLoopAddSource
Breakpoint 1 at 0x624dd2f195cfa8
(gdb) r
Starting program: /Applications/TextEdit.app/Contents/MacOS/TextEdit
Reading symbols for shared libraries […]
Breakpoint 1, 0x00007fff8609bfa8 in CFRunLoopAddSource ()
(gdb) # Check that the prologue hasn't changed $rdi.
(gdb) p/a 0x00007fff8609bfa8
$1 = 0x7fff8609bfa8 <CFRunLoopAddSource+24>
(gdb) p/a $pc
$2 = 0x7fff8609bfa8 <CFRunLoopAddSource+24>
(gdb) x/8i $pc-24
0x7fff8609bf90 <CFRunLoopAddSource>: push %rbp
0x7fff8609bf91 <CFRunLoopAddSource+1>: mov %rsp,%rbp
0x7fff8609bf94 <CFRunLoopAddSource+4>: mov %rbx,-0x20(%rbp)
0x7fff8609bf98 <CFRunLoopAddSource+8>: mov %r12,-0x18(%rbp)
0x7fff8609bf9c <CFRunLoopAddSource+12>: mov %r13,-0x10(%rbp)
0x7fff8609bfa0 <CFRunLoopAddSource+16>: mov %r14,-0x8(%rbp)
0x7fff8609bfa4 <CFRunLoopAddSource+20>: sub $0x40,%rsp
0x7fff8609bfa8 <CFRunLoopAddSource+24>: mov %rdi,%r12
(gdb) # Nope. Go ahead and CFShow it.
(gdb) call (void)CFShow($rdi)
<CFRunLoop 0x100115540 [0x7fff70b8bf20]>{
locked = false,
wakeup port = 0x1e07,
stopped = false,
current mode = (none),
common modes = <CFBasicHash 0x1001155a0 [0x7fff70b8bf20]>{
type = mutable set,
count = 1,
entries =>
2 : <CFString 0x7fff70b693d0 [0x7fff70b8bf20]>{
contents = "kCFRunLoopDefaultMode"
}
},
common mode items = (null),
modes = <CFBasicHash 0x1001155d0 [0x7fff70b8bf20]>{
type = mutable set,
count = 1,
entries =>
0 : <CFRunLoopMode 0x100115670 [0x7fff70b8bf20]>{
name = kCFRunLoopDefaultMode,
locked = false,
port set = 0x1f03,
sources = (null),
observers = (null),
timers = (null)
},
}
}
</pre></code>
重要:如果你沒有看到CFShow輸出任何東西,很可能是發送到控制臺。如何查看輸出,可參閱“查看調試輸出信息”
注意:在列表8中,CFShow的輸出已經格式化,更容易閱讀。
從GCB調用有很多其他CF程序,你會發現非常有用,包括CFGetRetainCount
, CFBundleGetMainBundle
,和 CFRunLoopGetCurrent
。
zombie
重要:如果你用Objective-C編程,你可能對
NSZombieEnabled
更感興趣,如更多zombie所述。
核心基礎支持叫做CFZombieLevel
的環境變量。該變量為包含一組標志位的正式。表7描述了當前定義的bit。可以幫助你追蹤各種CF內存管理問題。
表7 CFZombieLevel 環境變量的bit定義 | |
---|---|
Bit | Action |
0 | 釋放CF內存 |
1 | 當釋放CF內存,不要亂想對象頭(CFRuntimeBase) |
4 | 沒有空閑內存用于保存CF對象 |
7 | 如果設置,使用bit8..15釋放內存,否則使用0xFC |
8..15 | 如果設置bit 7,使用這個值來釋放內存 |
16 | 分配CF內存 |
23 | 如果設置,使用bit 24..31分配內存,否則使用0xCF |
24..31 | 如果設置為23bit,使用這個值分配內存 |
應用程序服務
核心動畫
核心動畫工具可以衡量應用程序的幀速率,查看各種類型的繪畫。詳情查看工具用戶指南(Instruments User Guide)。
Cocoa and Cocoa Touch
所有Cocoa對象(一切均源自NSObject)支持description
方法,該方法返回一個NSString
描述對象。訪問這個描述最方便的方法是通過Xcode打印描述到控制臺。或者,如果你是個命令行迷,你可以使用GDB的print-object
命令,如列表9所示。
列表9 使用GDB的命令
<pre><code>
$ gdb /Applications/TextEdit.app
GNU gdb 6.3.50-20050815 (Apple version gdb-1346) […]
(gdb) fb -[NSCFDictionary copyWithZone:]
Breakpoint 1 at 0x83126e97675259
(gdb) r
Starting program: /Applications/TextEdit.app/Contents/MacOS/TextEdit
Reading symbols for shared libraries […]
Breakpoint 1, 0x00007fff837aa259 in -[NSCFDictionary copyWithZone:] ()
(gdb) po $rdi
{
AddExtensionToNewPlainTextFiles = 1;
AutosaveDelay = 30;
CheckGrammarWithSpelling = 0;
CheckSpellingWhileTyping = 1;
[…]
}
</pre></code>
Objective-C
在一個Objective-C異常處中斷,不管它如何拋出異常,在objc_exception_throw
設置一個象征性的斷點。最簡單的方法是在Xcode中設置斷點,停止在Objective-C異常菜單命令。
注意:比在
-[NSException raise]``處
設置斷點更好,因為即使使用@throw
拋出異常,斷點仍會觸發。
Objective-C匯編級程序調試
在匯編級別調試Cocoa代碼時,請記住Objective-C運行時如下特性:
Objective-C編譯器為每個方法增加了兩個隱式參數,第一個參數是一個指向調用對象(self)。
第二個隱式參數是方法選擇器(_cmd)。在Objective-C中這是SEL類型,在GDB中你可以打印C字符串一樣打印它。
Objective-C運行時調度方法時通過C函數的一種。最常見的是
objc_msgSend
,但有一些架構使用objc_msgSend_stret
方法返回結構,另一些架構使用objc_msgSend_fpret
方法返回浮點值。調用super(objc_msgSendSuper
等等)都是等效函數。Objective-C對象的第一個單詞(isa字段)是一個指向對象類的指針。
注意:如果你對 Objective-C消息調度有興趣,可以查看這篇文章。
表8 總結了如何從GDB訪問self和_cmd,如果你停在方法的第一個指令。詳情,可閱讀‘Some Assembly Required’。
表8 訪問self和_cmd | ||
---|---|---|
Architecture | self | _cmd |
ARM | $r0 | $r1 |
Intel 32-bit | (int)($esp+4) | (int)($esp+8) |
列表10 展示了如何從使用GDB信息的例子
重要:列表10和列表11都是在Mac OS X上創建,但核心技術在iOS上同樣有用。有關如何轉化這些技術到iOS,可以閱讀‘Some Assembly Required for information ’。
列表10 Objective-C運行時‘秘密’
<pre><code>
$ gdb /Applications/TextEdit.app
GNU gdb 6.3.50-20050815 (Apple version gdb-1346) […]
(gdb) # Give the runtime a chance to start up.
(gdb) fb NSApplicationMain
Breakpoint 1 at 0x9374bc69df7307
(gdb) r
Starting program: /Applications/TextEdit.app/Contents/MacOS/TextEdit
Reading symbols for shared libraries […]
Breakpoint 1, 0x00007fff841a0307 in NSApplicationMain ()
(gdb) # Set a breakpoint on -retain.
(gdb) b *'-[NSObject(NSObject) retain]'
Breakpoint 2 at 0x7fff8608a860
(gdb) c
Continuing.
Breakpoint 2, 0x00007fff8608a860 in -[NSObject(NSObject) retain] ()
(gdb) # Hit the breakpoint; dump the first 4 words of the object
(gdb) x/4xg $rdi
0x1001138f0: 0x00007fff7055e6d8 0x0041002f01a00000
0x100113900: 0x0069006c00700070 0x0069007400610063
(gdb) # Now print the selector
(gdb) x/s $rsi
0x7fff848d73d8: "retain"
(gdb) # Want to 'po' object; must disable the breakpoint first
(gdb) dis
(gdb) po $rdi
/Applications/TextEdit.app
(gdb) # Print the 'isa' pointer, which is a Class object.
(gdb) po 0xa02d9740
NSPathStore2
</pre></code>
無標識調試時,你可以使用Objective-C運行時函數來輔助你的調試工作,表9中展示的程序特別有用
表9 有用的Objective-C運行時函數 | |
---|---|
Function | Summary |
id objc_getClass(const char *name); | 獲取給定類名的Objective-C類對象 |
SEL sel_getUid(const char *str); | 獲取給定方法名的Objective-C SEL |
IMP class_getMethodImplementation(Class cls, SEL name); | 獲取指向給定類給定方法的實現代碼的指針 |
列表11 展示了TextEdit的-[DocumentController openUntitledDocumentAndDisplay:error:]
方法的調試,盡管TextEdit沒有標識。
列表11 使用Objective-C運行時來無標識調試
<pre><code>
$ gdb -arch x86_64 /Applications/TextEdit.app
GNU gdb 6.3.50-20050815 (Apple version gdb-1346) […]
(gdb) r
Starting program: /Applications/TextEdit.app/Contents/MacOS/TextEdit
Reading symbols for shared libraries […]
^C
(gdb) # Try to find the
(gdb) # -[DocumentController openUntitledDocumentAndDisplay:error:]
(gdb) # symbol.
(gdb) info func openUntitledDocumentAndDisplay
All functions matching regular expression "openUntitledDocumentAndDisplay":
Non-debugging symbols:
0x00007fff843ac083 -[NSDocumentController openUntitledDocumentAndDisplay:error:]
(gdb) # These are not the droids we're looking for. It turns out that
(gdb) # TextEdit ships with its symbols stripped, so we'll have to do
(gdb) # this the hard way.
(gdb) #
(gdb) # Get the Class object for the DocumentController class.
(gdb) set $class=(void *)objc_getClass("DocumentController")
(gdb) # Get the SEL object for the "openUntitledDocumentAndDisplay:error:" method.
(gdb) set $sel=(void *)sel_getUid("openUntitledDocumentAndDisplay:error:")
(gdb) # Get a pointer to the method implementation.
(gdb) call (void*)class_getMethodImplementation($class, $sel)
$1 = (void *) 0x100001966
(gdb) # Confirm that this is sensible. Looks like a method prologue to me.
(gdb) x/4i 0x00009aa5
0x100001966: push %rbp
0x100001967: mov %rsp,%rbp
0x10000196a: push %r12
0x10000196c: push %rbx
(gdb) # Set a breakpoint on the method.
(gdb) b *0x100001966
Breakpoint 1 at 0x100001966
(gdb) # Resume execution, and then create a new, untitled document.
(gdb) c
Continuing.
[…]
Breakpoint 1, 0x0000000100001966 in ?? ()
(gdb) # We've hit our breakpoint; print the parameters, starting with
(gdb) # the implicit "self" and "SEL" parameters that are common to all
(gdb) # methods, followed by the method-specific "display" and
(gdb) # "error" parameters.
(gdb) po $rdi
<DocumentController: 0x100227a50>
(gdb) x/s $rsi
0x7fff848e4e04: "openUntitledDocumentAndDisplay:error:"
(gdb) p (int)$rdx
$2 = 1
(gdb) x/xg $rcx
0x7fff5fbff108: 0x00000001001238f0
</pre></code>
你可以通過/usr/include/objc/
的頭,了解更多關于Objective-C 運行時函數與數據結構
警告:Objective-C 運行時允許你發現更多系統類的私有實現細節。你在最終的產品中不能使用任何有關信息。
Foundation
Foundation有大量的調試工具,這些工具可以通過環境變量啟用。表10中突出顯示了這些變量。
表10 Foundation 環境變量 | ||
---|---|---|
Name | Default | Action |
NSZombieEnabled | NO | 如果設置為YES,已釋放對象是'zombified' ,在你發送消息到已釋放的對象時可以快速的調試問題。詳情查閱更多Zombies |
NSDeallocateZombies | NO | 如果設置為YES,已'zombified' 的對象的內存實際上是釋放掉的 |
NSUnbufferedIO | NO | 如果設置為YES,Foundation 將為stdout(stderr默認無緩沖)使用無緩沖 I/O |
重要:啟用或禁用基礎調試工具,你必須設置環境變量值為“是”或“否”,而非1或0,其他系統組件也一樣。
Retain 計數
你可以使用retain計數來獲取當前對象的retain計數。雖然有時候這種方法是有用的調試助手,當你解釋結果時要非常小心。列表12展示了一個混亂的潛在來源。
列表12 混亂的retain計數
<pre><code>
(gdb) set $s=(void *)[NSClassFromString(@"NSString") string]
(gdb) p (int)[$s retainCount]
$4 = 2147483647
(gdb) p/x 2147483647
$5 = 0x7fffffff
(gdb) # The system maintains a set of singleton strings for commonly
(gdb) # used values, like the empty string. The retain count for these
(gdb) # strings is a special value indicating that the object can't be
(gdb) # released.
</pre></code>
另一個常見混淆的來源時自動釋放機制。如果一個對象唄自動釋放,它的retain數可能比你想象的要高。在未來的某個時間點自動釋放池將釋放它。
你可以決定哪些對象在哪個自動釋放池中,通過調用 _CFAutoreleasePoolPrintPools
可以打印所有在指定釋放池堆棧上的自動釋放池的內容。
列表13 打印自動釋放池堆棧
<pre><code>
(gdb) call (void)_CFAutoreleasePoolPrintPools()
- -- ---- -------- Autorelease Pools -------- ---- -- -
==== top of stack ================
0x327890 (NSCFDictionary)
0x32cf30 (NSCFNumber)
[…]
==== top of pool, 10 objects ================
0x306160 (__NSArray0)
0x127020 (NSEvent)
0x127f60 (NSEvent)
==== top of pool, 3 objects ================
</pre></code>
注意:
_CFAutoreleasePoolPrintPools
只適用于iOS4.0及更高版本。
在早期系統,你可以使用函數來確定一個對象唄條件到自動釋放池的次數。例如,見列表14.
列表14 調用NSAutoreleasePoolCountForObject
<pre><code>
(gdb) # d is an NSDictionary created with -[NSDictionary dictionaryWithObjectsAndKeys:].
(gdb) p d
$1 = (NSDictionary *) 0x12d620
(gdb) po d
{
test = 12345;
}
(gdb) p (int)[d retainCount]
$2 = 1
(gdb) p (int)NSAutoreleasePoolCountForObject(d)
$3 = 1
</pre></code>
更多的zombie!
用Cocoa編程時常見的bug類型是過度釋放對象。這通常會導致應用程序崩潰,但當崩潰發生時最后一個引用計數會釋放(當你試圖給已釋放對象發消息),通常會從原bug移除。對于調試這樣的問題,NSZombieEnabled
是最好的選擇。它將展示任何試圖與釋放對象交互。
啟用zombie最好的方法是通過工具。然而,你可以可以通過一個環境變量來啟用zombie。列表15展示了在這種情況下你會看到的消息類型。
列表15 NSZombie的效果
<pre><code>
$ NSZombieEnabled=YES build/Debug/DummyMac.app/Contents/MacOS/DummyMac
[…] -[AppDelegate testAction:]
[…] *** -[CFNumber release]:
message sent to deallocated instance 0x3737c0 Trace/BPT trap
</pre></code>
如列表15所示,如果系統檢測到zombie,系統將執行一個斷點指令。如果你在GDB下運行,程序將停止,你可以查看backtrace 來找出觸發zombie檢查器的調用鏈。
NSZombieEnabled
也影響核心基礎對象以及Objective-C對象。然而,只有當你從Objective-C訪問一個zombie核心基礎對象時你會被通知,而不是當你通過一個CF API來訪問它。
其他Foundation
如果你使用鍵值觀察并且你想知道誰在觀察一個特定的對象,你可以使用GDB打印對象命令來獲取對象的觀察信息并將它打印。列表16給出了一個這樣的例子。
列表16 顯示鍵值觀察者
<pre><code>
(gdb) # self is some Objective-C object.
(gdb) po self
<ZoneInfoManager: 0x48340d0>
(gdb) # Let's see who's observing what key paths.
(gdb) po [self observationInfo]
<NSKeyValueObservationInfo 0x48702d0> (
<NSKeyValueObservance 0x4825490: Observer: 0x48436e0, \
Key path: zones, Options: <New: NO, Old: NO, Prior: NO> \
Context: 0x0, Property: 0x483a820>
)
</pre></code>
你可以設置NSShowNonLocalizedStrings
的偏好來找到該被本地化但沒有的字符串。一旦啟用,如果你請求一個本地字符串同時在字符串文件中沒找到該字符串,系統將返回大寫的字符串和日志消息到控制臺。這是一個很好的方法來發現過期本地化問題。
這個設置會影響NSLocalizedString
、所有變形以及包括CFCopyLocalizedString
的底層基礎結構。
UIKit
UIView實現了一個很有用的description
方法。此外,它還實現了recursiveDescription
方法,你可以調用這個方法獲取整個視圖層次。
列表17 UIView的description 和recursiveDescription 方法
<pre><code>
-- The following assumes that you're stopped in some view controller method.
(gdb) po [self view]
<UIView: 0x6a107c0; frame = (0 20; 320 460); autoresize = W+H; layer = […]
Current language: auto; currently objective-c
(gdb) po [[self view] recursiveDescription]
<UIView: 0x6a107c0; frame = (0 20; 320 460); autoresize = W+H; layer = […]
| <UIRoundedRectButton: 0x6a103e0; frame = (124 196; 72 37); opaque = NO; […]
| | <UIButtonLabel: 0x6a117b0; frame = (19 8; 34 21); text = 'Test'; […]
</pre></code>
網絡
調試網絡代碼最重要的工具是packet trace。“獲取到一個packet trace”( Technical Q&A QA1176, 'Getting a Packet Trace' )中討論了如何在 Mac OS X上獲取一個packet trace。
盡管iOS沒有內置packet trace工具,通常你可以通過Mac來獲取packet trace。
電源
能源診斷工具是理解你的應用程序如何影響電池壽命的好方法。詳情查看工具用戶指南(Instruments User Guide )。
推送通知
“推送通知故障排除”(Technical Note TN2265, 'Troubleshooting Push Notifications' )是調試推送通知問題的全面指南。它還包括一個配置概要文件(APNsLogging.mobileconfig
),你可以安裝這個文件啟用更多的調試日志。
iPhone模擬器
iPhone模擬器讓你可以在Mac OS X上構建和運行iOS代碼。因為iPhone模擬器是一個Mac OS X應用程序,你可以使用各種各樣的Mac OS X調試工具。例如,如果你有個問題只能在DTrace (它在iOS上不可用)上解決,你可以在模擬器運行你的程序,并將DTrace 指向它。
重要:模擬器很是和調試,但最終還是真正的設備決定是否在iOS可用。在做性能測試與調試的時候,一定要牢記這一點。
關于Mac OS X 調試工具的更多信息,參見“Mac OS X 調試”(Technical Note TN2124, 'Mac OS X Debugging Magic')。
文件修訂歷史
Date | Notes |
---|---|
2011-01-22 | 描述大量的iOS調試提示與技巧的新文檔 |
官方原文地址:
https://developer.apple.com/library/ios/technotes/tn2239/_index.html