《Chrome V8原理讲解》第十六篇 运行时辅助类,详解加载与调用过程

robots

 

前言

本系列的前十三篇文章,讲解了V8执行Javascript时最基础的工作流程和原理,包括词法分析、语法分析、字节码生成、Builtins方法、ignition执行单元,等等,达到了从零做起,入门学习的目的。
接下来的文章将以问题为导向讲解V8源码,例如:以闭包技术、或垃圾回收(GC)为专题讲解V8中的相关源码。V8代码过于庞大,以问题为导向可以使得学习主题更加明确、效果更好。同时,我争取做到每篇文章是一个独立的知识点,方便大家阅读。
读者可以把想学的内容在文末评论区留言,我汇总后出专题文章。

 

1 摘要

运行时辅助类(Runtime)在Javascript执行期提供众多辅助功能,如属性访问,新建对象、正则表达式等。上篇文章对“Runtime是什么?怎么用了?”做了详细介绍,本文将重点说明“他是怎么来的?他存储在哪?”,也就是Runtime在V8中的初始化过程,初始化完后的存储位置,以及在BytecodeHandler中的调用细节。

 

2 Runtime初始过程

Runtime属于V8的基础组件,在创建Isolate时完成初始化,下面是初始化阶段的源码:

1.   bool Isolate::Init(ReadOnlyDeserializer* read_only_deserializer,
2.                      StartupDeserializer* startup_deserializer) {
3.     TRACE_ISOLATE(init);
4.     const bool create_heap_objects = (read_only_deserializer == nullptr);
5.     // We either have both or neither.
6.     DCHECK_EQ(create_heap_objects, startup_deserializer == nullptr);
7.     base::ElapsedTimer timer;
8.    //省略很多...............................
9.      handle_scope_implementer_ = new HandleScopeImplementer(this);
10.      load_stub_cache_ = new StubCache(this);
11.      store_stub_cache_ = new StubCache(this);
12.      materialized_object_store_ = new MaterializedObjectStore(this);
13.      regexp_stack_ = new RegExpStack();
14.      regexp_stack_->isolate_ = this;
15.      date_cache_ = new DateCache();
16.      heap_profiler_ = new HeapProfiler(heap());
17.      interpreter_ = new interpreter::Interpreter(this);
18.      compiler_dispatcher_ =
19.          new CompilerDispatcher(this, V8::GetCurrentPlatform(), FLAG_stack_size);
20.      // Enable logging before setting up the heap
21.      logger_->SetUp(this);
22.      {  // NOLINT
23.        ExecutionAccess lock(this);
24.        stack_guard()->InitThread(lock);
25.      }
26.      // SetUp the object heap.
27.      DCHECK(!heap_.HasBeenSetUp());
28.      heap_.SetUp();
29.      ReadOnlyHeap::SetUp(this, read_only_deserializer);
30.      heap_.SetUpSpaces();
31.      isolate_data_.external_reference_table()->Init(this);
32.      //省略很多...................
33.    }

上述代码是isolate的初始化入口,行10~20包括了很多重要组件的初始化工作,例如Interpretercompiler_dispatcher等等,后续文章都会讲到。Runtime的初始化由external负责,代码31行Init()方法中完成相关工作,源码如下:

1.  void ExternalReferenceTable::Init(Isolate* isolate) {
2.    int index = 0;
3.    // kNullAddress is preserved through serialization/deserialization.
4.    Add(kNullAddress, &index);
5.    AddReferences(isolate, &index);
6.    AddBuiltins(&index);
7.    AddRuntimeFunctions(&index);
8.    AddIsolateAddresses(isolate, &index);
9.    AddAccessors(&index);
10.    AddStubCache(isolate, &index);
11.    AddNativeCodeStatsCounters(isolate, &index);
12.    is_initialized_ = static_cast<uint32_t>(true);
13.    CHECK_EQ(kSize, index);
14.  }

代码4行~11行是由external类负责管理的初始化,这其中包括了我们多次提到的BuiltinsAddRuntimeFunctions(&index)是Runtime的初始化函数,代码如下:

1.  void ExternalReferenceTable::AddRuntimeFunctions(int* index) {
2.    CHECK_EQ(kSpecialReferenceCount + kExternalReferenceCount +
3.                 kBuiltinsReferenceCount,
4.             *index);
5.    static constexpr Runtime::FunctionId runtime_functions[] = {
6.  #define RUNTIME_ENTRY(name, ...) Runtime::k##name,
7.        FOR_EACH_INTRINSIC(RUNTIME_ENTRY)
8.  #undef RUNTIME_ENTRY
9.    };
10.    for (Runtime::FunctionId fId : runtime_functions) {
11.      Add(ExternalReference::Create(fId).address(), index);
12.    }
13.    CHECK_EQ(kSpecialReferenceCount + kExternalReferenceCount +
14.                 kBuiltinsReferenceCount + kRuntimeReferenceCount,
15.             *index);
16.  }

它只有一个参数index,在本文所用的V8版本中它的值是430,代表下标,ExternalReferenceTable是表结构,它的核心成员是地址指针数组ref_addr_,index代表它的下标,表示Runtime函数在ExternalReferenceTable成员变量ref_addr_中的位置,本文所用V8中有468个Runtime方法,初始化完成后存储在ref_addr_的430~430+468-1这个区间内。ExternalReferenceTable表结构非常简单,稍后给出。

代码5行定义了Runtime的枚举变量,其中涉及的宏模板请自行展开,图1给出部分枚举变量。代码11行通过for循环添加函数,Create()方法根据Runtime Id创建表项,最终添加到ExternalReferenceTable表中,代码如下:

1.  ExternalReference ExternalReference::Create(Runtime::FunctionId id) {
2.    return Create(Runtime::FunctionForId(id));
3.  }
4.  //分隔线........................
5.  const Runtime::Function* Runtime::FunctionForId(Runtime::FunctionId id) {
6.    return &(kIntrinsicFunctions[static_cast<int>(id)]);
7.  }
8.  //分隔线.......................
9.  ExternalReference ExternalReference::Create(const Runtime::Function* f) {
10.    return ExternalReference(
11.        Redirect(f->entry, BuiltinCallTypeForResultSize(f->result_size)));
12.  }

上述代码是三个函数,依次调用。第一个函数Create(Runtime::FunctionId id)的参数是图1中的枚举值;第二个函数FunctionForId(Runtime::FunctionId id)的返回值是kIntrinsicFunctions类型的数据,该数据是以下几个宏代码的展开结果。

#define FUNCTION_ADDR(f) (reinterpret_cast<v8::internal::Address>(f))
#define F(name, number_of_args, result_size)                                  \
  {                                                                           \
    Runtime::k##name, Runtime::RUNTIME, #name, FUNCTION_ADDR(Runtime_##name), \
        number_of_args, result_size                                           \
  }                                                                           \
  ,


#define I(name, number_of_args, result_size)                       \
  {                                                                \
    Runtime::kInline##name, Runtime::INLINE, "_" #name,            \
        FUNCTION_ADDR(Runtime_##name), number_of_args, result_size \
  }                                                                \
  ,

static const Runtime::Function kIntrinsicFunctions[] = {
    FOR_EACH_INTRINSIC(F) FOR_EACH_INLINE_INTRINSIC(I)};

#undef I
#undef F

kIntrinsicFunctions数据是什么类型?它是数组,每个成员又是函数名,参数个数,返回值个数组成的另一个数组。上一篇文章中,我定义的MyRuntime方法,格式是:F(MyRuntime,1,1),正好和这个数据格式对应,下面是kIntrinsicFunctions展开的样例:

kIntrinsicFunctions []={
  //.....................
  {                                                                           
    Runtime::kDebugPrint, Runtime::RUNTIME, "DebugPrint", (reinterpret_cast<v8::internal::Address>(Runtime_DebugPrint)), 
        1, 1 
  },
  //.....................

kIntrinsicFunctions是一个二维数组,上述代码展示了其中的一组数据。下面是void ExternalReferenceTable::AddRuntimeFunctions(int* index)方法中11行Add()方法的源码:

void ExternalReferenceTable::Add(Address address, int* index) {
  ref_addr_[(*index)++] = address;
}

index每次添加后会增1,ref_addr_是什么呢?它是ExternalReferenceTable的成员变量,地址指针数组,下面是ExternalReferenceTable源码:

1.  class ExternalReferenceTable {
2.   public:
3.    static constexpr int kSpecialReferenceCount = 1;
4.    static constexpr int kExternalReferenceCount =
5.        ExternalReference::kExternalReferenceCount;
6.    static constexpr int kBuiltinsReferenceCount =
7.  #define COUNT_C_BUILTIN(...) +1
8.        BUILTIN_LIST_C(COUNT_C_BUILTIN);
9.  #undef COUNT_C_BUILTIN
10.     static constexpr int kRuntimeReferenceCount =
11.         Runtime::kNumFunctions -
12.         Runtime::kNumInlineFunctions;  // Don't count dupe kInline... functions.
13.     static constexpr int kIsolateAddressReferenceCount = kIsolateAddressCount;
14.     static constexpr int kAccessorReferenceCount =
15.         Accessors::kAccessorInfoCount + Accessors::kAccessorSetterCount;
16.     static constexpr int kStubCacheReferenceCount = 12;
17.     static constexpr int kStatsCountersReferenceCount =
18.   #define SC(...) +1
19.         STATS_COUNTER_NATIVE_CODE_LIST(SC);
20.   #undef SC
21.   //..............省略很多............................
22.     ExternalReferenceTable() = default;
23.     void Init(Isolate* isolate);
24.    private:
25.     void Add(Address address, int* index);
26.     void AddReferences(Isolate* isolate, int* index);
27.     void AddBuiltins(int* index);
28.     void AddRuntimeFunctions(int* index);
29.     void AddIsolateAddresses(Isolate* isolate, int* index);
30.     void AddAccessors(int* index);
31.     void AddStubCache(Isolate* isolate, int* index);
32.     Address GetStatsCounterAddress(StatsCounter* counter);
33.     void AddNativeCodeStatsCounters(Isolate* isolate, int* index);
34.     STATIC_ASSERT(sizeof(Address) == kEntrySize);
35.     Address ref_addr_[kSize];
36.     static const char* const ref_name_[kSize];
37.     uint32_t is_initialized_ = 0;
38.     uint32_t dummy_stats_counter_ = 0;
39.     DISALLOW_COPY_AND_ASSIGN(ExternalReferenceTable);
40.   };

代码7行给出了Builtin统计时使用的宏模板,代码行25~35说明了ExternalReferenceTable负责初始化和管理哪些数据。初始化后的数据,也就是函数地址,保存在代码35行的ref_addr_数组中,这个数组的类型Addressusing Address = uintptr_t;typedef unsigned __int64 uintptr_t;

图2中给出了三个关键信息,Add()方法的调用位置,对应的函数调用堆栈,以及展示了部分ref_addr_的内容。
总结,ExternalReferenceTable最重要的成员是ref_addr_,它本质是一个指针数组,Rumtime函数保存在下标430开始的成员中,调用时用下标值索引函数地址。

 

3 Runtime调用

CallRuntime()CallRuntimeWithCEntry()负责调用Runtime功能,上篇文章给出了调用样例并做了实验,本文不再赘述。我们以CallRuntime()为例讲解,源码如下:

1.   template <class... TArgs>
2.   TNode<Object> CallRuntime(Runtime::FunctionId function,
3.                             SloppyTNode<Object> context, TArgs... args) {
4.     return CallRuntimeImpl(function, context,
5.                            {implicit_cast<SloppyTNode<Object>>(args)...});
6.   }
7.  //分隔线.........................
8.   TNode<Object> CodeAssembler::CallRuntimeImpl(
9.       Runtime::FunctionId function, TNode<Object> context,
10.        std::initializer_list<TNode<Object>> args) {
11.      int result_size = Runtime::FunctionForId(function)->result_size;
12.      TNode<Code> centry =
13.          HeapConstant(CodeFactory::RuntimeCEntry(isolate(), result_size));
14.      return CallRuntimeWithCEntryImpl(function, centry, context, args);
15.    }
16.  //分隔线.........................
17.    TNode<Type> HeapConstant(Handle<Type> object) {
18.        return UncheckedCast<Type>(UntypedHeapConstant(object));
19.      }
20.  //分隔线.........................
21.  TNode<Object> CodeAssembler::CallRuntimeWithCEntryImpl(
22.        Runtime::FunctionId function, TNode<Code> centry, TNode<Object> context,
23.        std::initializer_list<TNode<Object>> args) {
24.      constexpr size_t kMaxNumArgs = 6;
25.      DCHECK_GE(kMaxNumArgs, args.size());
26.      int argc = static_cast<int>(args.size());
27         auto call_descriptor = Linkage::GetRuntimeCallDescriptor(zone(), function, argc, Operator::kNoProperties,
                      Runtime::MayAllocate(function) ? CallDescriptor::kNoFlags
                                     : CallDescriptor::kNoAllocate);
28.      for (auto arg : args) inputs.Add(arg);
29.      inputs.Add(ref);
30.      inputs.Add(arity);
31.      inputs.Add(context);
32.      CallPrologue();
33.      Node* return_value =
34.          raw_assembler()->CallN(call_descriptor, inputs.size(), inputs.data());
35.      HandleException(return_value);
36.      CallEpilogue();
37.      return UncheckedCast<Object>(return_value);
38.    }

代码2行,CallRuntime()声明中,可以看到它有三个参数,第一个是FunctionId枚举类型,前面提到过;第二个参数是context,第三参数args是传给Runtime函数的参数列表。CallRuntime()调用CallRuntimeImpl(),在CallRuntimeImpl()内部读取堆中的常量数据(HeapConstant),代码18行,该数据中保存了函数与下标的对应关系,用FunctionId在该表中查到对应的函数地址,代码27行建立调用描述符(参见之前的文章),最终在代码34行调用Runtime函数。
上述代码是Builtin,不能在C++级别做Debug,无法给出调用堆栈等调试信息。也许你会有疑问:为什么不用上篇文章中提到的RUNTIME_DebugPrint或是自定义Runtime功能做断点?答:我们现在讲的就是Runtime的调用过程,没办法使用Runtime调试自己:(((。
要调试也是有办法的,汇编调试,汇编调试超出了V8的学习范围,不在此讲解了,对此感兴趣的朋友,评论区私信我。
最后,纠正《第十五篇》中的一个错误,我曾写到:“context是传给MyRuntime()的第一个参数,这是格式要求,注意:它不计在参数的数量中! 通过下面的测试代码,对MyRuntime做测试:”。 正确的解释是:context不是MyRuntime()的第一个参数,它是CallRuntime()的第二参数,与MyRuntime()无关。
好了,今天到这里,下次见。

恳请读者批评指正、提出宝贵意见
微信:qq9123013 备注:v8交流 邮箱:v8blink@outlook.com

(完)