《Chrome V8原理講解》第十六篇 運行時輔助類,詳解加載與調用過程
前言
本系列的前十三篇文章,講解了V8執行Javascript時最基礎的工作流程和原理,包括詞法分析、語法分析、字節碼生成、Builtins方法、ignition執行單元,等等,達到了從零做起,入門學習的目的。
接下來的文章將以問題為導向講解V8源碼,例如:以閉包技術、或垃圾回收(GC)為專題講解V8中的相關源碼。V8代碼過于龐大,以問題為導向可以使得學習主題更加明確、效果更好。同時,我爭取做到每篇文章是一個獨立的知識點,方便大家閱讀。
讀者可以把想學的內容在文末評論區留言,我匯總后出專題文章。
一、摘要
運行時輔助類(Runtime)在Javascript執行期提供眾多輔助功能,如屬性訪問,新建對象、正則表達式等。上篇文章對“Runtime是什么?怎么用了?”做了詳細介紹,本文將重點說明“他是怎么來的?他存儲在哪?”,也就是Runtime在V8中的初始化過程,初始化完后的存儲位置,以及在BytecodeHandler中的調用細節。
二、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 heap21. logger_->SetUp(this);22. { // NOLINT23. 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包括了很多重要組件的初始化工作,例如Interpreter、compiler_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類負責管理的初始化,這其中包括了我們多次提到的Builtins。AddRuntimeFunctions(&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_ENTRY9. };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(...) +18. BUILTIN_LIST_C(COUNT_C_BUILTIN);9. #undef COUNT_C_BUILTIN10. 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(...) +119. STATS_COUNTER_NATIVE_CODE_LIST(SC);20. #undef SC21. //..............省略很多............................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_數組中,這個數組的類型Address是using Address = uintptr_t;,typedef unsigned __int64 uintptr_t;。

圖2中給出了三個關鍵信息,Add()方法的調用位置,對應的函數調用堆棧,以及展示了部分ref_addr_的內容。
總結,ExternalReferenceTable最重要的成員是ref_addr_,它本質是一個指針數組,Rumtime函數保存在下標430開始的成員中,調用時用下標值索引函數地址。
三、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()無關。
好了,今天到這里,下次見。
懇請讀者批評指正、提出寶貴意見