前言
LLDB是個開源的內(nèi)置于XCode的具有REPL(read-eval-print-loop)特征的Debugger,其可以安裝C++或者Python插件。在日常的開發(fā)和調(diào)試過程中給開發(fā)人員帶來了非常多的幫助。
(lldb)po std.name
Noskthing
了解并熟練掌握LLDB的使用是非常有必要的。這篇文章將會為大家總結(jié)日常高頻使用的一些技巧。文章分節(jié)的主要依據(jù)是功能的相關(guān)性,并且省略了很多Xcode已經(jīng)集成并且可視化的操作。
- 一些基礎(chǔ)
- 斷點相關(guān)
- 參數(shù)檢查
- expression指令
- hook的概念
- 流程控制
- script
- image
- register
- 結(jié)語
一些基礎(chǔ)
LLDB的基本語法如下
<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]
其中內(nèi)置了非常多的功能,選擇去硬背每一條指令并不是一個明智的選擇。我們只需要記住一些常用的指令,在需要的時候通過help命令來查看相關(guān)的描述即可。
(lldb)help
Debugger commands:
apropos -- List debugger commands related to a word or subject.
breakpoint -- Commands for operating on breakpoints (see 'help b' for shorthand.)
...
還可以通過apropos來獲取具體命令的合法參數(shù)信息以及含義
(lldb) apropos breakpoint
The following commands may relate to 'breakpoint':
_regexp-break -- Set a breakpoint using one of
several shorthand formats.
_regexp-tbreak -- Set a one-shot breakpoint using one
of several shorthand formats.
...
斷點相關(guān)
Xcode本身已經(jīng)將大部分的操作用UI展示了出來,比如說
- Breakpoint Navigator (? + 7)
- Debug Navigator (? + 6)
- Debug Area (? + Shift + Y)
-
Debug menu item
斷點列表
日常開發(fā)中大部分有關(guān)斷點的操作我們都可以不使用命令行直接通過Xcode的可視化操作來實現(xiàn),命令行的操作似乎是一種多余。但是使用(lldb)help breakpoint查看一下LLDB提供的所有幫助,你會發(fā)現(xiàn)在命令行中使用LLDB能夠給予我們更多更詳細(xì)的調(diào)試信息以及更廣闊的操作空間。
(lldb)help breakpoint
舉一個簡單的例子,我們需要為某一個函數(shù)設(shè)置一個斷點。比如說給ViewController的VviewDidLoad方法設(shè)置一個斷點。這對于Xcode而言非常的簡單。

編輯每一個斷點的各個選項也因為可視化的操作而變得非常的簡單。但是如果我們需要在系統(tǒng)調(diào)用的某個函數(shù)里設(shè)置斷點呢,抑或某個函數(shù)我們只能在crash log茫茫碌的堆棧信息里才能看到一點它的痕跡,這個時候如何操作呢?

假設(shè)我們現(xiàn)在需要給objc_msgSend函數(shù)設(shè)置斷點。首先先想辦法獲取objc_msgSend的地址。我們在Appdelegate.m文件給函數(shù)- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions打一個斷點,運行程序如下圖所示。

我們可以通過
(lldb)br set -a 0x0000000103c04ac0來為objc_msgSend()設(shè)置一個斷點。輸入continue繼續(xù)執(zhí)行你會發(fā)現(xiàn)如果程序再次調(diào)用objc_msgSend()會暫停。
Tips
* 圖片是用模擬器運行所以是x86,移動設(shè)備是arm。兩者的指令有所不同。圖中的callq指令對應(yīng)arm中的bl
* 由于 ASLR(地址空間配置隨機(jī)載入) 的原因地址是不固定的,所以圖中objc_msgSend()的地址在你的機(jī)器上是不可用的。
斷點相關(guān)的指令很多很雜,這里為大家列舉一些常用的。如果以后遇到一些特殊的需求,可以借助help()指令來自行查找相關(guān)指令。
設(shè)置斷點
- 給所有名為xx的函數(shù)設(shè)置一個斷點
(lldb)breakpoint set —name xx
(lldb)br s -n xx
(lldb)b xx
- 在文件F指定行L設(shè)置斷點
(lldb)breakpoint set —file F —line L
(lldb)br s -f F -l L
(lldb)b F:L
- 給所有名為xx的C++函數(shù)設(shè)置一個斷點(希望沒有同名的C函數(shù))
(lldb)breakpoint set —method xx
(lldb)br s -M xx
- 給一個OC函數(shù)[objc msgSend:]設(shè)置一個斷點
(lldb)breakpoint set —name “[objc msgSend:]”
(lldb)b -n “[objc msgSend:]”
- 給所有名為xx的OC方法設(shè)置一個斷點(希望沒有名為xx的C或者C++函數(shù))
(lldb)breakpoint set —selector xx
(lldb)br s -S count
- 給所有函數(shù)名正則匹配成功的函數(shù)設(shè)置一個斷點
(lldb)breakpoint set --func-regex regular-expression
(lldb)br s -r regular-expression
- 給指定函數(shù)地址func_addr的位置設(shè)置一個斷點
(lldb)br set -a func_addr
斷點查看
(lldb)breakpoint list
(lldb)br l
斷點刪除
(lldb)breakpoint delete index
(lldb)br del index
index指明斷點的序號,如果為空則刪除所有斷點
watchpoint
iOS開發(fā)當(dāng)中有一個重要的概念KVO,我們會給一個重要的變量設(shè)置一個觀察者,用以在它發(fā)生變化的時候做出相應(yīng)的操作。在調(diào)試過程中我們也可以借助LLDB來監(jiān)視某個變量或某一塊內(nèi)存的讀寫情況。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSString * str = @"First";
[self printString:str];
str = @"Second";
[self printString:str];
}
- (void)printString:(NSString *)str
{
NSLog(@"%@",str);
}
我們利用watchpoint指令來監(jiān)視變量str。需要重點說明的是-w選項,下例中并沒有寫出,缺省值是write,這意味著只有在str被寫入的時候程序會暫停。
(lldb) watchpoint set variable str
Watchpoint created: Watchpoint 1: addr = 0x7fff5997f9e8 size = 8 state = enabled type = w
declare @ '/Users/noskthing/Desktop/LLDBTest/LLDBTest/ViewController.m:22'
watchpoint spec = 'str'
new value: 0x0000000106280078
2017-07-22 17:35:13.534 LLDBTest[4585:521823] First
Watchpoint 1 hit:
old value: 0x0000000106280078
new value: 0x0000000106280098
(lldb) image lookup -a 0x0000000106280098
Address: LLDBTest[0x0000000100003098] (LLDBTest.__DATA.__cfstring + 32)
Summary: @"Second"
(lldb) image lookup -a 0x0000000106280078
Address: LLDBTest[0x0000000100003078] (LLDBTest.__DATA.__cfstring + 0)
Summary: @"First"
當(dāng)你輸入watchpoint list查看設(shè)置的watchpoint時系統(tǒng)會提示你當(dāng)前測試的機(jī)器允許設(shè)置的最大個數(shù)。
(lldb) watchpoint list
Number of supported hardware watchpoints: 4
No watchpoints currently set.
參數(shù)檢查
當(dāng)我們調(diào)試程序遇到斷點的時候Xcode會自動的將當(dāng)前作用域下的局部變量以及全局變量展示出來

借助命令行我們也能夠輕松的獲取這些參數(shù)的信息
- 展示當(dāng)前作用域下的參數(shù)和局部變量
(lldb)frame variable
(lldb)fr v
- 展示當(dāng)前作用域下的局部變量
(lldb)frame variable --no-args
(lldb)fr v -a
- 展示指定變量var的具體內(nèi)容
(lldb)frame variable *var*
(lldb)fr v *var*
(lldb)p *var*
- 展示當(dāng)前對象的全局變量
(lldb)target variable
(lldb)ta v
細(xì)心的朋友應(yīng)該能夠有所發(fā)現(xiàn),這些操作都有一個局限:我們查看的各個變量都是當(dāng)前作用域的。這意味著程序遇到斷點的時候暫停,所有的操作都是局限于當(dāng)前函數(shù)以及當(dāng)前函數(shù)所在線程的內(nèi)部。可視化的操作并沒有給我們太多操作的空間,但是借助命令行我們可以打破這樣一個局限。
命令行輸入(lldb)thread backtrace可以獲取當(dāng)前線程函數(shù)的調(diào)用棧
(lldb)thread backtrace
* frame #0: 0x0000000100057204 test`-[ViewController viewDidLoad](self=0x000000014fe0ad10, _cmd=<unavailable>) at ViewController.m:99 [opt]
frame #1: 0x000000018e1cfec0 UIKit`-[UIViewController loadViewIfRequired] + 1036
frame #2: 0x000000018e1cfa9c UIKit`-[UIViewController view] + 28
frame #3: 0x000000018e1d631c UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 76
frame #4: 0x000000018e1d37b8 UIKit`-[UIWindow _setHidden:forced:] + 272
frame #5: 0x000000018e245224 UIKit`-[UIWindow makeKeyAndVisible] + 48
輸入frame select指令我們可以任意的去選擇一個作用域去查看。
(lldb)frame select 2
類比frame的操作我們可以輕松看出線程選擇相關(guān)的操作
(lldb) thread list
Process 21035 stopped
* thread #1: tid = 0x27361a, 0x00000001892df224 libsystem_kernel.dylib`mach_msg_trap + 8, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
thread #2: tid = 0x273639, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
thread #3: tid = 0x27363a, 0x00000001893c2ca8 libsystem_pthread.dylib`start_wqthread
thread #4: tid = 0x27363e, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
thread #5: tid = 0x27363f, 0x00000001892df224 libsystem_kernel.dylib`mach_msg_trap + 8, name = 'com.apple.uikit.eventfetch-thread'
thread #6: tid = 0x273640, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
thread #7: tid = 0x273641, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
thread #8: tid = 0x273642, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
thread #10: tid = 0x273646, 0x00000001892df224 libsystem_kernel.dylib`mach_msg_trap + 8, name = 'com.apple.NSURLConnectionLoader'
thread #11: tid = 0x273644, 0x00000001892df224 libsystem_kernel.dylib`mach_msg_trap + 8, name = 'AFNetworking'
thread #12: tid = 0x27364a, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
thread #13: tid = 0x27364b, 0x00000001892fd23c libsystem_kernel.dylib`__select + 8, name = 'com.apple.CFSocket.private'
(lldb) thread select 2
以上提到的幾個指令意味著借助命令行,我們可以在斷點發(fā)生的時候跳轉(zhuǎn)到當(dāng)前存在的任一線程里的任一作用域去進(jìn)行操作。
除卻frame的操作,我們很多時候習(xí)慣借助NSLog去打印某些關(guān)鍵的信息。如果運行到一半的時候發(fā)現(xiàn)漏寫了某個地方的NSLog,加入相關(guān)代碼并重新運行也許不是一個讓人省心的方法。我們可以在需要打印的地方設(shè)置一個斷點,然后運行p object或者po object指令來查看指定對象。
(lldb) p userInfo
(__NSDictionaryM *) $0 = 0x0000000174242010 4 key/value pairs
(lldb) po userInfo
{
macAddressString = "60:01:94:80:37:6c";
payload = {
TimerAction = 0;
TimerStat = 0;
brightness = 70;
colortemp = 93;
remaining = "-1";
switch = 1;
};
serialNumberString = 60019480376C;
tcpPortString = "192.168.199.124";
}
兩個指令實際都是expression指令的縮寫。p打印的是當(dāng)前對象的地址而po則會調(diào)用對象的description方法,做法和NSLog是一致的。
expression指令
expression命令是執(zhí)行一個表達(dá)式,并將表達(dá)式返回的結(jié)果輸出。包括上文提到的p指令在內(nèi),以下幾個都是expression指令的別名。
(lldb)expression userInfo
(__NSDictionaryM *) $5 = 0x0000000174242010 4 key/value pairs
(lldb) p userInfo
(__NSDictionaryM *) $2 = 0x0000000174242010 4 key/value pairs
(lldb) print userInfo
(__NSDictionaryM *) $3 = 0x0000000174242010 4 key/value pairs
(lldb) e userInfo
(__NSDictionaryM *) $4 = 0x0000000174242010 4 key/value pairs
(lldb) call userInfo
(__NSDictionaryM *) $5 = 0x0000000174242010 4 key/value pairs
打印對象的時候我們也可以指定特定格式,詳細(xì)的格式查閱參見這里。
(lldb) p 16
16
(lldb)p/x 16
0x10
(lldb) p/t 16
0b00000000000000000000000000010000
(lldb) p/t (char)16
0b00010000
但是expression指令真正強(qiáng)大的部分應(yīng)該是它的寫入能力。我們可以通過expression來執(zhí)行一個表達(dá)式動態(tài)的修改我們程序中變量的值。
(lldb) p count
(NSUInteger) $4 = 12
(lldb)e count = 42
(lldb) p count
(NSUInteger) $5 = 42
在斷點處我們首先打印count變量的值,之后通過執(zhí)行expression指令來修改count變量,再次打印可以發(fā)現(xiàn)此時count已經(jīng)被修改。這對于調(diào)試時模擬一些極端情況非常的有幫助。這里有一個特殊一點的情況需要指明,如果你嘗試通過expression來修改UI可能會失效。
(lldb)expression -- self.view.backgroundColor = [UIColor redColor]
因為執(zhí)行斷點會打斷更新UI的進(jìn)程導(dǎo)致你的修改沒有及時渲染出來,執(zhí)行flush命令可以讓機(jī)器渲染出你修改后的界面。
實際上一些復(fù)雜的調(diào)試操作單單靠每次命令行去手動輸入指令是非常的繁瑣的,僅僅依靠單條指令和它提供的參數(shù)選項在一些針對界面的調(diào)試上并不能給予我們足夠多的支持。令人興奮的是facebook開源的Chisel為我們提供了更多實用的功能。整個開源庫是用Python實現(xiàn)的,基于LLDB 內(nèi)建的,完整的 Python 支持。這一部分我們后面聊到script指令再細(xì)細(xì)探討。
hook的概念
hook翻譯成中文是鉤子的意思。這個名詞在我從事iOS開發(fā)的過程中確實沒有太多的接觸,第一次碰到是在學(xué)習(xí)Flask框架時遇到的請求鉤子。我并不覺得鉤子的中文翻譯對于我們理解有所幫助,在我初學(xué)的階段甚至給我產(chǎn)生了一定的誤解,所以我后續(xù)還是以hook來描述。
簡單來說hook一個處理消息的程序段,通過系統(tǒng)調(diào)用,把它掛入系統(tǒng)。每當(dāng)特定的消息發(fā)出,在沒有到達(dá)目的窗口前,鉤子程序就先捕獲該消息,此時hook函數(shù)先得到控制權(quán)。這時hook函數(shù)即可以加工處理(改變)該消息,也可以不作處理而繼續(xù)傳遞該消息,還可以強(qiáng)制結(jié)束消息的傳遞。這意味著借助hook函數(shù)我們可以在指定在某些特殊的情況下做出一些包括但不限于參數(shù)驗證,消息攔截等操作來查驗當(dāng)前情況和修改后續(xù)程序的運行。
在LLDB中常見的操作有以下這些。本身指令并不復(fù)雜,但配合上其它的指令確實在某些情況下能節(jié)省我們很多的精力。
- 設(shè)置一個stop-hook用以在每次斷點被觸發(fā)時執(zhí)行
(lldb)target stop-hook add --one-liner stop-hook
- 設(shè)置一個stop-hook用以在指定函數(shù)func內(nèi)的斷點被觸發(fā)時執(zhí)行
(lldb) target stop-hook add --name func --one-liner stop-hook
- 設(shè)置一個stop-hook用以在名為className的C類的斷點被觸發(fā)時執(zhí)行
(lldb)target stop-hook add -- className MyClass --one-liner stop-hook

流程控制
Xcode已經(jīng)為我們提供了可視化的工具,但是如果你習(xí)慣了命令行操作不希望雙手離開鍵盤降低你的效率,了解一下也是很有幫助的。

- 繼續(xù)
(lldb)process continue
(lldb)continue
(lldb)c
- 下一步
(lldb)thread step-over
(lldb)next
(lldb)n
- 進(jìn)入
(lldb)thread step-in
(lldb)step
(lldb)s
- 跳出
(lldb)thread step-out
(lldb) finish
(lldb)f
除此以外我們還可以通過Thread return來控制流程。該指令有一個可選參數(shù),在執(zhí)行時它會把可選參數(shù)加載進(jìn)返回寄存器里,然后立刻執(zhí)行返回命令,跳出當(dāng)前棧幀。這意味這函數(shù)剩余的部分不會被執(zhí)行。當(dāng)然這也可能會給 ARC 的引用計數(shù)造成一些問題,或者會使函數(shù)內(nèi)的清理部分失效。但是在函數(shù)的開頭執(zhí)行這個命令,是個非常好的隔離這個函數(shù),偽造返回值的方一個方法。
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
if ([self isEvenNumber:2])
{
NSLog(@"First");
}
else
{
NSLog(@"Second");
}
}
- (BOOL)isEvenNumber:(NSInteger)num
{
if (num % 2 == 0)
{
return YES;
}
else
{
return NO;
}
}
我們在isEvenNumber:函數(shù)中設(shè)置斷點利用thread return函數(shù)返回NO。
(lldb) thread return NO
(lldb) c
Process 4784 resuming
2017-07-22 18:48:14.654 LLDBTest[4784:569378] Second
script
LLDB 有內(nèi)建的,完整的 Python支持。在LLDB中輸入 script,會打開一個 Python REPL。你也可以輸入一行 python 語句作為 script 命令的參數(shù),這可以運行 python 語句而不進(jìn)入REPL
(lldb) script print 'Hello World'
Hello World
借助LLDB提供的Python API我們可以實現(xiàn)很多復(fù)雜的功能。這里列舉一個簡單的例子,將以下內(nèi)容寫入~/myCommands.py文件
def caflushCommand(debugger, command, result, internal_dict):
debugger.HandleCommand("e (void)[CATransaction flush]")
在LLDB中執(zhí)行
command script import ~/myCommands.py
或者將這條指令寫入~ /.lldbinit中,每次進(jìn)入LLDB都會自動執(zhí)行這些函數(shù)。
如果沒有~ /.lldbinit 終端執(zhí)行
touch ~ /.lldbinit生成文件
你可以在這里提前設(shè)置好一些指令,然后disable。調(diào)試過程中再設(shè)置enable打開。相信經(jīng)過整理之后LLDB會讓你的調(diào)試如魚得水。
Facebook開源的Chisel就是基于此實現(xiàn)。我們通過brew安裝Chisel
brew install Chisel

在fblldbbase.py文件中定義了各個基礎(chǔ)類,fblldb.py負(fù)責(zé)遍歷commands文件夾里的各個類來加載自定義的指令。在Chisel基礎(chǔ)上我們也可以輕松的自定義指令。在commands文件夾內(nèi)新建py文件,實現(xiàn)函數(shù)lldbcommands返回一個數(shù)組,包含對象的類都是FBCommand的子類。
def lldbcommands():
return [
FBPrintAccessibilityLabels()
]
class FBPrintAccessibilityLabels(fb.FBCommand):
def name(self):
return 'pa11y'
def description(self):
return 'Print accessibility labels of all views in hierarchy of <aView>'
def args(self):
return [ fb.FBCommandArgument(arg='aView', type='UIView*', help='The view to print the hierarchy of.', default='(id)[[UIApplication sharedApplication] keyWindow]') ]
def run(self, arguments, options):
forceStartAccessibilityServer();
printAccessibilityHierarchy(arguments[0])
每一個類都繼承自FBCommand,我們需要分別復(fù)寫以下幾個函數(shù)
- def name()
返回一個字符串表示指令的名稱 - def description()
返回一個字符串表示指令的描述 - def args()
返回一個數(shù)組,其中的對象都是類FBCommandArgument構(gòu)建的,每一個對象表示一個指令的選項參數(shù)。 - def run()
指令的具體操作
image
image指令是target module指令的縮寫,借助它我們能夠查看當(dāng)前的Binary Images相關(guān)的信息。日常開發(fā)我們主要利用它尋址。
在日常開發(fā)的過程中,我們會收集到用戶各式各樣的crash log。log中會為我們提供崩潰前函數(shù)棧的運行情況,每一個函數(shù)都會對應(yīng)一個函數(shù)地址。

要解決問題首先我們需要確定的是程序最后調(diào)用了什么函數(shù)。由于ALSR的原因crash log中的函數(shù)地址我們不能夠直接的去使用,我們需要在測試的機(jī)器上自己去計算出對應(yīng)的函數(shù)地址。一般情況下crash log中會附帶一個Binary Images。我們要利用這個來計算出每一個函數(shù)地址相對于所在框架的偏移量。

之后利用
image指令來查看本機(jī)的Binary Images。
(lldb) image list
[ 0] 48EA38EC-6E36-3E77-A680-A4D04D3D3868 0x00000001014ac000 /Users/noskthing/Library/Developer/Xcode/DerivedData/LLDBTest-dfmaxwkizubjskftkbnlfzumauje/Build/Products/Debug-iphonesimulator/LLDBTest.app/LLDBTest
[ 1] 322C06B7-8878-311D-888C-C8FD2CA96FF3 0x0000000107c66000 /usr/lib/dyld
[ 2] 14AD0238-D077-378B-82A8-AC2D2ADC9DDF 0x00000001014b4000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/dyld_sim
[ 3] 61CD1144-BB93-3571-BDB3-9F9B56CECFFE 0x0000000101543000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//System/Library/Frameworks/Foundation.framework/Foundation
[ 4] 5F0E622C-86EC-3969-ACFB-CAAA10E21A31 0x0000000101a76000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//usr/lib/libobjc.A.dylib
有了本機(jī)的Binary Images我們就可以通過之前計算出的偏移量來獲取本機(jī)對應(yīng)函數(shù)的地址。通過image lookup指令查找對應(yīng)地址的函數(shù)就可以確定崩潰前究竟執(zhí)行了哪些函數(shù)
(lldb) image lookup -a 0x1025dd00a
Address: UIKit[0x00000000001cb00a] (UIKit.__TEXT.__text + 1869978)
Summary: UIKit`-[UIViewController loadViewIfRequired] + 1219
register
register指令能夠獲取和修改各個寄存器的信息。
我們需要明白一個典型的CPU是由運算器、控制器、寄存器等器件構(gòu)成的,而寄存器進(jìn)行的就是信息存儲。我們利用匯編語言來操作寄存器。

這里是蘋果官方文檔,介紹的是armv6。需要注意的是自從iPhone 5s之后已經(jīng)全部換到64-bit,在arm64下整數(shù)寄存器的個數(shù)已經(jīng)增加到31個。我們可以通過register read來進(jìn)行查看。

其中x0-x7八個寄存器是用來保存參數(shù)的。objc_msgSend會有兩個默認(rèn)參數(shù),這也就意味著x0保存的是self,x1保存的是_cmd。fr對應(yīng)frame point,lr對應(yīng)link point,在匯編中分別為x29,x30。最近正在準(zhǔn)備一篇從匯編的層面分析objc_msgSend的文章,會在那里結(jié)合官方文檔詳細(xì)介紹包括函數(shù)調(diào)用過程以及各個寄存器的作用。有興趣的朋友可以先關(guān)注一下作者:)
利用runtime動態(tài)調(diào)用Objective-C任意對象的任意方法,需要為NSInvocation設(shè)置參數(shù)。參數(shù)的index就是從2開始的。具體的實現(xiàn)可以參考Github-Tools中的NSObject+Runtime這個Category的實現(xiàn)。
雖然我們更多的時候只是借助read指令來獲取一下當(dāng)前各個寄存器的信息,但是對于一些替換參數(shù),模擬特殊輸入的需求,write指令也是非常的有幫助。
實現(xiàn)一個簡單的例子。
- (void)viewDidLoad {
[super viewDidLoad];
NSString * str = [NSString stringWithFormat:@"First"];
NSString * str1 = [NSString stringWithFormat:@"Second"];
[self printString:str];
[self printString:str1];
}
- (void)printString:(NSString *)str
{
NSLog(@"%@",str);
}
函數(shù)非常的簡單,會依次打印出First和Second。我們首先在第一次調(diào)用printString:之前打印一個斷點,調(diào)用frame variable來查看一下當(dāng)前兩個參數(shù)的地址.
(lldb) frame variable
(ViewController *) self = 0x000000010090ac30
(SEL) _cmd = <variable not available>
(NSTaggedPointerString *) str = 0xa000074737269465 @"First"
(NSTaggedPointerString *) str1 = 0xa00646e6f6365536 @"Second"
如果你的參數(shù)是unused編譯器會把它優(yōu)化掉,這樣你就無法獲取它的地址。注意NSString的創(chuàng)建方式,字符串常量創(chuàng)建會把str分配到常量區(qū),查看參數(shù)會得到
<variable not available>的提示。
之后在printString:中的NSLog之前設(shè)置一個斷點,continue。在printString:中遇到斷點的時候我們執(zhí)行register read指令。
(lldb) register read
General Purpose Registers:
x0 = 0x000000010090ac30
x1 = 0x000000010000995f "printString:"
x2 = 0xa000074737269465
x3 = 0x000000016fdfd876
x4 = 0x0000000000000000
x5 = 0x0000000000000000
x6 = 0x0000000000000064
x7 = 0x0000000000000000
x8 = 0x00000001ae36bc20 libsystem_pthread.dylib`_thread + 224
x9 = 0x00000001ae364fec runtimeLock + 28
x10 = 0x00000001ae364ff0 runtimeLock + 32
x11 = 0x003c6d01003c6d80
x12 = 0x0000000000000000
x13 = 0x00000000003c6d00
x14 = 0x00000000003c6e00
x15 = 0x00000000003c6dc0
x16 = 0x00000000003c6d01
x17 = 0x0000000100007250 test`-[ViewController printString:] at ViewController.m:103
x18 = 0x0000000000000000
x19 = 0x000000010090ac30
x20 = 0xa00646e6f6365536
x21 = 0xa000074737269465
x22 = 0x000000010000995f "printString:"
x23 = 0x0000000000000000
x24 = 0x0000000000000010
x25 = 0x0000000000000258
x26 = 0x000000018ed0e90e "window"
x27 = 0x0000000000000001
x28 = 0x0000000000000000
fp = 0x000000016fdfddc0
lr = 0x000000010000721c test`-[ViewController viewDidLoad] + 156 at ViewController.m:100
sp = 0x000000016fdfddb0
pc = 0x000000010000725c test`-[ViewController printString:] + 12 at ViewController.m:106
cpsr = 0x60000000
對比地址可以發(fā)現(xiàn)x0保存的是viewController的地址,x1注明了是函數(shù)printString:的地址,而x2就是str的地址。我們通過register write指令來修改x2的值。
(lldb) register write x2 0xa00646e6f6365536
contine之后你會發(fā)現(xiàn)打印出的不是First而是Second。
如果有朋友對匯編和函數(shù)調(diào)用感興趣,我會在之后結(jié)合objc_msgSend匯編部分的代碼在另一篇文章里來做個介紹。
結(jié)語
文章的目的是希望給大家展示LLDB強(qiáng)大的能力以及命令行的優(yōu)點,但實際以上篇幅介紹的只是冰山一角。希望這篇文章能夠給大家一些幫助,來更多的了解LLDB。
以下是一些有關(guān)LLDB的資料和文檔
