與調(diào)試器共舞 - LLDB 的華爾茲

LLDB調(diào)試

help

最簡(jiǎn)單命令是help,它會(huì)列舉出所有的命令。如果你忘記了一個(gè)命令是做什么的,或者想知道更多的話,你可以通過(guò)help 來(lái)了解更多細(xì)節(jié),例如help print或者h(yuǎn)elp thread。如果你甚至忘記了help命令是做什么的,你可以試試help help。不過(guò)你如果知道這么做,那就說(shuō)明你大概還沒(méi)有忘光這個(gè)命令。??

print

打印值很簡(jiǎn)單;只要試試print命令:

LLDB 實(shí)際上會(huì)作前綴匹配。所以你也可以使用prin,pri,或者p。但你不能使用pr,因?yàn)?LLDB 不能消除和process的歧義 (幸運(yùn)的是p并沒(méi)有歧義)。

你可能還注意到了,結(jié)果中有個(gè)$0。實(shí)際上你可以使用它來(lái)指向這個(gè)結(jié)果。試試print $0 + 7,你會(huì)看到106。任何以美元符開(kāi)頭的東西都是存在于 LLDB 的命名空間的,它們是為了幫助你進(jìn)行調(diào)試而存在的。

expression

如果想改變一個(gè)值怎么辦?你或許會(huì)猜modify。其實(shí)這時(shí)候我們要用到的是expression這個(gè)方便的命令。

這不僅會(huì)改變調(diào)試器中的值,實(shí)際上它改變了程序中的值。這時(shí)候繼續(xù)執(zhí)行程序,將會(huì)打印42 red balloons。神奇吧。

注意,從現(xiàn)在開(kāi)始,我們將會(huì)偷懶分別以p和e來(lái)代替print和expression。

什么是print命令

考慮一個(gè)有意思的表達(dá)式:p count = 18。如果我們運(yùn)行這條命令,然后打印count的內(nèi)容。我們將看到它的結(jié)果與expression count = 18一樣。

和expression不同的是,print命令不需要參數(shù)。比如e -h +17中,你很難區(qū)分到底是以-h為標(biāo)識(shí),僅僅執(zhí)行+17呢,還是要計(jì)算17和h的差值。連字符號(hào)確實(shí)很讓人困惑,你或許得不到自己想要的結(jié)果。

幸運(yùn)的是,解決方案很簡(jiǎn)單。用--來(lái)表征標(biāo)識(shí)的結(jié)束,以及輸入的開(kāi)始。如果想要-h作為標(biāo)識(shí),就用e -h -- +17,如果想計(jì)算它們的差值,就使用e -- -h +17。因?yàn)橐话銇?lái)說(shuō)不使用標(biāo)識(shí)的情況比較多,所以e --就有了一個(gè)簡(jiǎn)寫的方式,那就是print。

輸入help print,然后向下滾動(dòng),你會(huì)發(fā)現(xiàn):

'print'is an abbreviationfor'expression --'.? (print是`expression --`的縮寫)

打印對(duì)象

嘗試輸入

p objects

輸出會(huì)有點(diǎn)啰嗦

(NSString *) $7 =0x0000000104da4040@"red balloons"

如果我們嘗試打印結(jié)構(gòu)更復(fù)雜的對(duì)象,結(jié)果甚至?xí)?br>

(lldb) p @[ @"foo", @"bar"](NSArray *) $8 =0x00007fdb9b71b3e0@"2 objects"

實(shí)際上,我們想看的是對(duì)象的description方法的結(jié)果。我么需要使用-O(字母 O,而不是數(shù)字 0) 標(biāo)志告訴expression命令以對(duì)象(Object) 的方式來(lái)打印結(jié)果。

(lldb) e -O -- $8<__NSArrayI0x7fdb9b71b3e0>(foo,bar)

幸運(yùn)的是,e -o --有也有個(gè)別名,那就是po(printobject 的縮寫),我們可以使用它來(lái)進(jìn)行簡(jiǎn)化:

(lldb) po $8<__NSArrayI0x7fdb9b71b3e0>(foo,bar)

(lldb) po @"lunar"lunar

(lldb) p @"lunar"(NSString *) $13 =0x00007fdb9d0003b0@"lunar"

打印變量

可以給print指定不同的打印格式。它們都是以print/或者簡(jiǎn)化的p/格式書(shū)寫。下面是一些例子:

默認(rèn)的格式

(lldb) p 16

16

十六進(jìn)制:

(lldb) p/x 16

0x10

二進(jìn)制 (t代表two):

(lldb) p/t160b00000000000000000000000000010000(lldb) p/t (char)160b00010000

你也可以使用p/c打印字符,或者p/s打印以空終止的字符串 (譯者注:以 '\0' 結(jié)尾的字符串)。

這里是格式的完整清單。

變量

現(xiàn)在你已經(jīng)可以打印對(duì)象和簡(jiǎn)單類型,并且知道如何使用expression命令在調(diào)試器中修改它們了?,F(xiàn)在讓我們使用一些變量來(lái)減少輸入量。就像你可以在 C 語(yǔ)言中用int a = 0來(lái)聲明一個(gè)變量一樣,你也可以在 LLDB 中做同樣的事情。不過(guò)為了能使用聲明的變量,變量必須以美元符開(kāi)頭。

(lldb) e int $a =2(lldb) p $a *1938(lldb) e NSArray *$array = @[ @"Saturday", @"Sunday", @"Monday"](lldb) p [$array count]2

(lldb) po [[$arrayobjectAtIndex:0] uppercaseString]SATURDAY

(lldb) p [[$arrayobjectAtIndex:$a]characterAtIndex:0]

error:no known method'-characterAtIndex:'; cast the message send to the method's return type

error: 1 errors parsing expression

悲劇了,LLDB 無(wú)法確定涉及的類型 (譯者注:返回的類型)。這種事情常常發(fā)生,給個(gè)說(shuō)明就好了:

(lldb) p (char)[[$arrayobjectAtIndex:$a]characterAtIndex:0]'M'

(lldb) p/d (char)[[$arrayobjectAtIndex:$a]characterAtIndex:0]77

變量使調(diào)試器變的容易使用得多,想不到吧???

不用斷點(diǎn)調(diào)試

程序運(yùn)行時(shí),Xcode 的調(diào)試條上會(huì)出現(xiàn)暫停按鈕,而不是繼續(xù)按鈕:

點(diǎn)擊按鈕會(huì)暫停 app (這會(huì)運(yùn)行process interrupt命令,因?yàn)?LLDB 總是在背后運(yùn)行)。這會(huì)讓你可以訪問(wèn)調(diào)試器,但看起來(lái)可以做的事情不多,因?yàn)樵诋?dāng)前作用域沒(méi)有變量,也沒(méi)有特定的代碼讓你看。

這就是有意思的地方。如果你正在運(yùn)行 iOS app,你可以試試這個(gè): (因?yàn)槿肿兞渴强稍L問(wèn)的)

(lldb) po [[[UIApplicationsharedApplication] keyWindow] recursiveDescription]; layer = >? | >

更新UI

有了上面的輸出,我們可以獲取這個(gè) view:

(lldb) eid$myView = (id)0x7f82b1d01fd0

然后在調(diào)試器中改變它的背景色:

(lldb) e (void)[$myView setBackgroundColor:[UIColorblueColor]]

但是只有程序繼續(xù)運(yùn)行之后才會(huì)看到界面的變化。因?yàn)楦淖兊膬?nèi)容必須被發(fā)送到渲染服務(wù)中,然后顯示才會(huì)被更新。

渲染服務(wù)實(shí)際上是一個(gè)另外的進(jìn)程 (被稱作backboardd)。這就是說(shuō)即使我們正在調(diào)試的內(nèi)容所在的進(jìn)程被打斷了,backboardd也還是繼續(xù)運(yùn)行著的。

這意味著你可以運(yùn)行下面的命令,而不用繼續(xù)運(yùn)行程序:

(lldb) e (void)[CATransactionflush]

即使你仍然在調(diào)試器中,UI 也會(huì)在模擬器或者真機(jī)上實(shí)時(shí)更新。Chisel為此提供了一個(gè)別名叫做caflush,這個(gè)命令被用來(lái)實(shí)現(xiàn)其他的快捷命令,例如hide ,show 以及其他很多命令。所有Chisel的命令都有文檔,所以安裝后隨意運(yùn)行help show來(lái)看更多信息。

Push 一個(gè) View Controller

想象一個(gè)以UINavigationController為 root ViewController 的應(yīng)用。你可以通過(guò)下面的命令,輕松地獲取它:

(lldb) eid$nvc = [[[UIApplicationsharedApplication] keyWindow] rootViewController]

然后 push 一個(gè) child view controller:

(lldb) eid$vc = [UIViewControllernew](lldb) e (void)[[$vc view] setBackgroundColor:[UIColoryellowColor]](lldb) e (void)[$vc setTitle:@"Yay!"](lldb) e (void)[$nvc pushViewContoller:$vc animated:YES]

最后運(yùn)行下面的命令:

(lldb) caflush// e (void)[CATransaction flush]

navigation Controller 就會(huì)立刻就被 push 到你眼前。

查找按鈕的 target

想象你在調(diào)試器中有一個(gè)$myButton的變量,可以是創(chuàng)建出來(lái)的,也可以是從 UI 上抓取出來(lái)的,或者是你停止在斷點(diǎn)時(shí)的一個(gè)局部變量。你想知道,按鈕按下的時(shí)候誰(shuí)會(huì)接收到按鈕發(fā)出的 action。非常簡(jiǎn)單:

(lldb) po [$myButtonallTargets]{(? ? )}(lldb) po [$myButtonactionsForTarget:(id)0x7fb58bd2e240forControlEvent:0]<__NSArrayM 0x7fb58bd2aa40>(_handleTap:)

現(xiàn)在你或許想在它發(fā)生的時(shí)候加一個(gè)斷點(diǎn)。在-[MagicEventListener _handleTap:]設(shè)置一個(gè)符號(hào)斷點(diǎn)就可以了,在 Xcode 和 LLDB 中都可以,然后你就可以點(diǎn)擊按鈕并停在你所希望的地方了。

觀察實(shí)例變量的變化

假設(shè)你有一個(gè)UIView,不知道為什么它的_layer實(shí)例變量被重寫了 (糟糕)。因?yàn)橛锌赡懿⒉簧婕暗椒椒?,我們不能使用符?hào)斷點(diǎn)。相反的,我們想監(jiān)視什么時(shí)候這個(gè)地址被寫入。

首先,我們需要找到_layer這個(gè)變量在對(duì)象上的相對(duì)位置:

(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyViewclass], "_layer"))(ptrdiff_t) $0 =8

現(xiàn)在我們知道($myView + 8)是被寫入的內(nèi)存地址:

(lldb) watchpointsetexpression -- (int *)$myView+ 8Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabledtype= w? ? new value: 0x0000000000000000

這被以wivar $myView _layer加入到Chisel中。

非重寫方法的符號(hào)斷點(diǎn)

假設(shè)你想知道-[MyViewController viewDidAppear:]什么時(shí)候被調(diào)用。如果這個(gè)方法并沒(méi)有在MyViewController中實(shí)現(xiàn),而是在其父類中實(shí)現(xiàn)的,該怎么辦呢?試著設(shè)置一個(gè)斷點(diǎn),會(huì)出現(xiàn)以下結(jié)果:

(lldb) b -[MyViewController viewDidAppear:]

Breakpoint 1: no locations (pending).

WARNING:? Unable to resolve breakpoint to any actual locations.

因?yàn)?LLDB 會(huì)查找一個(gè)符號(hào),但是實(shí)際在這個(gè)類上卻找不到,所以斷點(diǎn)也永遠(yuǎn)不會(huì)觸發(fā)。你需要做的是為斷點(diǎn)設(shè)置一個(gè)條件[self isKindOfClass:[MyViewController class]],然后把斷點(diǎn)放在UIViewController上。正常情況下這樣設(shè)置一個(gè)條件可以正常工作。但是這里不會(huì),因?yàn)槲覀儧](méi)有父類的實(shí)現(xiàn)。

viewDidAppear:是蘋果實(shí)現(xiàn)的方法,因此沒(méi)有它的符號(hào);在方法內(nèi)沒(méi)有self。如果想在符號(hào)斷點(diǎn)上使用self,你必須知道它在哪里 (它可能在寄存器上,也可能在棧上;在 x86 上,你可以在$esp+4找到它)。但是這是很痛苦的,因?yàn)楝F(xiàn)在你必須至少知道四種體系結(jié)構(gòu) (x86,x86-64,armv7,armv64)。想象你需要花多少時(shí)間去學(xué)習(xí)命令集以及它們每一個(gè)的調(diào)用約定,然后正確的寫一個(gè)在你的超類上設(shè)置斷點(diǎn)并且條件正確的命令。幸運(yùn)的是,這個(gè)在Chisel被解決了。這被成為bmessage:

(lldb) bmessage -[MyViewController viewDidAppear:]Setting a breakpoint at -[UIViewControllerviewDidAppear:] with condition (void*)object_getClass((id)$rdi) ==0x000000010e2f4d28Breakpoint1: where =UIKit`-[UIViewControllerviewDidAppear:], address =0x000000010e11533c

LLDB 和 Python

LLDB 有內(nèi)建的,完整的Python支持。在LLDB中輸入script,會(huì)打開(kāi)一個(gè) Python REPL。你也可以輸入一行 python 語(yǔ)句作為script 命令的參數(shù),這可以運(yùn)行 python 語(yǔ)句而不進(jìn)入REPL:

(lldb) scriptimportos(lldb) script os.system("open http://www.objc.io/")

這樣就允許你創(chuàng)造各種酷的命令。把下面的語(yǔ)句放到文件~/myCommands.py中:

defcaflushCommand(debugger, command, result, internal_dict):? debugger.HandleCommand("e (void)[CATransaction flush]")

然后再 LLDB 中運(yùn)行:

command scriptimport~/myCommands.py

或者把這行命令放在/.lldbinit里,這樣每次進(jìn)入 LLDB 時(shí)都會(huì)自動(dòng)運(yùn)行。Chisel其實(shí)就是一個(gè) Python 腳本的集合,這些腳本拼接 (命令) 字符串 ,然后讓 LLDB 執(zhí)行。很簡(jiǎn)單,不是嗎?

緊握調(diào)試器這一武器

LLDB 可以做的事情很多。大多數(shù)人習(xí)慣于使用p,po,n,s和c,但實(shí)際上除此之外,LLDB 可以做的還有很多。掌握所有的命令 (實(shí)際上并不是很多),會(huì)讓你在揭示代碼運(yùn)行時(shí)的運(yùn)行狀態(tài),尋找 bug,強(qiáng)制執(zhí)行特定的運(yùn)行路徑時(shí)獲得更大的能力。你甚至可以構(gòu)建簡(jiǎn)單的交互原型 - 比如要是現(xiàn)在以 modal 方式彈出一個(gè) View Controller 會(huì)怎么樣?使用調(diào)試器,一試便知。

這篇文章是為了想你展示 LLDB 的強(qiáng)大之處,并且鼓勵(lì)你多去探索在控制臺(tái)輸入命令。

打開(kāi) LLDB,輸入help,看一看列舉的命令。你嘗試過(guò)多少?用了多少?

但愿NSLog看起來(lái)不再那么吸引你去用,每次編輯再運(yùn)行并不有趣而且耗時(shí)。

調(diào)試愉快!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容