什么是 LLDB狂芋?
LLDB(Low Level Debugger) is a next generation, high-performance debugger. It is built as a set of reusable components which highly leverage existing libraries in the larger LLVM Project, such as the Clang expression parser and LLVM disassembler.
LLDB is the default debugger in Xcode on Mac OS X and supports debugging C, Objective-C and C++ on the desktop and iOS devices and simulator.
All of the code in the LLDB project is available under the standard LLVM License, an open source "BSD-style" license.
基礎
LLDB 是一個有著 REPL 的特性的調試器榨馁,這說明他和Linux數(shù)據(jù)庫有一樣的"套路"。當一般不是手動把help
命令禁止掉的時候帜矾,就可以通過help
查找所有想要使用的命令翼虫。
現(xiàn)在iOS
的xcode
,安卓
的android studio
都有LLDB模塊支持屑柔。學好LLDB,各種難搞的問題都能找到一些可以原由蛙讥。
你可能還注意到了,結果中有個 $3灭衷。 實際上你可以使用它來指向這個結果次慢。試試 print $3 + 1
,你會看到 100翔曲。任何以美元符開頭的東西都是存在于 LLDB 的命名空間的迫像,它們是為了幫助你進行調試而存在的。
expression
如果想改變一個值怎么辦瞳遍?你或許會猜 modify闻妓。其實這時候我們要用到的是 expression 這個方便的命令。
這不僅會改變調試器中的值掠械,實際上它改變了程序中的值由缆。從現(xiàn)在開始,我們將會偷懶分別以p
和e
來代替print
和expression
猾蒂。
什么是 print 命令
考慮一個有意思的表達式:p count = 18均唉。如果我們運行這條命令,然后打印 count 的內容肚菠。我們將看到它的結果與 expression count = 18 一樣舔箭。
和 expression 不同的是,print 命令不需要參數(shù)蚊逢。比如 e -h +17 中层扶,你很難區(qū)分到底是以 -h 為標識,僅僅執(zhí)行 +17 呢烙荷,還是要計算 17 和 h 的差值镜会。連字符號確實很讓人困惑,你或許得不到自己想要的結果终抽。
幸運的是稚叹,解決方案很簡單。用 -- 來表征標識的結束拿诸,以及輸入的開始扒袖。如果想要 -h 作為標識,就用 e -h -- +17亩码,如果想計算它們的差值季率,就使用 e -- -h +17。因為一般來說不使用標識的情況比較多描沟,所以 e -- 就有了一個簡寫的方式飒泻,那就是 print鞭光。
輸入 help print
'print' is an abbreviation for 'expression --'.
(print是 `expression --` 的縮寫)
打印對象
(lldb) p @[ @"foo", @"bar" ]
(NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects"
實際上,我們想看的是對象的 description 方法的結果泞遗。我么需要使用 -O (字母 O惰许,而不是數(shù)字 0) 標志告訴 expression 命令以 對象 (Object) 的方式來打印結果。
(lldb) e -O -- $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)
幸運的是史辙,e -o -- 有也有個別名汹买,那就是 po (print object 的縮寫),我們可以使用它來進行簡化:
(lldb) po $8
<__NSArrayI 0x7fdb9b71b3e0>(
foo,
bar
)
(lldb) po @"lunar"
lunar
(lldb) p @"lunar"
(NSString *) $13 = 0x00007fdb9d0003b0 @"lunar"
Thread Return
調試時聊倔,還有一個很棒的函數(shù)可以用來控制程序流程:thread return 晦毙。它有一個可選參數(shù),在執(zhí)行時它會把可選參數(shù)加載進返回寄存器里耙蔑,然后立刻執(zhí)行返回命令见妒,跳出當前棧幀。這意味這函數(shù)剩余的部分不會被執(zhí)行甸陌。這會給 ARC 的引用計數(shù)造成一些問題须揣,或者會使函數(shù)內的清理部分失效。但是在函數(shù)的開頭執(zhí)行這個命令钱豁,是個非常好的隔離這個函數(shù)返敬,偽造返回值的方式 。
斷點
我們都把斷點作為一個停止程序運行寥院,檢查當前狀態(tài)劲赠,追蹤 bug 的方式。但是如果我們改變和斷點交互的方式秸谢,很多事情都變成可能凛澎。
想象把斷點放在函數(shù)的開頭,然后用 thread return 命令重寫函數(shù)的行為估蹄,然后繼續(xù)塑煎。想象一下讓這個過程自動化,聽起來不錯臭蚁,不是嗎最铁?
創(chuàng)建斷點
在上面的例子中,我們通過在源碼頁面器的滾槽 16 上點擊來創(chuàng)建斷點垮兑。你可以通過把斷點拖拽出滾槽冷尉,然后釋放鼠標來刪除斷點 (消失時會有一個非常可愛的噗的一下的動畫)系枪。你也可以在斷點導航頁選擇斷點雀哨,然后按下刪除鍵刪除。
要在調試器中創(chuàng)建斷點,可以使用breakpoint set
命令雾棺。
(lldb) breakpoint set -f main.m -l 16
Breakpoint 1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab
也可以在一個符號 (C 語言函數(shù)) 上創(chuàng)建斷點膊夹,而完全不用指定哪一行
(lldb) b isEven
Breakpoint 3: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00
(lldb) br s -F isEven
Breakpoint 4: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00
如果你 Xcode 的 UI 上右擊任意斷點,然后選擇 "Edit Breakpoint" 的話捌浩,會有一些非常誘人的選擇放刨。
你也可以添加多個行為,可以是調試器命令尸饺,shell 命令进统,也可以是更直接的打印:
UI操作
更新UI
(lldb) e id $myView = (id)0x7f82b1d01fd0
然后在調試器中改變它的背景色:
(lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]
但是只有程序繼續(xù)運行之后才會看到界面的變化侵佃。因為改變的內容必須被發(fā)送到渲染服務中麻昼,然后顯示才會被更新奠支。
渲染服務實際上是一個另外的進程 (被稱作 backboardd)馋辈。這就是說即使我們正在調試的內容所在的進程被打斷了,backboardd 也還是繼續(xù)運行著的倍谜。
這意味著你可以運行下面的命令迈螟,而不用繼續(xù)運行程序:
(lldb) e (void)[CATransaction flush]
Push 一個 View Controller
(lldb) e id $nvc = [[[UIApplication sharedApplication] keyWindow] rootViewController]
(lldb) e id $vc = [UIViewController new]
(lldb) e (void)[[$vc view] setBackgroundColor:[UIColor yellowColor]]
(lldb) e (void)[$vc setTitle:@"Yay!"]
(lldb) e (void)[$nvc pushViewContoller:$vc animated:YES]
(lldb) caflush // e (void)[CATransaction flush]
查找按鈕的 target
(lldb) po [$myButton allTargets]
{(
<MagicEventListener: 0x7fb58bd2e240>
)}
(lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0]
<__NSArrayM 0x7fb58bd2aa40>(
_handleTap:
)
觀察實例變量的變化
假設你有一個 UIView,不知道為什么它的 _layer 實例變量被重寫了 (糟糕)尔崔。因為有可能并不涉及到方法答毫,我們不能使用符號斷點。相反的季春,我們想監(jiān)視什么時候這個地址被寫入洗搂。
首先,我們需要找到 _layer 這個變量在對象上的相對位置:
(lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer"))
(ptrdiff_t) $0 = 8
現(xiàn)在我們知道 ($myView + 8) 是被寫入的內存地址:
(lldb) watchpoint set expression -- (int *)$myView + 8
Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabled type = w
new value: 0x0000000000000000
這被以 wivar $myView _layer 加入到 Chisel 中载弄。
非重寫方法的符號斷點
假設你想知道 -[MyViewController viewDidAppear:] 什么時候被調用耘拇。如果這個方法并沒有在MyViewController 中實現(xiàn),而是在其父類中實現(xiàn)的宇攻,該怎么辦呢惫叛?試著設置一個斷點,會出現(xiàn)以下結果
(lldb) b -[MyViewController viewDidAppear:]
Breakpoint 1: no locations (pending).
WARNING: Unable to resolve breakpoint to any actual locations.
因為 LLDB 會查找一個符號逞刷,但是實際在這個類上卻找不到嘉涌,所以斷點也永遠不會觸發(fā)。你需要做的是為斷點設置一個條件 [self isKindOfClass:[MyViewController class]]夸浅,然后把斷點放在 UIViewController 上仑最。正常情況下這樣設置一個條件可以正常工作。但是這里不會帆喇,因為我們沒有父類的實現(xiàn)词身。
viewDidAppear: 是蘋果實現(xiàn)的方法,因此沒有它的符號番枚;在方法內沒有 self 法严。如果想在符號斷點上使用 self损敷,你必須知道它在哪里 (它可能在寄存器上肄扎,也可能在棧上无牵;在 x86 上,你可以在 $esp+4 找到它)罕模。但是這是很痛苦的溯街,因為現(xiàn)在你必須至少知道四種體系結構 (x86诱桂,x86-64,armv7呈昔,armv64)挥等。想象你需要花多少時間去學習命令集以及它們每一個的調用約定,然后正確的寫一個在你的超類上設置斷點并且條件正確的命令堤尾。幸運的是肝劲,這個在 Chisel 被解決了。這被成為 bmessage:
(lldb) bmessage -[MyViewController viewDidAppear:]
Setting a breakpoint at -[UIViewController viewDidAppear:] with condition (void*)object_getClass((id)$rdi) == 0x000000010e2f4d28
Breakpoint 1: where = UIKit`-[UIViewController viewDidAppear:], address = 0x000000010e11533c
LLDB 和 Python
LLDB 有內建的郭宝,完整的 Python 支持辞槐。在LLDB中輸入 script,會打開一個 Python REPL粘室。你也可以輸入一行 python 語句作為 script 命令 的參數(shù)榄檬,這可以運行 python 語句而不進入REPL:
(lldb) script import os
(lldb) script os.system("open http://www.objc.io/")
這樣就允許你創(chuàng)造各種酷的命令。把下面的語句放到文件 ~/myCommands.py 中:
def caflushCommand(debugger, command, result, internal_dict):
debugger.HandleCommand("e (void)[CATransaction flush]")
然后再 LLDB 中運行:
command script import ~/myCommands.py
常用命令
frame variable
查看當前棧幀里幀參數(shù)和本地變量
register read
查看寄存器的值
register read $arg1 $arg2
查看兩個變量
memory read
查看內存地址的命令
(lldb) memory read --size 8 --format A --count 10 0x0000000100004630
memory write
內存值的修改
up down
stack frame 跳棧幀
bt
顯示所有的調用棧幀衔统。該命令可用來顯示函數(shù)的調用順序鹿榜。
disassemble --frame
disassemble相關指令用于輸出某段程序的匯編代碼 匯編當前棧幀的代碼。
使用disassemble -b則會輸出帶字節(jié)信息的匯編代碼: