zoukankan      html  css  js  c++  java
  • V8源码边缘试探-黑魔法指针偏移

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

      众多周知,在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地址也是唯一的。

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

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

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

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

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

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

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

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

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

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

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

    ------------------------------------------------------------------------------------------------分割线-------------------------------------------------------------------------------------------

      虽然解释的差不多了,但是还是有必要做一个补充,就是关于一个类是否为null值的判断。

      正推过去很简单,指定地址的值就是null,如果反推的话,那么想想也很简单,判断当前类的地址是否与指定地址相等。但是在源码里,这个过程可以说是相当的恶心了……

      这里简单的过一遍,首先是测试代码:

    auto t = Null(env->isolate());
    t->IsNull();

      每一个生成的null虽然是个废物,但是爸爸很厉害,父类Value有一个方法IsNull专门检测当前类是否是null值。

      这个方法非常简单:

    bool Value::IsNull() const {
    #ifdef V8_ENABLE_CHECKS
      return FullIsNull();
    #else
      return QuickIsNull();
    #endif
    }

      根据情况有两种检测,一种快速的,一种完全体的。默认都是走完全检测分支,里面会同时调用快速检测。

      方法源码如下:

    bool Value::FullIsNull() const {
        // 通过这个可以获取到当前的isolate实例
        i::Handle<i::Object> object = Utils::OpenHandle(this);
        bool result = false;
        // 判断object是否为空值
        if (!object->IsSmi()) {
            // 内部方法
            result = object->IsNull(i::HeapObject::cast(*object)->GetIsolate());
        }
        // 调用快速检测与返回结果进行比对
        DCHECK_EQ(result, QuickIsNull());
        return result;
    }

      那个内部方法,就是完全体的核心,看似简单,实则跟厕所里的石头一样,又臭又硬。因为从这里开始,就要进入宏的地狱了。

      因为调试模式对于宏的跳转十分不友好,所以只能一个一个的把宏复制到本地,然后进行拼接,看看最后出来的是什么。这里仅仅给出一系列的截图,看看什么是宏的地狱:

      这两步,还原了object->IsNull(Isolate* isolate)究竟是个什么东西,整理后如下:

    bool Object::IsNull(Isolate* isolate) const {     
        return this == isolate->heap()->null_value();          
    }

      看起来很简单,这里的null_value又是一个坑,如下:

      这三个宏定义了isolate->heap()->null_value()是个什么东西,整理后如下:

    Oddball* Heap::null_value() { return Oddball::cast(roots_[kNullValueRootIndex]); }

      你以为这就完了???nonono,这个Oddball::cast(Object* object)又要搞事,如下:

      转换成人话如下:

    Oddball* Oddball::cast(Object* object) {              
        SLOW_DCHECK(object->IsOddball());             
        return reinterpret_cast<type*>(object);       
    }   

      发现没,SLOW_DCHECK,大写+下划线分割,又是一个宏,真的是无穷无尽,不过这个宏只是检测表达式是否为真。

      这个Oddball是继承于HeapObject,而HeapObject继承于Object,这里只是简单判断当前类是否来源于Object,在上面生成null值的最后转换有这么一行代码:

    return reinterpret_cast<internal::Object**>(addr + index * kApiPointerSize);

      因此如果是null值,其内置类型必然为Object。

      抛去一切上面无关的因素,最终判断条件其实就是那一行代码:Heap::null_value() { Oddball::cast(roots_[kNullValueRootIndex]); }

      而这个roots_定义也是很魔性,来源于heap.h,简单的一行:

    Object* roots_[kRootListLength];

      这个length长达511,定义非常非常多的特殊值,初始化方式也是宏,这里仅仅调出null的定义:

      简单讲,roots_在V8引擎初始化时已经预存了所有特殊值的地址,这里直接取this的地址与root_中保存的null值地址进行比较,最后得出结果。

      因为宏调试很不直观,也很不方便,这里就不贴图了。

  • 相关阅读:
    C#中的String.Length获取中文字符串长度出错
    PHP+Jquery+Ajax实现checkbox多选删除功能
    WIndows下AppAche服务中调试php页面出现警告:Call to undefined function mysql_connect()
    简洁的SQL一条语句更新从属账号
    算法导论数论一般离散对数问题
    Poj 2115
    算法导论数论计算x^2=1(mod n) 在区间[1,n1]的解
    算法导论数论RSA公钥加密系统
    算法导论数论中国余数定理
    Poj 2891 中国剩余定理 非互素
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/9317297.html
Copyright © 2011-2022 走看看