欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

V8源码边缘试探-黑魔法指针偏移

程序员文章站 2022-07-10 22:03:03
这博客是越来越难写了,参考资料少,难度又高,看到什么写什么吧! 众多周知,在JavaScript中有几个基本类型,包括字符串、数字、布尔、null、undefined、Symbol,其中大部分都可以在我之前那篇博客(https://www.cnblogs.com/QH-Jimmy/p/9212923 ......

  这博客是越来越难写了,参考资料少,难度又高,看到什么写什么吧!

  众多周知,在JavaScript中有几个基本类型,包括字符串、数字、布尔、null、undefined、Symbol,其中大部分都可以在我之前那篇博客(https://www.cnblogs.com/QH-Jimmy/p/9212923.html)中找到,均继承于Primitive类。但是仔细看会发现少了两个,null和undefined呢?这一节,就来探索一下,V8引擎是如何处理null、undefined两种类型的。

  在没有看源码之前,我以为是这样的:

class Null : public Primitive {
public:
    // Type testing.
    bool IsNull() const { return true; }
    // ...
}

  然而实际上没有这么简单粗暴,V8对null、undefined(实际上还包括了true、false、空字符串)都做了特殊的处理。

  回到故事的起点,是我在研究LoadEnvironment函数的时候发现的。上一篇博客其实就是在讲这个方法,包装完函数名、函数体,最后一步就是配合函数参数来执行函数了,代码如下:

// Bootstrap internal loaders
Local<Value> bootstrapped_loaders;
if (!ExecuteBootstrapper(env, loaders_bootstrapper,
                        arraysize(loaders_bootstrapper_args),
                        loaders_bootstrapper_args,
                        &bootstrapped_loaders)) {
return;
}

  这里的参数分别为:

1、env => 当前V8引擎的环境变量,包含Isolate、context等。

2、loaders_bootstrapper => 函数体

3、arraysize(loaders_bootstrapper_args) => 参数长度,就是4

4、loaders_bootstrapper_args => 参数数组,包括process对象及3个C++内部方法

5、&bootstrapped_loaders => 一个局部变量指针

  参数是啥并不重要,进入方法,源码如下:

static bool ExecuteBootstrapper(Environment* env, Local<Function> bootstrapper,
                                int argc, Local<Value> argv[],
                                Local<Value>* out) {
  bool ret = bootstrapper->Call(
      env->context(), Null(env->isolate()), argc, argv).ToLocal(out);
  if (!ret) {
    env->async_hooks()->clear_async_id_stack();
  }

  return ret;
}

  看起来就像JS里面的call方法,其中函数参数包括context、null、形参数量、形参,当时看到Null觉得比较好奇,就仔细的看了一下实现。

 

  这个方法其实很简单,但是实现的方式非常有意思,源码如下:

Local<Primitive> Null(Isolate* isolate) {
    typedef internal::Object* S;
    typedef internal::Internals I;
    // 检测当前V8引擎实例是否存活
    I::CheckInitialized(isolate);
    // 核心方法
    S* slot = I::GetRoot(isolate, I::kNullValueRootIndex);
    // 类型强转 直接是Primitive类而不是继承
    return Local<Primitive>(reinterpret_cast<Primitive*>(slot));
}

  只有GetRoot是真正生成null值的地方,注意第二个参数 I::kNullValueRootIndex ,这是一个静态整形值,除去null还有其他几个,所有的类似值定义如下:

static const int kUndefinedValueRootIndex = 4;
static const int kTheHoleValueRootIndex = 5;
static const int kNullValueRootIndex = 6;
static const int kTrueValueRootIndex = 7;
static const int kFalseValueRootIndex = 8;
static const int kEmptyStringRootIndex = 9;

  上面的数字就是区分这几个类型的关键所在,继续进入GetRoot方法:

V8_INLINE static internal::Object** GetRoot(v8::Isolate* isolate,int index) {
    // 获取当前isolate地址并进行必要的空间指针偏移
    // static const int kIsolateRootsOffset = kExternalMemoryLimitOffset + kApiInt64Size + kApiInt64Size + kApiPointerSize + kApiPointerSize;
    uint8_t* addr = reinterpret_cast<uint8_t*>(isolate) + kIsolateRootsOffset;
    // 根据上面的数字以及当前操作系统指针大小进行偏移
    // const int kApiPointerSize = sizeof(void*);  // NOLINT
    return reinterpret_cast<internal::Object**>(addr + index * kApiPointerSize);
}

  这个方法就对应了标题,指针偏移。

  实际上根本不存在一个正规的null类来生成一个对应的对象,而只是把一个特定的地址当成一个null值。

  敢于用这个方法,是因为对于每一个V8引擎来说isolate对象是独一无二的,所以在当前引擎下,获取到的isolate地址也是唯一的。

  如果还不明白,我这个灵魂画手会让你明白,超级简单:

V8源码边缘试探-黑魔法指针偏移

  最后返回一个地址,这个地址就是null,强转成Local<Primitive>也只是为了垃圾回收与类型区分,实际上并不关心这个指针指向什么,因为null本身不存在任何方法可以调用,大多数情况下也只是用来做变量重置。

  就这样,只用了很小的空间便生成了一个null值,并且每一次获取都会返回同一个值。

 

  验证的话就很简单了,随意的在node启动代码里加一段:

auto test = Null(env->isolate());

  然后看局部变量的调试框,当前isolate的地址如下:

V8源码边缘试探-黑魔法指针偏移

  第一次指针偏移后,addr的地址为:

V8源码边缘试探-黑魔法指针偏移

  通过简单计算,这个差值是72(16进制的48),跟第一次偏移量大小一致,这里根本不关心指针指向什么东西,所以字符无效也没事。

  第二次偏移后,得到的null地址为:

V8源码边缘试探-黑魔法指针偏移

  通过计算得到差值为48(16进制的30),算一算,刚好是6*8。

  最后对这个地址进行强转,返回一个Local<Primitive>类型的null对象。