開始學習LLDB命令(第五章:表達式)

現在你已經學習了如何創建斷點, 因此調試器會在你的代碼里停下來, 現在是時候從你調試的程序里獲取一些有用的信息了.
你應該會經常想要查看對象的實例變量. 但是, 你知道嗎你甚至可以通過LLDB執行任意代碼?詳細說就是通過Objective-C的運行時你可以聲明,初始化,并且注入代碼來幫助你理解應用程序.
在本章中你將會學習到expression命令.這條命令允許你在調試器中執行任意代碼.

格式化p 和 po

你可能很熟悉go-to這個調試命令.po這個命令經常用來打印出對象的信息.可以是一個對象的實例變量, 也可以是一個對象的引用, 還可以是一個寄存器里的對象.它甚至可以是內存里任意對象的內存地址.
如果你在LLDB控制臺中查看po的快速幫助, 你會發現po實際是一個表達式expression -O --的縮寫. -o參數用來打印出對象的description.po的另一個兄弟指令p是省略掉-o選項的表達式的縮寫expression --.
p打印出的信息取決于LLDB type system.LLDB的值的格式化類型決定了它的輸出并且是完全可以自定義的(后面你就會看到).
是時候學習一下如何用ppo獲取他們的內容了.在本章中你依然后使用Signals項目.
在Xcode中打開Signals項目.接下來打開MasterViewController.swift并且在這個類的上方加上下面的代碼:

override var description: String {
  return "Yay! debugging " + super.description
}

viewDidLoad中的super.viewDidLoad()下面加上下面的代碼:

print("\(self)")

現在在MasterViewController.swift中在你剛剛添加的打印方法的下面創建一個斷點.
構建并運行APP:

page56image16240.png

SignalsviewDidLoad()中停下來的時候, 在LLDB控制臺中輸入下面的代碼:

(lldb) po self

你應該會看到下面這些輸出:

Yay! debugging <Signals.MasterViewController: 0x7f8a0ac06b70>

注意一下print語句的輸出和它與你在調試器中執行po self輸出的匹配度.
你也可以更進一步. NSObject有另外一個用來調試的description方法叫debugDescription.現在來嘗試實現一下. 在description變量定義的下面添加以下代碼:

override var debugDescription: String {
  return "debugDescription: " + super.debugDescription
}

構建并運行應用程序.當調試器在斷點處停下來的時候, 再次打印self:

(lldb) po self

LLDB控制臺的輸出看起來應該是下面的樣子:

 debugDescription: Yay! debugging <Signals.MasterViewController:
0x7fb71fd04080>

注意看po selfprint self在你實現了debugDescription之后的輸出有什么不同. 當你在LLDB中打印一個對象的時候調用的是debugDescription而不是description, 注意到了嗎!
正如你看到了, 當NSObject類或者它的子類有一個description或者debugDescription方法的時候會影響到po的輸出.
那么哪些對象需要重寫description方法呢?你可以簡單的通過image lookup命令加一個正則表達式捕獲那些重寫了此方法的對象.你在前面章節中學到的內容將要派上用場了.
例如, 如果你想要知道哪些Objective-C類重寫了debugDescription方法, 你可以通過下面的命令查詢所有這些方法:

(lldb) image lookup -rn '\ debugDescription\]'

根據輸出的內容可以看到, Foundation框架的作者已經在許多foundation類型(例如:NSArray)里面添加了debugDescription, 讓我們在調試的時候更簡單. 此外還有一些私有的類也重寫了debugDescription方法.
你可以能會注意到在列表里有CALayer類. 讓我們看一下在CALayer類中descriptiondebugDescription有哪些不同.
在LLDB控制臺中輸入下面的內容:

(lldb) po self.view!.layer.description

你將會看到類似下面的輸出:

"<CALayer: 0x61000022e980>"

只有一點點的信息. 現在輸入下面的內容:

(lldb) po self.view!.layer

你將會看到下面這些輸出:

<CALayer:0x61000022e980; position = CGPoint (187.5 333.5); bounds =
CGRect (0 0; 375 667); delegate = <UITableView: 0x7fdd04857c00; frame =
(0 0; 375 667); clipsToBounds = YES; autoresize = W+H; gestureRecognizers
= <NSArray: 0x610000048220>; layer = <CALayer: 0x61000022e980>;
contentOffset: {0, 0}; contentSize: {375, 0}>; sublayers = (<CALayer:
0x61000022d480>, <CALayer: 0x61000022da60>, <CALayer: 0x61000022d8c0>);
masksToBounds = YES; allowsGroupOpacity = YES; backgroundColor = <CGColor
0x6100000a64e0> [<CGColorSpace 0x61800002c580> (kCGColorSpaceICCBased;
kCGColorSpaceModelRGB; sRGB IEC61966-2.1; extended range)] ( 1 1 1 1 )>

這里有更多的能容-而且更加有用!很顯然Core Animation的開發者需要通過引用用description獲取更多更清楚的信息. 但是如果你在調試器中, 你將會看到更多信息.我們并不清楚他們為什么要制造這些不同.可能這些debug description需要執行大量的計算, 因此他們只在絕對必要的時候才用到.
接下來, 你應該還停留在調試器中, 嘗試執行p self:

(lldb) p self

你應該會得到一些類似下面的信息:

(Signals.MasterViewController) $R2 = 0x00007fb71fd04080 {
  UIKit.UITableViewController = {
    baseUIViewController@0 = <extracting data from value failed>
    _tableViewStyle = 0
    _keyboardSupport = nil
    _staticDataSource = nil
    _filteredDataSource = 0x000061800024bd90
    _filteredDataType = 0
}
  detailViewController = nil
}

這看起來可能有點嚇人, 但是讓我們來解析一下.
首先, LLDB輸出了self的類名. 在這里就是Signals.MasterViewController.緊跟著是一個你可以在LLDB中用來引用這個對象的指針. 在上面的例子中就是$R2.你的可能是不同的因為這個數字在你使用LLDB的時候是遞增的.
當你在后面的LLDB會話中想要回到這個對象的時候這個引用是非常有用的, 也許你在不同的范圍里self不再是同一個對象.在這里你可以通過R2引用這個對象. 想知道如何引用, 接著往下看:

(lldb) p $R2

你將會看到同樣的輸出.你會在本章后面的內容里學到更多這種LLDB變量的用法.
在LLDB變量名字的后面是這個對象的地址, 后面跟著一些這個類的明確信息. 在這里, 它顯示UITableViewController相關的詳情, MasterViewController的父類, 緊跟著是 detailViewController的實例變量.
正如你看到的, p命令輸出的信息和po命令輸出的信息是不同的.p的輸出依賴于類型格式, LLDB作者已經添加到Objective-C, Swift, 和其他語言中的每一個內部的數據結構.需要重點注意的是Swift的輸出格式在不同的Xcode發行版中可能有少許的不同.
鑒于類型格式化是LLDB處理的, 如果你想的話你有能力改變它們. 在你的LLDB會話中, 輸入以下命令:

 (lldb) type summary add Signals.MasterViewController --summary-string
"Wahoo!"

你已經告訴了LLDB在你打印一個MasterViewController類的實例的時候你僅僅只想返回靜態字符串, Wahoo!.Signals前綴的實質是為Swift類的鑒于Swift包含這個模塊類名來防止命名空間沖突. 現在再次嘗試輸出self, 像這樣:

(lldb) p self

輸出看起來應該像下面這個樣子:

(lldb) (Signals.MasterViewController) $R3 = 0x00007fb71fd04080 Wahoo!

這個格式在通過APP啟動的時候會被LLDB記住, 因此要確保你練習完p命令之后刪除它. 可以用下面的指令在LLDB會話中刪除:

(lldb) type summary clear

輸入p self將會回到LLDB作者默認的實現方式.類型格式化是一個值得我們在后面章節中詳細討論的話題.因為它可以在你沒有源代碼的情況下幫助你詳細的調試應用程序.

Swift vs Objective-C調試環境

注意到這里有兩個調試環境在調試你的代碼的時候是非常重要的:一個非Swift調試環境和一個swift調試環境.默認情況下, 當你在 Objective-C代碼中停下來的時候, LLDB將會使用非Swift(Objective-C)調試環境, 當你在Swift代碼中停下來的時候, LLDB將會使用Swift調試環境.聽起來很合邏輯, 對吧?
如果你將調試器停在了藍色斷點的外面, LLDB默認情況下將會選擇Objective-C環境.確保你之前在GUI里面創建的斷點依然存在并仍然可用然后構建并運行APP. 當斷點觸發的時候, 在你的LLDB會話里輸入下面的內容:

(lldb) po [UIApplication sharedApplication]

LLDB將會拋出一個錯誤給你:

error: <EXPR>:3:16: error: expected ',' separator
[UIApplication sharedApplication]
^ ,

你已經在Swift代碼中停下來了, 所以你在swift環境中.但是你卻嘗試運行Objective-C的代碼.那是行不通的.類似的在Objective-C環境中運行Swift代碼也是行不通的.
你可以用-l選項選擇一個語言強制讓表達式使用 Objective-C環境.然而, 由于poexpression - O --的縮寫, 你將因為提供在--后面的參數而不能夠使用po命令, 這就意味著你將不得不輸入expression. 在LLDB中, 輸入下面的內容:

(lldb) expression -l objc -O -- [UIApplication sharedApplication]

這里你已經告訴LLDB為Objective-C使用objc語言.如果必要的話你還可以為 Objective-C++用objc++.
LLDB將會輸出shared application的引用.嘗試在Swift里做同樣的事情. 既然你已經停在了Swift環境里, 嘗試用Swift的語法打印出UIApplication的引用, 想下面這樣:

(lldb) po UIApplication.shared

你將會得到在Objective-C環境通樣的輸出.輸入continue繼續運行應用程序, 然后在藍色斷點的外面暫停Signals項目.
在這里, 按下上箭頭按鈕得到你剛才執行的swift命令并看看發生了什么:

(lldb) po UIApplication.shared

LLDB將會再次拋出一個錯誤:

error: property 'shared' not found on object of type 'UIApplication'

記住, 在藍色斷點外面停下會讓LLDB進入Objective-C環境. 這就是為什么在你嘗試執行Swift代碼的時候會拋出一個錯誤.
你因該時刻注意調試器當前停在什么樣的語言環境里.

用戶定義的變量

正如你之前看到的, LLDB在打印出對象的時候會自動維護局部變量.你同樣也可以創建自己的變量.
從程序里移除所有的斷點構建并運行APP.在藍色斷點外面停止調試器所以默認的是Objective-C環境.在這里輸入:

(lldb) po id test = [NSObject new]

LLDB將會執行這段代碼, 這將會創建一個新的NSObject對象并存儲在test變量里.現在嘗試打印這個對象:

(lldb) po test

你將會得到一個類似下面的錯誤:

error: use of undeclared identifier 'test'

這是因為你需要讓LLDB記住這個變量你就要用到$修飾符.
再次嘗試聲明test變量在前面加上$:

(lldb) po id $test = [NSObject new]
(lldb) po $test
<NSObject: 0x60000001d190>

這個變量被創建為Objective-C對象.但是如果你想在swfit環境中訪問這個變量會發生什么呢?嘗試輸入下面的內容:

(lldb) expression -l swift -O -- $test

到現在為止,一直都還不錯.現在嘗試在這個Objective-C類上執行swift風格的代碼.

(lldb) exppression -l swift -O -- $test.description

你將會得到一個類似下面的錯誤:

error: <EXPR>:3:1: error: use of unresolved identifier '$test'
$test.description
^~~~~

如果你在Objective-C環境中創建了一個LLDB變量, 然后轉到了swift環境中, 不要期望一切都會照常工作.隨著時間的流失我們會看到swift和Objective-C通過LLDB橋接的改善.
那么如何在LLDB中創建應用與真實環境的引用?你可以將引用存在一個對象里并執行你選擇的任意代碼. 要看實際效果, 可以在MasterViewController的父視圖控制器里創建一個符號性的斷點, MasterContainerViewController使用了一個Xcode的符號斷點在viewDidLoad方法里.
在Symbol部分輸入:

Signals.MasterContainerViewController.viewDidLoad () -> ()

要注意參數和返回值之間的空格, 否則斷點是不生效的.
你的斷點看起來應該是下面這個樣子:

page62image17352.png

構建并運行APP.Xcode現在將會斷點在MasterContainerViewController.viewDidLoad().From there, type the following:

(lldb) p self

鑒于這是你再swift調試環境執行的第一個參數, LLDB將會創建一個變量$R0.在LLDB中輸入continue繼續執行程序.
因為執行移到更大和更好的運行循環事件并停留在了viewDidLoad()里, 所以所以現在你還不能通過使用self來引用 MasterContainerViewController實例.
但是你仍然有$R0這個變量!現在你可以應用MasterContainerViewController甚至可以執行任意代碼來幫助你調試.
手動的將APP暫停在調試器中, 然后鍵入下面內容:

(lldb) po $R0.title

不幸的是, 你將會得到:

error: use of undeclared identifier '$R0'

你將調試器停在了藍色斷點的外面!記住, LLDB默認的是Objective-C環境. 你需要使用-l選項進入swift環境:

(lldb) expression -l swift -- $R0.title

這將會輸出下面的內容:

(String?) $R1 = "Quarterback"

當然, 這是顯示在導航欄上的視圖控制器的標題.
現在, 輸入下面的內容:

(lldb) expression -l swift -- $R0.title = "! ! ! ! ! "

輸入continue繼續運行APP.

page64image1064.png

正如你看到的你可以輕松的操控你想操控的變量.
此外, 你也可以在代碼里創建一個斷點, 執行代碼, 并且在斷點觸發的時候暫停. 如果你正在調試一些事情并且想用特定的輸入執行一個函數看看它是如何執行的時候這是很有用的.
例如, 你仍然有在viewDidLoad()里的符號斷點, 所以嘗試執行那個方法去檢查代碼. 暫停程序的執行, 然后輸入:

(lldb) expression -l swift -O -- $R0.viewDidLoad()

什么事都沒有發生. 沒有觸發斷點. 怎么會這樣?事實上, MasterContainerViewController已經執行了這個方法, 但是在默認情況下, LLDB在執行命令的時候會忽略任何斷點.你可以用-i選項經用這個功能.
在LLDB控制臺中輸入下面的內容:

(lldb) expression -l swift -O -i 0 -- $R0.viewDidLoad()

現在LLDB會在你之前創建的符號斷點處停下來.這種策略是測試方法邏輯的絕佳方法.例如, 你可以實現測試驅動的調試, 通過給一個函數不同的參數來看看它如何處理不同的輸入.

類型格式化

LLDB中一個好的選項是你可以執行基本數據類型的輸出格式. 這讓LLDB成為了一個學習編譯器是格式化基本的C類型的偉大工具.這在你學習后面的匯編章節的時候是必須知道的.
在LLDB控制臺中輸入下面的命令:

(lldb) expression -G x -- 10

-G選項告訴LLDB你想要輸出什么樣的格式. G代表著GDB格式. 你可能不知道, GDB是LLDB前一代的調試器. 在這里, 使用的x代表著十六進制格式.
你將會看到下面的輸出:

(int) $0 = 0x0000000a

這是將十進制的10作為十六進制輸出.
LLDB還有一種指定輸出格式的更短的語法. 輸入下面的命令:

(lldb) p/x 10

你將會看到和前面一樣的輸出. 但是這一次你輸入的內容更少了!
這對于學習C數據類型的表示形式非常有幫助. 例如, 如何用二進制表示數字10呢?

(lldb) p/t 10

/t指明了二進制形式.你將會看到十進制的10是如何用二進制表示的.
負10又是如何表示的呢?

(lldb) p/t -10

由兩部分組成的10進制的10.夠清楚吧!
浮點數10.0又如何用二進制表示呢?

 (lldb) p/t 10.0

這可能派上用場!
字符D的ASCII值是怎樣的呢?

(lldb) p/d 'D'

所以D是68!/d指定的是十進制形式.
最后, 整數后面隱藏的縮寫是什么?

(lldb) p/c 1430672467

/c指明了字符形式. 它將數字轉換成二進制, 每8位作為一個整體(1字節), 然后將每一個字節都轉換成ASCII字符.在這里, 它有4個字符代碼STFU.
下面是所有輸出格式的列表(可以參考:[https://sourceware.org/gdb/ onlinedocs/gdb/Output-Formats.html](https://sourceware.org/gdb/ onlinedocs/gdb/Output-Formats.html)):
? x: hexadecimal
? d: decimal
? u: unsigned decimal
? o: octal
? t: binary
? a: address
? c: character constant
? f: float
? s: string
如果這個格式對你來說不夠用, 你可以使用LLDB拓展的格式, 但是你將不能夠使用GDB格式語法.
LLDB的格式可以像下面這樣使用:

(lldb) expression -f Y -- 1430672467

這將會輸出下面的內容:

(int) $0 = 53 54 46 55             STFU

這解釋了之前的FourCC代碼!
LLDB擁有下面的格式(可以參考[http://lldb.llvm.org/
varformats.html](http://lldb.llvm.org/
varformats.html)):
? B: boolean
? b: binary
? y: bytes
? Y: bytes with ASCII
? c: character
? C: printable character
? F: complex float
? s: c-string
? i: decimal
? E: enumeration
? x: hex
? f: float
? o: octal
? O: OSType
? U: unicode16
? u: unsigned decimal
? p: pointer

我們為什么要學習這些?

嘗試通過執行help expression查看其他的expression選項并查看你可以用它們來做什么.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容