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值地址進行比較,最后得出結果。

  因為宏調試很不直觀,也很不方便,這里就不貼圖了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM