[譯]用 LLDB 調試 Swift 代碼

用 LLDB 調試 Swift 代碼

做爲工程師,咱們花了差很少 70% 的時間在調試上,剩下的 20% 用來思考架構以及和組員溝通,僅僅只有 10% 的時間是真的在寫代碼的。html

調試就像是在犯罪電影中作偵探同樣,同時你也是兇手。前端

Filipe Fortes 來自 Twitterpython

因此讓咱們在這70%的時間儘量愉悅是至關重要的。LLDB 就是來打救咱們的。奇妙的 Xcode Debugger UI 展現了全部你可用的信息,而不用敲入任何一個 LLDB 命令。然而,控制檯在咱們的工做中一樣也是很重要的一部分。如今讓咱們來分析一些最有用的 LLDB 技巧。我本身天天都在用它們進行調試。android

從哪裏開始呢?

LLDB 是一個龐大的工具,內置了不少有用的命令。我不會所有講解,而是帶你瀏覽最有用的命令。這是咱們的計劃:ios

  1. 獲取變量值:expression, e, print, po, p
  2. 獲取整個應用程序的狀態以及特定語言的命令:bugreport, frame, language
  3. 控制應用的執行流程:process, breakpoint, thread, watchpoint
  4. 榮譽獎:command, platform, gui

我還準備好了有用的 LLDB 命令說明和實例的表格,有須要的能夠把它貼在 Mac 上面記住這些命令 🙂git

經過這條連接下載全尺寸的版本 —  www.dropbox.com/s/9sv67e7f2…github

1. 獲取變量值和狀態

命令:expression, e, print, po, pexpress

調試器的一個基礎功能就是獲取和修改變量的值。這就是 expression 或者 e 被創造的緣由(固然他們還有更高級的功能)。您能夠簡單的在運行時執行任何表達式或命令。macos

假設你如今正在調試方法 valueOfLifeWithoutSumOf() :對兩個數求和,再用42去減獲得結果。swift

繼續假設你一直獲得錯誤的結果而且你並不知道是什麼緣由。因此你能夠作如下的事來找到問題:

或者。。。使用 LLDB 表達式在運行時修改值纔是更好的方法,同時能夠找出問題是在哪裏出現的。首先,在你感興趣的地方設置一個斷點,而後運行你的應用。

爲了用 LLDB 格式打印指定的變量你應該調用:

(lldb) e <variable>
複製代碼

使用相同的命令來執行一些表達式:

(lldb) e <expression>
複製代碼

(lldb) e sum 
(Int) $R0 = 6 // 下面你也能夠用 $R0 來引用這個變量(在本次調試過程當中)

(lldb) e sum = 4 // 修改變量 sum 的值

(lldb) e sum 
(Int) $R2 = 4 // 直到本次調試結束變量 sum 都會是 "4" 
複製代碼

expression 命令也有一些標誌。在 expression 後面用雙破折號 -- 將標誌和實際的表達式分隔開,就像這樣:

(lldb) expression <some flags> -- <variable>
複製代碼

expression 命令差很少有30種不一樣的標誌。我鼓勵你多去探索它們。在終端中鍵入如下命令能夠看到完整的文檔:

> lldb
> (lldb) help # 獲取全部變量的命令
> (lldb) help expression # 獲取全部表達式的子命令
複製代碼

我會在下列 expression 的標誌上多停留一下子:

  • -D <count> (--depth <count>)  — 設置在轉儲聚合類型時的最大遞歸深度(默認爲無窮大)。
  • -O (--object-description)  — 若是可能的話,使用指定語言的描述API來顯示。
  • -T (--show-types)  — 在轉儲值的時候顯示變量類型。
  • -f <format> (--format <format>) — 指定一種用於顯示的格式。
  • -i <boolean> (--ignore-breakpoints <boolean>) — 在運行表達式時忽略斷點。

假設咱們有一個叫 logger 的對象,這個對象有一些字符串和結構體類型的屬性。好比說,你可能只是想知道第一層的屬性,那隻須要用 -D 標誌以及恰當的層級深度值,就像這樣:

(lldb) e -D 1 -- logger

(LLDB_Debugger_Exploration.Logger) $R5 = 0x0000608000087e90 {
  currentClassName = "ViewController"
  debuggerStruct ={...}
}
複製代碼

默認狀況下,LLDB 會無限地遍歷該對象而且給你展現每一個嵌套的對象的完整描述:

(lldb) e -- logger

(LLDB_Debugger_Exploration.Logger) $R6 = 0x0000608000087e90 {
  currentClassName = "ViewController"
  debuggerStruct = (methodName = "name", lineNumber = 2, commandCounter = 23)
}
複製代碼

你也能夠用 e -O -- 獲取對象的描述或者更簡單地用別名 po,就像下面的示例同樣:

(lldb) po logger

<Logger: 0x608000087e90>
複製代碼

並非頗有描述性,不是嗎?爲了獲取更加可閱讀的描述,你自定義的類必須遵循 CustomStringConvertible 協議,同時實現 var description: String { return ...} 屬性。接下來只須要用 po 就能返回可讀的描述。

在本節的開始,我也提到了 print 命令。基本上 print <expression/variable> 就等同於 expression -- <expression/variable>。可是 print 命令不能帶任何標誌或者額外的參數。

2. 獲取整個 APP 的狀態和指定語言的命令

bugreport, frame, language

你是否常常複製粘貼崩潰日誌到任務管理器中方便稍後能考慮這個問題嗎?LLDB 提供了一個很好用的命令叫 bugreport,這個命令能生成當前應用狀態的完整報告。在你偶然觸發某些問題可是想在稍後再解決它時這個命令就會頗有幫助了。爲了能恢復應用的狀態,你可使用 bugreport 生成報告。

(lldb) bugreport unwind --outfile <path to output file>
複製代碼

最終的報告看起來就像下面截圖中的例子同樣:

bugreport 命令輸出的示例。

假設你想要獲取當前線程的當前棧幀的概述,frame 命令能夠幫你完成:

使用下面的代碼片斷來快速獲取當前地址以及當前的環境條件:

(lldb) frame info

frame #0: 0x000000010bbe4b4d LLDB-Debugger-Exploration`ViewController.valueOfLifeWithoutSumOf(a=2, b=2, self=0x00007fa0c1406900) -> Int at ViewController.swift:96
複製代碼

這些信息在本文後面將要說到的斷點管理中很是有用。

LLDB 有幾個指定語言的命令,包括C++,Objective-C,Swift 和 RenderScript。在這篇文章中,咱們重點關注 Swift。這是兩個命令:demanglerefcount

demangle 正如其名字而言,就是用來重組 Swift 類型名的(由於 Swift 在編譯的時候會生成類型名來避免命名空間的問題)。若是你想了解多一點的話,我建議你看 WWDC14 的這個分享會 —  「Advanced Swift Debugging in LLDB」

refcount 一樣也是一個至關直觀的命令,能得到指定對象的引用數量。一塊兒來看一下對象輸出的示例,咱們用了上一節講到的對象 — logger

(lldb) language swift refcount logger

refcount data: (strong = 4, weak = 0)
複製代碼

固然了,在你調試某些內存泄露問題時,這個命令就會頗有幫助。

3. 控制應用的執行流程

process, breakpoint, thread

這節是我最喜歡的一節,由於在 LLDB 使用這幾個命令(尤爲是 breakpoint 命令),你能夠在調試的時候使不少常規任務變得自動化,這樣就能大大加快你的調試工做。

經過 process 基本上你就能夠控制調試的過程了,還能連接到特定的 target 或者中止調試器。 可是由於 Xcode 已經自動地幫咱們作好了這個工做了(Xcode 在任什麼時候候運行一個 target 時都會鏈接 LLDB)。我不會在這兒講太多,你能夠在這篇 Apple 的指南中閱讀一下如何用終端鏈接到一個 target — 「Using LLDB as a Standalone Debugger」

使用 process status 的話,你能夠知道當前調試器停住的地址:

(lldb) process status

Process 27408 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
frame #0: 0x000000010bbe4889 LLDB-Debugger-Exploration`ViewController.viewDidLoad(self=0x00007fa0c1406900) -> () at ViewController.swift:69
66
67           let a = 2, b = 2
68           let result = valueOfLifeWithoutSumOf(a, and: b)
-> 69           print(result)
70
71
72
複製代碼

想要繼續 target 的執行過程直到遇到下次斷點的話,運行這個命令:

(lldb) process continue

(lldb) c // 或者只鍵入 "c",這跟上一條命令是同樣的
複製代碼

這個命令等同於 Xcode 調試器工具欄上的」continue「按鈕:

breakpoint 命令容許你用任何可能的方式操做斷點。咱們跳過最顯而易見的命令:breakpoint enable, breakpoint disablebreakpoint delete

首先,查看你全部斷點的話能夠用以下示例中的 list 子命令:

(lldb) breakpoint list

Current breakpoints:
1: file = '/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift', line = 95, exact_match = 0, locations = 1, resolved = 1, hit count = 1

1.1: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.valueOfLifeWithoutSumOf (Swift.Int, and : Swift.Int) -> Swift.Int + 27 at ViewController.swift:95, address = 0x0000000107f3eb3b, resolved, hit count = 1

2: file = '/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift', line = 60, exact_match = 0, locations = 1, resolved = 1, hit count = 1

2.1: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.viewDidLoad () -> () + 521 at ViewController.swift:60, address = 0x0000000107f3e609, resolved, hit count = 1
複製代碼

列表中的第一個數字是是斷點的 ID,你能夠經過這個 ID 引用到指定的斷點。如今讓咱們在控制檯中設置一些新的斷點:

(lldb) breakpoint set -f ViewController.swift -l 96

Breakpoint 3: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.valueOfLifeWithoutSumOf (Swift.Int, and : Swift.Int) -> Swift.Int + 45 at ViewController.swift:96, address = 0x0000000107f3eb4d
複製代碼

這個例子中的 -f 是你想要放置斷點處的文件名,-l 是新斷點的行數。還有一種更簡潔的方式設置一樣的斷點,就是用快捷方式 b

(lldb) b ViewController.swift:96
複製代碼

一樣地,你也能夠用指定的正則(好比函數名)來設置斷點,使用下面的命令:

(lldb) breakpoint set --func-regex valueOfLifeWithoutSumOf

(lldb) b -r valueOfLifeWithoutSumOf // 上一條命令的簡化版本
複製代碼

有些時候設置斷點只命中一次也是有用的,而後指示這個斷點當即刪除本身,固然啦,有一個命令來處理這件事:

(lldb) breakpoint set --one-shot -f ViewController.swift -l 90

(lldb) br s -o -f ViewController.swift -l 91 // 上一條命令的簡化版本
複製代碼

如今咱們來到了最有趣的部分 — 自動化斷點。你知道你能夠設置一個特定的動做使它在斷點停住的時候執行嗎?是的,你能夠!你是否會在代碼中用 print() 來在調試的時候獲得你感興趣的值?請不要再這樣作了,這裏有一種更好的方法。🙂

經過 breakpoint 命令,你能夠設置好命令,使其在斷點命中時能夠正確執行。你甚至能夠設置」不可見「的斷點,這種斷點並不會打斷運行過程。從技術上講,這些「不可見的」斷點實際上是會中斷執行的,但若是在命令鏈的末尾添上「continue」命令的話,你就不會注意到它。

(lldb) b ViewController.swift:96 // Let's add a breakpoint first Breakpoint 2: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.valueOfLifeWithoutSumOf (Swift.Int, and : Swift.Int) -> Swift.Int + 45 at ViewController.swift:96, address = 0x000000010c555b4d (lldb) breakpoint command add 2 // 準備某些命令 Enter your debugger command(s). Type 'DONE' to end. > p sum // 打印變量 "sum" 的值 > p a + b // 運行 a + b > DONE 複製代碼

爲了確保你添加的命令是正確的,可使用 breakpoint command list <breakpoint id> 子命令:

(lldb) breakpoint command list 2

Breakpoint 2:
Breakpoint commands:
p sum
p a + b
複製代碼

當下次斷點命中時咱們就會在控制檯看到下面的輸出:

Process 36612 resuming
p sum
(Int) $R0 = 6

p a + b
(Int) $R1 = 4
複製代碼

太棒了!這正是咱們想要的。你能夠經過在命令鏈的末尾添加 continue 命令讓執行過程更加順暢,這樣你就不會停在這個斷點。

(lldb) breakpoint command add 2 // 準備某些命令

Enter your debugger command(s).  Type 'DONE' to end.
> p sum // 打印變量 "sum" 的值
> p a + b // 運行 a + b
> continue // 第一次命中斷點後直接恢復
> DONE
複製代碼

結果會是這樣:

p sum
(Int) $R0 = 6

p a + b
(Int) $R1 = 4

continue
Process 36863 resuming
Command #3 'continue' continued the target.
複製代碼

經過 thread 命令和它的子命令,你能夠徹底操控執行流程:step-over, step-in, step-outcontinue。這些命令等同於 Xcode 調試器工具欄上的流程控制按鈕。

LLDB 一樣也對這些特殊的命令預先定義好了快捷方式:

(lldb) thread step-over
(lldb) next // 和 "thread step-over" 命令效果同樣
(lldb) n // 和 "next" 命令效果同樣

(lldb) thread step-in
(lldb) step // 和 "thread step-in" 命令效果同樣
(lldb) s // 和 "step" 命令效果同樣
複製代碼

爲了獲取當前線程的更多信息,咱們只須要調用 info 子命令:

(lldb) thread info 

thread #1: tid = 0x17de17, 0x0000000109429a90 LLDB-Debugger-Exploration`ViewController.sumOf(a=2, b=2, self=0x00007fe775507390) -> Int at ViewController.swift:90, queue = 'com.apple.main-thread', stop reason = step in
複製代碼

想要看到當前全部的活動線程的話使用 list 子命令:

(lldb) thread list

Process 50693 stopped

* thread #1: tid = 0x17de17, 0x0000000109429a90 LLDB-Debugger-Exploration`ViewController.sumOf(a=2, b=2, self=0x00007fe775507390) -> Int at ViewController.swift:90, queue = 'com.apple.main-thread', stop reason = step in

  thread #2: tid = 0x17df4a, 0x000000010daa4dc6 libsystem_kernel.dylib`kevent_qos + 10, queue = 'com.apple.libdispatch-manager'
  
  thread #3: tid = 0x17df4b, 0x000000010daa444e libsystem_kernel.dylib`__workq_kernreturn + 10

  thread #5: tid = 0x17df4e, 0x000000010da9c34a libsystem_kernel.dylib`mach_msg_trap + 10, name = 'com.apple.uikit.eventfetch-thread'
複製代碼

榮譽獎

command, platform, gui

在 LLDB 中你能夠找到一個命令管理其餘的命令,聽起來很奇怪,但實際上它是很是有用的小工具。首先,它容許你從文件中執行一些 LLDB 命令,這樣你就能夠建立一個儲存着一些實用命令的文件,而後就能馬上容許這些命令,就像是單個命令那樣。這是所說的文件的簡單例子:

thread info // 顯示當前線程的信息
br list // 顯示全部的斷點
複製代碼

下面是實際命令的樣子:

(lldb) command source /Users/Ahmed/Desktop/lldb-test-script

Executing commands in '/Users/Ahmed/Desktop/lldb-test-script'.

thread info
thread #1: tid = 0x17de17, 0x0000000109429a90 LLDB-Debugger-Exploration`ViewController.sumOf(a=2, b=2, self=0x00007fe775507390) -> Int at ViewController.swift:90, queue = 'com.apple.main-thread', stop reason = step in

br list
Current breakpoints:
1: file = '/Users/Ahmed/Desktop/Recent/LLDB-Debugger-Exploration/LLDB-Debugger-Exploration/ViewController.swift', line = 60, exact_match = 0, locations = 1, resolved = 1, hit count = 0
1.1: where = LLDB-Debugger-Exploration`LLDB_Debugger_Exploration.ViewController.viewDidLoad () -> () + 521 at ViewController.swift:60, address = 0x0000000109429609, resolved, hit count = 0
複製代碼

遺憾的是還有一個缺點,你不能傳遞任何參數給這個源文件(除非你在腳本文件自己中建立一個有效的變量)。

若是你須要更高級的功能,你也可使用 script 子命令,這個命令容許你用自定義的 Python 腳本 管理(add, delete, importlist),經過 script 命令能實現真正的自動化。請閱讀這個優秀的教程 Python scripting for LLDB。爲了演示的目的,讓咱們建立一個腳本文件 script.py,而後寫一個簡單的命令 print_hello(),這個命令會在控制檯中打印出「Hello Debugger!「:

import lldb

def print_hello(debugger, command, result, internal_dict):
	print "Hello Debugger!"
    
def __lldb_init_module(debugger, internal_dict):
	debugger.HandleCommand('command script add -f script.print_hello print_hello') // 控制腳本的初始化同時從這個模塊中添加命令
	print 'The "print_hello" python command has been installed and is ready for use.' // 打印確認一切正常
複製代碼

接下來咱們須要導入一個 Python 模塊,就能開始正常地使用咱們的腳本命令了:

(lldb) command import ~/Desktop/script.py

The "print_hello" python command has been installed and is ready for use.

(lldb) print_hello

Hello Debugger!
複製代碼

你可使用 status 子命令來快速檢查當前的環境信息,status 會告訴你:SDK 路徑、處理器的架構、操做系統版本甚至是該 SDK 可支持的設備的列表。

(lldb) platform status

Platform: ios-simulator
Triple: x86_64-apple-macosx
OS Version: 10.12.5 (16F73)
Kernel: Darwin Kernel Version 16.6.0: Fri Apr 14 16:21:16 PDT 2017; root:xnu-3789.60.24~6/RELEASE_X86_64
Hostname: 127.0.0.1
WorkingDir: /
SDK Path: "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk"

Available devices:
614F8701-3D93-4B43-AE86-46A42FEB905A: iPhone 4s
CD516CF7-2AE7-4127-92DF-F536FE56BA22: iPhone 5
0D76F30F-2332-4E0C-9F00-B86F009D59A3: iPhone 5s
3084003F-7626-462A-825B-193E6E5B9AA7: iPhone 6
...
複製代碼

你不能在 Xcode 中使用 LLDB GUI 模式,但你老是能夠從終端使用(LLDB GUI 模式)。

(lldb) gui

// 若是你試着在 Xcode 中執行這個 gui 命令的話,你將會看到這個錯誤:the gui command requires an interactive terminal。
複製代碼

這就是 LLDB GUI 模式看起來的樣子。

結論:

在這篇文章中,我只是淺析了 LLDB 的皮毛知識而已,即便 LLDB 已經有好些年頭了,可是仍然有許多人並無徹底發揮出它的潛能。我只是對基本的方法作了一個概述,以及談了 LLDB 如何自動化調試步驟。我但願這會是有幫助的。

還有不少 LLDB 的方法並無寫到,而後還有一些視圖調試技術我沒有說起。若是你對這些話題感興趣的話,請在下面留下你的評論,我會更加樂於寫這些話題。

我強烈建議你打開終端,啓動 LLDB,只須要敲入 help,就會向你展現完整的文檔。你能夠花費數小時去閱讀,可是我保證這將是一個合理的時間投資。由於瞭解你的工具是工程師真正產出的惟一途徑。

  • LLDB 官方網站 —  你會在這裏找到全部與 LLDB 相關的材料。文檔、指南、教程、源文件以及更多。
  • LLDB Quick Start Guide by Apple — 一樣地,Apple 提供了很好的文檔。這篇指南能幫你快速上手 LLDB,固然,他們也敘述了怎樣不經過 Xcode 地用 LLDB 調試。
  • How debuggers work: Part 1 — Basics — 我很是喜歡這個系列的文章,這是對調試器實際工做方式很好的概述。文章介紹了用 C 語言手工編寫的調試器代碼要遵循的全部基本原理。我強烈建議你去閱讀這個優秀系列的全部部分(第2部分, 第3部分)。
  • WWDC14 Advanced Swift Debugging in LLDB — 關於在 LLDB 中用 Swift 調試的一篇不錯的概述,也講了 LLDB 如何經過內建的方法和特性實現完整的調試操做,來幫你變得更加高效。
  • Introduction To LLDB Python Scripting — 這篇介紹 LLDB Python 腳本的指南能讓你快速上手。
  • Dancing in the Debugger. A Waltz with LLDB  — 對 LLDB 一些基礎知識的介紹,有些知識有點過期了(好比說 (lldb) thread return 命令)。遺憾的是,它不能直接用於 Swift,由於它會對引用計數帶了一些潛在的隱患。可是,這仍然是你開始 LLDB 之旅不錯的文章。

掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索