- 原文地址:Debugging Swift code with LLDB
- 原文作者:Ahmed Sulaiman
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:VernonVan
- 校對者:ZhiyuanSun、Danny1451
用 LLDB 除錯 Swift 程式碼
作為工程師,我們花了差不多 70% 的時間在除錯上,剩下的 20% 用來思考架構以及和組員溝通,僅僅只有 10% 的時間是真的在寫程式碼的。
除錯就像是在犯罪電影中做偵探一樣,同時你也是凶手。
— Filipe Fortes 來自 Twitter
所以讓我們在這70%的時間儘可能愉悅是相當重要的。LLDB 就是來打救我們的。奇妙的 Xcode Debugger UI 展示了所有你可用的資訊,而不用敲入任何一個 LLDB 命令。然而,控制檯在我們的工作中同樣也是很重要的一部分。現在讓我們來分析一些最有用的 LLDB 技巧。我自己每天都在用它們進行除錯。
從哪裡開始呢?
LLDB 是一個龐大的工具,內建了很多有用的命令。我不會全部講解,而是帶你瀏覽最有用的命令。這是我們的計劃:
- 獲取變數值:
expression
,e
,print
,po
,p
- 獲取整個應用程式的狀態以及特定語言的命令:
bugreport
,frame
,language
- 控制應用的執行流程:
process
,breakpoint
,thread
,watchpoint
- 榮譽獎:
command
,platform
,gui
我還準備好了有用的 LLDB 命令說明和例項的表格,有需要的可以把它貼在 Mac 上面記住這些命令 ?
通過這條連結下載全尺寸的版本 — www.dropbox.com/s/9sv67e7f2…
1. 獲取變數值和狀態
命令:expression
, e
, print
, po
, p
偵錯程式的一個基礎功能就是獲取和修改變數的值。這就是 expression
或者 e
被創造的原因(當然他們還有更高階的功能)。您可以簡單的在執行時執行任何表示式或命令。
假設你現在正在除錯方法 valueOfLifeWithoutSumOf()
:對兩個數求和,再用42去減得到結果。
繼續假設你一直得到錯誤的結果並且你並不知道是什麼原因。所以你可以做以下的事來找到問題:
或者。。。使用 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。這是兩個命令:demangle
和 refcount
。
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 disable
和 breakpoint 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-out
和 continue
。這些命令等同於 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
, import
和 list
),通過 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 之旅不錯的文章。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。