Apple高级调试与逆向工程

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

2020-03-10  本文已影响0人  收纳箱

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

1.1 内存布局

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

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

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

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

看看最左边的前四位:

这个字符串引用是一个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

上一篇下一篇

猜你喜欢

热点阅读