(十二)自定义LLDB命令 内存布局和SBValue

1. 自定义LLDB命令 Value和内存

1.1 内存布局

为了真正理解SBValue类的强大功能,我们将探索分配器应用程序中三个对象的内存布局。从一个Objective-C类开始,然后探索一个没有超类的Swift类,最后探索一个继承自NSObject的Swift类。

这三个类都有三个属性,其顺序如下:

  • 名为eyeColorUIColor
  • 名为firstName的字符串(string/NSString)。
  • 名为lastName的字符串(string/NSString)。

这些类的每个实例都使用相同的值初始化:

  • eyeColorUIColor.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!
//

在文档中,tvow位用于帮助确定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类)。

SBTargetSBFrame类中,有一个名为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

很好,现在我们有一个存储在aSBValue实例,并且已经知道了DSObjectiveCObject的内存布局。

我们知道a保存的SBValue是指向DSObjectiveCObject类的指针。可以使用GetDescription()或更简单的SBValuedescription属性获取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。因为它是由isaeyeColorfirstNamelastName构成的,它们各自都是8字节长的指针。

这里有另一种方法来看看deref的属性在做什么。探索SBValueSBType类。

(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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容