精彩文章免费看

(十二)自定义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;
    @implementation DSObjectiveCObject
    - (instancetype)init
      self = [super init];
      if (self) {
        self.eyeColor = [UIColor brownColor];
        self.firstName = @"Derek";
        self.lastName = @"Selander";
      return self;
    

    编译后,这个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())
    

    我们可以将其理解为一个数组,用一个特殊的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
    

    输出值表示a长8字节。但如果我们想知道真正的大小呢?幸运的是,SBValue有一个deref属性,该属性返回另一个SBValue

    (lldb) script a.deref.size
    

    这将返回值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