LLDB:lower level debugge/底層調試器。
本節要介紹的所有的内容幾乎都是針對LLDB的,因為蘋果已将GDB替換成LLDB。Xcode4.0開始Xcode4.2,他們預設的編譯器都是LLVM3.0,使用Clang作為編譯器前端,取代了GCC作為編譯器前端會有很多優勢;到了Xcode4.5(同iOS6同時釋出)預設的編譯器就是LLVM4.0。LLVM搭配Clang,可以提供更快更好地編譯過程,更好地支援代碼補全。
1. LLDB
LLDB是用LLVM中可以重用元件建構的額下一代高性能調試器,包括完整的LLVM編譯器,其中就有LLVM的Clang表達式解析器和反彙程式設計式。對于開發者而言,這意味着LLDB能了解你的編譯器所能了解的文法,包括OC字面量,OC屬性的點标記法。
前一版調試器GDB性能并不好,像po self.view.frame 他是不能了解的,你需要輸入po [[self view] frame]。當替換編譯器時,APPLE就需要改進調試器,由于GDB是一個整體,是以沒辦法解決。開發者需要重新編寫一個調試器。LLDB是子產品化的,而為調試器提供API支援和腳本程式設計接口是設計目标之一。 LLDB指令行調試器會通過這個API連結到LLDB庫。
實用LLDB指令
指令名 用法 說明
1.1 dSYM檔案
Xcode的調試資訊檔案稱為 dSYM檔案(因為擴充名叫為.dSYM),又叫調試資訊檔案,它存儲着與目标相關的調試資訊。
他會在每次建構工程時自動建立:
用任何一種程式設計語言編寫的代碼都需要一個編譯器,将這些代碼翻譯成可以被IDE了解的某種中間語言,或者是可在機器的體系結構上直接運作的原生機器碼。調試器通常會內建在開發環境中。Xcode支援放置斷點使應用停止運作,進而檢視代碼中變量的值。也就是說,調試器能夠實時的使應用停止運作,這樣就可以檢視變量和寄存器。
有兩類重要的調試器:
a. 符号調試器 :能夠在調試代碼時顯示應用中使用的符号或變量。跟機器語言不同,符号調試器容許你觀察代碼中得符号,而不是寄存器和位址
b. 機器語言調試器:能夠在運作到斷電時顯示你想過來的彙編代碼,容許你觀察寄存器的值和記憶體位址。
讓符号調試器工作起來,需要一個編譯過的代碼和你編寫的源代碼之前的連結或映射。這正是調試資訊檔案中所包含的内容。
調試器使用這個調試資訊檔案将編譯過的代碼---不管是中間代碼還是機器碼----映射回源代碼。可以将調試資訊檔案當做遊客浏覽陌生城市時參考的地圖。調試器能參考調試資訊檔案,根據你再源代碼中放置的斷電讓應用停在正确的位置。
在XCODE編譯項目之後,會在app旁看見一個同名的dSYM檔案。他是一個編譯的中轉檔案,簡單說就是debug的symbols包含在這個檔案中。
他有什麼作用? 當release的版本 crash的時候,會有一個日志檔案,包含出錯的記憶體位址, 使用symbolicatecrash工具能夠把日志和dSYM檔案轉換成可以閱讀的log資訊,也就是将記憶體位址,轉換成程式裡的函數或變量和所屬于的檔案
1.2 符号化
包括LLVM在内的編譯器都是用來将源代碼轉換成彙編代碼的。所有彙編代碼都有一個基位址,。你定義的變量,用到的棧和堆都會依賴這個基位址。每次運作應用時,這個基位址都會改變,尤其是在iOS4.3及以上版本的作業系統中,這些作業系統都采用了位址空間布局随機化的機制。符号化使用方法名和變量名來替換基位址的過程。基位址是應用的入口位址,通常就是main方法,除非你是在寫一個靜态庫。可以符号化其他符号,方法是計算他們相對基位址的偏移,然後将他們映射到dSYM檔案中。符号化過程再用Xcode調試應用時才會進行,或者在用Instruments做性能計數分析時進行。
2. 斷點
添加到工程中的斷點會自動在斷點導航面闆中列出。可以使用快捷鍵組合Cmd+6來通路斷點導航面闆。斷點導航面闆還支援為異常和符号設定斷點。
2.1. 異常斷點
在代碼有問題導緻抛出異常時,異常斷點會停止程式的執行。Foundation.framework的NSArray、NSDictionary或UIKit類(比如UITableView方法)中的一些方法會在不能滿足特定條件的情況下抛出異常。這些場景包括嘗試改變NSArray或是嘗試通路越界的數組元素。UITableView會在将行數聲明為“n”而沒有給每行都提供一個單元格時抛出異常。調試異常在理論上比較容易,但了解造成異常的源相當複雜。應用在崩潰時可能隻會在日志中顯示造成崩潰的那條異常。這些Foundation.framework方法會在整個工程中都用到,不設定異常斷點,即使看了日志也不知道究竟發生什麼了。設定了異常斷點後,調試器會在異常抛出的瞬間暫停程式的執行,但在捕獲異常之前,你需要在斷點導航面闆中檢視崩潰了的那個線程的棧軌迹。
為了友善了解,我們比較一下使用和不使用異常斷點調試應用的不同。
在Xcode中建立一個空應用(任何模闆都能工作)。在應用委托中,添加以下行:
NSLog(@"%@", [@[] objectAtIndex:100]); |
它會建立一個空數組,然後通路第一百個元素,并記錄它。由于這種用法并不符合規範,執行該程式時它會崩潰,控制台會有如下輸出,Xcode會跳轉到main.m:
2012-08-27 15:25:23.040 Test[31224:c07] (null) libc++abi.dylib: terminate called throwing an exception (lldb) 現在: 2013-07-24 09:39:08.776 testDemo[961:c07] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 100 beyond bounds for empty array' *** First throw call stack: (0x1c90012 0x10cde7e 0x1c45b44 0x1ec3 0xf157 0xf747 0x1094b 0x21cb5 0x22beb 0x14698 0x1bebdf9 0x1bebad0 0x1c05bf5 0x1c05962 0x1c36bb6 0x1c35f44 0x1c35e1b 0x1017a 0x11ffc 0x1bd2 0x1b05) libc++abi.dylib: terminate called throwing an exception |
但看看這難懂的日志消息,沒人曉得背後發生了什麼。要調試這樣的異常,需要設定一個異常斷點。
可以在斷點導航面闆中設定一個異常斷點。打開斷點導航面闆,點選左下角的+按鈕,選擇Add Exception Breakpoint,接受預設設定,新加一個斷點,如圖19-2所示。
Exception:可選 all 所有語言引起的異常,objective-c語言和c++語言引起的異常。
Break:可選onThrow和onCatch。
Action:可在程式斷點執行後增加額外動作(Applescript,捕捉動畫幀速,調試器指令(lldb),輸入log記錄,終端指令(shell),播放聲音)
例如:Debugger Commond中可填入
po item 輸出 item變量的值
bt 表示輸出 方法調用堆棧資訊
圖19-2 增加一個異常斷點
再次運作該工程。你應該能看到調試器暫停了應用的執行,程式正好停在抛出異常的那行,如圖19-3所示。
圖19-3 Xcode在設定斷點的位置停止執行應用
異常斷點能幫你了解異常的起因。我在建立工程時,要做的第一件事就是設定一個異常斷點。我強烈推薦這麼做。
如果想快速運作應用而不想在任何斷點處停留,那麼可以在鍵盤上用快捷鍵Cmd+Y來禁用所有斷點。
2. 符号斷點
符号斷點會在執行到特定符号時暫停程式。符号可以是一個方法名、類中的一個方法或者任何C方法(objc_msgSend)。
可以在斷點導航面闆中設定符号斷點,跟設定異常斷點差不多,不過要選擇符号斷點而不是異常斷點。現在,對話框中輸入了你關注的符号,如圖19-4所示。
圖 增加一個符号斷點
Symbol:填入你想檢測消息發送實體的方法
(例如:-[NSException raise],-号是執行個體方法,+号是類方法)。
你也可以輸入:
objc_exception_throw
malloc_error_break //跟蹤調試釋放了2次的對象
-[NSObject doesNotRecognizeSelector:] //向某個object發送沒有的方法
Module:填入是否在一個dylib中,預設不用填。
Conditon:填入條件,例如:
(BOOL)[item isEqualToString:@"test"]
前面的(BOOL)是必須的。否則console會提示類型不符号,導緻條件不能生效。
意思是item(NSString)是test時停下。
同樣可以寫一下判斷的方法比如用來确定類類型的isKindOfClass:,确定對象在繼承體系中的位置的isMemberOfClass:,判斷一個對象是否能接收某個特定消息的respondsToSelector:,判斷一個對象是否遵循某個協定的conformsToProtocol:,以及提供方法實作位址的methodForSelector:。
Ignore:忽略幾次。
Action:同上表示在執行後附加動作。
現在鍵入application:didFinishLaunchingWithOptions:,然後按下Enter鍵。建構并運作應用。你應該看到調試器會在程式剛開始運作時就停止執行應用,并顯示棧軌迹。
你檢視的符号除了在application:didFinishLaunchingWithOptions:中放置了一個斷點,再沒有其他好處。符号斷點通常用來觀察你要關注的方法,比如:
-[NSException raise] malloc_error_break -[NSObject doesNotRecognizeSelector:] |
事實上,前一節建立的第一個異常斷點與指向[NSException raise]的符号斷點的意思是一樣的。
malloc_error_break和[NSObject doesNotRecognizeSelector:]對調試與記憶體相關的崩潰非常有幫助。如果應用崩潰了并抛出EXC_BAD_ACCESS,那麼在其中一個或全部兩個符号上設定斷點能夠幫助你定位問題。
3. 編輯斷點
建立的每個斷點都可以在斷點導航面闆中修改。按住Ctrl鍵并點選斷點,然後從菜單中選擇Edit
Breakpoint的方式來編輯斷點。你會看到一個斷點編輯頁,如圖19-5所示。
圖19-5 編輯斷點
通常,斷點會在每次執行到該行時停止程式的執行。你可以編輯斷點來設定一個條件,進而建立一個條件斷點,隻在滿足設定的條件時該斷點才會執行。為什麼這種斷點會有用呢?假設你在周遊一個大型數組(*n*>10000),很确定5500之後的對象都有問題,你想知道為什麼會出問題。常見的做法是,(在應用的代碼中)編寫額外的代碼檢查5500之後的索引值,然後在調試環節結束後删除這段代碼。
舉個例子,你可能會寫出如下代碼:
for(int i = 0 ; i < 10000; i ++) { if(i>5500) { NSLog(@"%@", [self.dataArray objectAtIndex:i]); } } |
并在NSLog處設定一個斷點。更簡潔的做法是向斷點增加這個條件。在圖19-5中,文本框是用來添加條件的。将這個條件設為i>5500,然後運作應用。現在,斷點隻會在滿足這個條件時停止應用的執行,而不是每次循環都停下來。
你可以定制斷點來列印一個值、播放音頻檔案,或是執行一段動作腳本(添加了動作腳本的話)。舉個例子,如果你正在周遊的對象是一些使用者,想知道某個使用者是否在這個清單中,這時可以編輯斷點使其在運作到你關注的對象時再停下來。除此之外,在這個動作中,還可以選擇一些音頻片段來播放,執行一段AppleScript或其他功能。點選Action按鈕(參考圖19-5),選擇自定義動作Sound。現在,在斷點處Xcode會播放你選擇的音樂片段,而不是停下來。如果你是一名遊戲開發人員,你感興趣的可能是在特定條件發生時捕捉一個OpenGL ES幀,這個選項在Action按鈕中也可以找到。
4. 共享斷點
斷點現在與要儲存到版本控制系統中的代碼(或者隻是代碼片段)關聯了起來。Xcode 4(及以上版本)允許将斷點送出到版本控制系統,進而與合作者共享它們。你所要做的就是按住Ctrl鍵并點選一個斷點,然後點選Share。你的斷點現在已經儲存到了工程檔案包的xcshareddata目錄中。将該目錄送出到版本控制系統中,就可以跟團隊中的所有其他程式員共享你的斷點了。
3. 觀察點(沒用過)
利用斷點,能夠在執行到特定行時暫停程式的執行。利用觀察點,可以在某個變量中儲存的值發生變化時暫停程式的執行。段差點可以幫助解決與全局變量有關的問題,追蹤具體是哪個方法改變了特定的全局變量。觀察點和斷點很像,當不是在執行到某段代碼時停止執行,而是在資料被修改時停止執行。
觀察點可能不常用,不過,用它來跟蹤單例,或者其他全局變量時會很有用。
預設情況下,觀察視窗會列出局部作用域内的變量,在觀察視窗中按下Ctrl鍵并點選一個變量。再點選Watch 菜單,就在哪個變量上添加了一個觀察點。觀察點會在斷點導航面闆中列出。
4. 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`-[AppDelegate appLaunchProcess] + 194 at AppDelegate.m:59
eip = 0x00010d18 YuWan`-[AppDelegate application: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.
也可以指定多個寄存器:
register read 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 | grep 3238b32c |
這裡,要将替換為崩潰的應用圖檔(可以将它送出到代碼倉庫中,或者儲存到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, "")
>>> print yesOrNo
這段代碼假設你在mypython_script檔案中寫了一個`SearchObject`函數。本書不會介紹Python腳本的具體實作機制。
5. NSZombieEnabled
NSZombieEnabled變量用來調試與記憶體相關的問題,跟蹤對象的釋放過程。啟動了它的話,他會用一個僵屍實作來替換預設的dealloc實作,也就是在引用計數降到0時,該僵屍實作回将該對象轉換成僵屍對象。
啟動他之後,當一個錯誤的記憶體通路就會變成一條無法是别的消息發送給僵屍對象。僵屍對象會顯示接受到得消息然後跳入調試器,這樣你就可以檢視到底是哪裡出了問題。
6. 不同的崩潰類型
崩潰通常是指作業系統向正在運作的程式發送的信号。
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
SIGNAL ILLEGAL INSTRUCTION (非法指令信号)。當在處理器商之行非法指令時。他就會發生。
執行非法指令:将函數指針傳給另外一個函數式,該函數指針由于某種原因是壞的,指向了一段已經釋放了的記憶體或是一個資料段。
6.7 SIGABRT
SIGNAL ABORT中止信号。當作業系統發現不安全的情況時,它能夠對這種情況進行跟多的控制;必要時他能要求程序進行清理工作。
當它出現時,控制台會輸出大量資訊,說明具體哪裡出錯了。可以在LLDB控制台輸入bt指令列印處回溯資訊。
6.8 看門狗
0x8badf00d;固定的錯誤編碼。他經常出現在執行一個同步網絡調用而阻塞了主線程時。
7. 收集崩潰報告
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