likes
comments
collection
share

iOS平台使用LLDB调试JS逻辑之——对象属性

作者站长头像
站长
· 阅读数 9

更多精彩内容,欢迎关注作者微信公众号:码工笔记

一、Native、JS混合调试的问题

随着基于RN或小程序的跨端框架越来越流行,调试Native和JS之间的数据交互也成了移动端开发者绕不开的技能需求。

以JavaScriptCore为例,JSC框架把Native和JS的数据交互封装得比较好,两个世界要交互的时候,两侧各自通过一些magic方法,就能将各自的读写顺利传递到对岸。

到具体调试问题的时候,JS侧是用Safari的Inspector,Native侧则用Xcode里的lldb,虽然经常需要在Xcode和Safari的Inspector之间频繁切换,但大部分情况下,各取所需,也能满足需求。

但这里面其实有一个潜藏的尴尬点,就是JS的调试与Native的调试本质上是割裂的,其割裂不仅反映在工具链的不同,更重要的是反映在数据上。

调试JS侧的代码时,在Safari Inspector里面只能看到JS侧的对象及其调用栈;而调试Native代码时在LLDB中只能看到Native侧的数据和调用栈。

而在真正复杂的交互流程中,JS和Native两边的对象就分得没那么开了。 JS侧会将包装了native对象的JS对象传来传去做处理,native侧也会将包装了JS对象的native对象传来传去做处理,有些情况下,是需要在单步调试的过程中同时查看native和js侧两边的数据的。

另外,大家应该也注意到过,在LLDB将Native侧程序断点停住的时候Safari Inspector也会失去响应(因为Native进程进止的情况下,无法响应WebKit Inspector Protocol的调试消息),因此这种情况下Inspector侧无法进行任何有意义的debug操作。

我们知道JS是运行在虚拟机中的,在Safari Inspector中是不可能看到Native侧内存的,那反过来,能不能在LLDB中直接查看JS对象的值呢?

二、使用LLDB分析JS对象

这一节咱们就来看看,如何在LLDB中分析JS对象。

Butterfly

下面是一个简单的例程:

//创建一个JSContext
self.jsContext = [[JSContext alloc] init];

//在JS侧创建一个数组 x 并设置其属性
[self.jsContext evaluateScript:@"x = [2,3,4]; x.abc = 5; x.def = 6;"];

我们在 LLDB 中看看x是什么样子:

(lldb) p self.jsContext[@"x"]
(JSValue *) $1 = 0x000060000308f620  //对象x_native的地址

不出所料,是个JSValue *类型,它的具体内容如何看呢?能从LLDB中看到[2,3,4]和它的属性abcdef吗?

先看看这个地址里放了啥内容:

(lldb) x/2xg 0x000060000308f620  //对象x_native的地址
0x60000308f620: 0x0000000106094aa0 0x000000012a9af6e8

JSValue*是个OC对象,第一个8byte是isa,不管它。

第二个8 byte应该就是它的成员了。翻了一下JavaScriptCore的源码,JSValue只有一个类型为JSValueRef的成员变量m_value,所以这里 m_value == 0x000000012a9af6e8。

JSValueRef是个typedef:

typedef const struct OpaqueJSValue* JSValueRef;

搜了下源码,OpaqueJSValue *基本都被强转转成了JSCell*。JSCell 是个C++类,看看这个m_value里面放了啥:

(lldb) x/8xg 0x000000012a9af6e8  //JSCell
0x12a9af6e8: 0x010823050000a92f 0x000000012aba8028
0x12a9af6f8: 0x0000000000000000 0x000000012a9f9840
0x12a9af708: 0x0000000000000000 0x0000000000000000
0x12a9af718: 0x0000000000000000 0x0000000000000000

这是个C++对象,第一个8 byte是JSCell的一些成员变量,记录了其StructureID、cell类型等信息:

StructureID m_structureID;          //uint32_t
IndexingType m_indexingTypeAndMisc; //uint8_t
JSType m_type;                      //uint8_t
TypeInfo::InlineTypeFlags m_flags;  //uint8_t
CellState m_cellState;              //uint8_t

其中0x0000a92f就是对象的StuctureID。在JSC中,每个JS对象的prototype对应一个Structure,当一个对象的prototype类型发生变化时,其StructureID也会随之发生改变。

然后看看后面的第二个8 byte:

(lldb) x/8xg 0x000000012aba8028  //JSValue->m_value->ptr(JSCell *)
0x12aba8028: 0xfffe000000000002 0xfffe000000000003
0x12aba8038: 0xfffe000000000004 0x0000000000000000
0x12aba8048: 0x0000000000000000 0x0000000000000000
0x12aba8058: 0x0000000000000000 0x0000000000000000

这里就有点意思了,这里分明存了encode以后的[1,2,3,4],不过高位加了0xfffe,翻一下相关文档,可知,JSC将64位的数据做了encoding,具体如下(详见webkit源码中的JSCJSValue.h文件):

Pointer {  0000:PPPP:PPPP:PPPP
         / 0002:****:****:****
Double  {         ...
         \ FFFC:****:****:****
Integer {  FFFE:0000:IIII:IIII

False:     0x06
True:      0x07
Undefined: 0x0a
Null:      0x02

指针的前两个字节为0x0000;32位整数的前两个字节为0xFFFE;浮点数的前两个字节介于0x00020xFFFC之间。

对象x的数组成员找到了,那它的属性值放在哪里呢?

咱们把指针往前倒一倒看看,指针往前挪0x28看看:

(lldb) x/16xg 0x000000012aba8000
0x12aba8000: 0x0000000000000000 0x0000000000000000
0x12aba8010: 0xfffe000000000006 0xfffe000000000005
0x12aba8020: 0x0000000500000003 0xfffe000000000002
0x12aba8030: 0xfffe000000000003 0xfffe000000000004
0x12aba8040: 0x0000000000000000 0x0000000000000000

x的属性值5和6在这儿呢!

原来我们之前拿到的地址指向的并不是对象所占内存的起始地址,而是中间,其后面是数组元素,前面是属性值!这个结构在JSC里面就叫做Butterfly。对象指针指向的是蝴蝶(对象内存)的肚子,左翅膀是属性值,右翅膀是数组元素。

普通对象的存储

是不是所有JS对象都是这么存的呢?我们看一个简单的JS对象(非Array)的情况:

//在JS侧创建一个简单对象,并设置其属性
a = {'prop1':10, 'prop2':'value2'};

还是跟之前一样,一步步看看它的内存数据:

//查看a_native的地址
(lldb) p self.jsContext[@"a"]
(JSValue *) $0 = 0x00006000004f6e20

//看JSValue的具体数据,m_value = 0x0000000114540000
(lldb) x/8xg 0x00006000004f6e20
0x6000004f6e20: 0x00007fff86e74c38 0x0000000114540000
0x6000004f6e30: 0x0000600000a899b0 0x0000000000000000
0x6000004f6e40: 0x00007fff86e74c38 0x00000001130421c8
0x6000004f6e50: 0x0000600000a899b0 0x0000000000000000

//查看JSCell的数据
(lldb) x/8xg 0x0000000114540000
0x114540000: 0x0100180000000790 0x0000000000000000
0x114540010: 0xfffe00000000000a 0x0000000114421820
0x114540020: 0x0000000000000000 0x0000000000000000
0x114540030: 0x0000000000000000 0x0000000000000000

可以看到在 0x114540010 地址处存了 0xfffe00000000000a,也就是prop1的值(整数10)。

它后面跟着的地址 0x0000000114421820 应该就是prop2的值了吧?我们确认一下:

(lldb) x/8xg 0x0000000114421820
0x114421820: 0x0118020000002d76 0x0000000113057720
0x114421830: 0x0000000000000000 0x0000000000000000
0x114421840: 0x0000000000000000 0x0000000000000000
0x114421850: 0x0000000000000000 0x0000000000000000

(lldb) x/8xg 0x0000000113057720
0x113057720: 0x0000000600000002 0x0000000113057734
0x113057730: 0x756c6176c6a4dd1c 0x0000000000003265
0x113057740: 0x947dfbfa94fb8887 0x000000fbf83010f9
0x113057750: 0x0000001511f92f00 0x0000000000000000

(lldb) x/s 0x113057734
0x113057734: "value2"

果然在这里。

JSC公开类型与内部类型对应关系

下面总结一些JSC中常见的公开类型与内部类型的对应关系,方便后续查看:

JSContextRef == OpaqueJSContext* == JSGlobalObject* == JSGlobalContextRef

JSValueRef == OpaueJSValue* == JSCell*  

JSObjectRef == JSObject*

JSContextGroupRef == OpaqueJSContextGroup* == JSC::VM*

小结

本文介绍了使用LLDB分析JS对象的基本流程,同时介绍了Butterfly和普通对象数据的内存布局以及如何查找相应的属性值。至于如何分析Structure链,以及JS运行时调用栈等信息,且看下回。