第二十一章:SBValue和Memory桥接脚本(一)

到目前位置, 当执行JIT代码的时候(例如:Objective-C, Swift, C等等. 代码是通过你的Python脚本执行的), 你用了一小部分API去执行代码.
例如, 你使用了SBDebuggerSBCommandReturnObjectHandleCommand方法去执行脚本.SBDebuggerHandleCommand直接输出到stderr, 同时你可以控制SBCommandReturnObject的结果结束的位置. 一旦执行起来以后, 你不得不手动解析返回的输出你感兴趣的部分. 从JIT代码的输出中手动的搜索
有点难看. 没有人喜欢文字型的事情!
因此是时候介绍一下lldb Python模块中的一个新类, SBValue, 已经他如何让解析JIT代码的输出变的简单.
打开本章中starter目录下的Allocator项目.这是一个可以根据输入框中的内容动态生成类的一个简单的应用程序.

图片.png

这是一个完成了的将输入框的字符串传入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看看这些操作. 执行下面的步骤:

  1. 确保在UIPickerView中选中了DSObjectiveCObject.
  2. Allocate Class按钮上点击.
  3. 一旦引用地址输出到了控制台上, 复制那个地址到你的剪贴板上.
  4. 暂停执行然后提出LLDB控制台窗口.

图片.png

一个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". 很酷, 对吧?
让我们用一个链表图看一下你刚才做的事情:

图片.png

你从指向一个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按钮并且复制控制台输出的内存地址.

图片.png

你将会得到一些类似下面的输出:

<Allocator.ASwiftClass: 0x61800009d830>

通常情况下, swift隐藏了descriptiondebugDescription的指针, 但是有一些鬼祟的东西被编译进这个项目里, 你稍后就会看到.
但是现在, 抓取内存地址并且将他复制到剪切板上.
首先用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

图片.png

这一次尝试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

再一次, 这代表着flagslength. 因为"Derek"的长度是5, 在这个十六进制数的最后位置你得到的是5. 其余的0指示着这里没有应用flags(也就是说, 用unichar格式替代了char).
最后, 增加你的偏移量然后越过swift string的_owner.

(lldb) po *(id *)(0x61800009d830 + 0x28)

这将会提取出nil因为这个Objective-C等同于0x0000000000000000.正如先前提到的, 这里不需要"owner"因为这个字符串是在编译吃创建的.
不需要去看后面的lastName属性. 你已经知道了这是如何工作的.

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

推荐阅读更多精彩内容