LLDB探究

一、LLDB 是什么?

LLDB是Mac OS X上Xcode的默認(rèn)調(diào)試器,支持在桌面和iOS設(shè)備和模擬器上調(diào)試C ,Objective-C和C++。它是新一代高性能調(diào)試器,它可以高效利用LLVM項(xiàng)目中的現(xiàn)有庫(kù),例如Clang表達(dá)式解析器和LLVM反匯編程序。

隨著Xcode 5的發(fā)布,LLDB調(diào)試器已經(jīng)取代了GDB,成為了Xcode工程中默認(rèn)的調(diào)試器。它與LLVM編譯器一起,帶給我們更豐富的流程控制和數(shù)據(jù)檢測(cè)的調(diào)試功能。LLDB為Xcode提供了底層調(diào)試環(huán)境,其中包括內(nèi)嵌在Xcode IDE中的位于調(diào)試區(qū)域的控制面板。

Chisel是facebook下一個(gè)開(kāi)源LLDB命令集合,由于我這里調(diào)試的是Xcode自帶的LLDB命令,如果想探究Chisel的請(qǐng)移步到文章最下方查看文章相關(guān)鏈接。

與此同時(shí),讓我們以在調(diào)試器中打印變量來(lái)開(kāi)始我們的旅程吧。

二、基礎(chǔ)命令

image.png

這是一個(gè)簡(jiǎn)單加了斷點(diǎn)的程序,程序會(huì)在這一行停止運(yùn)行,并且控制臺(tái)會(huì)被打開(kāi),允許我們和調(diào)試器交互。這時(shí)候我們應(yīng)該打些什么命令呢?

1、幫助 help

最簡(jiǎn)單命令是help,它會(huì)列舉出所有的命令。如果你忘記了一個(gè)命令是做什么的,或者想知道更多的話,你可以通過(guò)help <command-name>來(lái)了解更多細(xì)節(jié),例如help print或者help thread。只需要在控制臺(tái)上圖lldb字樣的地方鍵入help即可。

image.png
2、打印對(duì)象 print 和 po

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

image.png

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

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

打印復(fù)雜對(duì)象時(shí),print可能顯得力不從心 ,我們想看的是對(duì)象的 description 方法的結(jié)果,這時(shí)可以使用po,po其實(shí)是e -o --的別名。

甚至可以給print 指定不同的打印格式。它們都是以print/<fmt>或者簡(jiǎn)化的p/<fmt>格式書(shū)寫。

//默認(rèn)的格式
(lldb) p 16
(int) $3 = 16
//十六進(jìn)制:
(lldb) p/x 16
(int) $4 = 0x00000010
//二進(jìn)制 (t 代表 two):
(lldb) p/t 16
(int) $5 = 0b00000000000000000000000000010000
(lldb) p/t (char)16
(char) $6 = 0b00010000
3、修改對(duì)象 expression

如果想改變一個(gè)值怎么辦?我們要用到的是expression這個(gè)方便的命令。

image.png

上圖中修改了num的值,斷點(diǎn)步進(jìn)后可以看到NSLog的對(duì)應(yīng)值已經(jīng)發(fā)生了變化。

三、變量

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

(lldb) e int $a = 2
(lldb) p $a * 19
(int) $1 = 38
(lldb) e NSArray *$array = @[ @"Saturday", @"Sunday", @"Monday" ]
(lldb) p [$array count]
(NSUInteger) $2 = 3
(lldb) po [[$array objectAtIndex:0] uppercaseString]
SATURDAY
(lldb) p (char)[[$array objectAtIndex:$a] characterAtIndex:0]
(char) $4 = 'M'

四、UI調(diào)試

因?yàn)槿肿兞渴强稍L問(wèn)的,可以像這樣打印整個(gè)視圖層級(jí):

(lldb) po [[[UIApplication sharedApplication] keyWindow] recursiveDescription]
<UIWindow: 0x7fb3fbf05b70; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x60000165f1b0>; layer = <UIWindowLayer: 0x600001809a80>>
   | <UITransitionView: 0x7fb3fbc19e30; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x60000182f320>>
   |    | <UIDropShadowView: 0x7fb3fbc1a570; frame = (0 0; 375 667); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x60000182f3e0>>
   |    |    | <UIView: 0x7fb3ff116c30; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x600001802e00>>
   |    |    |    | <UIView: 0x7fb3fbe0a090; frame = (87 384; 240 128); autoresize = RM+BM; layer = <CALayer: 0x600001835e00>>
更新UI

就像上文變量中提到那樣,我們可以拿到這個(gè)view:

(lldb) expression id $myView = (id)0x7fb3fbe0a090

嘗試做一些修改:

(lldb) expression (void)[$myView setBackgroundColor:[UIColor redColor]]

但是只有程序繼續(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) expression (void)[CATransaction flush]

這個(gè)時(shí)候就能看到背景顏色的改變了。

五、流程控制

通過(guò)xcode加斷點(diǎn)調(diào)試時(shí),調(diào)試條上回出現(xiàn)四個(gè)可以控制程序執(zhí)行流程的按鈕:

image.png

從左到右分別是 continue program execution 、 step over 、 step into 和 step out。

1、 continue program execution 按鈕,會(huì)取消程序的暫停,允許程序正常執(zhí)行 (要么一直執(zhí)行下去,要么到達(dá)下一個(gè)斷點(diǎn))。

在 LLDB 中,你可以使用process continue或者thread continue命令來(lái)達(dá)到同樣的效果。

2、 step over 按鈕,會(huì)以黑盒的方式執(zhí)行一行代碼。如果所在這行代碼是一個(gè)函數(shù)調(diào)用,那么就不會(huì)跳進(jìn)這個(gè)函數(shù),而是會(huì)執(zhí)行這個(gè)函數(shù),然后繼續(xù)。

LLDB 則可以使用 thread step-over,next,或者 n 命令。

3、 step in 按鈕,可以跳進(jìn)一個(gè)函數(shù)調(diào)用來(lái)調(diào)試或者檢查程序的執(zhí)行情況。

在LLDB中使用 thread step-instep,或者 s 命令。注意,當(dāng)前行不是函數(shù)調(diào)用時(shí),nextstep 效果是一樣的。

4、step out 按鈕 ,如果你曾經(jīng)不小心跳進(jìn)一個(gè)函數(shù),但實(shí)際上你想跳過(guò)它,常見(jiàn)的反應(yīng)是重復(fù)的運(yùn)行 n 直到函數(shù)返回。其實(shí)這種情況,step out 按鈕是你的救世主。它會(huì)繼續(xù)執(zhí)行到下一個(gè)返回語(yǔ)句 (直到一個(gè)堆棧幀結(jié)束) 然后再次停止。

在LLDB中使用 thread step-out 命令。

六、斷點(diǎn)

Xcode在斷點(diǎn)導(dǎo)航中提供了一系列工具創(chuàng)建和管理斷點(diǎn),我們可以來(lái)看LLDB中等價(jià)的命令,主要是breakpoint命令。

1、查看 啟用/禁用
image.png

上圖是xcode查看斷點(diǎn)的地方,點(diǎn)擊斷點(diǎn)會(huì)開(kāi)啟或關(guān)閉斷點(diǎn)。對(duì)應(yīng)的LLDB如下:

//查看斷點(diǎn) 命令輸出列表顯示每個(gè)邏輯斷點(diǎn)都有一個(gè)整數(shù)標(biāo)識(shí)
//輸出列表中另一個(gè)信息是斷點(diǎn)位置是否是已解析的(resolved)。這個(gè)標(biāo)識(shí)表示當(dāng)與之相關(guān)的文件地址被加載到程序進(jìn)行調(diào)試時(shí),其位置是已解析的。
  //例如,如果在共享庫(kù)中設(shè)置的斷點(diǎn)之后被卸載了,則斷點(diǎn)的位置還會(huì)保留,但其不能再被解析。
(lldb) breakpoint list
Current breakpoints:
1: file = '/Users/qdd7/Documents/OC-Demo/Demo29-master/Demo29-master/main.m', line = 23, exact_match = 0, locations = 1, resolved = 1, hit count = 1

  1.1: where = Demo29-master`main + 248 at main.m:23:9, address = 0x0000000109a10728, resolved, hit count = 1 
//禁用斷點(diǎn)
(lldb) breakpoint disable 1
1 breakpoints disabled.
//啟用斷點(diǎn)
(lldb) breakpoint enable 1
1 breakpoints enabled.
2、 創(chuàng)建/刪除

在Xcode創(chuàng)建斷點(diǎn)的方式一種是 直接在代碼左邊的行數(shù)出點(diǎn)擊 即可創(chuàng)建斷點(diǎn)。對(duì)應(yīng)的LLDB如下:

//在main.m的第24行創(chuàng)建斷點(diǎn)
(lldb) breakpoint set -f main.m -l 24
Breakpoint 2: where = Demo29-master`main + 264 at main.m:28:5, address = 0x0000000109a10738
//刪除剛才的斷點(diǎn)
(lldb) breakpoint delete 2
1 breakpoints deleted; 0 breakpoint locations disabled.

七、查看 線程/調(diào)用棧 狀態(tài)

在進(jìn)程停止后,LLDB會(huì)選擇一個(gè)當(dāng)前線程和線程中當(dāng)前幀(frame)。很多檢測(cè)狀態(tài)的命令可以用于這個(gè)線程或幀。

為了檢測(cè)進(jìn)程的當(dāng)前狀態(tài),可以從以下命令thread list開(kāi)始:

(lldb) thread list
Process 13180 stopped
* thread #1: tid = 0x3cd30e, 0x00007fff5182322a libsystem_kernel.dylib`mach_msg_trap + 10, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
  thread #2: tid = 0x3cd499, 0x00007fff51824bfe libsystem_kernel.dylib`__workq_kernreturn + 10
  thread #5: tid = 0x3cd49a, 0x00007fff51824bfe libsystem_kernel.dylib`__workq_kernreturn + 10
  ...

星號(hào)(*)表示thread #1為當(dāng)前線程。為了獲取線程的跟蹤棧,可以使用以下命令thread backtrace

//默認(rèn)為當(dāng)前線程 也可以指定線程 : thread backtrace 2
(lldb) thread backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
  * frame #0: 0x00007fff5182322a libsystem_kernel.dylib`mach_msg_trap + 10
    frame #1: 0x00007fff5182376c libsystem_kernel.dylib`mach_msg + 60
    frame #2: 0x00007fff23b0caf5 CoreFoundation`__CFRunLoopServiceMachPort + 197
    ...

如果想查看所有線程的調(diào)用棧,則可以使用以下命令:thread backtrace all

檢查幀參數(shù)和本地變量的最簡(jiǎn)便的方式是使用frame variable命令:

(lldb) frame variable
(ViewController *) self = 0x00007fb7e180a5f0
(SEL) _cmd = "viewDidLoad"
(NSUInteger) num = 123
(__NSCFConstantString *) str = 0x000000010c6590b0 @"learning LLDB"
(__NSArrayI *) arr = 0x00006000036fede0 @"2 elements"

如果沒(méi)有指定任何變量名,則會(huì)顯示所有參數(shù)和本地變量。如果指定參數(shù)名或變量名,則只打印指定的值。如:

(lldb) frame variable self
(ViewController *) self = 0x00007fb7e180a5f0
image

image指令是target module指令的縮寫,借助它我們能夠查看當(dāng)前的Binary Images相關(guān)的信息。日常開(kāi)發(fā)我們主要利用它尋址。image命令的用法也挺多,首先可以用它來(lái)查看工程中使用的庫(kù),如下所示:

(lldb) image list
[  0] 4EA7C9AA-212E-3B57-9A3D-B78FB0DBC3E7 0x000000010c656000 /Users/qdd7/Library/Developer/Xcode/DerivedData/Demo29-master-bsvyynbgkgcozefqlgdaouuznpzi/Build/Products/Debug-iphonesimulator/Demo29-master.app/Demo29-master 
[  1] CE635DB2-D47E-3C05-A0A3-6BD982E7E750 0x0000000114088000 /usr/lib/dyld 
[  2] 32684ACA-A9FF-35E2-BB46-62CFF84251FE 0x000000010c662000 /Users/qdd7/Documents/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim 
...

我們還可以用它來(lái)查找可執(zhí)行文件或共享庫(kù)的原始地址,這一點(diǎn)還是很有用的,當(dāng)我們的程序崩潰時(shí),我們可以使用這條命令來(lái)查找崩潰所在的具體位置,如下所示:

NSArray *array = @[@1,@2];
NSLog(@"array 3 : %@",array[2]);
// 以下是異常代碼
2019-10-17 15:54:53.153364+0800 Demo29-master[13218:3994766] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndexedSubscript:]: index 2 beyond bounds [0 .. 1]'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff23baa1ee __exceptionPreprocess + 350
    1   libobjc.A.dylib                     0x00007fff50864b20 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff23c3cb71 _CFThrowFormattedException + 194
    3   CoreFoundation                      0x00007fff23c1b30d -[__NSArrayI objectAtIndexedSubscript:] + 93
    4   Demo29-master                       0x0000000100a16121 -[ViewController viewDidLoad] + 273
    5   UIKitCore                           0x00007fff46f03d96 -[UIViewController _sendViewDidLoadWithAppearanceProxyObjectTaggingEnabled] + 83
    6   UIKitCore                           0x00007fff46f08cef -[UIViewController loadViewIfRequired] + 1084
    ...

根據(jù)以上信息,我們可以判斷崩潰位置是在ViewController中,要想知道具體在哪一行,可以使用以下命令image lookup --address

(lldb) image lookup --address 0x0000000100a16121
      Address: Demo29-master[0x0000000100001121] (Demo29-master.__TEXT.__text + 273)
      Summary: Demo29-master`-[ViewController viewDidLoad] + 273 at ViewController.m:32

可以看到,最后定位到了ViewController.m:32行,正是我們代碼所在的位置。

文章相關(guān)鏈接

深入了解GDB和LLDB
Chisel的初步使用

最后編輯于
?著作權(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ù)。

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