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

2017-07-12  本文已影响52人  股金杂谈
SwiftObject类: 弯路中的弯路

但是等一下, 你已经大概看了一下SwiftObject类, 这个类是ASwiftClass类的父类!让我们用image lookup这个方法来提取这个类实现的方法.
在LLDB中输入下面内容:

(lldb) image lookup -rn SwiftObject
图片.png

我不知道你的输出内容, 但是我实在厌倦了查看这么丑的格式的输出. 要正确的读这些内容实在太难了. 既然你已经读这本书这么久, 你就知道你可以自由的做出任何必要的改变来让你的生活更轻松.
使用lldb的Python API来创建一个更漂亮的查询:

(lldb) script srch = lldb.target.FindGlobalFunctions('SwiftObject', 0,lldb.eMatchTypeRegex)

你声明了一个SBSymbolContextList类型的搜索查询并且将它赋值给srch. 这是一些类似SBSymbolContext的Python数组的东西.
我要把通过gdocumentation中的SBTarget抓取FindGlobalFunctions文档的工作留给你.
现在在LLDB中输入下面的内容:

(lldb) script print "\n".join(map(lambda a: str(a.symbol.name), srch))

这里用一个lambda抓取函数名字然后将每一个对象通过换行符分割加到这个list中.

图片.png
更好一点了. 也许对于接下来的章节来说那是一个好的脚本. OK, 回到我们手里的目标上. 你正在浏览SwiftObject类实现的方法. 注意SwiftObject这个类实现的descriptiondebugDescription方法.
这个Allocator项目实际上交换了SwiftObjectdebugDescription去修改输出并且显示SwiftObject子类的指针. 这个逻辑是在NSObject+DS_SwiftObject.m文件里发现的. Swift语言和Swift LLDB 环境隐藏了这个指针, 这一点十分讨厌.
提取到的输出意味着任何swift类仍然开放了swizzling和Objective-C的运行时, 无论它是否继承自NSObject, 或者是一个根本没有父类的类.出于你应用的安全的角度考虑将这些记在脑海里是好的.
以NSObject为父类的swift的内存布局

最后一点. 你知道这个训练, 因此我们将讲的快一点而且会跳过实际调试会话.
检查ASwiftNSObjectClass.swift的源代码:

class ASwiftNSObjectClass: NSObject {
  let eyeColor = UIColor.brown
  let firstName = "Derek"
  let lastName = "Selander"
  required override init() { }
}

这是与ASwiftClass同样的内容, 除了它继承自NSObject而不是继承自nothing之外.
那么这里生成的C结构体的伪代码是否有什么不同呢?

struct ASwiftNSObjectClass {
  Class isa;
  uintptr_t referenceCounts;
  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;
}

不是的!这里生成的伪代码与ASwiftClass的伪代码比没有什么不同的. 最大的不同是这个类有着更简单的内观因为NSObject与SwiftObject相比实现了更多的方法.
让我们跳过调试会话而且仅仅只讨论当你尝试retain这个类的一个实例的时候会发生什么: refCounts变量不会被修改. 这个现象是因为Objective-C有它自己实现的 retain/release那些与Swift实现的是不同的.
你终于可以学习SBValue类了我已经迫不及待的想讲给你听了!.

SBValue

耶!是时候讨论一下这个让人惊喜的类了.
SBValue负责从你的JIT代码中解释剖析表达式. 将SBValue想想成一个让你浏览你对象的成员变量的代表, 仅仅在你做上面事情的时候, 但是没有那些难看的解引用. 在SBValue实例内部, 你可以轻松的访问你结构体中的所有变量..., 我的意思是, 你的Objective-C 或者swift类.
在SBTarget和SBFrame类里, 这里有一个叫做EvaluateExpression的方法, 这个方法会将你的表达式作为一个Python str然后返回一个SBValue实例. 此外, 这里有一个可选的第二个参数可以让你指明你希望你的代码怎样被解析. 你刚开始的时候不会用到可选的第二个参数, 但是后面会用到.
回到LLDB控制台中然后确保Allocator项目仍然在运行.确保LLDB控制台在前台(也就是说, 程序是暂停的), 清空控制台并且输入下面的内容:

(lldb) po [DSObjectiveCObject new]

你将会得到一些类似下面的内容:

<DSObjectiveCObject: 0x61800002eec0>

这确保你可以创建一个DSObjectiveCObject的有效实例.
这些代码是可以工作的, 因此你可以将它应用到EvaluateExpression方法上可以是全局的SBTarget实例或者SBFrame实例:

(lldb) script lldb.frame.EvaluateExpression('[DSObjectiveCObject new]')

你将会得到这个类通常的神秘输出但是没有这些做的内容的上下文描述:

<lldb.SBValue; proxy of <Swig Object of type 'lldb::SBValue *' at0x10ac78b10> >

你已经使用print获取这些类的上下文:

(lldb) script print lldb.target.EvaluateExpression('[DSObjectiveCObjectnew]')

你将会得到你喜欢的你已经变得习惯了的debugDescription.

(DSObjectiveCObject *) $2 = 0x0000618000034280
注意: 如果你输入错误一些东西, 你将仍然得到一个`SBValue`实例, 因此确保它打印出了你期望的内容.例如, 如果你输入错误了JIT代码, 你将会从SBValue中得到一些类似`** = <could not resolvetype>**`的内容.
你可以通过检查你SBValue中的SBError实例确认SBValue成功了.如果你的SBvalue叫做`sbval`, 你一个通过`sbval.GetError().Success()`, 或者更简单的`sbval.error.success. print`是一种开苏查看是否工作的方法.

修改这个命令以便你将这个值赋值给了Python上下文中的变量a:

(lldb) script a = lldb.target.EvaluateExpression('[DSObjectiveCObjectnew]')

现在将Python的print函数应用到a变量上:

(lldb) script print a

再一次你将得到类似下面的输出:

(DSObjectiveCObject *) $0 = 0x0000608000033260

太棒了!你有一个SBValue实例存储在a变量里而且已经见识到了DSObjectiveCObject的内存布局.
你知道a持有一个SBValue, 这个值指向DSObjectiveCObject类.你可以抓取通过使用GetDescription()抓取DSObjectiveCObject的description, 或者更简单的SBValue的description属性.
输入下面的内容:

(lldb) script print a.description

你将会看到类似下面的内容:

<DSObjectiveCObject: 0x608000033260>

你也可以获取value属性, 这个属性返回一个PythonString包含着这个实例的地址:

(lldb) script print a.value

这一次的值是:

0x0000608000033260

复制a.value的输出然后确保po这个给你的原始的指针, 当前引用:

(lldb) po 0x0000608000033260

是的:

<DSObjectiveCObject: 0x608000033260>

如果你想让这个地址通过一个Python的number表达而不是一个Python的str, 你可以使用signed或者unsigned属性:

(lldb) script print a.signed

像这样:

106102872289888

将这个数字格式化为16进制将会产生这个DSObjectiveCObject实例的指针:

(lldb) p/x 106102872289888

现在你就会到了之前的地方:

(long) $3 = 0x0000608000033260
通过SBValue的偏移浏览属性

这些属性是如何存储在DSObjectiveCObject实例里的呢?让我们浏览一下它们!
使用SBValue可用的GetNumChildren方法获取它child的数量:

(lldb) script print a.GetNumChildren()4

你可以将children看成一个数组. 这里有一个特殊的API可以爬取一个类里的children, 这个API叫做GetChildAtIndex. 在LLDB中浏览children 0-3:
Child 0:

(lldb) script print a.GetChildAtIndex(0)
(NSObject) NSObject = {
  isa = DSObjectiveCObject
}

Child 1:

 (lldb) script print a.GetChildAtIndex(1)
(UICachedDeviceRGBColor *) _eyeColor = 0x0000608000070e00

Child 2:

 (lldb) script print a.GetChildAtIndex(2)
(__NSCFConstantString *) _firstName = 0x000000010db83368 @"Derek"

Child 3:

 (lldb) script print a.GetChildAtIndex(3)
(__NSCFConstantString *) _lastName = 0x000000010db83388 @"Selander"

它们中的每一个都会返回一个它自己内部的SBValue, 因此你甚至可以进一步浏览那个对象如果你渴望的话. 将firstName属性带进账户中. 输入下面内容来获取description:

(lldb) script print a.GetChildAtIndex(2).description
Derek

记住Python变量a是指向一个对象的指针是很重要的.输入下面内容:

(lldb) script a.size
8

这将会打印出一个值告诉我们a是8字节长.但是你想要获取a里面实际的内容!幸运的是, SBValue有一个deref属性返回了另一个SBValue.浏览size属性的输出:

(lldb) script a.deref.size

这一次的返回值是32因为它是由isa, eyeColor, firstNamelastName组成的, 他们中的每一个都是8字节长.

(lldb) script print a.type.name

你将会得到这些:

DSObjectiveCObject *

现在通过deref属性做同样的事情:

(lldb) script print a.deref.type.name

现在你将会这个普通的类:

DSObjectiveCObject
通过SBValue查看raw data (原始数据)

你甚至可以通过SBValue中的data属性提取出原始数据! 这是一个SBData的类, 你可以在空闲的时候自己检查一下这个类.
打印出DSObjectiveCObjectdata的指针:

(lldb) script print a.data

这将会打印出组成这个对象的物理字节.再一次, 这是指向DSObjectiveCObject的指针, 并不是这个对象本身.

60 32 03 00 80 60 00 00 `2...`..

记住, 十六进制中的内一个字节都代表两个位.
你还记得第十一章“Assembly& Memory”中的小端格式吗以及原始数据是如何被反转的?
将这个值与SBValue的value属性做一个比较.

(lldb) script print a.value
0x0000608000033260
图片.png
注意这些值是如何排位的. 例如, 我的指针的原始数据中最后两个16进制位是一组. 在我这里, 原始数据中的0x60是第一个值, 与此同时指针中包含的0x60作为最后一个值.
使用deref属性去抓取组成DSObjectiveCObject的所有字节.
(lldb) script print a.deref.data f054b80d01000000000e070080600000 .T...........`.. 6833b80d010000008833b80d01000000 h3.......3......

这是另外一种可视化发生了什么的方式. 你每次跳过了8字节当你用po *(id*)(0x0000608000033260 + multiple_of_8)命令查看内存的时候.

SBExpressionOptions

正如我们在讨论EvaluateExpressionAPI的时候提到的那样, 这里有一个可选的第二个参数将会带入一个SBExpressionOptions实例. 你可以用这个选项为JIT的执行传入一个特殊的选项.
在LLDB中, 清空屏幕, 输入下面的内容:

(lldb) script options = lldb.SBExpressionOptions()

上面的命令执行成功之后不会有任何的输出. 接下来输入:

(lldb) script options.SetLanguage(lldb.eLanguageTypeSwift)

SBExpressionOptions有一个叫做SetLanguage的方法(当有疑惑的时候, 使用gdocumentation SBExpressionOptions), 这个方法带了一个LLDB模块枚举类型的参数lldb::LanguageType. LLDB的作者有一个约定在一个枚举的前面粘贴一个"e", 这个枚举的名字, 然后是唯一的值.
这设定了作为swift代码执行替换饿了默认的类型, 基于SBFrame语言类型.
现在告诉options变量翻译JIT代码作为ID型的a(也就是说, po取代了p).

(lldb) script options.SetCoerceResultToId()

SetCoerceResultToId带了一个可选的Boolean, 这个Boolean值决定了是否将它翻译为id型. 默认情况下, 这里设置的是True.
概括一下你再这里做的事情: 你设置了选项用Python API来解析这个表达式替代了通过表达式命令传给我们的选项.例如, 截至目前你已经声明的SBExpressionOptions相当与下面的表达式命令中的options:

expression -lswift -O -- your_expression_here

接下来, 仅仅使用表达式命令创建一个ASwiftClass实例方法. 如果这是可行的, 你将会在EvaluateExpression命令中尝试同样的命令. 在LLDB中输入下面的内容:

(lldb) e -lswift -O -- ASwiftClass()

你将会得到输出内容将会是一个有点难看的错误...

error: <EXPR>:3:1: error: use of unresolved identifier 'ASwiftClass'
ASwiftClass()
^~~~~~~~~~~

欧耶, 你需要导入Allocator模块来确保swift可以在调试器中很好的运行.
在LLDB中:

(lldb) e -lswift -- import Allocator
注意:这是一个许多LLDB用户抱怨的问题: LLDB不能正确的执行应该能够执行的代码. 添加这个导入逻辑将会将会修改LLDB的swift ** expression prefix**, 这个前缀是在执行你的JIT代码之前正确引用了的基本的头文件的集合.
当你停止在非swift调试环境的时候LLDB不能看见JIT代码中的`ASwiftClass`. 这就意味着你需要附加属于`Allocator`模块的表达式前缀的头部. 这里有一个来自LLDB的作者的关于这个问题的非常好的解释:[http://stackoverflow.com/questions/19339493/why-cant-lldb-evaluate-this-expression](http://stackoverflow.com/questions/19339493/why-cant-lldb-evaluate-this-expression).

再次执行前一个命令. 按两次上箭头键然后回车:

(lldb) e -lswift -O -- ASwiftClass()

你将会得到一个 ASwiftClass()实例的引用.
现在你知道了这是可行的, 使用EvaluateExpression方法这一次将options作为第二个参数然后将输出赋值给变量b, 像下面这样:

(lldb) script b = lldb.target.EvaluateExpression('ASwiftClass()',options)

如果一切顺利的话, Python变量b中将会存储一个SBValue的引用.

注意: 需要指出的是SBValue的一些属性在Swift中不能很好的运行. 例如, 使用SBValue的`deref`或者`address_of`属性解引用一个swift对象将不能恰当的工作. 你可以通过指明这个指针是一个SwiftObject强制将这个指针指向一个Objective-C引用, 然后一切就都可以照常工作了. 就像我说的那样, 当你尝试在swift中追逐的时候他们让你为它做事!
通过名字引用SBValue中的变量

通过GetChildAtIndexSBValue中引用子SBValues来查看内存中的对象是一个相当无聊的方式. 如果这个类的作者在eyeColor前添加了一个属性那么当你爬去这个SBValue的时候的总的偏移逻辑是什么呢?
幸运的是, SBValue还有另外一个方法让你通过名字替代偏移来引用实例变量: GetValueForExpressionPath.
跳回到LLDB中然后输入下面内容:

(lldb) script print b.GetValueForExpressionPath('.firstName')

如果你想的话你可以继续练习进入下面的child的自己的结构体:

(lldb) script
b.GetValueForExpressionPath('.firstName._core._baseAddress._rawValue').va
lue

我如何或者那个疯狂的_core._baseAddress._rawValue部分呢?如果你没有子SBValue的名字的线索, 你所需要做的就是使用GetIndexOfChildAPI获取child, 然后使用那个子SBValue上的name属性.
例如, 如果我不知道在bSBValue中找到的UIColor属性的名字, 我可能做下面的事情:

图片.png
lldb.value

最后一件你可以做的很酷的事情是创建一个包含SBValue属性的Python引用作为Python对象的属性(等一下...什么?).将这想象成一个你可以使用Python属性来替代替代字符串来引用变量的对象.
回到控制台中, 从你的bSBValue中创建一个新的value对象:

(lldb) script c = lldb.value(b)

这将会创建一个value类型的特殊Python对象. 现在你可以像使用一个普通的对象一样引用他的实例变量!
在LLDB中输入下面的内容:

(lldb) script print c.firstName

当然, 它自己的所有的child属性你也可以用. 输入下面的内容:

(lldb) script print c.firstName._core._countAndFlags

你将会得到:

_countAndFlags = 5

你同样可以将child对象解析到一个SBValue以便你可以查询它或者将它应用到一个for循环上, 像这样:

(lldb) script print c.firstName._core._countAndFlags.sbvalue.signed

这将会在控制台中输出Python int 5.
再说一次, 如果你不知道child SBValue的名字, 使用GetChildAtIndexAPI获取child然后从name属性上获取它的名字.

注意: 尽管`lldb.value `类很强大, 但是这里也有一个代价. 它创建和访问属性耗费的性能相当大. 如果你在剖析一个很大的数组, 使用这个类将会显著的降低运行速度. 练习使用这个类然后找到速度和性能的平衡点.
我们为什么要学习这些?

哇噻!... 这一章的内容知识点多么稠密啊?幸运的是你完整的看了下来. 你可以使用你自定义命令中的options动态的生成你的JIT脚本代码. 从你JIT代码的返回值中, 你可以书写基于 EvaluateExpressionAPI解析后返回的SBValue的自定义逻辑脚本 .
这可以为你解锁一些惊喜的脚本. 在任何你可以用LLDB附加的进程中, 你可以返回你自己的自定义脚本然后处理你的Python脚本返回的的自定义值. 这样不用处理签名问题或者加载frameworks的问题或者其他类似的问题.
这一部分剩下的章节将会集中与创作一些创造性的脚本以及这些脚本如何让你的调试(或者逆向工程)生活变得更加简单.
理论时间已经结束了. 是时候做一些有趣的事情了!

上一篇下一篇

猜你喜欢

热点阅读