到目前位置, 当执行JIT代码的时候(例如:Objective-C, Swift, C等等. 代码是通过你的Python脚本执行的), 你用了一小部分API去执行代码.
例如, 你使用了SBDebugger
和SBCommandReturnObject
的HandleCommand方法去执行脚本.SBDebugger
的HandleCommand
直接输出到stderr
, 同时你可以控制SBCommandReturnObject
的结果结束的位置. 一旦执行起来以后, 你不得不手动解析返回的输出你感兴趣的部分. 从JIT代码的输出中手动的搜索
有点难看. 没有人喜欢文字型的事情!
因此是时候介绍一下lldb Python模块中的一个新类, SBValue, 已经他如何让解析JIT代码的输出变的简单.
打开本章中starter目录下的Allocator项目.这是一个可以根据输入框中的内容动态生成类的一个简单的应用程序.
这是一个完成了的将输入框的字符串传入
NSClassFromString
函数生成一个类. 如果返回了一个有效的类, 就会使用古老的init
方法初始化. 否则, 就会输出一个错误.在** iPhone 7 Plus**模拟器上构建并运行这个应用程序. 你不需要对这个程序做任何修改, 而且你将会使用
SBValue
查看对象在内存里的布局, 以及通过LLDB手动的指针.
内存布局的弯路
真的非常感谢强大的SBValue
类, 你将会浏览Allocator应用程序中三个唯一对象的内存布局. 你将会以一个Objective-C类为开始, 然后浏览一个没有父类的swift类, 最后浏览一个继承自NSObject的swift类.
这三个类都有下面三个属性:
• 一个叫做eyeColor的color属性
• 一个叫做firstName的字符串(String/NSString)属性.
• 一个叫做lastName的字符串(String/NSString)属性.
它们中的每一个类都用同样的初始化值. 它们是:
• eyeColor
的值将是UIColor.brown
或者[UIColor brownColor]
, 这取决语言.
• firstName
的值将是"Derek"或者@"Derek"这也取决于语言.
• lastName
的值将是"Selander"或者@"Selander"这也取决于语言.
Objective-C 内存布局
你首先会浏览Objective-C类, 因为它是这些对象在内存中如何布局的基础. 跳到DSObjectiveCObject.h中然后观察一下它. 这里是给你的参考:
@interface DSObjectiveCObject : NSObject
@property (nonatomic, strong) UIColor *eyeColor;
@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, strong) NSString *lastName;
@end
正如前面提到的, 这里有三个属性: eyeColor
, firstName
以及lastName
.
跳到实现文件DSObjectiveCObject.m里, 然后看一下并理解一下Objective-C对象初始化的时候做了哪些事情:
@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
}
注意一下这个类的作为第一个参数的isa
变量. 这就是一个Objective-C 类被认为是一个Objective-C 类的背后的魔法.在对象实例的内存布局中isa
总是第一个第一个值, 而且总是指向这个实例所属的类. 然后, 属性按照你再代码中实现的顺序被加到这个结构体上.
让我们通过LLDB看看这些操作. 执行下面的步骤:
- 确保在
UIPickerView
中选中了DSObjectiveCObject. - 在Allocate Class按钮上点击.
- 一旦引用地址输出到了控制台上, 复制那个地址到你的剪贴板上.
- 暂停执行然后提出LLDB控制台窗口.
一个
DSObjectiveCObject
的实例已经被创建了. 现在你将使用LLDB探索这个对象内容的偏移.从控制台的输出中复制这个内存地址然后确保
po
这个内存地址以后给了你一个有效的引用(例如. 当打印出这个地址的时候你没有停在swift的栈帧上).在我这里, 我得到的指针是
0x600000031f80
. 正如往常一样, 你的可能与我的有所不同. 通过LLDB打印出这个地址:
(lldb) po 0x600000031f80
你应该会得到下面这行期望的输出:
<DSObjectiveCObject: 0x600000031f80>
因为这个可以作为一个C结构体使用, 你将开始探索这个指针内容的偏移.
在LLDB控制台中, 输入下面的内容(用你的指针替换下面的指针):
(lldb) po *(id *)(0x600000031f80)
这行代码指明这个指针是id
型然后解引用它. 这将会访问这个对象的isa指针.
你应该会看到这些输出:
DSObjectiveCObject
这就是这个类的描述, 跟我们期望的一样.
让我们用另外一种方式查看一下这个内存. 使用x命令(又名examine
, 一个从GDB流传下来的命令)来跳到起始指针的位置, 然后po
它. 输入下面内容:
(lldb) x/gx 0x600000031f80
这行命令做了下面的事情:
• 检查这一块内存(x)
• 打印出大端单词的大小, (64 位, 或者 8 字节) (g)
• 最后, 用16进制格式化它 (x).
如果, 假设, 你只想查看二进制文件中这个位置第一个字节的内容, 你可以输入x/bt 0x600000031f80
替代. 这将会解释为检查 (x), 一个字节 (b) 在二进制文件中 (t). 这examine
命令在浏览内存的时候是哪些好用的命令里比较好的一个.
你将会看到下面的输出(或者至少, 类似的输出, 尽管你的值与我的肯能会有不同):
0x600000031f80: 0x0000000108b06568
这些输出告诉你在内存地址0x600000031f80
里包含的值是0x0000000108b06568
. 很好, 这就是我得到的值!
回到我们手里的任务里去, 取到x/gx
命令打印出的地址然后用po
命令打印出新地址.
(lldb) po 0x0000000108b06568
再一次, 这将打印出isa
类, 也就是DSObjectiveCObject
这个类. 这是一个打印出isa
实例的备选方法, 这也许可以让你一些更深刻的理解发生了什么事. 然而, 它用两个LLDB命令替代了一个, 因此你将会解引用这个指针而且不使用x/gx
命令.
让我们更深入eyeColor
属性一点. 在LLDB控制台中:
(lldb) po *(id *)(0x600000031f80 + 0x8)
这行指令是说"从0x600000031f80开始, 增加8字节然后获取这里的指针指向的内容." 你将会得到下面的输出:
UIExtendedSRGBColorSpace 0.6 0.4 0.2 1
怎么知道我得到的数量是8呢? 在LLDB中试一下面的命令:
(lldb) po sizeof(Class)
isa
变量是Class
类型. 因此通过知晓一个类有多大, 你就可以知道这个它在这个结构体总占了多大的空间, 因此你就可以知道eyeColor
的偏移.
注意:当我们使用64位架构工作的时候(x64 or ARM64), 所有NSObject子类的指针都是8字节. 此外, `Class`这个类他自己是8字节. 这就意味着在64位架构上, 要在不同的类之间切换只需要移动8个字节!
这里有一些饿不同尺寸大小的类型, 例如`int`, `short`, `bool` 和其他基本类型, 而且在64位机器上编译器可能会补足预定义的8字节. 然而, 现在这里不需要担心这些, 因为`DSObjectiveCObject`只包含NSObject子类的指针, 沿着类对象可以拿到`isa`变量.
继续进行. 在LLDB中将偏移量再增加8个字节:
(lldb) po *(id *)(0x600000031f80 + 0x10)
现在你增加了另外8个字节, 用十六进制就是0x10
(或者十进制的16).你得到的结果将是@"Derek", 这就是firstName
属性的值. 再增加8位来获取lastName
属性的值:
(lldb) po *(id *)(0x600000031f80 + 0x18)
你将会得到@"Selander". 很酷, 对吧?
让我们用一个链表图看一下你刚才做的事情:
你从指向一个
DSObjectiveCObject
实例的基地址开始. 在这个例子中, 这个起始地址是0x600000031f80
. 你开始解引用这个指针, 这可以给你提供isa
变量, 然后你增加8个字节的偏移获取下一个Objective-C属性, 解引用偏移以后的地址, 将它映射为id
型然后将它输出到控制台中.探索内存是很有趣的而且指明了查看现象后面发生的事情的道路. 这让你更加感激
SBValue
类. 但是现在还不是讨论SBValue
类的时候, 因为你还有两个类要浏览. 第一个就是没有父类的swift类, 第二个是继承自NSObject的swift类. 你首先会浏览那个没有父类的swift类.
没有父类的swift类的内存布局
注意: 有一件事需要提前注意: swift的API仍然在变动. 这就意味着下面的信息可能会改变在swift的ABI完成(稳定下来)之前.Xcode的新版本发布的时间可能会破坏下面的下面这一部分的内容.
到了浏览没有父类的swift类的时间了!
在Allocator
项目中, 跳到ASwiftClass.swift类里然后看一下那里的代码.
class ASwiftClass {
let eyeColor = UIColor.brown
let firstName = "Derek"
let lastName = "Selander"
required init() { }
}
在这里, 你有一个等同于DSObjectiveCObject
的swift风格的对象.
在一次, 你可以将swift类想象成与Objective-C的C结构体类似但是有一些有趣的不同的一个C结构体. 查看下面的伪代码:
struct ASwiftClass {
Class isa;
uint64_t refCounts;
UIColor *eyeColor;
struct _StringCore {
uintptr_t _baseAddress;
uintptr_t _countAndFlags;
uintptr_t _owner;
} firstName;
struct _StringCore {
uintptr_t _baseAddress;
uintptr_t _countAndFlags;
uintptr_t _owner;
} firstName;
}
相当有趣对吧? 你仍然有isa
变量作为第一个参数. 在isa
变量后面, 是一个8位的保留的refCounts
变量. 这与典型的没有没有在这个位置包含这些引用计数器的Objective-C
对象有点不同.注意uint64_t
这种类型, 这种类型占用8字节内存--甚至在32位机器上也是这样.这不同于uintptr_t
类型, 这种类型即可以是32位也可以是64位这取决于机器的硬件.
接下来是普通的UIColor
.
swift的String
是一个非常有趣的对象. 事实上, 一个swift的String
是一个结构体内部包含ASwiftClass
结构体. 当你查看内存的时候每一个swift的String
都包含三个参数:
• 这是string的字符数据的实际地址.
• 接下来是长度和标志混合在一个参数里;它即可以是Unichar
, 也可以是ASCII
, 或者是其他我不知道如何解释的疯狂的东西.
• 最后, 是一个它的持有者的引用.
因为你是在编译时声明的这些string(用那些let
声明的), 这里不需要持有者因为编译器将会只需要应用string的实际位置的偏移, 因为他们是不可改变的.
这个swiftString
结构体实际上让汇编调用约定变得相当有趣. 如果你将一个String
传给一个函数, 他实际上会穿进三个参数(并且使用三个寄存器)来取代一个指向包含三个参数的结构体的指针(在一个寄存器里). 不相信我?当这一章结束的自己检查一下吧!
回到LLDB中并且跳转到一个对象上.
用⌘ + K
清空控制台, 然后通过LLDB或者Xcode继续运行应用程序.
你将会在ASwiftClass
上做与DSObjectiveCObject
上同样的事情. 使用开发者/设计者 "认可的"UIPickerView
然后选择Allocator.SwiftClass. 记住, 正确的引用一个swift类(例如, NSClassFromString
和其它类似的类), 你需要将模块的名字作为类名的前缀并用一个句点将两者分割开.
点击Allocate Class按钮并且复制控制台输出的内存地址.
你将会得到一些类似下面的输出:
<Allocator.ASwiftClass: 0x61800009d830>
通常情况下, swift隐藏了description
和debugDescription
的指针, 但是有一些鬼祟的东西被编译进这个项目里, 你稍后就会看到.
但是现在, 抓取内存地址并且将他复制到剪切板上.
首先用LLDB确保它是有效的, po
它一下:
(lldb) po 0x61800009d830
如果你得到了一些与下面的不同的输出, 你可能会有点惊讶:
<Allocator.ASwiftClass: 0x61800009d830>
尽管这是一个纯净的swift对象, 你依然能够在Objective-C环境中获取他的动态的description
. 这就意味着你可以爬去他的继承树来看他的父类!
(lldb) po [0x61800009d830 superclass]
你将会得到一个有趣的类名的类:
SwiftObject
稍后你会查看这个类的更多信息.现在, 开始跳到内存里查看内存. 解引用这个地址的指针然后自己验证一下第一个参数是isa
类变量:
(lldb) po *(id *)0x61800009d830
你将会得到Allocator.ASwiftClass
. 现在检查一个引用计数器变量:
(lldb) po *(id *)(0x61800009d830 + 0x8)
你将会得到一些类似下面的输出:
0x0000000200000004
很明显这个地址不是一个 Objective-C 地址, 因为*(id*)0x0000000200000004
将会指向一个类如果它是一个有效的实例/类的话.取而代之的是, 这是swift类唯一的引用计数器. 让我们看一下这些是怎么工作的.
使用LLDB手动的retain
这个类:
(lldb) po [0x61800009d830 retain]
按下向上的箭头按钮再次重新执行一下之前的命令:
(lldb) po *(id *)(0x61800009d830 + 0x8)
你将会得到一个稍微不同的数字:
0x0000000200000008
注意最后的非常重要的十六进制值向前跳4个字节, 同时2^33 (是的, 那就是0x0000000200000000
)就是同样的值. 看一下release
是否减少这个引用的计数:
(lldb) po [0x61800009d830 release]
同样的, 按两下上箭头键, 然后回车.
(lldb) po *(id *)(0x61800009d830 + 0x8)
你将会得到一个一个让你很开心的值, 原始值:
0x0000000200000004
这一次尝试
retain
这个对象两次, 然后打印出这个引用计数值:
(lldb) po [0x61800009d830 retain]
(lldb) po [0x61800009d830 retain]
(lldb) po *(id *)(0x61800009d830 + 0x8)
你将会得到这样一个值:
0x000000020000000c
每一次retain
, 这个值都会增加4. 那看起来好像是加法, 但实际上, 它只是最低的2位没有被使用而且保留的值每次增加1. 换句话说, 每次增加0x100
.
最后, release
这个对象两次来平衡那两个retains
:
(lldb) po [0x61800009d830 retain]
(lldb) po [0x61800009d830 retain]
现在你已经看过了isa
变量和refCounts
变量是时候将你的注意力放在ASwiftClass
实例上那些可爱的属性上了.
清空屏幕刷新一下, 在LLDB中增加你的偏移量.
(lldb) po *(id *)(0x61800009d830 + 0x10)
你将会得到内核中代表UIColor’中brown色的值:
UIExtendedSRGBColorSpace 0.6 0.4 0.2 1
跳过另外8个字节然后开始浏览firstName
String 结构体:
(lldb) po *(id *)(0x61800009d830 + 0x18)
0x0000000101f850d0
正如你在伪代码结构体上看到的, 这是swift string类起始地址的实际基地址. 本质上这个基地址可以被看做一个C char*
或者一个C unichar*
(对所有的emojis字符串很有用). 因此你所需要做的就是正确的指定它的类型. 因为swift string"Derek"
一定属于ASCII范围, 将这个地址指明为char*
型来替代id
:
(lldb) po *(char* *)(0x61800009d830 + 0x18)
"Derek"
现在看一下_countAndFlags偏移. 将你的偏移增加到0x20
然后将指明的类型恢复到id
然后继续浏览. id
是一个很好的默认类型, 因为它可以解决他能解决的Objective-C
类型, 如果它不能解析会转化为十六进制地址.
(lldb) po *(id *)(0x61800009d830 + 0x20)
你将会得到下面的输出:
0x0000000000000005
再一次, 这代表着flags
和length
. 因为"Derek"的长度是5, 在这个十六进制数的最后位置你得到的是5. 其余的0指示着这里没有应用flags
(也就是说, 用unichar
格式替代了char
).
最后, 增加你的偏移量然后越过swift string的_owner
.
(lldb) po *(id *)(0x61800009d830 + 0x28)
这将会提取出nil
因为这个Objective-C等同于0x0000000000000000
.正如先前提到的, 这里不需要"owner"因为这个字符串是在编译吃创建的.
不需要去看后面的lastName属性. 你已经知道了这是如何工作的.