現(xiàn)在, 你已經(jīng)有了堅實的調(diào)試基礎.你可以找到并附加到你感興趣的程序上, 高效的創(chuàng)建正則表達式斷點來覆蓋一個寬泛的范圍, 在棧幀中導航并且使用expression
命令查看變量.
然而, 是時候通過強大的LLDB來查看感興趣的代碼了.在本章中, 你會深入的學習image
命令.
image
命令是target modules
命令的別名. image
是專門用來查詢模塊(modules)相關信息的; 更確切的說, 代碼被加載到一個線程里面執(zhí)行.模塊可以包含許多事情, 包含主要的執(zhí)行代碼, 框架或者插件.然而, 大多數(shù)的模塊都來自動態(tài)庫.比如iOS的UIKit
和macOS的AppKit
都是常見的動態(tài)庫.
image
命令用來查詢?nèi)魏嗡接锌蚣艿男畔⒑退锩鏇]有在頭文件里公開的類和方法都是非常有用的.
等一下...模塊?
你將繼續(xù)用到Signals
項目.打開這個項目, 用iPhone7模擬器構建并運行.
暫停調(diào)試器并在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
頭兩個是動態(tài)加載器:一個是基于系統(tǒng)的另一個是專門為模擬器添加的. 這些都是必要的代碼, 它們允許你的程序將動態(tài)庫加載到內(nèi)存中用于程序的執(zhí)行.第三個是APP的主二進制文件, Signals
.
但是在這個列表中還有其他更多的內(nèi)容!你可以只過濾出那些你感興趣的內(nèi)容.在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
這對于只顯示你想要查看的模塊的信息是非常有用的.
讓我們看一下這些輸出. 這里有一些有趣的內(nèi)容:
- 首先打印出來的是模塊的UUID(4212F72C-2A19-323A-84A3-91FEABA7F900).這個UUID對于捕獲符號信息和唯一標示Foundation模塊是非常重要的.
2.緊接著UUID的是加載地址(0x0000000101435000).這標明了Foundation
模塊加載到Signals
可執(zhí)行進程空間后的地址.
3.最后, 你會得到這個模塊的二進制文件在本地的全路徑.
讓我們深入到另一個常用的模塊UIKit
中看一下.在LLDB中輸入下面的命令:
(lldb) image dump symtab UIKit -s address
這條命令會提取出UIKit
中所有可用的符號表信息. 這條命令輸出的內(nèi)容是按照函數(shù)在UIKit
中實現(xiàn)的順序排列的, 這是-s address
的作用.
這里有很多有用的信息, 但是你不可能把所有的內(nèi)容都讀一遍. 你需要一個高效的方法在UIKit
中查詢你感興趣的代碼.
image lookup
命令可以完美的過濾出所有的數(shù)據(jù).輸入下面的命令:
(lldb) image lookup -n "-[UIViewController viewDidLoad]"
這回提取出只與UIViewController
的viewDidLoad
實例方法相關的內(nèi)容.你會看到與這個方法相關的符號的名字, 還有那個方法在UIKit
框架中實現(xiàn)的代碼.
這很好并且很全, 但是輸入這些文字有些乏味并且這只能提取出指定的實例.
然而這正是正則表達式要做的事情.-r
選項將能夠讓你使用正則表達式查詢. 在LLDB中輸入下面的命令:
(lldb) image lookup -rn UIViewController
這條指令不僅僅會提取出所有的UIViewController
方法, 而且會輸類似UIViewControllerBuiltinTransitionViewAnimator
這樣包含有UIViewController
字樣的方法.你也可以使用這則表達式只輸出UIViewController
的方法.在LLDB中輸入下面的內(nèi)容:
(lldb) image lookup -rn '\[UIViewController\ '
這當然很好, 但是怎么處理分類呢?他們是UIViewController(CategoryName)
的形式.嘗試搜索UIViewController
的所有分類:
(lldb) image lookup -rn '\[UIViewController\(\w+\)\ '
現(xiàn)在指令開始變得復雜了. 開頭的反斜杠表明你是想要使用[
的字面意, 然后是UIViewController
. 最后是(
的字面意, 然后是一個或多個文字數(shù)字或下劃線字符(w+
的含義)., 然后是)
, 最后跟著一個空格.
正則表達式的知識將會幫助你創(chuàng)造性的查詢加載到二進制文件中的任何模塊的公有或者私有的代碼.
這不僅會打印出公有和私有的方法, 而且會給出UIViewController
類覆蓋的父類的方法.
捕獲代碼
無論你捕獲的是公有的代碼還是私有代碼, 有時只想弄明白編譯器是如何生成一個特定函數(shù)的函數(shù)名的.你已經(jīng)簡單的使用上面的image lookup
命令找到了UIViewController
的方法.你也在第四章中用它找到了swift屬性的setters
和 getter
方法.
然而, 這里還有許多例子可以幫助你更好的理解代碼是如何生成的以及如何在你感興趣的地方設置斷點. 一個特殊的例子就是查看 Objective-C代碼塊的方法聲明.
那么搜索Objective-C代碼塊方法聲明的最好的方法是什么呢?鑒于你沒有任何線索來尋找代碼塊是在那里被命名的, 所以一個好的方法就是在代碼塊的內(nèi)部創(chuàng)建一個斷點然后從那里開始檢查.
打開UnixSignalHandler.m
, 然后找到單例方法sharedHandler
.在這個函數(shù)中找到下面的代碼:
dispatch_once(&onceToken, ^{
sharedSignalHandler = [[UnixSignalHandler alloc] initPrivate];
});
在Xcode中在sharedSignalHandler
的起始位置設置一個斷點. 然后構建并運行. Xcode現(xiàn)在將會停在你設置斷點的地方.在調(diào)試窗口中查看棧幀的頂部.
你可以找到你在Xcode中的函數(shù)的名字. 在調(diào)試欄中你將會看到棧追蹤并且可以看到frame 0. 要復制粘貼的話有點小難. 我們可以用下面的命令替代:
(lldb) frame info
你將會得到類似下面的輸出:
frame #0: 0x0000000100cb20a0 Commons`__34+[UnixSignalHandler
sharedHandler]_block_invoke((null)=0x0000000100cb7210) + 16 at
UnixSignalHandler.m:68
正如你看到的, 函數(shù)的全名是__34+[UnixSignalHandler sharedHandler]_block_invoke
.
函數(shù)名中有一個很有趣的部分, _block_invoke
. 這也許就是在Objective-C
中幫助你標識一個代碼塊的標志.在LLDB中輸入下面的命令:
(lldb) image lookup -rn _block_invoke
這條命令用正則表達式搜索關鍵詞_block_invoke
. 它會將_block_invoke
作為通配符處理包含_block_invoke
的內(nèi)容.
但是等一下!實際上你打印出所有加載到程序中的Objective-C
代碼快.這個搜索包含UIKit
,Foundation
,iPhoneSimulator SDK
等等中的所有代碼塊.你應該將你的搜索范圍限定在Signals
模塊中.在LLDB在中輸入下面的命令:
(lldb) image lookup -rn _block_invoke Signals
什么都沒有打印出來.發(fā)生了什么事呢?打開Xcode右側的File Inspector
面板. 或者按下? + Option + 1
.
如果你看到
UnixSignalHandler.m
被編譯的地方, 你就會發(fā)現(xiàn)它實際上被編譯進了Commons
框架. 所以, 重新搜索并在Commons
模塊中搜索Objective-C
代碼塊. 在LLDB中輸入下面命令:
(lldb) image lookup -rn _block_invoke Commons
最后, 你會看到一些輸出.
現(xiàn)在你會看到你再Commons
框架中找到的所有Objective-C代碼塊的輸出.
現(xiàn)在, 讓我們在你找到的代碼塊的一個子集上創(chuàng)建一個斷點.
在LLDB中輸入下面的命令:
(lldb) rb appendSignal.*_block_invoke -s Commons
注意: 在模塊中搜索代碼和在代碼中搜索模塊有一些細微的不同.用上面的命令做一個例子.當你想要搜索`Commons`框架中所有的代碼塊, 你應該使用`image lookup -rn _block_invoke Commons`.當你想要為`Commons`框架中的代碼塊設置一個斷點, 你應該使用`rb appendSignal.*block_invoke -s Commons`.注意`-s`參數(shù)后面的空格.
這條指令會在所有appendSignal
方法的代碼塊處設置斷點.
在LLDB中輸入continue
繼續(xù)運行程序.跳進終端并輸入下面的命令:
pkill -SIGIO Signals
你發(fā)送給程序的信號將會被處理.然而, 在這個信號被更新到tableview之前, 你的正則斷點就會被觸發(fā).
觸發(fā)的第一個斷點應該是:
__38-[UnixSignalHandler appendSignal:sig:]_block_invoke
繼續(xù)運行調(diào)試器并跳過這一步.
接下來你會觸發(fā)另一個斷點:
__38-[UnixSignalHandler appendSignal:sig:]_block_invoke_2
這個函數(shù)名與第一個函數(shù)名相比有一個有趣的地方; 注意到數(shù)字2.編譯器使用<FUNCTION_NAME>_block_invoke
的格式定義叫做<FUNCTION_NAME>
的blocks
.然而, 在函數(shù)中不只有一個block時, 就會在尾部添加一個數(shù)字.
正如你在前面學到的內(nèi)容, frame variable
命令將會打印出指定函數(shù)中所有已知的實例變量. 現(xiàn)在嘗試執(zhí)行一下這個命令來看一下這個block的引用.輸入下面的命令:
(lldb) frame variable
輸出的內(nèi)容應該像下面這個樣子:
(__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)>
這些讀內(nèi)存時的故障看起來很不友好!步過一次, 即可以使用Xcode也可以在LLDB 中輸入next
.接下來, 再次在LLDB中執(zhí)行frame variable
.
這一次你會看到類似下面的輸出:
(__block_literal_5 *) = 0x0000608000275e80
(int) sig = 23
(siginfo_t *) siginfo = 0x00007fff587525e8
(UnixSignalHandler *) self = 0x000061800007d440
(UnixSignal *) unixSignal = 0x000000010bd9eebe
你需要步過函數(shù)的聲明, 以便代碼塊可以執(zhí)行一些初始化邏輯來設置這個函數(shù). 函數(shù)聲明是與匯編相關的內(nèi)容, 你會在第二部分學到.
這實際上非常有趣. 首先你看到了一個引用著這個代碼塊的對象, 那是被調(diào)用的地方.在這里是__block_literal_5
. 然后是一些傳到調(diào)用這個代碼塊的Objective-C方法里的sig 和 siginfo 參數(shù). 這些是如何傳到代碼塊里的呢?
好, 當一個代碼塊創(chuàng)建的時候, 編譯器聰明到足以弄明白它會用到哪些參數(shù).然后它會創(chuàng)建一個函數(shù)并把這些參數(shù)帶進去. 當代碼快被調(diào)用的時候, 就是這個函數(shù)被調(diào)用, 并將相關的參數(shù)穿進去.
在LLDB中輸入下面命令:
(lldb) image dump symfile Commons
你將會看到許多輸出. 使用? + F
通過編譯器搜索block類型的聲明:__block_literal_5
. 有一個比較重要的東西要提醒你的是當LLVM更新的時候你得到的類型可能有細微的不同, 所以請確保你從frame variable
命令的輸出中得到了正確的類型.
在搜索block
類型的聲明時, 會有幾種不同情形. 搜索與block的行號相匹配的結構體的聲明. 例如, 你最初創(chuàng)建的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;
}
這就是定義代碼塊的那個對象!
正如你看到的, 這就如同有一個頭文件在告訴你如何在代碼塊的內(nèi)存中自由的找到你想要的東西. 只要提供你找到的__block_literal_5
在內(nèi)存中的應用, 你就可以輕松的打印出這個block引用的所有變量.通過輸入下面的命令再次獲取棧幀的變量信息:
(lldb) frame variable
接下來, 找到__block_literal_5 對象的內(nèi)存地址并用下面的方式打印出來:
(lldb) po ((__block_literal_5 *)0x0000618000070200)
你將會看到類似下面的輸出:
<__NSMallocBlock__: 0x0000618000070200>
如果你的輸出與上面的不一樣, 確保你使用的__block_literal_5
的內(nèi)存地址是你的block
的地址,每一次運行的時候內(nèi)存地址都會有些許不同.
現(xiàn)在你可以查詢__block_literal_5的內(nèi)存結構了. 在LLDB中輸入:
(lldb) p/x ((__block_literal_5 *)0x0000618000070200)->__FuncPtr
這條指令會提取出這個block
的函數(shù)指針的位置.輸出的內(nèi)容看起來應該是下面這個樣子:
(void (*)()) $1 = 0x000000010756d8a0 (Commons`__38-[UnixSignalHandler appendSignal:sig:]_block_invoke_2 at UnixSignalHandler.m:123)
block
的函數(shù)指針指向, 運行時block
被調(diào)用時的函數(shù). 現(xiàn)在被執(zhí)行的時候他們是同樣的地址! 你可以輸入下面的命令進行確認, 用你最近一次的命令打印出來的函數(shù)指針的地址替換下面的地址:
(lldb) image lookup -a 0x000000010756d8a0
這里在image lookup
后面用了-a
(address)選項來查看給定地址相關的符號.回到block
結構體的成員變量, 你依然可以打印出傳遞給block
的所有的參數(shù).
輸入下面的命令, 再次用你的block
的地址替換下面的地址:
(lldb) po ((__block_literal_5 *)0x0000618000070200)->sig
這將會輸出作為block
的父函數(shù)的參數(shù)的signal
的序號.在結構體的成員變量里還有一個UnixSignalHandler
的引用叫做self
.
為什么會那樣呢? 看一下這個 block 并且 捕獲下面這行代碼:
[(NSMutableArray *)self.signals addObject:unixSignal];
它是block
捕獲的self
的引用., 是用來找到signals數(shù)組的偏移的.因此block需要知道self
是什么. 很酷, 對吧?
用image dump symfile
命令與module聯(lián)合起來是用來學習某種未知數(shù)據(jù)類型的好方法. 它還是用來學編譯器是如何用你的源代碼生成代碼的好工具.
此外, 你可以檢查blocks是如何持有指向外部的block的引用的-當出現(xiàn)運行循環(huán)的時候會是一個非常有用的工具.
窺探
你已經(jīng)知道了如何用靜態(tài)的方式檢查一個私有類的實例變量, 但是把block的內(nèi)存地址單獨留下來是在太折磨人了. 嘗試著把他打印出來并用動態(tài)分析的方法查看它.輸入下面的內(nèi)容, 用你block的地址替換下面的地址:
po 0x0000618000070200
LLDB會提取出一個表明自己是Objective-C類的類:
<__NSMallocBlock__: 0x618000070200>
這很有趣. 這是一個__NSMallocBlock__
類.現(xiàn)在你已經(jīng)學習了如何提取出一個類的共有方法和私有方法, 現(xiàn)在是時候查看一下__NSMallocBlock__
實現(xiàn)的方法了.在LLDB中輸入:
(lldb) image lookup -rn __NSMallocBlock__
什么都沒有發(fā)生. 這說明__NSMallocBlock__
沒有覆蓋它的父類的任何方法. 輸入下面的命令來查看__NSMallocBlock__
的父類:
(lldb) po [__NSMallocBlock__ superclass]
這回產(chǎn)生一個類似的名字叫做__NSMallocBlock
的類--注意尾部缺少的下劃線. 你可以找出這個類的哪些信息呢?這個類是否實現(xiàn)或者覆蓋了一些方法呢? 在LLDB中輸入下面的命令:
(lldb) image lookup -rn __NSMallocBlock
用這條命令提取出來的方法表明__NSMallocBlock
是負責內(nèi)存管理的, 因為它實現(xiàn)了像retain
和release
這樣的方法.__NSMallocBlock
的父類又是什么呢?在LLDB中輸入下面的命令:
(lldb) po [__NSMallocBlock superclass]
你會得到另一個類NSBlock
.這個類是干什么?它又實現(xiàn)了哪些方法呢?在LLDB中輸入下面的命令:
(lldb) image lookup -rn 'NSBlock\ '
注意最后的反斜杠和空格. 記住-這能確保沒有其它類能夠匹配這次查詢, 如果沒有它的話, 那么其它一些包含NSBlock名字的類也會被匹配到.一些方法將會被輸出.其中有一個叫做invoke
的方法, 看起來極為有趣:
Address: CoreFoundation[0x000000000018fd80] (CoreFoundation.__TEXT.__text
+ 1629760)
Summary: CoreFoundation`-[NSBlock invoke]
現(xiàn)在你將會嘗試著在block中調(diào)用這個方法.然而, 你并不想讓持有這個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已經(jīng)被調(diào)用了一次. 干凈漂亮!
它之所以會生效是應為當block被調(diào)用的時候所有設置都已經(jīng)準備就緒了, 因為你當前已經(jīng)正確的停在了block開始的位置.
這種類型的方法來查看公有和私有類的, 然后查看它們實現(xiàn)的方法, 是一種學習程序底層實現(xiàn)的好方法.后面你會用同樣的過程來查找方法并分析這些方法執(zhí)行時的匯編代碼, 會給到你一個非常接近源始方法的源代碼.
調(diào)試私有方法
image lookup
命令在尋找私有方法以及公有方法上面做的很漂亮, 他會貫穿你的整個apple開發(fā)生涯.
然而, 這里還有一些隱藏的方法在調(diào)試你自己的代碼的時候非常有用.例如, 以_
開頭的方法通常都表明它是一個私有(并且是潛在的很重要的)方法.
讓我們搜索一下所有模塊中的所有包含下劃線字符并包含description
關鍵字的的Objective-C的方法.
再次構建并運行項目. 當sharedHandler
處的斷點被觸發(fā)的時候, 在LLDB中輸入下面的內(nèi)容:
(lldb) image lookup -rn (?i)\ _\w+description\]
這個表達式有點復雜所以讓我們解析一下.
這個表達式會搜索空格(前面需要有一個)后面跟著下劃線, 接下來是, 一個或多個字母或數(shù)字后面跟著description
單詞, 最后跟著]
字符的方法.
在這個正則表達式開始的地方有一個有趣的字符集(?i)
. 這表明在搜索的時候不區(qū)分大小寫.
這個表達式有一個反斜杠前綴符. 這表明你想使用字符的字面量而不是字符在正則表達式中的含義.這叫做'escaping'.例如, 在一個正則表達式中, ]
字符是有特定含義的, 所以你需要是使用\]
.
在上面的正則表達式中\w
字符是個例外. 它指定的搜索內(nèi)容是下劃線, 字母或數(shù)字(例如, _, a- z, A-Z, 0-9).
如果你在閱讀這行代碼的時候依然有不理解的地方, 強烈推薦你看一下https://docs.python.org/2/library/re.html深入了理解正則表達式查詢.往后去正則表達式只會變得更加復雜.
仔細查看image lookup
的輸出. 在找到最佳答案之前查找是很乏味的, 因此確保你查看了所有的輸出.
你可能會注意到幾個UIKit
中的一個NSObject
的分類IvarDescription
中幾個比較有趣的方法.
嘗試只將這個分類的內(nèi)容打印出來.在LLDB中輸入下面的內(nèi)容:
(lldb) image lookup -rn NSObject\(IvarDescription\)
控制臺將會輸出這個分類實現(xiàn)的所有方法. 在打印出來的這些方法中, 有幾個比較有趣的方法:
_ivarDescription
_propertyDescription
_methodDescription
因為這是NSObject
的分類, 所以所有NSObject
的子類都能調(diào)用這些方法. 這讓一切都變得更完美, 當然!
嘗試在UIApplication
上調(diào)用這些方法. 在LLDB中輸入下面的命令:
(lldb) po [[UIApplication sharedApplication] _ivarDescription]
因為UIApplication
持有很多實例變量所以你會得到很多輸出.仔細察看并且找到你感興趣的內(nèi)容. 不用返回來繼續(xù)閱讀, 直到你找到感興趣的東西. 這很重要!
在仔細察看了輸出之后, 你會看到引用了一個私有類UIStatusBar
. UIStatusBar
的Objective-C 的setter
方法在那里呢, 我聽到你問?讓我們來看一下! 在LLDB中輸入下面的內(nèi)容:
(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中, 刪除你之前創(chuàng)建的所有斷點:
(lldb) breakpoint delete
繼續(xù)運行APP并看看你用指尖創(chuàng)造出來的美好世界!
現(xiàn)在還不是最漂亮的APP, 但是至少你已經(jīng)找到了一個私有方法并用它做了一些有趣的事情!
我們?yōu)槭裁匆獙W習這些呢?
作為一個挑戰(zhàn), 試著用image lookup
命令找出Signals
模塊中所有的閉包. 一旦你做到了, 在每一個Signals
模塊的每一個Swift
閉包中創(chuàng)建一個斷點. 如果它對你來說太簡單了, 嘗試著找到可以在didSet/willSet
屬性時停下來的代碼, 或者做一些/try/catch``blocks
的操作.
也可以, 找出更多隱藏在Foundation
或者UIKit
中的私有方法. 學習愉快!