標籤:lldb
現在, 你已經有了堅實的調試基礎.你可以找到並附加到你感興趣的程式上, 高效的建立Regex斷點來覆蓋一個寬泛的範圍, 在棧幀中導航並且使用expression命令查看變數.
然而, 是時候通過強大的LLDB來查看感興趣的代碼了.在本章中, 你會深入的學習image命令.
image命令是target modules命令的別名. image是專門用來查詢模組(modules)相關資訊的; 更確切的說, 代碼被載入到一個線程裡面執行.模組可以包含許多事情, 包含主要的執行代碼, 架構或者外掛程式.然而, 大多數的模組都來自動態庫.比如iOS的UIKit和macOS的AppKit都是常見的動態庫.
image命令用來查詢任何私人架構的資訊和它裡面沒有在標頭檔裡公開的類和方法都是非常有用的.
等一下...模組?
你將繼續用到Signals項目.開啟這個項目, 用iPhone7模擬器構建並運行.
暫停調試器並在LLDB控制台輸入下面的命令:
(lldb) image list
這條命令將會列出當前載入的所有的模組. 你會看到許多!這個列表的開頭看起來應該是下面這個樣子:
[ 0] 13A9466A-2576-3ABB-AD9D-D6BC16439B8F 0x00000001013aa000 /usr/lib/
dyld
[ 1] 493D07DF-3F9F-30E0-96EF-4A398E59EC4A 0x000000010118e000 /
Applications/Xcode.app/Contents/Developer/Platforms/
iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/
dyld_sim
[ 2] 4969A6DB-CE85-3051-9FB2-7D7B2424F235 0x000000010115c000 /Users/
derekselander/Library/Developer/Xcode/DerivedData/Signals-
bqrjxlceauwfuihjesxmgfodimef/Build/Products/Debug-iphonesimulator/
Signals.app/Signals
頭兩個是動態載入器:一個是基於系統的另一個是專門為模擬器添加的. 這些都是必要的代碼, 它們允許你的程式將動態庫載入到記憶體中用於程式的執行.第三個是APP的主二進位檔案, Signals.
但是在這個列表中還有其他更多的內容!你可以只過濾出那些你感興趣的內容.在LLDB中輸入下面命令:
(lldb) image list Foundation
你會得到類似於下面的輸出:
[ 0] 4212F72C-2A19-323A-84A3-91FEABA7F900 0x0000000101435000 /
Applications/Xcode.app/Contents/Developer/Platforms/
iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//System/
Library/Frameworks/Foundation.framework/Foundation
這對於只顯示你想要查看的模組的資訊是非常有用的.
讓我們看一下這些輸出. 這裡有一些有趣的內容:
首先列印出來的是模組的UUID(4212F72C-2A19-323A-84A3-91FEABA7F900).這個UUID對於捕獲符號資訊和唯一標示Foundation模組是非常重要的.
2.緊接著UUID的是載入地址(0x0000000101435000).這標明了Foundation模組載入到Signals可執行進程空間後的地址.
3.最後, 你會得到這個模組的二進位檔案在本地的全路徑.
讓我們深入到另一個常用的模組UIKit中看一下.在LLDB中輸入下面的命令:
(lldb) image dump symtab UIKit -s address
這條命令會提取出UIKit中所有可用的符號表資訊. 這條命令輸出的內容是按照函數在UIKit中實現的順序排列的, 這是-s address的作用.
這裡有很多有用的資訊, 但是你不可能把所有的內容都讀一遍. 你需要一個高效的方法在UIKit中查詢你感興趣的代碼.
image lookup命令可以完美的過濾出所有的資料.輸入下面的命令:
(lldb) image lookup -n "-[UIViewController viewDidLoad]"
這回提取出只與UIViewController的viewDidLoad執行個體方法相關的內容.你會看到與這個方法相關的符號的名字, 還有那個方法在UIKit架構中實現的代碼.
這很好並且很全, 但是輸入這些文字有些乏味並且這隻能提取出指定的執行個體.
然而這正是Regex要做的事情.-r選項將能夠讓你使用Regex查詢. 在LLDB中輸入下面的命令:
(lldb) image lookup -rn UIViewController
這條指令不僅僅會提取出所有的UIViewController方法, 而且會輸類似UIViewControllerBuiltinTransitionViewAnimator這樣包含有UIViewController字樣的方法.你也可以使用這則運算式只輸出UIViewController的方法.在LLDB中輸入下面的內容:
(lldb) image lookup -rn ‘[UIViewController\ ‘
這當然很好, 但是怎麼處理分類呢?他們是UIViewController(CategoryName)的形式.嘗試搜尋UIViewController的所有分類:
(lldb) image lookup -rn ‘[UIViewController(\w+)\ ‘
現在指令開始變得複雜了. 開頭的反斜線表明你是想要使用[的字面意, 然後是UIViewController. 最後是(的字面意, 然後是一個或多個文字數字或底線(w+的含義)., 然後是), 最後跟著一個空格.
Regex的知識將會協助你創造性的查詢載入到二進位檔案中的任何模組的公有或者私人的代碼.
這不僅會列印出公有和私人的方法, 而且會給出UIViewController類覆蓋的父類的方法.
捕獲代碼
無論你捕獲的是公有的代碼還是私人代碼, 有時只想弄明白編譯器是如何產生一個特定函數的函數名的.你已經簡單的使用上面的image lookup命令找到了UIViewController的方法.你也在第四章中用它找到了swift屬性的setters 和 getter方法.
然而, 這裡還有許多例子可以協助你更好的理解代碼是如何產生的以及如何在你感興趣的地方設定斷點. 一個特殊的例子就是查看 Objective-C代碼塊的方法聲明.
那麼搜尋Objective-C代碼塊方法聲明的最好的方法是什麼呢?鑒於你沒有任何線索來尋找代碼塊是在那裡被命名的, 所以一個好的方法就是在代碼塊的內部建立一個斷點然後從那裡開始檢查.
開啟UnixSignalHandler.m, 然後找到單例方法sharedHandler.在這個函數中找到下面的代碼:
dispatch_once(&onceToken, ^{
sharedSignalHandler = [[UnixSignalHandler alloc] initPrivate];
});
在Xcode中在sharedSignalHandler的起始位置設定一個斷點. 然後構建並運行. Xcode現在將會停在你設定斷點的地方.在調試視窗中查看棧幀的頂部.
page82image1080.png
你可以找到你在Xcode中的函數的名字. 在調試欄中你將會看到棧追蹤並且可以看到frame 0. 要複製粘貼的話有點小難. 我們可以用下面的命令替代:
(lldb) frame info
你將會得到類似下面的輸出:
frame #0: 0x0000000100cb20a0 Commons`34+[UnixSignalHandler
sharedHandler]_block_invoke((null)=0x0000000100cb7210) + 16 at
UnixSignalHandler.m:68
正如你看到的, 函數的全名是34+[UnixSignalHandler sharedHandler]_block_invoke.
函數名中有一個很有趣的部分, _block_invoke. 這也許就是在Objective-C中協助你標識一個代碼塊的標誌.在LLDB中輸入下面的命令:
(lldb) image lookup -rn _block_invoke
這條命令用Regex搜尋關鍵詞_block_invoke. 它會將_block_invoke作為萬用字元處理包含_block_invoke的內容.
但是等一下!實際上你列印出所有載入到程式中的Objective-C代碼快.這個搜尋包含UIKit,Foundation,iPhoneSimulator SDK等等中的所有代碼塊.你應該將你的搜尋範圍限定在Signals模組中.在LLDB在中輸入下面的命令:
(lldb) image lookup -rn _block_invoke Signals
什麼都沒有列印出來.發生了什麼事呢?開啟Xcode右側的File Inspector面板. 或者按下? + Option + 1.
page83image8096.png
如果你看到UnixSignalHandler.m被編譯的地方, 你就會發現它實際上被編譯進了Commons架構. 所以, 重新搜尋並在Commons模組中搜尋Objective-C代碼塊. 在LLDB中輸入下面命令:
(lldb) image lookup -rn _block_invoke Commons
最後, 你會看到一些輸出.
現在你會看到你再Commons架構中找到的所有Objective-C代碼塊的輸出.
現在, 讓我們在你找到的代碼塊的一個子集上建立一個斷點.
在LLDB中輸入下面的命令:
(lldb) rb appendSignal._block_invoke -s Commons
注意: 在模組中搜尋代碼和在代碼中搜尋模組有一些細微的不同.用上面的命令做一個例子.當你想要搜尋Commons
架構中所有的代碼塊, 你應該使用image lookup -rn _block_invoke Commons
.當你想要為Commons
架構中的代碼塊設定一個斷點, 你應該使用`rb appendSignal.block_invoke -s Commons.注意
-s`參數後面的空格.
這條指令會在所有appendSignal方法的代碼塊處設定斷點.
在LLDB中輸入continue繼續運行程式.跳進終端並輸入下面的命令:
pkill -SIGIO Signals
你發送給程式的訊號將會被處理.然而, 在這個訊號被更新到tableview之前, 你的正則斷點就會被觸發.
觸發的第一個斷點應該是:
__38-[UnixSignalHandler appendSignal:sig:]_block_invoke
繼續運行調試器並跳過這一步.
接下來你會觸發另一個斷點:
__38-[UnixSignalHandler appendSignal:sig:]_block_invoke_2
這個函數名與第一個函數名相比有一個有趣的地方; 注意到數字2.編譯器使用<FUNCTION_NAME>_block_invoke的格式定義叫做<FUNCTION_NAME>的blocks.然而, 在函數中不只有一個block時, 就會在尾部添加一個數字.
正如你在前面學到的內容, frame variable命令將會列印出指定函數中所有已知的執行個體變數. 現在嘗試執行一下這個命令來看一下這個block的引用.輸入下面的命令:
(lldb) frame variable
輸出的內容應該像下面這個樣子:
(__block_literal_5 ) = 0x0000608000275e80
(int) sig = <read memory from 0x41 failed (0 of 4 bytes read)>
(siginfo_t ) siginfo = <read memory from 0x39 failed (0 of 8 bytes
read)>
(UnixSignalHandler *const) self = <read memory from 0x31 failed (0 of 8
bytes read)>
這些讀記憶體時的故障看起來很不友好!步過一次, 即可以使用Xcode也可以在LLDB 中輸入next.接下來, 再次在LLDB中執行frame variable.
這一次你會看到類似下面的輸出:
(__block_literal_5 ) = 0x0000608000275e80
(int) sig = 23
(siginfo_t ) siginfo = 0x00007fff587525e8
(UnixSignalHandler ) self = 0x000061800007d440
(UnixSignal ) unixSignal = 0x000000010bd9eebe
你需要步過函數的聲明, 以便代碼塊可以執行一些初始化邏輯來設定這個函數. 函式宣告是與彙編相關的內容, 你會在第二部分學到.
這實際上非常有趣. 首先你看到了一個引用著這個代碼塊的對象, 那是被調用的地方.在這裡是__block_literal_5. 然後是一些傳到調用這個代碼塊的Objective-C方法裡的sig 和 siginfo 參數. 這些是如何傳到代碼塊裡的呢?
好, 當一個代碼塊建立的時候, 編譯器聰明到足以弄明白它會用到哪些參數.然後它會建立一個函數並把這些參數帶進去. 當代碼快被調用的時候, 就是這個函數被調用, 並將相關的參數穿進去.
在LLDB中輸入下面命令:
(lldb) image dump symfile Commons
你將會看到許多輸出. 使用? + F 通過編譯器搜尋block類型的聲明:__block_literal_5. 有一個比較重要的東西要提醒你的是當LLVM更新的時候你得到的類型可能有細微的不同, 所以請確保你從frame variable 命令的輸出中得到了正確的類型.
在搜尋block類型的聲明時, 會有幾種不同情形. 搜尋與block的行號相匹配的結構體的聲明. 例如, 你最初建立的123行的斷點, 同樣也可以在聲明中搜尋. 最終你會得到一些類似下面的輸出:
0x7fefe24bcf90: Type{0x100000e06} , name = "block_literal_5", size =
52, decl = UnixSignalHandler.m:123, compiler_type = 0x00007fefd86d0410
struct block_literal_5 {
void *isa;
int flags;
int reserved;
void (FuncPtr)();
block_descriptor_withcopydispose __descriptor;
UnixSignalHandler const self;
siginfo_t siginfo;
int sig;
}
這就是定義代碼塊的那個對象!
正如你看到的, 這就如同有一個標頭檔在告訴你如何在代碼塊的記憶體中自由的找到你想要的東西. 只要提供你找到的block_literal_5在記憶體中的應用, 你就可以輕鬆的列印出這個block引用的所有變數.通過輸入下面的命令再次擷取棧幀的變數資訊:
(lldb) frame variable
接下來, 找到__block_literal_5 對象的記憶體位址並用下面的方式列印出來:
(lldb) po ((__block_literal_5 *)0x0000618000070200)
你將會看到類似下面的輸出:
<NSMallocBlock: 0x0000618000070200>
如果你的輸出與上面的不一樣, 確保你使用的block_literal_5 的記憶體位址是你的block的地址,每一次啟動並執行時候記憶體位址都會有些許不同.
現在你可以查詢block_literal_5的記憶體結構了. 在LLDB中輸入:
(lldb) p/x ((block_literal_5 *)0x0000618000070200)->FuncPtr
這條指令會提取出這個block的函數指標的位置.輸出的內容看起來應該是下面這個樣子:
(void (*)()) $1 = 0x000000010756d8a0 (Commons`__38-[UnixSignalHandler appendSignal:sig:]_block_invoke_2 at UnixSignalHandler.m:123)
block的函數指標指向, 運行時block被調用時的函數. 現在被執行的時候他們是同樣的地址! 你可以輸入下面的命令進行確認, 用你最近一次的命令列印出來的函數指標的地址替換下面的地址:
(lldb) image lookup -a 0x000000010756d8a0
這裡在image lookup後面用了-a(address)選項來查看給定地址相關的符號.回到block結構體的成員變數, 你依然可以列印出傳遞給block的所有的參數.
輸入下面的命令, 再次用你的block的地址替換下面的地址:
(lldb) po ((__block_literal_5 *)0x0000618000070200)->sig
這將會輸出作為block的父函數的參數的signal的序號.在結構體的成員變數裡還有一個UnixSignalHandler的引用叫做self.
為什麼會那樣呢? 看一下這個 block 並且 捕獲下面這行代碼:
[(NSMutableArray *)self.signals addObject:unixSignal];
它是block捕獲的self的引用., 是用來找到signals數組的位移的.因此block需要知道self 是什麼. 很酷, 對吧?
用image dump symfile命令與module聯合起來是用來學習某種未知資料類型的好方法. 它還是用來學編譯器是如何用你的原始碼產生代碼的好工具.
此外, 你可以檢查blocks是如何持有指向外部的block的引用的-當出現運行迴圈的時候會是一個非常有用的工具.
窺探
你已經知道了如何用靜態方式檢查一個私人類的執行個體變數, 但是把block的記憶體位址單獨留下來是在太折磨人了. 嘗試著把他列印出來並用動態分析的方法查看它.輸入下面的內容, 用你block的地址替換下面的地址:
po 0x0000618000070200
LLDB會提取出一個表明自己是Objective-C類的類:
<NSMallocBlock: 0x618000070200>
這很有趣. 這是一個NSMallocBlock類.現在你已經學習了如何提取出一個類的共有方法和私人方法, 現在是時候查看一下NSMallocBlock實現的方法了.在LLDB中輸入:
(lldb) image lookup -rn NSMallocBlock
什麼都沒有發生. 這說明NSMallocBlock沒有覆蓋它的父類的任何方法. 輸入下面的命令來查看NSMallocBlock的父類:
(lldb) po [NSMallocBlock superclass]
這回產生一個類似的名字叫做__NSMallocBlock的類--注意尾部缺少的底線. 你可以找出這個類的哪些資訊呢?這個類是否實現或者覆蓋了一些方法呢? 在LLDB中輸入下面的命令:
(lldb) image lookup -rn NSMallocBlock
用這條命令提取出來的方法表明NSMallocBlock是負責記憶體管理的, 因為它實現了像retain和release這樣的方法.__NSMallocBlock的父類又是什麼呢?在LLDB中輸入下面的命令:
(lldb) po [__NSMallocBlock superclass]
你會得到另一個類NSBlock.這個類是幹什麼?它又實現了哪些方法呢?在LLDB中輸入下面的命令:
(lldb) image lookup -rn ‘NSBlock\ ‘
注意最後的反斜線和空格. 記住-這能確保沒有其它類能夠匹配這次查詢, 如果沒有它的話, 那麼其它一些包含NSBlock名字的類也會被匹配到.一些方法將會被輸出.其中有一個叫做invoke的方法, 看起來極為有趣:
Address: CoreFoundation[0x000000000018fd80] (CoreFoundation.TEXT.text
- 1629760)
Summary: CoreFoundation`-[NSBlock invoke]
現在你將會嘗試著在block中調用這個方法.然而, 你並不想讓持有這個block的的引用在release的時候消失, release減少它的retainCount, 所以block有被釋放的風險.
有一個非常簡單的方法可以保留這個block-只需要retain一下!輸入下面的命令, 用你的block的地址替換下面代碼中的地址:
(lldb) po id $block = (id)0x0000618000070200
(lldb) po [$block retain]
(lldb) po [$block invoke]
在最後一行你將會看到下面這些輸出:
Appending new signal: SIGIO
nil
這表明你的block已經被調用了一次. 乾淨漂亮!
它之所以會生效是應為當block被調用的時候所有設定都已經準備就緒了, 因為你當前已經正確的停在了block開始的位置.
這種類型的方法來查看公有和私人類的, 然後查看它們實現的方法, 是一種學習程式底層實現的好方法.後面你會用同樣的過程來尋找方法並分析這些方法執行時的彙編代碼, 會給到你一個非常接近源始方法的原始碼.
調試私人方法
image lookup命令在尋找私人方法以及公有方法上面做的很漂亮, 他會貫穿你的整個apple開發生涯.
然而, 這裡還有一些隱藏的方法在調試你自己的代碼的時候非常有用.例如, 以_開頭的方法通常都表明它是一個私人(並且是潛在的很重要的)方法.
讓我們搜尋一下所有模組中的所有包含底線並包含description關鍵字的的Objective-C的方法.
再次構建並運行項目. 當sharedHandler處的斷點被觸發的時候, 在LLDB中輸入下面的內容:
(lldb) image lookup -rn (?i)\ \w+description]
這個運算式有點複雜所以讓我們解析一下.
這個運算式會搜尋空格(前面需要有一個)後面跟著底線, 接下來是, 一個或多個字母或數字後面跟著description單詞, 最後跟著]字元的方法.
在這個Regex開始的地方有一個有趣的字元集(?i). 這表明在搜尋的時候不區分大小寫.
這個運算式有一個反斜線首碼符. 這表明你想使用字元的字面量而不是字元在Regex中的含義.這叫做‘escaping‘.例如, 在一個Regex中, ]字元是有特定含義的, 所以你需要是使用].
在上面的Regex中\w字元是個例外. 它指定的搜尋內容是底線, 字母或數字(例如, , a- z, A-Z, 0-9).
如果你在閱讀這行代碼的時候依然有不理解的地方, 強烈推薦你看一下https://docs.python.org/2/library/re.html深入了理解Regex查詢.往後去Regex只會變得更加複雜.
仔細查看image lookup的輸出. 在找到最佳答案之前尋找是很乏味的, 因此確保你查看了所有的輸出.
你可能會注意到幾個UIKit中的一個NSObject的分類IvarDescription中幾個比較有趣的方法.
嘗試只將這個分類的內容列印出來.在LLDB中輸入下面的內容:
(lldb) image lookup -rn NSObject(IvarDescription)
控制台將會輸出這個分類實現的所有方法. 在列印出來的這些方法中, 有幾個比較有趣的方法:
_ivarDescription
_propertyDescription
_methodDescription
因為這是NSObject的分類, 所以所有NSObject的子類都能調用這些方法. 這讓一切都變得更完美, 當然!
嘗試在UIApplication上調用這些方法. 在LLDB中輸入下面的命令:
(lldb) po [[UIApplication sharedApplication] _ivarDescription]
因為UIApplication持有很多執行個體變數所以你會得到很多輸出.仔細察看並且找到你感興趣的內容. 不用返回來繼續閱讀, 直到你找到感興趣的東西. 這很重要!
在仔細察看了輸出之後, 你會看到引用了一個私人類UIStatusBar. UIStatusBar的Objective-C 的setter方法在那裡呢, 我聽到你問?讓我們來看一下! 在LLDB中輸入下面的內容:
(lldb) image lookup -rn ‘[UIStatusBar\ set‘
這會提取出UIStatusBar所有可用的setter方法.此外還有UIStatusBar中聲明的和覆蓋的方法, 你可以訪問到它父類的所有可有的方法.查看一下UIStatusBar是否是UIView的子類.
(lldb) po (BOOL)[[UIStatusBar class] isSubclassOfClass:[UIView class]]
提示一下, 你可以重複使用superclass方法這繼承樹上往上爬.正如你看到了, UIStatusBar看起來是UIView的子類, 因此在這個類中backgroundColor屬性是可以用的. 讓我們練習一下.
首先, 在LLDB中輸入下面的指令:
(lldb) po [[UIApplication sharedApplication] statusBar]
你將會看到一些類似下面的輸出;
<UIStatusBar: 0x7fb8d400d200; frame = (0 0; 375 20); opaque = NO;
autoresize = W+BM; layer = <CALayer: 0x61800003aec0>>
這回列印出你APP的UIStatusBar執行個體.接下來使用, status bar的地址, 在LLDB中輸出下面的命令:
(lldb) po [0x7fb8d400d200 setBackgroundColor:[UIColor purpleColor]]
在LLDB中, 刪除你之前建立的所有斷點:
(lldb) breakpoint delete
繼續運行APP並看看你用指尖創造出來的美好世界!
page91image1048.png
現在還不是最漂亮的APP, 但是至少你已經找到了一個私人方法並用它做了一些有趣的事情!
我們為什麼要學習這些呢?
作為一個挑戰, 試著用image lookup命令找出Signals模組中所有的閉包. 一旦你做到了, 在每一個Signals模組的每一個Swift閉包中建立一個斷點. 如果它對你來說太簡單了, 嘗試著找到可以在didSet/willSet屬性時停下來的代碼, 或者做一些/try/catch``blocks的操作.
也可以, 找出更多隱藏在Foundation或者UIKit中的私人方法. 學習愉快!
Advanced+Apple+Debugging(7)