(十二)自定义LLDB命令 内存布局和SBValue
1. 自定义LLDB命令 Value和内存
1.1 内存布局
为了真正理解SBValue类的强大功能,我们将探索分配器应用程序中三个对象的内存布局。从一个Objective-C类开始,然后探索一个没有超类的Swift类,最后探索一个继承自NSObject的Swift类。
这三个类都有三个属性,其顺序如下:
- 名为
eyeColor
的UIColor
。 - 名为
firstName
的字符串(string/NSString)。 - 名为
lastName
的字符串(string/NSString)。
这些类的每个实例都使用相同的值初始化:
-
eyeColor
是UIColor.brown
或[UIColor brownColor]
。 -
firstName
是"Derek"
或@"Derek"
。 -
lastName
为"Selander"
或@"Selander"
。
Objective-C内存布局
@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
编译后,这个Objective-C类实际上看起来像一个C结构。编译器将创建类似于以下伪代码的结构:
struct DSObjectiveCObject {
Class isa;
UIColor *eyeColor;
NSString *firstName
NSString *lastName
}
注意第一个参数Class isa
。这就是将Objective-C类视为Objective-C类背后的魔力。isa
始终是对象实例的内存布局中的第一个值,并且是指向该对象是其实例的类的指针。之后,这些属性将按照它们在源代码中的写入顺序添加到此结构中。
//项目打印的对象
<DSObjectiveCObject: 0x600003865180>
//lldb打印的对象
(lldb) po 0x600003865180
<DSObjectiveCObject: 0x600003865180>
//将内存地址转成id指针,再取出指针里面的值,我们就访问到了isa指针
(lldb) po *(id *)(0x600003865180)
DSObjectiveCObject
//通过内存读取可以获得一样的效果
(lldb) x/gx 0x600003865180
0x600003865180: 0x000000010c2d55d8
(lldb) po 0x000000010c2d55d8
DSObjectiveCObject
//偏移一个指针的大小,就是我们的eyeColor
(lldb) po *(id *)(0x600003865180 + 0x8)
UIExtendedSRGBColorSpace 0.6 0.4 0.2 1
//继续偏移指针的大小,就是我们的firstName
(lldb) po *(id *)(0x600003865180 + 0x10)
Derek
//继续偏移指针的大小,就是我们的lastName
(lldb) po *(id *)(0x600003865180 + 0x18)
Selander
没有父类的Swift内存布局
class ASwiftClass {
let eyeColor = UIColor.brown
let firstName = "Derek"
let lastName = "Selander"
required init() { }
}
同样,我们可以将这个Swift类想象为一个C结构,它与Objective-C对应的类有一些有趣的区别:
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
变量作为第一个参数。在isa
变量之后,有一个8字节的变量被保留用于引用计数和对齐,称为refCounts
。这与典型的Objective-C对象不同,后者在此偏移处不包含此变量。
接下来,一个普通的UIColor
,但这就是ASwiftClass
结构完全偏离轨道的地方。
Swift字符串是一个非常有趣的“对象”。实际上,Swift
字符串是ASwiftClass
结构中的一个结构。可以将Swift字符串看作是一种外观设计模式,它隐藏不同类型的Swift
字符串类型,这取决于它们是否是硬编码的、Cocoa、使用ASCII的等等。如果Swift是为32位或64位平台编译的,则类型和布局会有所不同。为了简单起见,只讨论64位平台。
对于64位平台,Swift字符串的内存布局由16个字节组成,结构布局取决于字符串的类型。也就是说,首先需要确定字符串的类型,然后才能正确分析字符串的内容。
那么怎样才能确定类型呢?下面的文档摘自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个u
位将由特定字符串类型使用。也就是说,上面提到的StringCore
结构的对象变量的前4位将提供此信息。
Swift字符串结构的布局使程序汇编调用约定变得相当有趣。如果向函数传递字符串,它实际上将传入两个参数(并使用两个寄存器),而不是指向包含这两个参数(在一个寄存器中)的结构的指针。
像OC一样,我们在LLDB中查看一下。
<ASwiftClass: 0x60000313bcc0>
//虽然Swift隐藏了description和debugDescription,我们进行类型转换仍可以调用
(lldb) po 0x60000313bcc0
//在OC上下文中我们甚至可以查看它的父类,虽然我们没有声明
(lldb) po [0x60000313bcc0 superclass]
SwiftObject
//查看Swift类的isa指针
(lldb) po *(id *)0x60000313bcc0
Allocator.ASwiftClass
结构中的引用计数是Swift独有的,我们详细看看。
//查看引用计数
(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
时中间的十六进制值增加了2。这个地址实际上应该被视为两个独立的32位字段,而不是一个64位字段。我们接着看下面的属性:
//和OC一样,是我们的UIColor brown
(lldb) po *(id *)(0x60000313bcc0 + 0x10)
UIExtendedSRGBColorSpace 0.6 0.4 0.2 1
注意,我们下面开始研究Swift的字符串,它的前4个比特决定了它的类型。
(lldb) x/gt '0x60000313bcc0 + 0x18'
0x60000313bcd8: 0b1110010100000000000000000000000000000000000000000000000000000000
看看最左边的前四位:
- 比特0(t):该对象不使用ARC计算引用。这解释了为什么在前面执行retain方法时,该值最初为零。
- 比特1(v):
isSmall
,在这种情况下,字符串在内部称为Swift small String
。 - 比特2(o):实例存储为不透明字符串
- 比特3(w):未设置该值,这意味着此引用使用了ASCII。
这个字符串引用是一个small String
,它是一个占用少于15字节的Swift字符串。这意味着所有的内容都可以在Swift String结构中引用。如果字符串大于15字节,则需要一个指针来引用数据,而不只是将其打包到16字节的结构中。关于small String,详细信息可以在这里查看:
https://github.com/apple/swift/blob/master/stdlib/public/core/SmallString.swift
下面是UTF-8 small Swift String
的简化C布局:
typedef struct {
char spillover[7];
char bits; // msb (tvow) bit types, lsb (uuuu) string length
char start[8]; // start address of String
} SmallUTF8String;
在这个结构中,如果字符串的长度大于8字节,则spillover
是剩余的字符的开始。还有一个bits
值,它存储类型和计数(较低的4位)。
下面探索firstName
变量的布局:
(lldb) x/s '0x60000313bcc0 + 0x20'
0x60000313bce0: "Derek"
那它的长度呢?
//对应SmallUTF8String
//char bits; // msb (tvow) bit types, lsb (uuuu) string length
(lldb) x/gx '0x60000313bcc0 + 0x18'
0x000060000313bcd8: 0xe500000000000000
//可以多验证一下
(lldb) p/d *(int *)(0x60000313bcc0 + 0x18 + 7) & 0xf
(int) $10 = 5
5就是我们想要的值。
NSObject为父类的Swift内存布局
class ASwiftNSObjectClass: NSObject {
let eyeColor = UIColor.brown
let firstName = "Derek"
let lastName = "Selander"
required override init() { }
}
那么生成的C结构伪代码有什么区别吗?
struct ASwiftNSObjectClass {
Class isa;
UIColor *eyeColor;
struct _StringCore {
uintptr_t _object;
uintptr_t rawBits;
} firstName;
struct _StringCore {
uintptr_t _object;
uintptr_t rawBits;
} lastName;
}
唯一的区别是ASwiftNSObjectClass
实例在偏移量0x8
处缺少refCounts
变量,内存中的其余布局将相同。因为Objective-C有自己的retain/release
实现,它不同于Swift实现。
1.2 SBValue
SBValue负责解释来自JIT代码的表达式解析。把SBValue
看作是一种表示,它允许我们像上面那样探索对象中的成员。在SBValue实例中,可以轻松访问结构的所有成员(Objective-C或Swift类)。
在SBTarget
和SBFrame
类中,有一个名为EvaluateExpression的方法,接受Python字符串表达式并返回一个SBValue
实例。此外,还有一个可选的参数,用于指定希望如何解析代码。
在么我们的在LLDB中进行探索。
(lldb) po [DSObjectiveCObject new]
<DSObjectiveCObject: 0x6000014794e0>
//用这节提到的方式执行一次
(lldb) script lldb.frame.EvaluateExpression('[DSObjectiveCObject new]')
<lldb.SBValue; proxy of <Swig Object of type 'lldb::SBValue *' at 0x1087105a0> >
//上面的结果可能有点看不懂,打印一下
(lldb) script print(lldb.target.EvaluateExpression('[DSObjectiveCObject new]'))
(DSObjectiveCObject *) $2 = 0x000060000147bc60
//通过使用变量的方式
(lldb) script a = lldb.target.EvaluateExpression('[DSObjectiveCObject new]')
(lldb) script print(a)
(DSObjectiveCObject *) $3 = 0x000060000147bca0
很好,现在我们有一个存储在a
的SBValue
实例,并且已经知道了DSObjectiveCObject
的内存布局。
我们知道a
保存的SBValue
是指向DSObjectiveCObject
类的指针。可以使用GetDescription()
或更简单的SBValue
的description
属性获取DSObjectiveCObject
类的描述。同样我们可以通过value
获得这个对象的地址。
//打印描述
(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
通过SBValue偏移量探索属性
(lldb) script print(a.GetNumChildren())
4
我们可以将其理解为一个数组,用一个特殊的APIGetChildAtIndex
来遍历类中的项目。我们得到了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
将返回一个SBValue
。因此如果需要,可以进一步探索该对象。用firstName
举例:
(lldb) script print(a.GetChildAtIndex(2).description)
Derek
记住Python变量a
是指向对象的指针。
(lldb) script a.size
8
输出值表示a
长8字节。但如果我们想知道真正的大小呢?幸运的是,SBValue
有一个deref
属性,该属性返回另一个SBValue
。
(lldb) script a.deref.size
32
这将返回值32。因为它是由isa
、eyeColor
、firstName
和lastName
构成的,它们各自都是8字节长的指针。
这里有另一种方法来看看deref
的属性在做什么。探索SBValue
的SBType
类。
(lldb) script print(a.type.name)
DSObjectiveCObject *
(lldb) script print(a.deref.type.name)
DSObjectiveCObject
通过SBValue查看原始数据
我们甚至可以使用SBValue
中的data
属性查看原始数据。这个属性是一个SBData
类。
//这将输出指针的地址,注意是大端的
(lldb) script print(a.data)
a0 bc 47 01 00 60 00 00 ..G..`..
//与上面的值进行对比
(lldb) script print(a.value)
0x000060000147bca0
使用deref
属性可以获取构成这个DSObjectiveCObject
的所有字节。
(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字节查看这些属性。
SBExpressionOptions
在讨论EvaluateExpression
时提到还有一个可选的参数,它将接受SBExpressionOptions
类型的实例。可以使用此命令为JIT执行传递特定选项。
(lldb) script options = lldb.SBExpressionOptions()
(lldb) script options.SetLanguage(lldb.eLanguageTypeSwift)
SBExpressionOptions有一个名为SetLanguage的方法,该方法接受lldb::LanguageType类型的LLDB模块枚举。LLDB作者有一个约定,在枚举、枚举名和唯一值之前添加一个e
。
这个设置选项意思是,现在将以Swift
执行代码,而不是SBFrame
的默认语言类型。
现在告诉options变量将JIT代码解释为ID类型:
(lldb) script options.SetCoerceResultToId()
setConverteResultToID
接受一个可选的布尔值,该值决定是否应将其解释为id,默认值是True
。
回顾一下我们在这里所做的:设置了使用Python API解析这个expression
的选项,而不是通过expression
命令传递给我们的选项。
例如,我们现在声明的SBExpressionOptions
相当于expression
命令中的以下选项:
expression -lswift -O -- your_expression_here
接下来,只使用expression
命令创建ASwiftClass
的实例。如果这有效,我们将在EvaluateExpression
命令中尝试相同的表达式。在LLDB中键入以下内容:
(lldb) e -lswift -O -- ASwiftClass()
error: <EXPR>:3:1: error: use of unresolved identifier 'ASwiftClass'
ASwiftClass()
^~~~~~~~~~~
我们需要导入Allocator
模块才能使Swift在调试器正确运行。
(lldb) e -lswift -- import Allocator
(lldb) e -lswift -O -- ASwiftClass()
<ASwiftClass: 0x60000238f500>
下面我们用EvaluateExpression
再来一次。
(lldb) script b = lldb.target.EvaluateExpression('ASwiftClass()', options)
(lldb) script print(b.description)
<ASwiftClass: 0x6000023f4b40>
注意:值得指出的是,SBValue的一些特性在Swift中不能很好地发挥作用。例如,使用deref或address_of属性解引用Swift对象将无法正常工作。通过将指针强制转换为SwiftObject,可以将此指针强制为Objective-C引用,然后一切都将正常工作。
通过变量名解引用SBValue中的值
从SBValue
通过GetChildAtIndex
引用子SBValues
是一种非常简单的导航到内存中对象的方法。如果这个类的作者在eyeColor
之前添加了一个属性,在遍历这个SBValue
时完全破坏了偏移逻辑,会怎么样?
幸运的是,SBValue
还有另一个方法可以按名称而不是偏移量引用实例变量:GetValueForExpressionPath
。
(lldb) script print(b.GetValueForExpressionPath('.firstName'))
(String) firstName = "Derek"
那如何获得子SBValues
的名称呢?如果我们不知道子SBValue的名称
,可以使用GetChildAtIndex
找到子SBValue
,然后对该子SBValue
使用name
属性。
例如,如果我不知道在b
中找到的UIColor
属性的名称,我可以执行以下操作:
(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
最后一件很酷的事情是创建一个Python引用,它包含SBValue
的属性作为Python对象的属性。可以把它看作一个对象,通过它可以使用Python属性而不是字符串引用变量。
(lldb) script c = lldb.value(b)
(lldb) script print(c.firstName)
(String) firstName = "Derek"
(lldb) script print(c.firstName.sbvalue.description)
"Derek"
上面的代码将创建一个特殊LLDB Python对象。现在我们可以像引用普通对象一样引用它的实例变量。我们还可以把它的子对象转回SBValue
。