與偵錯程式共舞 – LLDB 的華爾茲

發表於2015-03-27

你是否曾經苦惱於理解你的程式碼,而去嘗試列印一個變數的值?

或者跳過一個函式呼叫來簡化程式的行為?

或者短路一個邏輯檢查?

或者偽造一個函式實現?

並且每次必須重新編譯,從頭開始?

構建軟體是複雜的,並且 Bug 總會出現。一個常見的修復週期就是修改程式碼,編譯,重新執行,並且祈禱出現最好的結果。

但是不一定要這麼做。你可以使用偵錯程式。而且即使你已經知道如何使用偵錯程式檢查變數,它可以做的還有很多。

這篇文章將試圖挑戰你對除錯的認知,並詳細地解釋一些你可能還不瞭解的基本原理,然後展示一系列有趣的例子。現在就讓我們開始與偵錯程式共舞一曲華爾茲,看看最後能達到怎樣的高度。

LLDB

LLDB 是一個有著 REPL 的特性和 C++ ,Python 外掛的開源偵錯程式。LLDB 繫結在 Xcode 內部,存在於主視窗底部的控制檯中。偵錯程式允許你在程式執行的特定時暫停它,你可以檢視變數的值,執行自定的指令,並且按照你所認為合適的步驟來操作程式的進展。(這裡有一個關於偵錯程式如何工作的總體的解釋。)

你以前有可能已經使用過偵錯程式,即使只是在 Xcode 的介面上加一些斷點。但是通過一些小的技巧,你就可以做一些非常酷的事情。GDB to LLDB 參考是一個非常好的偵錯程式可用命令的總覽。你也可以安裝 Chisel,它是一個開源的 LLDB 外掛合輯,這會使除錯變得更加有趣。

與此同時,讓我們以在偵錯程式中列印變數來開始我們的旅程吧。

基礎

這裡有一個簡單的小程式,它會列印一個字串。注意斷點已經被加在第 8 行。斷點可以通過點選 Xcode 的原始碼視窗的側邊槽進行建立。

程式會在這一行停止執行,並且控制檯會被開啟,允許我們和偵錯程式互動。那我們應該打些什麼呢?

help

最簡單命令是 help,它會列舉出所有的命令。如果你忘記了一個命令是做什麼的,或者想知道更多的話,你可以通過 help <command> 來了解更多細節,例如 help print 或者 help thread。如果你甚至忘記了 help 命令是做什麼的,你可以試試 help help。不過你如果知道這麼做,那就說明你大概還沒有忘光這個命令。

print

列印值很簡單;只要試試 print 命令:

LLDB 實際上會作字首匹配。所以你也可以使用 prin,pri,或者 p。但你不能使用 pr,因為 LLDB 不能消除和 process 的歧義 (幸運的是 p 並沒有歧義)。

你可能還注意到了,結果中有個 $0。實際上你可以使用它來指向這個結果。試試 print $0 + 7,你會看到 106。任何以美元符開頭的東西都是存在於 LLDB 的名稱空間的,它們是為了幫助你進行除錯而存在的。

expression

如果想改變一個值怎麼辦?你或許會猜 modify。其實這時候我們要用到的是 expression 這個方便的命令。

這不僅會改變偵錯程式中的值,實際上它改變了程式中的值。這時候繼續執行程式,將會列印 42 red balloons。神奇吧。

注意,從現在開始,我們將會偷懶分別以 p 和 e 來代替 print 和 expression。

什麼是 print 命令

考慮一個有意思的表示式:p count = 18。如果我們執行這條命令,然後列印 count 的內容。我們將看到它的結果與 expression count = 18 一樣。

和 expression 不同的是,print 命令不需要引數。比如 e -h +17 中,你很難區分到底是以 -h 為標識,僅僅執行 +17 呢,還是要計算 17 和 h 的差值。連字元號確實很讓人困惑,你或許得不到自己想要的結果。

幸運的是,解決方案很簡單。用 — 來表徵標識的結束,以及輸入的開始。如果想要 -h 作為標識,就用 e -h — +17,如果想計算它們的差值,就使用 e — -h +17。因為一般來說不使用標識的情況比較多,所以 e — 就有了一個簡寫的方式,那就是 print。

輸入 help print,然後向下滾動,你會發現:

列印物件

嘗試輸入

輸出會有點囉嗦

如果我們嘗試列印結構更復雜的物件,結果甚至會更糟

實際上,我們想看的是物件的 description 方法的結果。我麼需要使用 -O (字母 O,而不是數字 0) 標誌告訴 expression 命令以物件 (Object) 的方式來列印結果。

幸運的是,e -o — 有也有個別名,那就是 po (print object 的縮寫),我們可以使用它來進行簡化:

列印變數

可以給 print 指定不同的列印格式。它們都是以 print/<fmt> 或者簡化的 p/<fmt> 格式書寫。下面是一些例子:

預設的格式

十六進位制:

二進位制 (t 代表 two):

你也可以使用 p/c 列印字元,或者 p/s 列印以空終止的字串 (譯者注:以 ” 結尾的字串)。
這裡是格式的完整清單。

變數

現在你已經可以列印物件和簡單型別,並且知道如何使用 expression 命令在偵錯程式中修改它們了。現在讓我們使用一些變數來減少輸入量。就像你可以在 C 語言中用 int a = 0 來宣告一個變數一樣,你也可以在 LLDB 中做同樣的事情。不過為了能使用宣告的變數,變數必須以美元符開頭。

悲劇了,LLDB 無法確定涉及的型別 (譯者注:返回的型別)。這種事情常常發生,給個說明就好了:

變數使偵錯程式變的容易使用得多,想不到吧?

流程控制

當你通過 Xcode 的原始碼編輯器的側邊槽 (或者通過下面的方法) 插入一個斷點,程式到達斷點時會就會停止執行。

除錯條上會出現四個你可以用來控制程式的執行流程的按鈕。

從左到右,四個按鈕分別是:continue,step over,step into,step out。

第一個,continue 按鈕,會取消程式的暫停,允許程式正常執行 (要麼一直執行下去,要麼到達下一個斷點)。在 LLDB 中,你可以使用 process continue 命令來達到同樣的效果,它的別名為 continue,或者也可以縮寫為 c。

第二個,step over 按鈕,會以黑盒的方式執行一行程式碼。如果所在這行程式碼是一個函式呼叫,那麼就不會跳進這個函式,而是會執行這個函式,然後繼續。LLDB 則可以使用 thread step-over,next,或者 n 命令。

如果你確實想跳進一個函式呼叫來除錯或者檢查程式的執行情況,那就用第三個按鈕,step in,或者在LLDB中使用 thread step in,step,或者 s 命令。注意,當前行不是函式呼叫時,next 和 step 效果是一樣的。

大多數人知道 c,n 和 s,但是其實還有第四個按鈕,step out。如果你曾經不小心跳進一個函式,但實際上你想跳過它,常見的反應是重複的執行 n 直到函式返回。其實這種情況,step out 按鈕是你的救世主。它會繼續執行到下一個返回語句 (直到一個堆疊幀結束) 然後再次停止。

例子

考慮下面一段程式:

假如我們執行程式,讓它停止在斷點,然後執行下面一些列命令:

這裡,frame info 會告訴你當前的行數和原始碼檔案,以及其他一些資訊;檢視 help frame,help thread 和 help process 來獲得更多資訊。這一串命令的結果會是什麼?看答案之前請先想一想。

它始終在 17 行的原因是 finish 命令一直執行到 isEven() 函式的 return,然後立刻停止。注意即使它還在 17 行,其實這行已經被執行過了。

Thread Return

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

讓我們稍微修改一下上面程式碼段並執行:

看答案前思考一下。下面是答案:

斷點

我們都把斷點作為一個停止程式執行,檢查當前狀態,追蹤 bug 的方式。但是如果我們改變和斷點互動的方式,很多事情都變成可能。

斷點允許控制程式什麼時候停止,然後允許命令的執行。

想象把斷點放在函式的開頭,然後用 thread return 命令重寫函式的行為,然後繼續。想象一下讓這個過程自動化,聽起來不錯,不是嗎?

管理斷點

Xcode 提供了一系列工具來建立和管理斷點。我們會一個個看過來並介紹 LLDB 中等價的命令 (是的,你可以在偵錯程式內部新增斷點)。

在 Xcode 的左側皮膚,有一組按鈕。其中一個看起來像斷點。點選它開啟斷點導航,這是一個可以快速管理所有斷點的皮膚。

在這裡你可以看到所有的斷點 – 在 LLDB 中通過 breakpoint list (或者 br li) 命令也做同樣的事兒。你也可以點選單個斷點來開啟或關閉 – 在 LLDB 中使用 breakpoint enable <breakpointID> 和 breakpoint disable <breakpointID>:

建立斷點

在上面的例子中,我們通過在原始碼頁面器的滾槽 16 上點選來建立斷點。你可以通過把斷點拖拽出滾槽,然後釋放滑鼠來刪除斷點 (消失時會有一個非常可愛的噗的一下的動畫)。你也可以在斷點導航頁選擇斷點,然後按下刪除鍵刪除。

要在偵錯程式中建立斷點,可以使用 breakpoint set 命令。

也可以使用縮寫形式 br。雖然 b 是一個完全不同的命令 (_regexp-break 的縮寫),但恰好也可以實現和上面同樣的效果。

也可以在一個符號 (C 語言函式) 上建立斷點,而完全不用指定哪一行

這些斷點會準確的停止在函式的開始。Objective-C 的方法也完全可以:

如果想在 Xcode 的UI上建立符號斷點,你可以點選斷點欄左側的 + 按鈕。

然後選擇第三個選項:

這時會出現一個彈出框,你可以在裡面新增例如 -[NSArray objectAtIndex:] 這樣的符號斷點。這樣每次呼叫這個函式的時候,程式都會停止,不管是你呼叫還是蘋果呼叫。

如果你 Xcode 的 UI 上右擊任意斷點,然後選擇 “Edit Breakpoint” 的話,會有一些非常誘人的選擇。

這裡,斷點已經被修改為只有當 i 是 99 的時候才會停止。你也可以使用 “ignore” 選項來告訴斷點最初的 n 次呼叫 (並且條件為真的時候) 的時候不要停止。

接下來介紹 ‘Add Action’ 按鈕…

斷點行為 (Action)

上面的例子中,你或許想知道每一次到達斷點的時候 i 的值。我們可以使用 p i 作為斷點行為。這樣每次到達斷點的時候,都會自動執行這個命令。

你也可以新增多個行為,可以是偵錯程式命令,shell 命令,也可以是更直接的列印:

可以看到它列印 i,然後大聲念出那個句子,接著列印了自定義的表示式。

下面是在 LLDB 而不是 Xcode 的 UI 中做這些的時候,看起來的樣子。

接下來說說自動化。

賦值後繼續執行

看編輯斷點彈出視窗的底部,你還會看到一個選項: “Automatically continue after evaluation actions.” 。它僅僅是一個選擇框,但是卻很強大。選中它,偵錯程式會執行你所有的命令,然後繼續執行。看起來就像沒有執行任何斷點一樣 (除非斷點太多,執行需要一段時間,拖慢了你的程式)。

這個選項框的效果和讓最後斷點的最後一個行為是 continue 一樣。選框只是讓這個操作變得更簡單。偵錯程式的輸出是:

執行斷點後自動繼續執行,允許你完全通過斷點來修改程式!你可以在某一行停止,執行一個 expression 命令來改變變數,然後繼續執行。

例子

想想所謂的”列印除錯”技術吧,不要這麼做:

而是用個列印變數的斷點替換 log 語句,然後繼續執行。

也不要:

而是加一個使用 thread return 9 命令的斷點,然後讓它繼續執行。

符號斷點加上 action 真的很強大。你也可以在你朋友的 Xcode 工程上新增一些斷點,並且加上大聲朗讀某些東西的 action。看看他們要花多久才能弄明白髮生了什麼。

相關文章