iOS開發除錯 LLDB使用概覽

Mansk發表於2017-12-13

前言


LLDB是個開源的內建於XCode的具有REPL(read-eval-print-loop)特徵的Debugger,其可以安裝C++或者Python外掛。在日常的開發和除錯過程中給開發人員帶來了非常多的幫助。

(lldb)po std.name
Noskthing
複製程式碼

瞭解並熟練掌握LLDB的使用是非常有必要的。這篇文章將會為大家總結日常高頻使用的一些技巧。文章分節的主要依據是功能的相關性,並且省略了很多Xcode已經整合並且視覺化的操作。

  • 一些基礎
  • 斷點相關
  • 引數檢查
  • expression指令
  • hook的概念
  • 流程控制
  • script
  • image
  • register
  • 結語

一些基礎


LLDB的基本語法如下

<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]
複製程式碼

其中內建了非常多的功能,選擇去硬背每一條指令並不是一個明智的選擇。我們只需要記住一些常用的指令,在需要的時候通過help命令來檢視相關的描述即可。

(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來獲取具體命令的合法引數資訊以及含義

(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.
  ...
複製程式碼

斷點相關


Xcode本身已經將大部分的操作用UI展示了出來,比如說

  • Breakpoint Navigator (⌘ + 7)
  • Debug Navigator (⌘ + 6)
  • Debug Area (⌘ + Shift + Y)
  • Debug menu item
    斷點列表

日常開發中大部分有關斷點的操作我們都可以不使用命令列直接通過Xcode的視覺化操作來實現,命令列的操作似乎是一種多餘。但是使用**(lldb)help breakpoint**檢視一下LLDB提供的所有幫助,你會發現在命令列中使用LLDB能夠給予我們更多更詳細的除錯資訊以及更廣闊的操作空間。

(lldb)help breakpoint
複製程式碼

舉一個簡單的例子,我們需要為某一個函式設定一個斷點。比如說給ViewController的VviewDidLoad方法設定一個斷點。這對於Xcode而言非常的簡單。

新增斷點

編輯每一個斷點的各個選項也因為視覺化的操作而變得非常的簡單。但是如果我們需要在系統呼叫的某個函式裡設定斷點呢,抑或某個函式我們只能在crash log茫茫碌的堆疊資訊裡才能看到一點它的痕跡,這個時候如何操作呢?

crash log

假設我們現在需要給objc_msgSend函式設定斷點。首先先想辦法獲取objc_msgSend的地址。我們在Appdelegate.m檔案給函式- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions打一個斷點,執行程式如下圖所示。

iOS開發除錯   LLDB使用概覽
我們可以通過(lldb)br set -a 0x0000000103c04ac0來為objc_msgSend()設定一個斷點。輸入continue繼續執行你會發現如果程式再次呼叫objc_msgSend()會暫停。
斷點設定

Tips * 圖片是用模擬器執行所以是x86,移動裝置是arm。兩者的指令有所不同。圖中的callq指令對應arm中的bl * 由於 ASLR(地址空間配置隨機載入) 的原因地址是不固定的,所以圖中objc_msgSend()的地址在你的機器上是不可用的。

斷點相關的指令很多很雜,這裡為大家列舉一些常用的。如果以後遇到一些特殊的需求,可以藉助**help()**指令來自行查詢相關指令。

設定斷點

  • 給所有名為xx的函式設定一個斷點
(lldb)breakpoint set —name xx
(lldb)br s -n xx
(lldb)b xx
複製程式碼
  • 在檔案F指定行L設定斷點
(lldb)breakpoint set —file F —line L
(lldb)br s -f F -l L
(lldb)b F:L
複製程式碼
  • 給所有名為xx的C++函式設定一個斷點(希望沒有同名的C函式)
(lldb)breakpoint set —method xx
(lldb)br s -M xx
複製程式碼
  • 給一個OC函式[objc msgSend:]設定一個斷點
(lldb)breakpoint set —name “[objc msgSend:]”
(lldb)b -n “[objc msgSend:]”
複製程式碼
  • 給所有名為xx的OC方法設定一個斷點(希望沒有名為xx的C或者C++函式)
(lldb)breakpoint set —selector xx
(lldb)br s -S count
複製程式碼
  • 給所有函式名正則匹配成功的函式設定一個斷點
(lldb)breakpoint set --func-regex regular-expression
(lldb)br s -r regular-expression
複製程式碼
  • 給指定函式地址func_addr的位置設定一個斷點
(lldb)br set -a func_addr
複製程式碼

斷點檢視

(lldb)breakpoint list
(lldb)br l
複製程式碼

斷點刪除

(lldb)breakpoint delete index
(lldb)br del index
複製程式碼

index指明斷點的序號,如果為空則刪除所有斷點

watchpoint

iOS開發當中有一個重要的概念KVO,我們會給一個重要的變數設定一個觀察者,用以在它發生變化的時候做出相應的操作。在除錯過程中我們也可以藉助LLDB來監視某個變數或某一塊記憶體的讀寫情況。

- (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指令來監視變數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"
複製程式碼

當你輸入watchpoint list檢視設定的watchpoint時系統會提示你當前測試的機器允許設定的最大個數。

(lldb) watchpoint list
Number of supported hardware watchpoints: 4
No watchpoints currently set.
複製程式碼

引數檢查


當我們除錯程式遇到斷點的時候Xcode會自動的將當前作用域下的區域性變數以及全域性變數展示出來

當前作用

藉助命令列我們也能夠輕鬆的獲取這些引數的資訊

  • 展示當前作用域下的引數和區域性變數
(lldb)frame variable
(lldb)fr v
複製程式碼
  • 展示當前作用域下的區域性變數
(lldb)frame variable --no-args
(lldb)fr v -a
複製程式碼
  • 展示指定變數var的具體內容
(lldb)frame variable *var*
(lldb)fr v *var*
(lldb)p *var*
複製程式碼
  • 展示當前物件的全域性變數
(lldb)target variable
(lldb)ta v
複製程式碼

細心的朋友應該能夠有所發現,這些操作都有一個侷限:我們檢視的各個變數都是當前作用域的。這意味著程式遇到斷點的時候暫停,所有的操作都是侷限於當前函式以及當前函式所線上程的內部。視覺化的操作並沒有給我們太多操作的空間,但是藉助命令列我們可以打破這樣一個侷限。

命令列輸入**(lldb)thread backtrace**可以獲取當前執行緒函式的呼叫棧

(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的操作我們可以輕鬆看出執行緒選擇相關的操作

(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
複製程式碼

以上提到的幾個指令意味著藉助命令列,我們可以在斷點發生的時候跳轉到當前存在的任一執行緒裡的任一作用域去進行操作。

除卻frame的操作,我們很多時候習慣藉助NSLog去列印某些關鍵的資訊。如果執行到一半的時候發現漏寫了某個地方的NSLog,加入相關程式碼並重新執行也許不是一個讓人省心的方法。我們可以在需要列印的地方設定一個斷點,然後執行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列印的是當前物件的地址而po則會呼叫物件的description方法,做法和NSLog是一致的。

expression指令


expression命令是執行一個表示式,並將表示式返回的結果輸出。包括上文提到的p指令在內,以下幾個都是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
複製程式碼

列印物件的時候我們也可以指定特定格式,詳細的格式查閱參見這裡。

(lldb) p 16
16
(lldb)p/x 16
0x10
(lldb) p/t 16
0b00000000000000000000000000010000
(lldb) p/t (char)16
0b00010000
複製程式碼

但是expression指令真正強大的部分應該是它的寫入能力。我們可以通過expression來執行一個表示式動態的修改我們程式中變數的值。

(lldb) p count
(NSUInteger) $4 = 12
(lldb)e count = 42
(lldb) p count
(NSUInteger) $5 = 42
複製程式碼

在斷點處我們首先列印count變數的值,之後通過執行expression指令來修改count變數,再次列印可以發現此時count已經被修改。這對於除錯時模擬一些極端情況非常的有幫助。這裡有一個特殊一點的情況需要指明,如果你嘗試通過expression來修改UI可能會失效。

(lldb)expression -- self.view.backgroundColor = [UIColor redColor]
複製程式碼

因為執行斷點會打斷更新UI的程式導致你的修改沒有及時渲染出來,執行flush命令可以讓機器渲染出你修改後的介面。

實際上一些複雜的除錯操作單單靠每次命令列去手動輸入指令是非常的繁瑣的,僅僅依靠單條指令和它提供的引數選項在一些針對介面的除錯上並不能給予我們足夠多的支援。令人興奮的是facebook開源的Chisel為我們提供了更多實用的功能。整個開源庫是用Python實現的,基於LLDB 內建的,完整的 Python 支援。這一部分我們後面聊到script指令再細細探討。

hook的概念


hook翻譯成中文是鉤子的意思。這個名詞在我從事iOS開發的過程中確實沒有太多的接觸,第一次碰到是在學習Flask框架時遇到的請求鉤子。我並不覺得鉤子的中文翻譯對於我們理解有所幫助,在我初學的階段甚至給我產生了一定的誤解,所以我後續還是以hook來描述。

簡單來說hook一個處理訊息的程式段,通過系統呼叫,把它掛入系統。每當特定的訊息發出,在沒有到達目的視窗前,鉤子程式就先捕獲該訊息,此時hook函式先得到控制權。這時hook函式即可以加工處理(改變)該訊息,也可以不作處理而繼續傳遞該訊息,還可以強制結束訊息的傳遞。這意味著藉助hook函式我們可以在指定在某些特殊的情況下做出一些包括但不限於引數驗證,訊息攔截等操作來查驗當前情況和修改後續程式的執行。

在LLDB中常見的操作有以下這些。本身指令並不複雜,但配合上其它的指令確實在某些情況下能節省我們很多的精力。

  • 設定一個stop-hook用以在每次斷點被觸發時執行
(lldb)target stop-hook add --one-liner stop-hook
複製程式碼
  • 設定一個stop-hook用以在指定函式func內的斷點被觸發時執行
(lldb) target stop-hook add --name func --one-liner stop-hook
複製程式碼
  • 設定一個stop-hook用以在名為className的C類的斷點被觸發時執行
(lldb)target stop-hook add -- className MyClass --one-liner stop-hook
複製程式碼

stop-hook示例

流程控制


Xcode已經為我們提供了視覺化的工具,但是如果你習慣了命令列操作不希望雙手離開鍵盤降低你的效率,瞭解一下也是很有幫助的。

流程控制

  • 繼續
(lldb)process continue
(lldb)continue
(lldb)c
複製程式碼
  • 下一步
(lldb)thread step-over
(lldb)next
(lldb)n
複製程式碼
  • 進入
(lldb)thread step-in
(lldb)step
(lldb)s
複製程式碼
  • 跳出
(lldb)thread step-out
(lldb) finish
(lldb)f
複製程式碼

除此以外我們還可以通過Thread return來控制流程。該指令有一個可選引數,在執行時它會把可選引數載入進返回暫存器裡,然後立刻執行返回命令,跳出當前棧幀。這意味這函式剩餘的部分不會被執行。當然這也可能會給 ARC 的引用計數造成一些問題,或者會使函式內的清理部分失效。但是在函式的開頭執行這個命令,是個非常好的隔離這個函式,偽造返回值的方一個方法。

- (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:函式中設定斷點利用thread return函式返回NO。

(lldb) thread return NO
(lldb) c
Process 4784 resuming
2017-07-22 18:48:14.654 LLDBTest[4784:569378] Second
複製程式碼

script


LLDB 有內建的,完整的 Python支援。在LLDB中輸入 script,會開啟一個 Python REPL。你也可以輸入一行 python 語句作為 script 命令的引數,這可以執行 python 語句而不進入REPL

(lldb) script print 'Hello World'
Hello World
複製程式碼

藉助LLDB提供的Python API我們可以實現很多複雜的功能。這裡列舉一個簡單的例子,將以下內容寫入~/myCommands.py檔案

def caflushCommand(debugger, command, result, internal_dict): 
    debugger.HandleCommand("e (void)[CATransaction flush]")
複製程式碼

在LLDB中執行

command script import ~/myCommands.py
複製程式碼

或者將這條指令寫入~ /.lldbinit中,每次進入LLDB都會自動執行這些函式。

如果沒有~ /.lldbinit 終端執行touch ~ /.lldbinit生成檔案 你可以在這裡提前設定好一些指令,然後disable。除錯過程中再設定enable開啟。相信經過整理之後LLDB會讓你的除錯如魚得水。

Facebook開源的Chisel就是基於此實現。我們通過brew安裝Chisel

brew install Chisel
複製程式碼

chise檔案層次

fblldbbase.py檔案中定義了各個基礎類,fblldb.py負責遍歷commands資料夾裡的各個類來載入自定義的指令。在Chisel基礎上我們也可以輕鬆的自定義指令。在commands資料夾內新建py檔案,實現函式lldbcommands返回一個陣列,包含物件的類都是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,我們需要分別複寫以下幾個函式

  • def name() 返回一個字串表示指令的名稱
  • def description() 返回一個字串表示指令的描述
  • def args() 返回一個陣列,其中的物件都是類FBCommandArgument構建的,每一個物件表示一個指令的選項引數。
  • def run() 指令的具體操作

image


image指令是target module指令的縮寫,藉助它我們能夠檢視當前的Binary Images相關的資訊。日常開發我們主要利用它定址。

在日常開發的過程中,我們會收集到使用者各式各樣的crash log。log中會為我們提供崩潰前函式棧的執行情況,每一個函式都會對應一個函式地址。

crash log
要解決問題首先我們需要確定的是程式最後呼叫了什麼函式。由於ALSR的原因crash log中的函式地址我們不能夠直接的去使用,我們需要在測試的機器上自己去計算出對應的函式地址。一般情況下crash log中會附帶一個Binary Images。我們要利用這個來計算出每一個函式地址相對於所在框架的偏移量。

Binary Images
之後利用image指令來檢視本機的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 
複製程式碼

有了本機的Binary Images我們就可以通過之前計算出的偏移量來獲取本機對應函式的地址。通過image lookup指令查詢對應地址的函式就可以確定崩潰前究竟執行了哪些函式

(lldb) image lookup -a 0x1025dd00a
      Address: UIKit[0x00000000001cb00a] (UIKit.__TEXT.__text + 1869978)
      Summary: UIKit`-[UIViewController loadViewIfRequired] + 1219
複製程式碼

register


register指令能夠獲取和修改各個暫存器的資訊。

我們需要明白一個典型的CPU是由運算器、控制器、暫存器等器件構成的,而暫存器進行的就是資訊儲存。我們利用匯編語言來操作暫存器。

彙編

這裡是蘋果官方文件,介紹的是armv6。需要注意的是自從iPhone 5s之後已經全部換到64-bit,在arm64下整數暫存器的個數已經增加到31個。我們可以通過register read來進行檢視。

register

其中x0-x7八個暫存器是用來儲存引數的。objc_msgSend會有兩個預設引數,這也就意味著x0儲存的是self,x1儲存的是_cmd。fr對應frame point,lr對應link point,在彙編中分別為x29,x30。最近正在準備一篇從彙編的層面分析objc_msgSend的文章,會在那裡結合官方文件詳細介紹包括函式呼叫過程以及各個暫存器的作用。有興趣的朋友可以先關注一下作者:)

利用runtime動態呼叫Objective-C任意物件的任意方法,需要為NSInvocation設定引數。引數的index就是從2開始的。具體的實現可以參考Github-Tools中的NSObject+Runtime這個Category的實現。

雖然我們更多的時候只是藉助read指令來獲取一下當前各個暫存器的資訊,但是對於一些替換引數,模擬特殊輸入的需求,write指令也是非常的有幫助。

實現一個簡單的例子。

- (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);
}
複製程式碼

函式非常的簡單,會依次列印出First和Second。我們首先在第一次呼叫printString:之前列印一個斷點,呼叫frame variable來檢視一下當前兩個引數的地址.

(lldb) frame variable
(ViewController *) self = 0x000000010090ac30
(SEL) _cmd = <variable not available>

(NSTaggedPointerString *) str = 0xa000074737269465 @"First"
(NSTaggedPointerString *) str1 = 0xa00646e6f6365536 @"Second"
複製程式碼

如果你的引數是unused編譯器會把它優化掉,這樣你就無法獲取它的地址。注意NSString的建立方式,字串常量建立會把str分配到常量區,檢視引數會得到<variable not available>的提示。

之後在printString:中的NSLog之前設定一個斷點,continue。在printString:中遇到斷點的時候我們執行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
複製程式碼

對比地址可以發現x0儲存的是viewController的地址,x1註明了是函式printString:的地址,而x2就是str的地址。我們通過register write指令來修改x2的值。

(lldb) register write x2 0xa00646e6f6365536
複製程式碼

contine之後你會發現列印出的不是First而是Second。

如果有朋友對彙編和函式呼叫感興趣,我會在之後結合objc_msgSend彙編部分的程式碼在另一篇文章裡來做個介紹。

結語


文章的目的是希望給大家展示LLDB強大的能力以及命令列的優點,但實際以上篇幅介紹的只是冰山一角。希望這篇文章能夠給大家一些幫助,來更多的瞭解LLDB。

以下是一些有關LLDB的資料和文件

相關文章