標籤:

iOS開發斷點調試高級技巧

iOS開發斷點調試高級技巧

作者:JoySeeDog

關於LLDB調試,很多iOS開發者可能就是停留在會下簡單的斷點,使用最多命令也就是po。無可厚非,這些簡單的調試對於簡單的問題來說應該是遊刃有餘。但是如果稍微複雜一些的問題,比如我之前遇到過友盟SDK裡面的一個問題。我很想往裡面下一個斷點,可是對於.a的靜態庫來說,這根本不可能,最終還是我們組大牛使用命令的方式下了斷點解決了這個問題。感覺這些知識很有必要,我於是把LLDB的基本調試命令都學習了一下,並在此與大家分享。

雖然博客很長,不過耐心看完,然後動手實踐,一定會有很大幫助。

breakpoint

給某個文件的某一行下斷點。可以使用如下兩種方法,比如我想給Foo.m文件的26行下一個斷點。可以使用如下的方法。

(lldb) breakpoint set --file Foo.m --line 26n

如果出現如下提示則說明設置斷點成功

Breakpoint 2: where = BreakPointDemo`-[Foo foo] + 23 at Foo.m:26, address = 0x000000010b22e687n

也可以使用簡寫的形式如下。

(lldb) breakpoint set -f Foo.m -l 26n

當然我們也可以直接給某個函數下斷點,可以使用下面兩種方法

(lldb) breakpoint set --name foon(lldb) breakpoint set -n foon

當然我們也可以在一次命令中為下多個函數下斷點

(lldb) breakpoint set --name foo --name barn

我們也可以更明確的指定是方法,如果是C的方法,可以使用如下兩種的方法打斷點,第二種方法M需要大寫

(lldb) breakpoint set --method cplusFoon(lldb) breakpoint set -M cplusFoon

如果是OC的方法,可以使用以下兩種方式打斷點,第二種S需要大寫

(lldb) breakpoint set --selector foonn(lldb) breakpoint set -S foon

如果是C語言,還是只能使用上面介紹的--name的方式,不能直接指定對應的方法

當然,還有一個必殺器,就是使用正則,匹配你要打斷點的函數。這個不限語言

(lldb) breakpoint set -r cFoon(lldb) breakpoint set -r foon

也可以指定載入的動態庫

(lldb) breakpoint set --shlib foo.dylib --name foo n(lldb) breakpoint set -s foo.dylib -n foon

我們同樣可以對命令進行簡寫。下面兩個命令的效果是一樣的

(lldb) breakpoint set -n "-[Foo foo]"n(lldb) br s -n "-[Foo foo]"n

想要查看有多少斷點可以使用

(lldb) breakpoint listn

列印的結果如下

Current breakpoints:n1: file = /Users/jianquan/Xcode/BreakPointDemo/BreakPointDemo/ViewController.m, line = 20, exact_match = 0, locations = 0 (pending)n2: file = /Users/jianquan/Xcode/BreakPointDemo/BreakPointDemo/ViewController.mm, line = 33, exact_match = 0, locations = 1, resolved = 1, hit count = 0n 2.1: where = BreakPointDemo`::-[ViewController viewDidLoad]() + 186 at ViewController.mm:34, address = 0x0000000105f8362a, resolved, hit count = 0 n......n

我們可以對斷點進行相關的操作,比如在執行到2.1斷點的時候列印追蹤軌跡。bt是

(lldb) breakpoint command add 2.1nEnter your debugger command(s). Type DONE to end.n> btn> DONEn

除了add,還要delete等命令,這些命令不需要死記硬背,可以使用help命令。

(lldb) help break commandnn add -- Add LLDB commands to a breakpoint, to be executed whenever then breakpoint is hit. If no breakpoint is specified, adds then commands to the last created breakpoint.n delete -- Delete the set of commands from a breakpoint.n list -- List the script or set of commands to be executed when then breakpoint is hit.n

要查看更詳細的命令用途,使用help <command> <subcommand>.比如查看add命令用法

(lldb) help break command addn......nnEnter your Python command(s). Type DONE to end.n> def breakpoint_output (bp_no):n> out_string = "Hit breakpoint number " + repr (bp_no)n> print out_stringn> return Truen> breakpoint_output (1)n> DONEn

可以看到其實這裡面的命令大部分是Python腳本,不熟悉Python,暫時還沒有仔細研究。

補充一點使用了之後如何刪除斷點呢,命令說明如下。

breakpoint delete [-Df] [<breakpt-id | breakpt-id-list>]n

我現在用breakpoint list查我的進程

Current breakpoints:n1: file = /Users/jianquan/Xcode/BreakPointDemo/BreakPointDemo/ViewController.m, line = 20, exact_match = 0, locations = 0 (pending)nnn2: file = /Users/jianquan/Xcode/BreakPointDemo/BreakPointDemo/ViewController.mm, line = 29, exact_match = 0, locations = 1, resolved = 1, hit count = 1nn 2.1: where = BreakPointDemo`::-[ViewController viewDidLoad]() + 105 at ViewController.mm:30, address = 0x00000001025b55c9, resolved, hit count = 1 nn4: name = foo, locations = 1, resolved = 1, hit count = 0n 4.1: where = BreakPointDemo`-[Foo foo] + 23 at Foo.m:26, address = 0x00000001025b5517, resolved, hit count = 0 nn5: regex = cFoo, locations = 2, resolved = 2, hit count = 0n 5.1: where = BreakPointDemo`cFoo + 15 at CFoo.c:13, address = 0x00000001025b591f, resolved, hit count = 0 n 5.2: where = libicucore.A.dylib`icu::MeasureUnit::createCubicFoot(UErrorCode&), address = 0x00000001051b808a, resolved, hit count = 0n

若果我要刪除5.1斷點我就使用breakpoint delete 5.1,如果我要刪除5下面的所有斷點,使用breakpoint delete 5,這樣5.1和5.2都會刪除。

刪除所有的斷點使用

(lldb) breakpoint deletenAbout to delete all breakpoints, do you want to do that?: [Y/n] ynAll breakpoints removed. (4 breakpoints)n

watchpoint

這個主要是用於觀察變數值的具體變化

比如我需要觀察某個變數a的值變化,我可以使用如下命令

(lldb) watchpoint set variable an

成功添加watchpoint後結果如下。

Watchpoint created: Watchpoint 1: addr = 0x7fff5913ca3c size = 4 state = enabled type = wn declare @ /Users/jianquan/Xcode/BreakPointDemo/BreakPointDemo/ViewController.mm:25n watchpoint spec = an new value: 10n

也可以在這裡添加.

然後我們可以設置在a的值變化為某個特定值之後觸。

(lldb) watchpoint modify -c (a=100)n

我們這個時候可以看一下具體斷點的參數,使用watchpoint list命令

(lldb) watchpoint listnNumber of supported hardware watchpoints: 4nCurrent watchpoints:nWatchpoint 1: addr = 0x7fff4fcb7a3c size = 4 state = enabled type = wn declare @ /Users/jianquan/Xcode/BreakPointDemo/BreakPointDemo/ViewController.mm:25n watchpoint spec = an new value: 10n condition = (a=100)n

可以看到我們觀察的變數的地址,聲明變數的代碼在第幾行,已經具體的變數名是a,當前的值是10,觸發的條件是(a=100)

然後我們執行如下命令,就可以看到斷點到a的值變為100的地方

(lldb) cnProcess 16596 resumingn2017-02-09 11:12:14.693 BreakPointDemo[16596:6050498] foo is foon2017-02-09 11:12:14.693 BreakPointDemo[16596:6050498] bar is barnnWatchpoint 1 hit:nold value: 10nnew value: 100n

可以看到這個地方a的值已經發生改變。我們可以再使用watchpoint list命令看看具體值的變化

(lldb) watchpoint listnNumber of supported hardware watchpoints: 4nCurrent watchpoints:nWatchpoint 1: addr = 0x7fff4fcb7a3c size = 4 state = enabled type = wn declare @ /Users/jianquan/Xcode/BreakPointDemo/BreakPointDemo/ViewController.mm:25n watchpoint spec = an old value: 10n new value: 100n condition = (a=100)n

當然,還有一個特別好用的命令就是bt命令我們可以用它來追蹤程序運行的過程。

(lldb) btn* thread #1: tid = 0x5c52c2, 0x000000010ff465fe BreakPointDemo`::-[ViewController viewDidLoad](self=0x00007f932cc07c50, _cmd="viewDidLoad") + 158 at ViewController.mm:36, queue = com.apple.main-thread, stop reason = watchpoint 1n * frame #0: 0x000000010ff465fe BreakPointDemo`::-[ViewController viewDidLoad](self=0x00007f932cc07c50, _cmd="viewDidLoad") + 158 at ViewController.mm:36n frame #1: 0x000000011112ba3d UIKit`-[UIViewController loadViewIfRequired] + 1258n ......n

我們可以使用frame命令查看變數a的具體值。

(lldb) frame variable an(int) a = 100n

最後補充一點watchpoint list的東西。這個命令包括了三個可選參數,我們可以使用help命令查看具體的值

(lldb) help watchpoint listnn -b ( --brief )n Give a brief description of the watchpoint (no location info).nn -f ( --full )n Give a full description of the watchpoint and its locations.nn -v ( --verbose )n Explain everything we know about the watchpoint (for debuggingn debugger bugs).n

-b是比較簡略的信息,-f是比較全面的信息,-v是完整的信息。經過我的實驗,如果使用watchpoint list,默認的是 watchpoint list -f。

process

使用process命令也可以做很多有趣的操作。具體能做什麼,我們也可使用help命令查看

(lldb) process helpnn attach -- Attach to a process.n connect -- Connect to a remote debug service.n continue -- Continue execution of all threads in the current process.n detach -- Detach from the current target process.n handle -- Manage LLDB handling of OS signals for the current targetn ......n

查看更詳細的命令使用help <command> <subcommand>。比如

(lldb) help process attachn

這些命令在我目前日常開發中其實不怎麼使用,可能我功力還不足吧。

thread

其實這個功能主要就是斷點調試裡面的如下這個功能。

我們可以使用thread命令來做一些斷點的操作,具體有那些命令我們可以使用thread help進行查看。

(lldb) thread helpnn ......nn select -- Change the currently selected thread.n step-in -- Source level single step, stepping into calls. n Defaults to current thread unless specified.n step-inst -- Instruction level single step, stepping into calls. n Defaults to current thread unless specified.n step-inst-over -- Instruction level single step, stepping over calls. n Defaults to current thread unless specified.n step-out -- Finish executing the current stack frame and stop aftern returning. Defaults to current thread unlessn specified.n step-over -- Source level single step, stepping over calls. n Defaults to current thread unless specified.n step-scripted -- Step as instructed by the script class passed in the -Cn option.n until -- Continue until a line number or address is reached byn the current or specified thread. Stops when returningn from the current function as a safety measure.n

用得比較多的應該是 step-開頭的這幾個命令,使用起來很容易。我個人感覺比用滑鼠點擊斷點好用多了~

EXAMINING THREAD STATE

這個使用的也主要還是thread命令,主要是使用以下幾個命令。

檢查當前進程的狀態,可以使用如下命令。

lldb) thread listnProcess 22323 stoppedn* thread #1: tid = 0x62d0d7, 0x00000001082185fe BreakPointDemo`::-[ViewController viewDidLoad](self=0x00007ff81b60ab20, _cmd="viewDidLoad") + 158 at ViewController.mm:36, queue = com.apple.main-thread, stop reason = step untiln......n

*表明的就是當前的線程,可以使用如下的命令得到線程的回溯,這個詞我也不確定怎麼表達好,backtrace,也可以說是追蹤。

lldb) thread backtracen* thread #1: tid = 0x62d0d7, 0x00000001082185fe BreakPointDemo`::-[ViewController viewDidLoad](self=0x00007ff81b60ab20, _cmd="viewDidLoad") + 158 at ViewController.mm:36, queue = com.apple.main-thread, stop reason = step untiln * frame #0: 0x00000001082185fe BreakPointDemo`::-[ViewController viewDidLoad](self=0x00007ff81b60ab20, _cmd="viewDidLoad") + 158 at ViewController.mm:36n frame #1: 0x00000001093fda3d UIKit`-[UIViewController loadViewIfRequired] + 1258n frame #2: 0x00000001093fde70 UIKit`-[UIViewController view] + 27n frame #3: 0x00000001092c74b5 UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 71n frame #4: 0x00000001092c7c06 UIKit`-[UIWindow _setHidden:forced:] + 293n frame #5: 0x00000001092db519 UIKit`-[UIWindow makeKeyAndVisible] + 42n frame #6: 0x0000000109253f8d UIKit`-[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 4818n frame #7: 0x000000010925a0ed UIKit`-[UIApplication _runWithMainScene:transitionContext:completion:] + 1731n frame #8: 0x000000010925726d UIKit`-[UIApplication workspaceDidEndTransaction:] + 188n frame #9: 0x000000010c3886cb FrontBoardServices`__FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 24n frame #10: 0x000000010c388544 FrontBoardServices`-[FBSSerialQueue _performNext] + 189n frame #11: 0x000000010c3888cd FrontBoardServices`-[FBSSerialQueue _performNextFromRunLoopSource] + 45n frame #12: 0x0000000108ddc761 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17n frame #13: 0x0000000108dc198c CoreFoundation`__CFRunLoopDoSources0 + 556n frame #14: 0x0000000108dc0e76 CoreFoundation`__CFRunLoopRun + 918n frame #15: 0x0000000108dc0884 CoreFoundation`CFRunLoopRunSpecific + 420n frame #16: 0x0000000109255aea UIKit`-[UIApplication _run] + 434n frame #17: 0x000000010925bc68 UIKit`UIApplicationMain + 159n frame #18: 0x000000010821899f BreakPointDemo`main(argc=1, argv=0x00007fff579e7600) + 111 at main.m:14n frame #19: 0x000000010bbee68d libdyld.dylib`start + 1n

當然我們如果想看所有線程的backtrace,可以使用thread backtrace all命令。內容太多,我這裡就不演示log輸出了。

如果我們想單獨查看某個線程,我們可以先使用thread select 2跳到某個具體的線程,然後再進行其他操作,比如thread backtrace

EXAMINING STACK FRAME STATE

為了方便的觀測架構參數和本地變數,我們可以使用 frame variable 命令

如果我什麼參數也不加,將會把所有的參數和本地變數到列印出來。

(lldb) frame variable n(ViewController *) self = 0x00007ff81b60ab20n(SEL) _cmd = "viewDidLoad"n(int) a = 100n(Foo *) foo = 0x000061800000e820n(BreakPointDemoNameSpace::BreakPointClass *) cplusFoo = 0x3ff0000000000000n

要列印某個變數需要在參數裡面指定,這個命令我們在前面也使用過,比如要查看self

(lldb) frame variable selfn(ViewController *) self = 0x00007ff81b60ab20n

更進一步,我們可以查看一些子元素

(lldb) frame variable self->isan(Class) self->isa = ViewControllern

命令雖然不是完整的表達式解釋器,當時可以識別一些基本的操作 比如 &, *, ->, [],不是重載運算符,數組也可以使用,因為數組本身也是指針。

(lldb) frame variable *self nn(ViewController) *self = {n UIViewController = {n UIResponder = {n NSObject = {n isa = ViewControllern }n ......n}n

和之前thread命令很類似,我可以使用frame select去選擇另外的一個frame

(lldb) frame select 9n

如果想看更複雜的數據,我們可以使用expression命令

(lldb) expression selfn(ViewController *) $0 = 0x00007fefa4705110n

更複雜一些,我們可以用來輸出一個表達式

(lldb) expr (int) printf ("I have a pointer 0x%llx.n", self)nI have a pointer 0x7fefa4705110.n(int) $1 = 33n

我們可以繼續以之前的命令來操作

(lldb) expr self = $0n(ViewController *) $2 = 0x00007fefa4705110n

當然這個expr用途感覺不大。

call

其實這個命令完全可以使用po進行替代,call一般可以用來調用不需要返回值的調試命令,比如更改View的背景顏色,以下兩個命令都可以達到相似的作用,更改當前View的背景顏色值。

(lldb) po [self.view setBackgroundColor:[UIColor redColor]]n(lldb) call [self.view setBackgroundColor:[UIColor redColor]]n

image

雖然只是一個簡單的命令,但是我還是感覺這是一個比較重要也比較實用的命令, 命令可用於定址。比較實用的用法是用於尋找棧地址對應的代碼位置。 下面我寫了一段代碼

//測試image命令使用n NSArray *arr=[[NSArray alloc] initWithObjects:@"1",@"2", nil];n NSLog(@"%@",arr[2]);n

可以很明顯的看到數組越界了,然後我們運行程序,可以看到程序報如下錯誤

this is cplusFoothis is cFoo2017-02-09 16:33:52.143 BreakPointDemo[26121:6901793] *** Terminating app due to uncaught exception NSRangeException, reason: *** -[__NSArrayI objectAtIndex:]: index 2 beyond bounds [0 .. 1]n*** First throw call stack:n(n 0 CoreFoundation 0x0000000104d67d4b __exceptionPreprocess + 171n 1 libobjc.A.dylib 0x000000010471e21e objc_exception_throw + 48n 2 CoreFoundation 0x0000000104ca22bb -[__NSArrayI objectAtIndex:] + 155n 3 BreakPointDemo -[ViewController viewDidLoad] + 340n 4 UIKit 0x000000010675ba3d -[UIViewController loadViewIfRequired] + 1258n 5 UIKit 0x000000010675be70 -[UIViewController view] + 27n 6 UIKit 0x00000001066254b5 -[UIWindow addRootViewControllerViewIfPossible] + 71n 7 UIKit 0x0000000106625c06 -[UIWindow _setHidden:forced:] + 293n 8 UIKit 0x0000000106639519 -[UIWindow makeKeyAndVisible] + 42n 9 UIKit 0x00000001065b1f8d -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 4818n 10 UIKit 0x00000001065b80ed -[UIApplication _runWithMainScene:transitionContext:completion:] + 1731n 11 UIKit 0x00000001065b526d -[UIApplication workspaceDidEndTransaction:] + 188n 12 FrontBoardServices 0x00000001083456cb __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 24n 13 FrontBoardServices 0x0000000108345544 -[FBSSerialQueue _performNext] + 189n 14 FrontBoardServices 0x00000001083458cd -[FBSSerialQueue _performNextFromRunLoopSource] + 45n 15 CoreFoundation 0x0000000104d0c761 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17n 16 CoreFoundation 0x0000000104cf198c __CFRunLoopDoSources0 + 556n 17 CoreFoundation 0x0000000104cf0e76 __CFRunLoopRun + 918n 18 CoreFoundation 0x0000000104cf0884 CFRunLoopRunSpecific + 420n 19 UIKit 0x00000001065b3aea -[UIApplication _run] + 434n 20 UIKit 0x00000001065b9c68 UIApplicationMain + 159n 21 BreakPointDemo 0x000000010414794f main + 111n 22 libdyld.dylib 0x00000001062b068d start + 1n 23 ??? 0x0000000000000001 0x0 + 1n)nnlibc++abi.dylib: terminating with uncaught exception of type NSExceptionn

我們大概可以猜測程序是崩潰在第三行log,也就是地址為0x0000000104147544的地方,怎麼來呢,瞎猜的,哈哈。其實原理很簡單,因為我的Demo名字叫BreakPointDemo。其他的名字很明顯是系統的庫。雖然log的21行也有BreakPointDemo,但是經過觀察應該是main函數,不在考慮範圍之內。

我們使用image 的 lookup命令,可以很快的定位到具體的代碼行。

(lldb) image lookup --address 0x0000000104147544n Address: BreakPointDemo[0x0000000100001544] (BreakPointDemo.__TEXT.__text + 644)n Summary: BreakPointDemo`::-[ViewController viewDidLoad]() + 340 at ViewController.mm:46n

看看我們的Xcode文件的代碼。確實是46行

當然還有很多的命令我們可以探索,使用image help可以查看,這些命令我暫時沒有接觸過,後續工作或者學習中使用到了我會更新上來。

為命令設置別名

比如p是frame variable的別名,p view實際上是frame variable view。除了系統自建的LLDB別名,你也可以自定義別名。比如下面這個命令。掌握了規律之後,任何的命令我們都可以自己設置別名。

(lldb) command alias bfl breakpoint set -f %1 -l %2n(lldb) bfl Foo.m 12n

如果想要撤銷別名使用

(lldb) command unalias bfln

當然還有一些LLDB的具體命令,我們可以在官網查看: The LLDB Debugger

Demo地址

總結

這麼長的文章,看到這裡真的不容易,不過我相信你應該有所收穫了。另外我的博客長期歡迎評論留言,相互探討,不足之處歡迎批准指正。

閱讀原文

推薦閱讀:

讓你手裡的 iPhone 再智能一點高效一點
如何做好一名移動部門的開發經理?
未來的聯繫人中心? - Contact Center
32 位應用已死
[C in ASM(ARM64)]第六章 結構體

TAG:iOS |