1. 自定義LLDB命令 Value和內(nèi)存
1.1 內(nèi)存布局
為了真正理解SBValue類的強(qiáng)大功能僵朗,我們將探索分配器應(yīng)用程序中三個(gè)對(duì)象的內(nèi)存布局。從一個(gè)Objective-C類開(kāi)始,然后探索一個(gè)沒(méi)有超類的Swift類保檐,最后探索一個(gè)繼承自NSObject的Swift類。
這三個(gè)類都有三個(gè)屬性,其順序如下:
- 名為
eyeColor
的UIColor
街夭。 - 名為
firstName
的字符串(string/NSString)。 - 名為
lastName
的字符串(string/NSString)躏筏。
這些類的每個(gè)實(shí)例都使用相同的值初始化:
-
eyeColor
是UIColor.brown
或[UIColor brownColor]
板丽。 -
firstName
是"Derek"
或@"Derek"
。 -
lastName
為"Selander"
或@"Selander"
趁尼。
Objective-C內(nèi)存布局
@interface DSObjectiveCObject : NSObject
@property (nonatomic, strong) UIColor *eyeColor;
@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;
@end
@implementation DSObjectiveCObject
- (instancetype)init
{
self = [super init];
if (self) {
self.eyeColor = [UIColor brownColor];
self.firstName = @"Derek";
self.lastName = @"Selander";
}
return self;
}
@end
編譯后埃碱,這個(gè)Objective-C類實(shí)際上看起來(lái)像一個(gè)C結(jié)構(gòu)。編譯器將創(chuàng)建類似于以下偽代碼的結(jié)構(gòu):
struct DSObjectiveCObject {
Class isa;
UIColor *eyeColor;
NSString *firstName
NSString *lastName
}
注意第一個(gè)參數(shù)Class isa
酥泞。這就是將Objective-C類視為Objective-C類背后的魔力砚殿。isa
始終是對(duì)象實(shí)例的內(nèi)存布局中的第一個(gè)值,并且是指向該對(duì)象是其實(shí)例的類的指針芝囤。之后似炎,這些屬性將按照它們?cè)谠创a中的寫(xiě)入順序添加到此結(jié)構(gòu)中。
//項(xiàng)目打印的對(duì)象
<DSObjectiveCObject: 0x600003865180>
//lldb打印的對(duì)象
(lldb) po 0x600003865180
<DSObjectiveCObject: 0x600003865180>
//將內(nèi)存地址轉(zhuǎn)成id指針凡人,再取出指針里面的值名党,我們就訪問(wèn)到了isa指針
(lldb) po *(id *)(0x600003865180)
DSObjectiveCObject
//通過(guò)內(nèi)存讀取可以獲得一樣的效果
(lldb) x/gx 0x600003865180
0x600003865180: 0x000000010c2d55d8
(lldb) po 0x000000010c2d55d8
DSObjectiveCObject
//偏移一個(gè)指針的大小,就是我們的eyeColor
(lldb) po *(id *)(0x600003865180 + 0x8)
UIExtendedSRGBColorSpace 0.6 0.4 0.2 1
//繼續(xù)偏移指針的大小挠轴,就是我們的firstName
(lldb) po *(id *)(0x600003865180 + 0x10)
Derek
//繼續(xù)偏移指針的大小传睹,就是我們的lastName
(lldb) po *(id *)(0x600003865180 + 0x18)
Selander
沒(méi)有父類的Swift內(nèi)存布局
class ASwiftClass {
let eyeColor = UIColor.brown
let firstName = "Derek"
let lastName = "Selander"
required init() { }
}
同樣,我們可以將這個(gè)Swift類想象為一個(gè)C結(jié)構(gòu)岸晦,它與Objective-C對(duì)應(yīng)的類有一些有趣的區(qū)別:
struct ASwiftClass {
Class isa;
// Simplified, see "InlineRefCounts"
// in https://github.com/apple/swift
uintptr_t refCounts;
UIColor *eyeColor;
// Simplified, see "_StringGuts"
// in https://github.com/apple/swift
struct _StringCore {
uintptr_t _object; // packed bits for string type
uintptr_t rawBits; // raw data
} firstName;
struct _StringCore {
uintptr_t _object; // packed bits for string type
uintptr_t rawBits; // raw data
} lastName;
}
Swift仍然將isa
變量作為第一個(gè)參數(shù)欧啤。在isa
變量之后,有一個(gè)8字節(jié)的變量被保留用于引用計(jì)數(shù)和對(duì)齊启上,稱為refCounts
邢隧。這與典型的Objective-C對(duì)象不同,后者在此偏移處不包含此變量冈在。
接下來(lái)倒慧,一個(gè)普通的UIColor
,但這就是ASwiftClass
結(jié)構(gòu)完全偏離軌道的地方。
Swift字符串是一個(gè)非常有趣的“對(duì)象”纫谅。實(shí)際上炫贤,Swift
字符串是ASwiftClass
結(jié)構(gòu)中的一個(gè)結(jié)構(gòu)「讹酰可以將Swift字符串看作是一種外觀設(shè)計(jì)模式兰珍,它隱藏不同類型的Swift
字符串類型,這取決于它們是否是硬編碼的询吴、Cocoa掠河、使用ASCII的等等。如果Swift是為32位或64位平臺(tái)編譯的猛计,則類型和布局會(huì)有所不同唠摹。為了簡(jiǎn)單起見(jiàn),只討論64位平臺(tái)有滑。
對(duì)于64位平臺(tái)跃闹,Swift字符串的內(nèi)存布局由16個(gè)字節(jié)組成,結(jié)構(gòu)布局取決于字符串的類型毛好。也就是說(shuō)望艺,首先需要確定字符串的類型,然后才能正確分析字符串的內(nèi)容肌访。
那么怎樣才能確定類型呢找默?下面的文檔摘自Swift 4.2 https://github.com/apple/swift/blob/master/stdlib/public/core/StringObject.swift
// ## _StringObject bit layout //
// x86-64 and arm64: (one 64-bit word)
// +---+---+---|---+------+------------------------------------------+
// + t | v | o | w | uuuu | payload (56 bits) |
// +---+---+---|---+------+------------------------------------------+
// most significant bit least significatn bit
//
// where t: is-a-value, i.e. a tag bit that says not to perform ARC
// v: sub-variant bit, i.e. set for isCocoa or isSmall
// o: is-opaque, i.e. opaque vs contiguously stored strings
// w: width indicator bit (0: ASCII, 1: UTF-16)
// u: unused bits
//
// payload is:
// isNative: the native StringStorage object
// isCocoa: the Cocoa object
// isOpaque & !isCocoa: the _OpaqueString object
// isUnmanaged: the pointer to code units
// isSmall: opaque bits used for inline storage // TODO: use them!
//
在文檔中,t
吼驶、v
惩激、o
、w
位用于幫助確定Swift字符串的類型蟹演。后面4個(gè)u
位將由特定字符串類型使用风钻。也就是說(shuō),上面提到的StringCore
結(jié)構(gòu)的對(duì)象變量的前4位將提供此信息酒请。
Swift字符串結(jié)構(gòu)的布局使程序匯編調(diào)用約定變得相當(dāng)有趣骡技。如果向函數(shù)傳遞字符串,它實(shí)際上將傳入兩個(gè)參數(shù)(并使用兩個(gè)寄存器)羞反,而不是指向包含這兩個(gè)參數(shù)(在一個(gè)寄存器中)的結(jié)構(gòu)的指針布朦。
像OC一樣,我們?cè)贚LDB中查看一下昼窗。
<ASwiftClass: 0x60000313bcc0>
//雖然Swift隱藏了description和debugDescription是趴,我們進(jìn)行類型轉(zhuǎn)換仍可以調(diào)用
(lldb) po 0x60000313bcc0
//在OC上下文中我們甚至可以查看它的父類,雖然我們沒(méi)有聲明
(lldb) po [0x60000313bcc0 superclass]
SwiftObject
//查看Swift類的isa指針
(lldb) po *(id *)0x60000313bcc0
Allocator.ASwiftClass
結(jié)構(gòu)中的引用計(jì)數(shù)是Swift獨(dú)有的澄惊,我們?cè)敿?xì)看看唆途。
//查看引用計(jì)數(shù)
(lldb) po *(id *)(0x60000313bcc0 + 0x8)
0x0000000000000002
(lldb) po [0x60000313bcc0 retain]
(lldb) po *(id *)(0x60000313bcc0 + 0x8)
0x0000000200000002
(lldb) po [0x60000313bcc0 release]
(lldb) po *(id *)(0x60000313bcc0 + 0x8)
0x0000000000000002
注意retain
時(shí)中間的十六進(jìn)制值增加了2富雅。這個(gè)地址實(shí)際上應(yīng)該被視為兩個(gè)獨(dú)立的32位字段,而不是一個(gè)64位字段肛搬。我們接著看下面的屬性:
//和OC一樣吹榴,是我們的UIColor brown
(lldb) po *(id *)(0x60000313bcc0 + 0x10)
UIExtendedSRGBColorSpace 0.6 0.4 0.2 1
注意,我們下面開(kāi)始研究Swift的字符串滚婉,它的前4個(gè)比特決定了它的類型。
(lldb) x/gt '0x60000313bcc0 + 0x18'
0x60000313bcd8: 0b1110010100000000000000000000000000000000000000000000000000000000
看看最左邊的前四位:
- 比特0(t):該對(duì)象不使用ARC計(jì)算引用帅刀。這解釋了為什么在前面執(zhí)行retain方法時(shí)让腹,該值最初為零。
- 比特1(v):
isSmall
扣溺,在這種情況下骇窍,字符串在內(nèi)部稱為Swift small String
。 - 比特2(o):實(shí)例存儲(chǔ)為不透明字符串
- 比特3(w):未設(shè)置該值锥余,這意味著此引用使用了ASCII腹纳。
這個(gè)字符串引用是一個(gè)small String
,它是一個(gè)占用少于15字節(jié)的Swift字符串驱犹。這意味著所有的內(nèi)容都可以在Swift String結(jié)構(gòu)中引用嘲恍。如果字符串大于15字節(jié),則需要一個(gè)指針來(lái)引用數(shù)據(jù)雄驹,而不只是將其打包到16字節(jié)的結(jié)構(gòu)中佃牛。關(guān)于small String,詳細(xì)信息可以在這里查看:
https://github.com/apple/swift/blob/master/stdlib/public/core/SmallString.swift
下面是UTF-8 small Swift String
的簡(jiǎn)化C布局:
typedef struct {
char spillover[7];
char bits; // msb (tvow) bit types, lsb (uuuu) string length
char start[8]; // start address of String
} SmallUTF8String;
在這個(gè)結(jié)構(gòu)中医舆,如果字符串的長(zhǎng)度大于8字節(jié)俘侠,則spillover
是剩余的字符的開(kāi)始。還有一個(gè)bits
值蔬将,它存儲(chǔ)類型和計(jì)數(shù)(較低的4位)爷速。
下面探索firstName
變量的布局:
(lldb) x/s '0x60000313bcc0 + 0x20'
0x60000313bce0: "Derek"
那它的長(zhǎng)度呢?
//對(duì)應(yīng)SmallUTF8String
//char bits; // msb (tvow) bit types, lsb (uuuu) string length
(lldb) x/gx '0x60000313bcc0 + 0x18'
0x000060000313bcd8: 0xe500000000000000
//可以多驗(yàn)證一下
(lldb) p/d *(int *)(0x60000313bcc0 + 0x18 + 7) & 0xf
(int) $10 = 5
5就是我們想要的值霞怀。
NSObject為父類的Swift內(nèi)存布局
class ASwiftNSObjectClass: NSObject {
let eyeColor = UIColor.brown
let firstName = "Derek"
let lastName = "Selander"
required override init() { }
}
那么生成的C結(jié)構(gòu)偽代碼有什么區(qū)別嗎惫东?
struct ASwiftNSObjectClass {
Class isa;
UIColor *eyeColor;
struct _StringCore {
uintptr_t _object;
uintptr_t rawBits;
} firstName;
struct _StringCore {
uintptr_t _object;
uintptr_t rawBits;
} lastName;
}
唯一的區(qū)別是ASwiftNSObjectClass
實(shí)例在偏移量0x8
處缺少refCounts
變量,內(nèi)存中的其余布局將相同里烦。因?yàn)镺bjective-C有自己的retain/release
實(shí)現(xiàn)凿蒜,它不同于Swift實(shí)現(xiàn)。
1.2 SBValue
SBValue負(fù)責(zé)解釋來(lái)自JIT代碼的表達(dá)式解析胁黑。把SBValue
看作是一種表示废封,它允許我們像上面那樣探索對(duì)象中的成員。在SBValue實(shí)例中丧蘸,可以輕松訪問(wèn)結(jié)構(gòu)的所有成員(Objective-C或Swift類)漂洋。
在SBTarget
和SBFrame
類中怜珍,有一個(gè)名為EvaluateExpression的方法,接受Python字符串表達(dá)式并返回一個(gè)SBValue
實(shí)例址儒。此外蜒秤,還有一個(gè)可選的參數(shù),用于指定希望如何解析代碼贝咙。
在么我們的在LLDB中進(jìn)行探索样悟。
(lldb) po [DSObjectiveCObject new]
<DSObjectiveCObject: 0x6000014794e0>
//用這節(jié)提到的方式執(zhí)行一次
(lldb) script lldb.frame.EvaluateExpression('[DSObjectiveCObject new]')
<lldb.SBValue; proxy of <Swig Object of type 'lldb::SBValue *' at 0x1087105a0> >
//上面的結(jié)果可能有點(diǎn)看不懂,打印一下
(lldb) script print(lldb.target.EvaluateExpression('[DSObjectiveCObject new]'))
(DSObjectiveCObject *) $2 = 0x000060000147bc60
//通過(guò)使用變量的方式
(lldb) script a = lldb.target.EvaluateExpression('[DSObjectiveCObject new]')
(lldb) script print(a)
(DSObjectiveCObject *) $3 = 0x000060000147bca0
很好庭猩,現(xiàn)在我們有一個(gè)存儲(chǔ)在a
的SBValue
實(shí)例窟她,并且已經(jīng)知道了DSObjectiveCObject
的內(nèi)存布局。
我們知道a
保存的SBValue
是指向DSObjectiveCObject
類的指針蔼水≌鹛牵可以使用GetDescription()
或更簡(jiǎn)單的SBValue
的description
屬性獲取DSObjectiveCObject
類的描述。同樣我們可以通過(guò)value
獲得這個(gè)對(duì)象的地址趴腋。
//打印描述
(lldb) script print(a.description)
<DSObjectiveCObject: 0x60000147bca0>
//得到str類型的地址
(lldb) script print(a.value)
0x000060000147bca0
(lldb) po 0x000060000147bca0
<DSObjectiveCObject: 0x60000147bca0>
//得到signed類型的地址
(lldb) script print(a.signed)
105553137745056
(lldb) p/x 105553137745056
(long) $5 = 0x000060000147bca0
通過(guò)SBValue偏移量探索屬性
(lldb) script print(a.GetNumChildren())
4
我們可以將其理解為一個(gè)數(shù)組吊说,用一個(gè)特殊的APIGetChildAtIndex
來(lái)遍歷類中的項(xiàng)目。我們得到了4优炬,因此可以在LLDB中探索索引0~3颁井。
(lldb) script print(a.GetChildAtIndex(0))
(NSObject) NSObject = {
isa = DSObjectiveCObject
}
(lldb) script print(a.GetChildAtIndex(1))
(UICachedDeviceRGBColor *) _eyeColor = 0x00006000001de340
(lldb) script print(a.GetChildAtIndex(2))
(__NSCFConstantString *) _firstName = 0x00000001059b04b0 @"Derek"
(lldb) script print(a.GetChildAtIndex(3))
(__NSCFConstantString *) _lastName = 0x00000001059b04d0 @"Selander"
GetChildAtIndex
將返回一個(gè)SBValue
。因此如果需要蠢护,可以進(jìn)一步探索該對(duì)象蚤蔓。用firstName
舉例:
(lldb) script print(a.GetChildAtIndex(2).description)
Derek
記住Python變量a
是指向?qū)ο蟮闹羔?/strong>。
(lldb) script a.size
8
輸出值表示a
長(zhǎng)8字節(jié)糊余。但如果我們想知道真正的大小呢秀又?幸運(yùn)的是,SBValue
有一個(gè)deref
屬性贬芥,該屬性返回另一個(gè)SBValue
吐辙。
(lldb) script a.deref.size
32
這將返回值32。因?yàn)樗怯?code>isa蘸劈、eyeColor
昏苏、firstName
和lastName
構(gòu)成的,它們各自都是8字節(jié)長(zhǎng)的指針威沫。
這里有另一種方法來(lái)看看deref
的屬性在做什么贤惯。探索SBValue
的SBType
類。
(lldb) script print(a.type.name)
DSObjectiveCObject *
(lldb) script print(a.deref.type.name)
DSObjectiveCObject
通過(guò)SBValue查看原始數(shù)據(jù)
我們甚至可以使用SBValue
中的data
屬性查看原始數(shù)據(jù)棒掠。這個(gè)屬性是一個(gè)SBData
類孵构。
//這將輸出指針的地址,注意是大端的
(lldb) script print(a.data)
a0 bc 47 01 00 60 00 00 ..G..`..
//與上面的值進(jìn)行對(duì)比
(lldb) script print(a.value)
0x000060000147bca0
使用deref
屬性可以獲取構(gòu)成這個(gè)DSObjectiveCObject
的所有字節(jié)烟很。
(lldb) script print(a.deref.data)
d8 25 9b 05 01 00 00 00 40 e3 1d 00 00 60 00 00 .%......@....`..
b0 04 9b 05 01 00 00 00 d0 04 9b 05 01 00 00 00 ................
我們可以使用po *(id*) (0x000060000147bca0 + multiple_of_8)
每次跳8字節(jié)查看這些屬性颈墅。
SBExpressionOptions
在討論EvaluateExpression
時(shí)提到還有一個(gè)可選的參數(shù)蜡镶,它將接受SBExpressionOptions
類型的實(shí)例⌒羯福可以使用此命令為JIT執(zhí)行傳遞特定選項(xiàng)官还。
(lldb) script options = lldb.SBExpressionOptions()
(lldb) script options.SetLanguage(lldb.eLanguageTypeSwift)
SBExpressionOptions有一個(gè)名為SetLanguage的方法,該方法接受lldb::LanguageType類型的LLDB模塊枚舉毒坛。LLDB作者有一個(gè)約定望伦,在枚舉、枚舉名和唯一值之前添加一個(gè)e
煎殷。
這個(gè)設(shè)置選項(xiàng)意思是屡谐,現(xiàn)在將以Swift
執(zhí)行代碼,而不是SBFrame
的默認(rèn)語(yǔ)言類型蝌数。
現(xiàn)在告訴options變量將JIT代碼解釋為ID類型:
(lldb) script options.SetCoerceResultToId()
setConverteResultToID
接受一個(gè)可選的布爾值,該值決定是否應(yīng)將其解釋為id度秘,默認(rèn)值是True
顶伞。
回顧一下我們?cè)谶@里所做的:設(shè)置了使用Python API解析這個(gè)expression
的選項(xiàng),而不是通過(guò)expression
命令傳遞給我們的選項(xiàng)剑梳。
例如唆貌,我們現(xiàn)在聲明的SBExpressionOptions
相當(dāng)于expression
命令中的以下選項(xiàng):
expression -lswift -O -- your_expression_here
接下來(lái),只使用expression
命令創(chuàng)建ASwiftClass
的實(shí)例垢乙。如果這有效锨咙,我們將在EvaluateExpression
命令中嘗試相同的表達(dá)式。在LLDB中鍵入以下內(nèi)容:
(lldb) e -lswift -O -- ASwiftClass()
error: <EXPR>:3:1: error: use of unresolved identifier 'ASwiftClass'
ASwiftClass()
^~~~~~~~~~~
我們需要導(dǎo)入Allocator
模塊才能使Swift在調(diào)試器正確運(yùn)行追逮。
(lldb) e -lswift -- import Allocator
(lldb) e -lswift -O -- ASwiftClass()
<ASwiftClass: 0x60000238f500>
下面我們用EvaluateExpression
再來(lái)一次酪刀。
(lldb) script b = lldb.target.EvaluateExpression('ASwiftClass()', options)
(lldb) script print(b.description)
<ASwiftClass: 0x6000023f4b40>
注意:值得指出的是,SBValue的一些特性在Swift中不能很好地發(fā)揮作用钮孵。例如骂倘,使用deref或address_of屬性解引用Swift對(duì)象將無(wú)法正常工作。通過(guò)將指針強(qiáng)制轉(zhuǎn)換為SwiftObject巴席,可以將此指針強(qiáng)制為Objective-C引用历涝,然后一切都將正常工作。
通過(guò)變量名解引用SBValue中的值
從SBValue
通過(guò)GetChildAtIndex
引用子SBValues
是一種非常簡(jiǎn)單的導(dǎo)航到內(nèi)存中對(duì)象的方法漾唉。如果這個(gè)類的作者在eyeColor
之前添加了一個(gè)屬性荧库,在遍歷這個(gè)SBValue
時(shí)完全破壞了偏移邏輯,會(huì)怎么樣赵刑?
幸運(yùn)的是分衫,SBValue
還有另一個(gè)方法可以按名稱而不是偏移量引用實(shí)例變量:GetValueForExpressionPath
。
(lldb) script print(b.GetValueForExpressionPath('.firstName'))
(String) firstName = "Derek"
那如何獲得子SBValues
的名稱呢般此?如果我們不知道子SBValue的名稱
丐箩,可以使用GetChildAtIndex
找到子SBValue
摇邦,然后對(duì)該子SBValue
使用name
屬性。
例如屎勘,如果我不知道在b
中找到的UIColor
屬性的名稱施籍,我可以執(zhí)行以下操作:
(lldb) script print(b)
(Allocator.ASwiftClass) $R4 = 0x00006000023f4b40 {
eyeColor = 0x000060000238f540 {
ObjectiveC.NSObject = {}
}
firstName = "Derek"
lastName = "Selander"
}
(lldb) script print(b.GetChildAtIndex(0))
(UIColor) eyeColor = 0x000060000238f540 {
baseUIDeviceRGBColor@0 = {
baseUIColor@0 = {
baseNSObject@0 = {
isa = UICachedDeviceRGBColor
}
_systemColorName = 0x000060000363ff80 "brownColor"
_cachedStyleString = nil
}
redComponent = 0.59999999999999998
greenComponent = 0.40000000000000002
blueComponent = 0.20000000000000001
alphaComponent = 1
_cachedColor = 0x0000000000000000
}
}
(lldb) script print(b.GetChildAtIndex(0).name)
eyeColor
(lldb) script print(b.GetValueForExpressionPath('.eyeColor'))
(UIColor) eyeColor = 0x000060000238f540 {
ObjectiveC.NSObject = {}
}
(lldb) script print(b.GetValueForExpressionPath('.eyeColor').description)
UIExtendedSRGBColorSpace 0.6 0.4 0.2 1
1.3 lldb.value
最后一件很酷的事情是創(chuàng)建一個(gè)Python引用,它包含SBValue
的屬性作為Python對(duì)象的屬性概漱〕笊鳎可以把它看作一個(gè)對(duì)象,通過(guò)它可以使用Python屬性而不是字符串引用變量瓤摧。
(lldb) script c = lldb.value(b)
(lldb) script print(c.firstName)
(String) firstName = "Derek"
(lldb) script print(c.firstName.sbvalue.description)
"Derek"
上面的代碼將創(chuàng)建一個(gè)特殊LLDB Python對(duì)象「土眩現(xiàn)在我們可以像引用普通對(duì)象一樣引用它的實(shí)例變量。我們還可以把它的子對(duì)象轉(zhuǎn)回SBValue
照弥。