本文是Advanced Apple Debugging的學習筆記.
首先將Xcode升級到8.3版本.可以通過下載地址下載.
我們主要是通過LLDB,Python和DTrace來查看apple code.
在這一章中,你將會熟悉如何使用LLDB查看內部進程,并且調試一個程序.
你將在使用LLDB 的調試的過程中會發現你可以對一個你沒有源代碼的程序做出令人驚喜的改變.第一章會作為學習的一個過度章節, 因此許多重要的概念和深入到LLDB函數的功能將會在后面的章節介紹.
通過Rootless開始
在你開始使用LLDB之前, 你需要學習一些關于apple挫敗惡意軟件的特點介紹.讓人不爽的是,這些特點會打擊你使用LLDB和其他類似的工具比如DTrace調試內部代碼的企圖.但是不要慫,因為apple為那些知道自己在做什么的人提供了一個方法來關閉這個功能.并且你就將成為那些知道自己在做什么的人中的一員.
阻止你企圖進入內部調試的功能是System Integrity Protection
, 也號稱Rootless
.
這個系統限制了哪些程序可以(即便是他們本身有root權限)阻止惡意程序安裝在你系統的中.
盡管rootless 在安全方面是實質性的進步, 但同時他也帶來了一些讓程序變得難以調試的麻煩.說白了就是它阻止了其他進程調試apple簽名了的進程.
由于本書要調試的不僅僅是你自己的程序, 還有你非常感興趣的程序, 因此在你學習的時候移除這些功能就變得非常重要,以便你可以查看你選擇的程序.
如果你現在已經啟用了roorless, 你將不能夠調試apple的主要程序.但是也有例外, 比如iOS模擬器里的自帶的幾個程序可以用.
例如, 嘗試將LLDB附加到Finder應用程序.
打開Terminal, 通過下面的命令查看finder的進程.
lldb -n Finder
你將會看到下面的錯誤:
error: attach failed: cannot attach to process due to System Integrity
注意: 有許多方法可以附加一個進程, 此外當LLDB附加成功的時候可以指定配置. 想要學習更多關于附加進程的知識可以查看第三章的內容,"用LLDB附加"
禁用Rootless
要禁用Rootless, 請執行下面的步驟:
- 重啟你的macOS 設備
- 當屏幕變成空白的時候, 按住Command+R直到apple的啟動logo出現,這將會使你的電腦進入恢復模式.
3.現在, 在頂部找到Utilities菜單然后選擇Terminal,.
4.當Terminal窗口打開的時候, 輸入:
csrutil disable; reboot
5.你的電腦將會用禁用Rootles的模式重新啟動.
注意:練習本書中所有操作的一個安全的方法是在VMWare or VirtualBox創建一個虛擬機來操作, 并僅僅在虛擬機中禁用Rootless.
你可以通過在終端中輸入同樣的命令來驗證一下你是否成功的禁用了Rootless.
lldb -n Finder
LLDB應該會講自己附加到當前Finder的進程.附加成功后的輸出因該是下面這個樣子.
在確認成功附加以后.通過關閉Terminal窗口或者輸入
quit
退出LLDB并且在LLDB的控制臺中確認是否已經退出.
附加LLDB到Xcode
現在你已經禁用了Rootless, 并且你可以將LLDB附加到進程上, 是時候開始你調試的旋風之旅了.你查看的第一個應用程序將是你在日復一日的開發中經常使用的Xcode!
打開一個新的Terminal窗口. 接下來, 通過按下? + Shift + I來編輯, 窗口的標題. 一個新的彈窗將會出現.在Tab Title 一欄輸入LLDB.
然后確保Xcode沒有運行, 否則你最終會因為運行了多個Xcode的實例而造成混亂.
在Terminal中輸入下面的命令:
lldb
這將會啟動LLDB.
現在通過按住? + T
來創建一個新的Terminal窗口.再次通過按住? + Shift + I
編輯這個窗口的標題, 并將這個窗口命名為Xcode stderr.這個窗口將會輸出你在調試器中打印的所有的內容.
確保你再Xcode stderr 這個終端的窗口中, 并且輸入下面的命令:
tty
你將會看到一些類似于下面的輸出:
/dev/ttys027
如果你輸出的內容跟上面的不一樣也不要擔心.如果輸出的結果與我的不同的話我并不會很驚訝.因為這是您的終端會話的地址.
舉例說明一下你將會用Xcode stderr這個窗口來做哪些事情, 創建另外一個窗口并鍵入一下內容:
echo "hello debugger" 1>/dev/ttys027
確保你將上面的/dev/ttys027
替換成了你通過tty
命令得到的終端的路徑.
現在切換到Xcode stderr窗口中, hello debugger
這些單詞應該已經出現了.你將使用同樣的方法將Xcode’s stderr這個窗口作為輸出的管道.
最后, 關閉第三個沒有命名的終端的窗口, 并返回到LLDB選項卡中.
在LLDB選項卡中, 將以下命令鍵入到LLDB中:
(lldb) file /Applications/Xcode.app/Contents/MacOS/Xcode
這條命令會將Xcode設置為可執行的目標文件.
注意:如果你是用的是之前發布的Xcode的版本, Xcode的名字和路徑可能會有所不同.
你可以通過在Terminal鍵入以下命令來查看你當前運行的Xcode的路徑.
$ ps -ef `pgrep -x Xcode`
如果你的到了Xcode的路徑, 用新的路徑來替換
現在從LLDB中啟動Xcode進程, 在Xcode stderr 選項卡中鍵入以下命令, 并將/dev/ttys027
用tty
命令得到的路徑替換掉.
(lldb) process launch -e /dev/ttys027 --
啟動參數e
指定了stderr的位置.常見的日志功能, 比如 Objective-C的NSLog或者Swift的print函數, 輸出到stderr-是的沒錯, 不是stdout! 稍后你將會打印自己的日志到stderr.
過一會兒xcode就會啟動. 切換到Xcode并且選擇File\New\Project....然后選擇, iOS\Application\Single View Application并且點擊Next.將工程命名為Hello Debugger.確保選擇了Swift作為程序語言,并且沒有選擇Unit 和 UI tests中的任意一項.點擊Next并且將工程保存到你希望的位置.
現在你有了一個新的Xcode的工程. 整理一下程序的窗口以便你可以同時看到Xcode和Terminal.
在Xcode中打開
ViewController.swift
.
注意: 你可能會留意到Xcode stderr終端窗口中有一些輸出.這是由于Xcode的作者通過NSLog或者其他stderr 控制臺打印函數輸出的日志.
點擊一下找到一個類
現在Xcode已經設置好了,而且終端調試窗口也已經正確的創建并擺放在正確的位置上, 是時候開始使用調試的help來瀏覽Xcode了.
在調試的過程中, Cocoa SDK的知識會有極大的幫助.例如, [NSView hitTest:]
是一個在run loop中返回響應當前點擊或者手勢操作的類的非常有用的方法.這個方法首先得到觸發時包含的NSView 并且最大程度的遞歸可以處理此次觸摸事件的的子視圖.你可以使用這些Cocoa SDK來幫助找出你點擊的視圖的類.
在LLDB選項卡中, 鍵入Ctrl + C來暫定調試器, 鍵入:
(lldb) breakpoint set -n "-[NSView hitTest:]"
Breakpoint 1: where = AppKit`-[NSView hitTest:], address =
0x000000010338277b
這是即將學習的許多斷點中的第一個.你將在第四章'Stopping in Code'中學習到如何創建,修改和刪除斷點, 但是現在只需要簡單的知道你在-[NSView hitTest:]
中創建了一個斷點.
Xcode現在因為調試器而暫停了. 鍵入以下命令繼續運行程序:
(lldb) continue
在Xcode窗口中點擊任意位置Xcode將會立即暫停這表明LLDB觸發了一個斷點.
那個
hitTest:
斷點被觸發了.你可以通過檢查CPU注冊表的RDI
來檢查哪一個視圖被點擊了.將他打印在LLDB中:
(lldb) po $rdi
這個命令要求LLDB
打印出匯編寄存器中的內存地址引用的對象的內容.
好奇為什么這個命令是po
?po
的含義是print object
.這里也可以使用p
簡單的打印出RDI的內容. po
通常情況下更有用, 它給出的是NSObject的description或者debugDescription(如果可用的話).
如果你想將你的調試能力提升一個等級的話,匯編是一個你要學的非常重要的技能. 它將讓你洞察apple的代碼, 即便是你從來沒有閱讀過源碼.它將讓你非常了解Swift編譯團隊是如何在Objective-C中用Swift跳來跳去.它將會讓你非常了解你的Apple設備是如何做每一件事的.你將會在第十章“Assembly Register Calling Convention”中學到更多關于寄存器和匯編的知識.
但是現在, 簡單的知道$rdi
寄存器包含著上面調用hitTest:
方法的NSView或者NSView子類的一個實例.
注意輸出可能產生不同的結構這取決于你使用的Xcode的版本和你點擊的位置.他可能會給出一個xcode特有的私有類, 也有可能給出一個Cocoa中的公有類.
在LLDB選項卡中鍵入一下命令繼續運行程序:
(lldb) continue
替代繼續運行的是, Xcode將會觸發hitTest:
的另外一個斷點并且暫停執行.這是由于hitTest:
這個方法會被所有包含在被點擊視圖的父視圖里的所有子視圖遞歸調用的事實決定的. 你可以檢查這個斷點的內容但是這會很乏味因為Xcode里包含很多的視圖.
為重要的內容過濾斷點
鑒于Xcode創建了很多的NSView實例, 你需要過濾掉那些沒用的NSView, 只關注于與你尋找的那些目標相關的NSView上.在你需找一個唯一的對象的時候這是一個在調試過程中經常調用的方法, 有助于你找到你想要的對象.
在Xcode8中, 你用來寫編輯代碼的地方是一個私有類NSTextView的一個子類.它就類似于UIKit中的UITextView.這個類作為可視化的界面來處理你所有的代碼, 并幫助其他私有類來編譯和創建你的應用程序.
如果說你只想在點擊NSTextView實例的時候觸發斷點.你可以通過修改已有斷點的breakpoint conditions
來設置只有在NSTextView被點擊的時候才停止.
假如你前面設置的-[NSView hitTest:]
斷點還在, 并且它是LLDB會話中唯一的斷點, 你可以用下面的LLDB命令修改這個斷點:
(lldb) breakpoint modify 1 -c "(BOOL)[$rdi isKindOfClass:[NSTextView
class]]"
這條命令修改了斷點1并且設置了一個觸發條件,只有條件語句返回的Boolean值為true的時候才觸發斷點.截至目前你只有一個斷點,這就是為什么斷點號為1.
這條Boolean表達式通過isKindOfClass:
來檢查當前的類是否是NSTextView的子類.
你的斷點經過上面的修改以后, 在Xcode的區域里點擊. LLDB應該會停在hitTest:
.通過下面的命令打印出當前實例所屬的類:
(lldb) po $rdi
輸出的內容應該像下面的樣子:
<NSTextViewSubclass: 0x14b7a65c0>
Frame = {{0.00, 0.00}, {1089.00, 1729.00}}, Bounds = {{0.00, 0.00},
{1089.00, 1729.00}}
Horizontally resizable: NO, Vertically resizable: YES
MinSize = {1089.00, 259.00}, MaxSize = {10000000.00, 10000000.00}
上面的NSTextViewSubclass
是一個私有類的名字.在本章節余下的內容里面你都會用到它.在輸出里里面你會得到一個唯一的指針.在這個例子中, 指針的內存地址是0x14b7a65c0
, 但你得到的內存地址可能是不同的.
鑒于它沒有立即顯示出它是一個NSTextView
的子類, 但是你可以通過重復的手動輸入下面的命令來查看它的父類來查看當前實例是否是NSTextViewsubclass
的子類.
(lldb) po [$rdi superclass]
... 一直重復到你找到為止.
(lldb) po [[$rdi superclass] superclass]
等一下-那是Objective-C. 但你要想到Swift的情況. 在swift中要那樣做, 首先鍵入一下命令:
(lldb) ex -l swift -- import Foundation
(lldb) ex -l swift -- import AppKit
ex
命令是expression
的縮寫, 可以讓你執行代碼.-l swift
告訴LLDB這是swift的代碼.這些命令告訴LLDB需要知道Foundation 和 AppKit的相關情況.
你還將用到下面的兩條命令.
輸入下面的命令, 并將0x14bdd9b50
替換成你在前面獲取到的NSTextView
子類的內存地址.
(lldb) ex -l swift -o -- unsafeBitCast(0x14bdd9b50, to: NSObject.self)
(lldb) ex -l swift -o -- unsafeBitCast(0x14bdd9b50, to: NSObject.self) is
NSTextView
這些命令打印出了, NSTextView的子類, 并且檢查它是否是 NSTextView
的子類-但這一次使用的是swift!你將會看到類似下面的這些輸出:
(lldb) ex -l swift -o -- unsafeBitCast(0x14bdd9b50, NSObject.self)
<NSTextViewSubclass: 0x14b7a65c0>
Frame = {{0.00, 0.00}, {1089.00, 1729.00}}, Bounds = {{0.00, 0.00},
{1089.00, 1729.00}}
Horizontally resizable: NO, Vertically resizable: YES
MinSize = {1089.00, 259.00}, MaxSize = {10000000.00, 10000000.00}
(lldb) ex -l swift -o -- unsafeBitCast(0x14bdd9b50, NSObject.self) is
NSTextView
true
使用swift需要輸入更多的東西,.此外, 當調試器在在藍色的地方停下來的時候, 或者在Objective-C 的代碼中, LLDB將默認使用Objective-C.這些是可以改變的, 但是這本書更推薦使用 Objective-C,鑒于Swift REPL可能在調試器中無緣無故的檢查出錯誤.
從現在開始, 你將會使用 Objective-C 的調試環境幫助控制NSTextView
.
鑒于這是一個NSTextView
的子類, 所以可以調用NSTextView
的所有方法.
輸入下面的命令:
(lldb) po [$rdi string]
這將會打印出你再Xcode中打開的文件的內容.
甚至還可以設置text view的內容:
(lldb) po [$rdi setString:@"http:// Yay! Debugging!"]
(lldb) po [CATransaction flush]
可以看到Xcode窗口中的內容已經改變了.
獲取modules中的私有類和私有方法
現在回到NSTextView
的子類, 選擇從NSTextView
子類開始是因為這里可能有一些額外的或者重寫了父類的方法.但是你如何去找到他們呢?Xcode中的私有類Apple是不會公開在文檔中的.
輸入下面的命令, 將NSTextViewSubclass
替換成你找到的text view的類:
(lldb) image lookup -rn 'NSTextViewSubclass\ '
這條命令可以讓你查看正在運行的二進制文件和已經加載的所有動態庫的內部.r
選項的含義是用正則表達式搜索. n
選項的含義是通過名字搜索函數或者符號表.
你將會看到NSTextView
子類實現了的方法列表.很酷是不是?如果你想要學習使用image lookup
命令查找代碼, 你可以參考第七章"Image".
用代碼塊注入切換方法
Objective-C的運行時在逆向工程中真的很有用很有幫助.你現在即將感受到Objective-C運行時的強大, 并且了解你可以用它在LLDB中做什么.
首先, 用頭文件導入的方法導入Foundation庫中Objective-C的運行時信息:
(lldb) po @import Foundation
盡管代碼已經編譯過了,在Xcode內部知道包含哪些方法, 然而LLDB進程卻不知道.通過導入Foundation可以讓你通過LLDB訪問Objective-C 運行時的所有內容.
現在在LLDB控制臺中輸入po
指令, 不必輸入其他內容, 就像這樣:
(lldb) po
LLDB將會進入多行模式. 你會看到下面這些輸出:
(lldb) po
Enter expressions, then terminate with an empty line to evaluate:
1:
在這里, 你可以一次輸入多行表達式來執行. 添加下面的代碼:
@import Cocoa;
id $class = [NSObject class];
SEL $sel = @selector(init);
void *$method = (void *)class_getInstanceMethod($class, $sel);
IMP $oldImp = (IMP)method_getImplementation($method);
再次按下Return
鍵來輸入一個空行.LLDB將會按照順序執行上面的表達式.
注意:在鍵入這些代碼的時候必須非常小心,只有在你按下`Return`以后你才會知道. 如果你出現了一個錯誤,所有這些代碼你都必須重新輸入, 盡管你可以通過朝上的箭頭按鈕使用歷史輸入, 確認一下你沒有忘記句子末尾的分號.
你已經通過LLDB在內存中創建了幾個變量: $class
, $sel
, 和 $oldImp
.LLDB中的變量需要$
作為前綴.其他部分就跟你在Xcode編輯代碼的時候一樣.
試著打印出一些變量來確保你正確的創建了它們:
(lldb) po $class
NSObject
(lldb) po $oldImp
(libobjc.A.dylib`-[NSObject init])
現在通過鍵入po
指令來回到LLDB的多行模式.
用imp_implementationWithBlock
函數來創建一個新的IMP:
id (^$block)(id) = ^id(id object) {
if ((BOOL)[object isKindOfClass:[NSView class]]) {
fprintf(stderr, "%s\n", (char *)[[[object class] description]
UTF8String]);
}
return object;
};
IMP $newImp = (IMP)imp_implementationWithBlock($block);
method_setImplementation($method, $newImp);
再次在結尾的地方輸入一個空行來告訴LLDB來處理這些表達式.
這些代碼的目標是替換你剛剛發現的-[NSObject init]
方法.在默認情況下, -[NSObject init]
除了返回對象本身一外不會做任何事情.這個代碼塊檢查這個對象本事是不是NSView的一個實例.如果是, 這個對象的類就會被打印出來.
它的工作原理是這樣的:
1.你創建了一個持有對象指針的代碼塊.
2.這個代碼快檢查傳入的是不是NSView類型的對象.
3.如果是, 代碼塊會把view的description打印到stderr, 也就會出現在Xcode stderr
選項卡中.
4.代碼快返回的是執行了被替換過的-[NSObject init]
方法的對象.在理想狀況下,會徹底的替換這些方法的實現, 并且你可以用正確的參數簡單的執行$oldImp
.然而, LLDB在這里卻有一個bug, 當你用IMPs來代替block執行的時候LLDB會閃退.
5.最后, 一個新的IMP在代碼塊里被創建, 并且放的實現也被設置到了新的IMP里.這樣你就用新的實現替換了-[NSObject init]
方法.
接下來, 通過鍵入continue
來繼續調試.
觀察Xcode stderr
控制臺中的輸出.檢查你通過點擊Xcode中不同的項目時創建的所有的類.
你也可以用這種方法將LLDB附加到任何程序上.不管是apple的程序還是你感興趣的第三方應用程序.你可以用同樣的技巧去瀏覽他們的類名.唯一不同的是你只要改變可執行文件的路徑即可.
我們學這些干嘛?
這是我們第一次在你沒有任何源代碼的情況下使用LLDB并且附加到其他程序上.本章節忽略了很多細節, 但我們的目標是讓你正確的調試進程.接下來還有許多章節讓你深入的了解細節.