Advanced+Apple+Debugging(13)

來源:互聯網
上載者:User

標籤:lldb

這篇文章主要介紹將lldb Python模組的知識和Objective-C 的運行時結合起來可以做的事情. 當LLDB來解析精簡過的可執行檔(一個沒有DWARF調試資訊的可執行檔)時, LLDB沒有棧記錄的符號化的資訊.
取而代之的是, LLDB將會為一個它認識的方法產生一個合成的名字作為方法名, 但是不知道什麼調用了這個方法.
這裡有一個LLDB建立的合成方法名的例子:

___lldb_unnamed_symbol906$$SpringBoard
一個逆向工程這個方法名字的策略就是在這個方法出建立一個斷點然後瀏覽這個方法開始執行時的寄存器.
用你Objective-C運行時的彙編知識, 你知道RSI寄存器(x64)或者X1寄存器(ARM64)將會儲存著Objective-C Selector的名字. 此外, 你還有RDI(x64)或者X0(ARM64)寄存器持有這執行個體(或者類)的引用.
然而, 你一離開函數實現位置的函數名, 你就無法保證寄存器裡的值還是我們想要的那個值, 因為他們的值很有可能被重寫. 如果一個被精簡過的函數調用另一個函數會怎麼樣呢? 那麼你關心的寄存器裡的值就會丟失, 因為i它們被設定為了這個新函數的參數. 你需要一種沒必要已來上面這些寄存器的方式去重新符號化棧記錄.
在本章中, 你將會構建一個能夠重新符號化棧記錄中精簡過的Objective-C函數的 LLDB指令碼.

圖片.png

當你在這個進程上調用bt的時候, LLDB沒有高亮部分的函數名字.
你將會構建一個新的叫做sbt的可以查看精簡過的函數並嘗試將這些函數使用Objective-C的運行時重新符號化的命令. 在這一章的末尾, 你的sbt命令將會產生下面的結果:
圖片.png
那些曾經被精簡過的Objective-C函數現在被重新符號化了. 因為這些指令碼是獨立的, 所以你可以在任何LLDB可以附加到的Objective-C可執行檔上運行這個新的sbt指令碼.

你是如何準確的做到這些的呢?

讓我們首先討論一下如何用 Objective-C 的運行時重新符號化Objective-C代碼.
假如你有這個模組的完整路徑的話 Objective-C 的運行時的運行時可以列出一個特定模組中所有的類名(被映射到主執行檔案的一個動態庫,一個NSBundle等等). 這個可以通過牛逼的objc_copyClassNamesForImageAPI實現.
從這裡開始, 你可以使用class_copyMethodList API列出objc_copyClassNamesForImage方法返回的所有類的中的類方法和執行個體方法.
因此, 你可以抓取所有的方法地址並且將這些地址與棧記錄中的地址做一個比較.如果棧記錄的函數不能產生一個預設的函數名(例如如果SBSymbol是LLDB合成的), 然後你就可以假設LLDB在這個地址出沒有調試資訊.
使用LLDB的Python模組, 你可以得到一個函數的起始地址--甚至一個函數已經執行一部分以後的地址. 這就是用SBValue引用一個SBAddress. 從這裡開始, 你可以比較所有你已經獲得的SBSymbol合成的Objective-C方法的起始地址. 如果有兩個地址是匹配的, 然後你就可以換掉精簡過的(合成的)方法名字並且用包含在Objective-C運行時裡的函數名代替.
不要擔心: 在我們構建這些Python指令碼之前你將會用LLDB的指令碼命令系統的學習這些內容.

50 Shades of Ray

在starter檔案夾中包含一個叫做50 Shades of Ray的應用程式. 我為顯示許多Ray Wenderlich的臉的項目起的一個很好的名字.
這就是紳士的Ray, 這就是超級英雄Ray, 這就是困惑的Ray!
圖片.png

當點擊底部按鈕的時候, 一個隨機產生的隨機大小的Ray的圖片就會彈出來.
哇, 它將在App Store上創造收入!
開啟50 Shades of Ray項目然後構架並運行這個APP. 在Xcode項目中, 有兩個scheme. 確保你選中的是50 Shades of Ray scheme而不是Stripped scheme. 你會在後面用到那個精簡過的scheme.
如果你已經隨機產生了一張Ray的圖片, 點擊右上方的ObjCUIBarButtonItem.
圖片.png

這個UIBarButtonItem綁定了一個能夠列印出主執行檔案裡實現的所有方法的IBAction並且會將他們輸出到你的控制台中.
事實上, 你可以看到輸出到控制台裡的這些方法的名字!
在控制台中找到-[ViewController dumpObjCMethodsTapped:]方法.這就是在主執行檔案中提取出所有Objective-C 方法的方法.
在這個函數的前面是一個數字(在我這裡的數字是, 4483016672), 這個數字持有這個Objective-C 方法的起始地址.
不相信我嗎?暫停執行並在LLDB中輸入下面內容:
(lldb) image lookup -a 4483016672
你的地址與我的可能有些不同. 這條指令會提取出4483016672這個地址在記憶體中的位置並且查看它在你的項目中相關的引用.

Address: 50 Shades of Ray[0x00000001000017e0] (50 Shades of
Ray.TEXT.text + 624)
Summary: 50 Shades of Ray`-[ViewController dumpObjCMethodsTapped:] at
ViewController.m:36
很巧妙. 這告訴了我們4483016672在記憶體中的位置也就是-[ViewController dumpObjCMethodsTapped:]載入到記憶體中的位置. 讓我們看看這個方法的代碼.
進入到ViewController.m中的頂部然後找到dumpObjCMethodsTapped:. 它的注釋寫的很詳細不需要在這裡過度描述, 但是有下面幾點需要指出的:
? 所有在主執行檔案裡實現的方法都會通過objc_copyClassNamesForImage被枚舉出來.
? 對每一個類而言, 這裡有都說有抓取所有的類方法和執行個體方法的邏輯.
? 為了抓取一個特定的Objective-C類的類方法, 你必須得到meta類. meta類是負責響應一個類的靜態方法. 例如, 所有以+開頭的方法都是被meta實現的而不是那個類本身.
? 所有的方法都被匯聚在一個NSMutableDictionary裡, 字典裡的key就是每個方法在記憶體裡的位置.

用指令碼指引你的道路

是時候用指令碼產生的LLDB命令來瀏覽lldb模組的APIs並構建一個快速的POC來看一下你如何一個函數在記憶體中的起始地址.
在LLDB控制台中, 在NSLog處設定一個斷點:

(lldb) b NSLog
你會多次觸發SBBreakpointLocation. 這是好事. 現在繼續運行這個應用程式.
在模擬器中點擊右上方的ObjC 按鈕.
執行應該在內容輸出到stderr之前正確的停下來.
使用全域變數lldb.frame, 挖掘出哪些APIs可以讓你用來抓取NSLog函數的起始地址.
從這裡開始構建並使用全域變數:

(lldb) script print lldb.frame
你將會得到代表SBFrame的str(). 沒有新鮮的東西.

frame #0: 0x000000010b472390 Foundation`NSLog
如果你決定用gdocumentation去搜尋SBFrame的文檔(從第19章, “Script Bridging Classes and Hierarchy”), 你將會看到SBFrame有一些潛在的方法來擷取一個函數的起始地址. pc 看起來是抓取RIP寄存器(X64)或者PC(ARM64), 但是它們只在函數的起始位置生效. 你需要從SBFrame裡的任何位移抓取起始地址.
不幸的是, 在SBFrame裡沒有你可以使用的APIs可以讓你從函數的任何指令位移裡擷取起始地址. 你需要將你的注意力轉移到SBFrame引用的其它類來找到你需要的東西.
從SBFrame上抓取SBSymbol引用:

(lldb) script print lldb.frame.symbol
SBSymbol是負責NSLog的位移地址的. 也就是說, SBSymbol將會告訴你這個函數在模組中實現的位置; 它並不是NSLog函數被載入到記憶體裡的實際地址.
然而, 你可以使用SBAddress屬性沿著SBAddress屬性的GetLoadAddressAPI去發現NSLog在你當前進程裡的起始位置.

(lldb) script print lldb.frame.symbol.addr.GetLoadAddress(lldb.target)
你將會得到一個10進位的數字.我得到的是4484178832. 用LLDB將它轉換成16進位然後比較輸出的NSLog的起始地址:

(lldb) p/x 4484178832
我得到的16進位地址是0x000000010b472390.
將你的輸出和NSLog函數的其真實位址做一個比較看他們兩個時候匹配.

圖片.png

哇! 是匹配的! 這就是我們重新符號化的過程.

帶有NSDictionary的lldb.value

鑒於你已經讀到這裡了, 因此你可以瀏覽更多的內容. 你如何去解析匯聚了所有地址的NSDictionary呢?
你將會複製這些代碼並且幾乎是逐字複製那些產生的所有方法並且將它們應用到EvaluateExpressionAPI上來擷取SBValue.
你應該仍然會暫停在NSLog的起始位置. 跳轉到調用-[ViewController dumpObjCMethodsTapped:]的那一幀.

(lldb) f 1
這將會擷取前面的那一幀, dumpObjCMethodsTapped:. 你現在已經訪問了這個方法裡的所有變數, retdict裡包含的是負責提取主可執行檔中實現的所有方法.
抓取SBValue說明retdict的引用.

(lldb) script print lldb.frame.FindVariable(‘retdict‘)
這行指令會列印出retdict的SBValue:

(__NSDictionaryM *) retdict = 0x000060800024ce10 10 key/value pairs
鑒於這是一個NSDictionary, 實際上你需要解引用這個值以便你可以枚舉他的內容.

(lldb) script print lldb.frame.FindVariable(‘retdict‘).deref
你將會得到更多相關的輸出(下面只是輸出內容的一部分):

(__NSDictionaryM) *retdict = {
[0] = {
key = 0x000060800002bb80 @"4411948768"
value = 0x000060800024c660 @"-[AppDelegate window]"
}
[1] = {
key = 0x000060800002c1e0 @"4411948592"
value = 0x000060800024dd10 @"-[ViewController toolBar]"
}
[2] = {
key = 0x000060800002bc00 @"4411948800"
value = 0x000060800024c7e0 @"-[AppDelegate setWindow:]"
}
[3] = {
key = 0x000060800002bba0 @"4411948864"
value = 0x000060800004afe0 @"-[AppDelegate .cxx_destruct]"
}
這就是你開始著手的地方, 因為它列印出了所有key的值.
在這個SBValue的外面建立一個lldb.value然後將它賦值給a.

(lldb) script a = lldb.value(lldb.frame.FindVariable(‘retdict‘).deref)
這就是我更喜歡使用lldb.value而不是SBValue的一次表現. 在這裡, 你可以輕鬆的瀏覽NSDictionary裡的值.
列印出lldb.value NSDictionary裡的第一個值.

(lldb) script print a[0]
在這裡, 你可以同時列印出key或者value.
首先列印出key:

(lldb) script print a[0].key
你將會得到一些類似下面的輸出:

(__NSCFString *) key = 0x000060800002bb80 @"4411948768"
然後列印出value:

(lldb) script print a[0].value
這將會列印出類似下面的內容:

(__NSCFString *) value = 0x000060800024c660 @"-[AppDelegate window]"
圖片.png

如果你只想要沒有引用地址的傳回值, 你需要指明將這個lldb.value返回到一個SBValue裡然後抓取description.
(lldb) script print a[0].value.sbvalue.description
這將會輸出你渴望的-[AppDelegate window]. 注意你也許會有一個不同的方法.
如果你想提取出這個lldb.value執行個體裡所有的keys, 你可以使用Python的數組嘗試將所有的key提取出來.

(lldb) script print ‘\n‘.join([x.key.sbvalue.description for x in a])
你將會得到一些類似下面的輸出:

4411948768
4411948592
4411948800
4411948864
4411948656
4411948720
4411949072
4411946944
4411946352
4411946976
用同樣的方法提取出所有的value:

(lldb) script print ‘\n‘.join([x.value.sbvalue.description for x in a])
現在你知道了如何剖析這個NSDictionary, 試想一下啊, 如果它被一些JIT 代碼替代了的話怎麼辦...
我們的計劃是將代碼從dumpObjCMethodsTapped:拷貝到Python指令碼中, 並將它作為JIT代碼執行. 在這裡, 你需要用同樣的程式將它從NSDictionary中解析出來.
聽起來很不錯是吧?既然你的計劃已經準備好了那就進入下一章節吧!

精簡過的50 Shades of Ray

耶, 這個標題吸引了你的注意, 對吧?
在50 Shades of Ray的Xcode schemes中, 有一個叫做Stripped 50 Shades of Ray的scheme.
停止執行當前進程(? + .)然後選擇Stripped 50Shades of Ray這個scheme.

圖片.png

這個scheme將會構建一個可調試的可執行檔, 但是移除了在你日複一日開發中習慣了的調試資訊.構建並運行. 在這個項目中包含一個shared symbolicbreakpoint. 啟用這個斷點.
這裡不需要修改這個符號斷點, 但是這個斷點做的時候漢武價值.

這個斷點將停止在-[UIView initWithFrame:]並且有一個條件只在這個UIView類是RayView的類型時才會停下來. 這個RayView師傅則顯示可愛的Ray Wenderlich的頭像的.
點擊Generate a Ray! 按鈕. 執行將會停在-[UIView initWithFrame:]方法處.
注意觀察棧記錄.

圖片.png

在第一幀和第三幀處有一些有趣的事情: 這裡沒有調試資訊. LLDB預設為這些方法產生了一個合成的函數名.
在LLDB中確認這一點.
在LLDB, 確保你再起始幀的位置(initWithFrame:):

(lldb) f 0
使用指令碼來查看一些它是否是合成的:

(lldb) script lldb.frame.symbol.synthetic
你將會得到False. 情理之中, 因為你知道這是initWithFrame:. 跳轉到一個合成的幀上面:

(lldb) f 1
執行前一個指令碼的邏輯:

(lldb) script lldb.frame.symbol.synthetic
這一次你得到的結果將是true.

構建sbt.py

在starter檔案夾中包含這一個名字叫做sbt.py的Python指令碼.
將這個指令碼粘貼到你的~/lldb目錄下. 假設你已經安裝了lldbinit.py指令碼, 這會將所有的Python檔案載入LLDB目錄中.
如果你沒有看第二十二章, “SB Examples, Improved Lookup”, 你可以通過修改你的~/.lldbinit檔案手動安裝sbt.py.
如果你已經替換了~/lldb目錄下的sbt.py檔案, 使用你再第十九章中建立的reload_script重新載入~/.lldbinit中的命令.
檢查LLDB是否正確的識別除了sbt命令:

(lldb) help sbt
如果LLDB識別出了這個命令你將會得到一些協助資訊. 這將是sbt命令的起點.
開啟這個檔案並且跳轉到generateExecutableMethodsScript. 這裡有一些有趣的事情需要指出來.
你是否還記得之前的章節中, 我如何注意到lldb.value是如此的慢? 如果你正在瀏覽一個包含許多方法的巨大的可執行檔, Python檢查NSDictionary中每一個值的時間將是永遠.
取而代之的是, 你不需要抓取NSDictionary中每一個函數的引用. 你只需要抓取每一個函數在棧記錄中的起始位置.

def generateExecutableMethodsScript(frame_addresses):
frame_addr_str = ‘NSArray *ar = @[‘
for f in frame_addresses:
frame_addr_str += ‘@"‘ + str(f) + ‘",‘
frame_addr_str = frame_addr_str[:-1]
frame_addr_str += ‘];‘

# Truncated content...# #############################command_script += frame_addr_strcommand_script += r‘‘‘

NSMutableDictionary stackDict = [NSMutableDictionary dictionary];
[retdict keysOfEntriesPassingTest:^BOOL(id key, id obj, BOOL
stop) {
if ([ar containsObject:key]) {
[stackDict setObject:obj forKey:key];
return YES;
}
return NO;
}];
stackDict;
‘‘‘
return command_script
這是一個甜美的最佳化, 因為取代評估成千上萬的Objective-C 方法的是, 你只需要不超過20個keys或者是一個NSDictionary, 或者只是棧幀裡面合成函數的數量.
伴隨著符號斷點仍然是啟用狀態並且程式是暫停, 運行一下指令碼. 只有一個沒有邏輯去重新符號化符號的普通棧幀將會被列印出來.
是時候做一點修改來修複這個問題了.

實現這些代碼

JIT代碼已經設定好了. 你所需要做的就是調用它, 然後將返回的NSDictionary與任何合成的SBValues做一個對比.
在processStackTraceStringFromAddresses裡, 搜尋下面的注釋:

New content start 1New content end 1

將你的新代碼粘貼到這裡調用JIT代碼去產生一個NSDictionary中的潛在方法列表.

New content start 1

methods = target.EvaluateExpression(script, generateOptions())
methodsVal = lldb.value(methods.deref)

New content end 1

你已經調用了返回NSDictionary的代碼並且將它賦值給了SBValue執行個體變數的methods.
你可以將SBValue傳到lldb.value裡(技術上講他只是一個值, 但是你可能會困惑在這裡我是不是沒有模組)並且將它複製給變數methodsVal.
現在到了Python代碼的最後一部分. 你所需要做的就是檢查一個SBFrame的SBSymbol是否是合成的然後執行相應的邏輯.
在processStackTraceStringFromAddresses:中進一步搜尋出下面的代碼:

New content start 2

name = symbol.name

New content end 2

將這一部分改成下面的樣子:

New content start 2

if symbol.synthetic: # 1
children = methodsVal.sbvalue.GetNumChildren() # 2
name = symbol.name + r‘ ... unresolved womp womp‘ # 3
loadAddr = symbol.addr.GetLoadAddress(target) # 4
for i in range(children):
key = long(methodsVal[i].key.sbvalue.description) # 5
if key == loadAddr:
name = methodsVal[i].value.sbvalue.description # 6
break
else:
name = symbol.name # 7

New content end 2

offset_str = ‘‘
講這一部分拆解如下:

你已經枚舉了這些發生在這個代碼塊外部的幀. 對每一個符號而言, 執行了一個檢查來查看這個符號是否是合成的. 如果它是合成的, 他的記憶體位址就會與我們收集到NSDictionary裡的地址做一個比較.
這將會抓取lldb.value中在Objective-C類列表裡是匹配到的子類的數量.
沒關係, 棧記錄中一個name變數的有效引用需要被產生並顯示出來 . 你正準備說你知道這是一個合成的函數, 但是解決失敗了如果你的即將到來的邏輯產生一個結果的時候失敗了.
這擷取了問題中的合成函數在記憶體中的地址.
lldb.value給出的索引值本質上來自NSNumber, 因此你需要抓取這個方法的description並且將它複製給一個number.令人困惑的是, 它同樣賦值給了一個叫做key的Python變數.
如果key變數與loadAddr相等, 然後你就匹配到了一個值. 將name變數賦值給NSDictionary中變數的description.
那應該就是它. 儲存你的成果然後使用reload_script重新載入你的LLDB內容並試著運行一下.
假設你現在仍然在Stripped 50 Shades of Ray這個scheme下並且暫停在只在UIView的initWithFrame:掉使用時才會觸發的符號斷點處, 在調試器中運行sbt命令來看一下原來停用的第一幀和第三幀是否可讀.
frame #0: 0x1053fe694 UIKit-[UIView initWithFrame:]<br/>frame #1: 0x103cf53ac ShadesOfRay-[RayView initWithFrame:] + 924
frame #2: 0x1053fdda2 UIKit-[UIView init] + 62<br/>frame #3: 0x103cf45bf ShadesOfRay-[ViewController
generateRayViewTapped:] + 79
太漂亮了!

我們為什麼要學這些?

祝賀你!你已經用Objective-C 的運行時成功的重新符號化了一個精簡過的二機制檔案! 你在恰當的Objective-C應用程式上可以做的事情簡直太瘋狂了.
在這個指令碼中仍然有幾個漏洞. 這個指令碼不能夠很好的支援Objective-C的blocks. 然而, 仔細的學習blocks是如何?的以及瀏覽LLDB的Python模組可能會揭示出一個方法來指明已經被精簡過的Objective-C block函數.
此外, 這個指令碼不支援release模式的iOS可執行檔. LLDB不會發現合成的SBSymbol引用的起始地址的函數. 這就意味著在ARM64彙編中你將不得不手動向上搜尋直到你在通過一個看起阿里像函數起始位置的彙編指令被絆了一跤的時候(你可以猜出要尋找的那個指令嗎?).
如果你對這些指令碼的的拓展不感興趣, 試著弄清楚如果重新符號化Swift可執行檔. 這個挑戰一定會讓你有一個巨大的提升, 但是它仍然屬於LLDB能做的事情的範圍.
祝你開心!

Advanced+Apple+Debugging(13)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.