第二十一章:SBValue和Memory橋接腳本(一)

到目前位置, 當執行JIT代碼的時候(例如:Objective-C, Swift, C等等. 代碼是通過你的Python腳本執行的), 你用了一小部分API去執行代碼.
例如, 你使用了SBDebuggerSBCommandReturnObjectHandleCommand方法去執行腳本.SBDebuggerHandleCommand直接輸出到stderr, 同時你可以控制SBCommandReturnObject的結果結束的位置. 一旦執行起來以后, 你不得不手動解析返回的輸出你感興趣的部分. 從JIT代碼的輸出中手動的搜索
有點難看. 沒有人喜歡文字型的事情!
因此是時候介紹一下lldb Python模塊中的一個新類, SBValue, 已經他如何讓解析JIT代碼的輸出變的簡單.
打開本章中starter目錄下的Allocator項目.這是一個可以根據輸入框中的內容動態生成類的一個簡單的應用程序.

圖片.png

這是一個完成了的將輸入框的字符串傳入NSClassFromString函數生成一個類. 如果返回了一個有效的類, 就會使用古老的init方法初始化. 否則, 就會輸出一個錯誤.
在** iPhone 7 Plus**模擬器上構建并運行這個應用程序. 你不需要對這個程序做任何修改, 而且你將會使用SBValue查看對象在內存里的布局, 以及通過LLDB手動的指針.

內存布局的彎路

真的非常感謝強大的SBValue類, 你將會瀏覽Allocator應用程序中三個唯一對象的內存布局. 你將會以一個Objective-C類為開始, 然后瀏覽一個沒有父類的swift類, 最后瀏覽一個繼承自NSObject的swift類.
這三個類都有下面三個屬性:
? 一個叫做eyeColor的color屬性
? 一個叫做firstName的字符串(String/NSString)屬性.
? 一個叫做lastName的字符串(String/NSString)屬性.
它們中的每一個類都用同樣的初始化值. 它們是:
? eyeColor的值將是UIColor.brown或者[UIColor brownColor], 這取決語言.
? firstName的值將是"Derek"或者@"Derek"這也取決于語言.
? lastName的值將是"Selander"或者@"Selander"這也取決于語言.

Objective-C 內存布局

你首先會瀏覽Objective-C類, 因為它是這些對象在內存中如何布局的基礎. 跳到DSObjectiveCObject.h中然后觀察一下它. 這里是給你的參考:

@interface DSObjectiveCObject : NSObject
@property (nonatomic, strong) UIColor *eyeColor;
@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;
@end

正如前面提到的, 這里有三個屬性: eyeColor, firstName以及lastName.
跳到實現文件DSObjectiveCObject.m里, 然后看一下并理解一下Objective-C對象初始化的時候做了哪些事情:

@implementation DSObjectiveCObject
- (instancetype)init
{
  self = [super init];
  if (self) {
    self.eyeColor = [UIColor brownColor];
    self.firstName = @"Derek";
    self.lastName = @"Selander";
}
  return self;
}
@end

并沒有什么瘋狂的事情. 這些屬性都會別初始化為上面描述的內容.
這些代碼在編譯的時候, 這個Objective-C類看起來實際上像一個C結構體. 編譯器會創建一個類似下面的結構體的偽代碼:

struct DSObjectiveCObject {
  Class isa;
  UIColor *eyeColor;
  NSString *firstName
  NSString *lastName
}

注意一下這個類的作為第一個參數的isa變量. 這就是一個Objective-C 類被認為是一個Objective-C 類的背后的魔法.在對象實例的內存布局中isa總是第一個第一個值, 而且總是指向這個實例所屬的類. 然后, 屬性按照你再代碼中實現的順序被加到這個結構體上.
讓我們通過LLDB看看這些操作. 執行下面的步驟:

  1. 確保在UIPickerView中選中了DSObjectiveCObject.
  2. Allocate Class按鈕上點擊.
  3. 一旦引用地址輸出到了控制臺上, 復制那個地址到你的剪貼板上.
  4. 暫停執行然后提出LLDB控制臺窗口.

圖片.png

一個DSObjectiveCObject的實例已經被創建了. 現在你將使用LLDB探索這個對象內容的偏移.
從控制臺的輸出中復制這個內存地址然后確保po這個內存地址以后給了你一個有效的引用(例如. 當打印出這個地址的時候你沒有停在swift的棧幀上).
在我這里, 我得到的指針是0x600000031f80. 正如往常一樣, 你的可能與我的有所不同. 通過LLDB打印出這個地址:

(lldb) po 0x600000031f80

你應該會得到下面這行期望的輸出:

<DSObjectiveCObject: 0x600000031f80>

因為這個可以作為一個C結構體使用, 你將開始探索這個指針內容的偏移.
在LLDB控制臺中, 輸入下面的內容(用你的指針替換下面的指針):

(lldb) po *(id *)(0x600000031f80)

這行代碼指明這個指針是id型然后解引用它. 這將會訪問這個對象的isa指針.
你應該會看到這些輸出:

DSObjectiveCObject

這就是這個類的描述, 跟我們期望的一樣.
讓我們用另外一種方式查看一下這個內存. 使用x命令(又名examine, 一個從GDB流傳下來的命令)來跳到起始指針的位置, 然后po它. 輸入下面內容:

(lldb) x/gx 0x600000031f80

這行命令做了下面的事情:
? 檢查這一塊內存(x)
? 打印出大端單詞的大小, (64 位, 或者 8 字節) (g)
? 最后, 用16進制格式化它 (x).
如果, 假設, 你只想查看二進制文件中這個位置第一個字節的內容, 你可以輸入x/bt 0x600000031f80替代. 這將會解釋為檢查 (x), 一個字節 (b) 在二進制文件中 (t). 這examine命令在瀏覽內存的時候是哪些好用的命令里比較好的一個.
你將會看到下面的輸出(或者至少, 類似的輸出, 盡管你的值與我的肯能會有不同):

0x600000031f80: 0x0000000108b06568

這些輸出告訴你在內存地址0x600000031f80里包含的值是0x0000000108b06568. 很好, 這就是我得到的值!
回到我們手里的任務里去, 取到x/gx命令打印出的地址然后用po命令打印出新地址.

(lldb) po 0x0000000108b06568

再一次, 這將打印出isa類, 也就是DSObjectiveCObject這個類. 這是一個打印出isa實例的備選方法, 這也許可以讓你一些更深刻的理解發生了什么事. 然而, 它用兩個LLDB命令替代了一個, 因此你將會解引用這個指針而且不使用x/gx命令.
讓我們更深入eyeColor屬性一點. 在LLDB控制臺中:

(lldb) po *(id *)(0x600000031f80 + 0x8)

這行指令是說"從0x600000031f80開始, 增加8字節然后獲取這里的指針指向的內容." 你將會得到下面的輸出:

UIExtendedSRGBColorSpace 0.6 0.4 0.2 1

怎么知道我得到的數量是8呢? 在LLDB中試一下面的命令:

(lldb) po sizeof(Class)

isa變量是Class類型. 因此通過知曉一個類有多大, 你就可以知道這個它在這個結構體總占了多大的空間, 因此你就可以知道eyeColor的偏移.

注意:當我們使用64位架構工作的時候(x64 or ARM64), 所有NSObject子類的指針都是8字節. 此外, `Class`這個類他自己是8字節. 這就意味著在64位架構上, 要在不同的類之間切換只需要移動8個字節!
這里有一些餓不同尺寸大小的類型, 例如`int`, `short`, `bool` 和其他基本類型, 而且在64位機器上編譯器可能會補足預定義的8字節. 然而, 現在這里不需要擔心這些, 因為`DSObjectiveCObject`只包含NSObject子類的指針, 沿著類對象可以拿到`isa`變量.

繼續進行. 在LLDB中將偏移量再增加8個字節:

(lldb) po *(id *)(0x600000031f80 + 0x10)

現在你增加了另外8個字節, 用十六進制就是0x10(或者十進制的16).你得到的結果將是@"Derek", 這就是firstName屬性的值. 再增加8位來獲取lastName屬性的值:

(lldb) po *(id *)(0x600000031f80 + 0x18)

你將會得到@"Selander". 很酷, 對吧?
讓我們用一個鏈表圖看一下你剛才做的事情:

圖片.png

你從指向一個DSObjectiveCObject實例的基地址開始. 在這個例子中, 這個起始地址是0x600000031f80. 你開始解引用這個指針, 這可以給你提供isa變量, 然后你增加8個字節的偏移獲取下一個Objective-C屬性, 解引用偏移以后的地址, 將它映射為id型然后將它輸出到控制臺中.
探索內存是很有趣的而且指明了查看現象后面發生的事情的道路. 這讓你更加感激SBValue類. 但是現在還不是討論SBValue類的時候, 因為你還有兩個類要瀏覽. 第一個就是沒有父類的swift類, 第二個是繼承自NSObject的swift類. 你首先會瀏覽那個沒有父類的swift類.

沒有父類的swift類的內存布局
注意: 有一件事需要提前注意: swift的API仍然在變動. 這就意味著下面的信息可能會改變在swift的ABI完成(穩定下來)之前.Xcode的新版本發布的時間可能會破壞下面的下面這一部分的內容. 

到了瀏覽沒有父類的swift類的時間了!
Allocator項目中, 跳到ASwiftClass.swift類里然后看一下那里的代碼.

class ASwiftClass {
  let eyeColor = UIColor.brown
  let firstName = "Derek"
  let lastName = "Selander"
  required init() { }
}

在這里, 你有一個等同于DSObjectiveCObject的swift風格的對象.
在一次, 你可以將swift類想象成與Objective-C的C結構體類似但是有一些有趣的不同的一個C結構體. 查看下面的偽代碼:

struct ASwiftClass {
  Class isa;
  uint64_t refCounts;
  UIColor *eyeColor;
  struct _StringCore {
    uintptr_t _baseAddress;
    uintptr_t _countAndFlags;
    uintptr_t _owner;
} firstName;
  struct _StringCore {
    uintptr_t _baseAddress;
    uintptr_t _countAndFlags;
    uintptr_t _owner;
  } firstName;
}

相當有趣對吧? 你仍然有isa變量作為第一個參數. 在isa變量后面, 是一個8位的保留的refCounts變量. 這與典型的沒有沒有在這個位置包含這些引用計數器的Objective-C對象有點不同.注意uint64_t這種類型, 這種類型占用8字節內存--甚至在32位機器上也是這樣.這不同于uintptr_t類型, 這種類型即可以是32位也可以是64位這取決于機器的硬件.
接下來是普通的UIColor.
swift的String是一個非常有趣的對象. 事實上, 一個swift的String是一個結構體內部包含ASwiftClass結構體. 當你查看內存的時候每一個swift的String都包含三個參數:
? 這是string的字符數據的實際地址.
? 接下來是長度和標志混合在一個參數里;它即可以是Unichar, 也可以是ASCII, 或者是其他我不知道如何解釋的瘋狂的東西.
? 最后, 是一個它的持有者的引用.
因為你是在編譯時聲明的這些string(用那些let聲明的), 這里不需要持有者因為編譯器將會只需要應用string的實際位置的偏移, 因為他們是不可改變的.
這個swiftString結構體實際上讓匯編調用約定變得相當有趣. 如果你將一個String傳給一個函數, 他實際上會穿進三個參數(并且使用三個寄存器)來取代一個指向包含三個參數的結構體的指針(在一個寄存器里). 不相信我?當這一章結束的自己檢查一下吧!
回到LLDB中并且跳轉到一個對象上.
? + K清空控制臺, 然后通過LLDB或者Xcode繼續運行應用程序.
你將會在ASwiftClass上做與DSObjectiveCObject上同樣的事情. 使用開發者/設計者 "認可的"UIPickerView然后選擇Allocator.SwiftClass. 記住, 正確的引用一個swift類(例如, NSClassFromString和其它類似的類), 你需要將模塊的名字作為類名的前綴并用一個句點將兩者分割開.
點擊Allocate Class按鈕并且復制控制臺輸出的內存地址.

圖片.png

你將會得到一些類似下面的輸出:

<Allocator.ASwiftClass: 0x61800009d830>

通常情況下, swift隱藏了descriptiondebugDescription的指針, 但是有一些鬼祟的東西被編譯進這個項目里, 你稍后就會看到.
但是現在, 抓取內存地址并且將他復制到剪切板上.
首先用LLDB確保它是有效的, po它一下:

(lldb) po 0x61800009d830

如果你得到了一些與下面的不同的輸出, 你可能會有點驚訝:

<Allocator.ASwiftClass: 0x61800009d830>

盡管這是一個純凈的swift對象, 你依然能夠在Objective-C環境中獲取他的動態的description. 這就意味著你可以爬去他的繼承樹來看他的父類!

(lldb) po [0x61800009d830 superclass]

你將會得到一個有趣的類名的類:

SwiftObject

稍后你會查看這個類的更多信息.現在, 開始跳到內存里查看內存. 解引用這個地址的指針然后自己驗證一下第一個參數是isa類變量:

(lldb) po *(id *)0x61800009d830

你將會得到Allocator.ASwiftClass. 現在檢查一個引用計數器變量:

(lldb) po *(id *)(0x61800009d830 + 0x8)

你將會得到一些類似下面的輸出:

0x0000000200000004

很明顯這個地址不是一個 Objective-C 地址, 因為*(id*)0x0000000200000004將會指向一個類如果它是一個有效的實例/類的話.取而代之的是, 這是swift類唯一的引用計數器. 讓我們看一下這些是怎么工作的.
使用LLDB手動的retain這個類:

(lldb) po [0x61800009d830 retain]

按下向上的箭頭按鈕再次重新執行一下之前的命令:

(lldb) po *(id *)(0x61800009d830 + 0x8)

你將會得到一個稍微不同的數字:

0x0000000200000008

注意最后的非常重要的十六進制值向前跳4個字節, 同時2^33 (是的, 那就是0x0000000200000000)就是同樣的值. 看一下release是否減少這個引用的計數:

(lldb) po [0x61800009d830 release]

同樣的, 按兩下上箭頭鍵, 然后回車.

(lldb) po *(id *)(0x61800009d830 + 0x8)

你將會得到一個一個讓你很開心的值, 原始值:

0x0000000200000004

圖片.png

這一次嘗試retain這個對象兩次, 然后打印出這個引用計數值:

(lldb) po [0x61800009d830 retain]
(lldb) po [0x61800009d830 retain]
(lldb) po *(id *)(0x61800009d830 + 0x8)

你將會得到這樣一個值:

0x000000020000000c

每一次retain, 這個值都會增加4. 那看起來好像是加法, 但實際上, 它只是最低的2位沒有被使用而且保留的值每次增加1. 換句話說, 每次增加0x100.
最后, release這個對象兩次來平衡那兩個retains:

(lldb) po [0x61800009d830 retain]
(lldb) po [0x61800009d830 retain]

現在你已經看過了isa變量和refCounts變量是時候將你的注意力放在ASwiftClass實例上那些可愛的屬性上了.
清空屏幕刷新一下, 在LLDB中增加你的偏移量.

(lldb) po *(id *)(0x61800009d830 + 0x10)

你將會得到內核中代表UIColor’中brown色的值:

UIExtendedSRGBColorSpace 0.6 0.4 0.2 1

跳過另外8個字節然后開始瀏覽firstName String 結構體:

(lldb) po *(id *)(0x61800009d830 + 0x18)
0x0000000101f850d0

正如你在偽代碼結構體上看到的, 這是swift string類起始地址的實際基地址. 本質上這個基地址可以被看做一個C char*或者一個C unichar*(對所有的emojis字符串很有用). 因此你所需要做的就是正確的指定它的類型. 因為swift string"Derek"
一定屬于ASCII范圍, 將這個地址指明為char*型來替代id:

(lldb) po *(char* *)(0x61800009d830 + 0x18)
"Derek"

現在看一下_countAndFlags偏移. 將你的偏移增加到0x20然后將指明的類型恢復到id然后繼續瀏覽. id是一個很好的默認類型, 因為它可以解決他能解決的Objective-C類型, 如果它不能解析會轉化為十六進制地址.

(lldb) po *(id *)(0x61800009d830 + 0x20)

你將會得到下面的輸出:

0x0000000000000005

再一次, 這代表著flagslength. 因為"Derek"的長度是5, 在這個十六進制數的最后位置你得到的是5. 其余的0指示著這里沒有應用flags(也就是說, 用unichar格式替代了char).
最后, 增加你的偏移量然后越過swift string的_owner.

(lldb) po *(id *)(0x61800009d830 + 0x28)

這將會提取出nil因為這個Objective-C等同于0x0000000000000000.正如先前提到的, 這里不需要"owner"因為這個字符串是在編譯吃創建的.
不需要去看后面的lastName屬性. 你已經知道了這是如何工作的.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容