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

浏览器安全 3年前 (2021) admin
715 0 0
【技术分享】《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表结构非常简单,稍后给出。

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

代码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;。

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

图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()无关。
好了,今天到这里,下次见。
恳请读者批评指正、提出宝贵意见
微信:qq9123013 备注:v8交流 邮箱:
[email protected]
【技术分享】《Chrome V8原理讲解》第十六篇 运行时辅助类,详解加载与调用过程

- 结尾 -
精彩推荐
【技术分享】浅谈Sonicwall SonicOS的host头注入,防火墙绝对安全?
【技术分享】纸上得来终觉浅——Redis 个人总结
【技术分享】Kernel pwn CTF 入门 – 2
【技术分享】《Chrome V8原理讲解》第十六篇 运行时辅助类,详解加载与调用过程
戳“阅读原文”查看更多内容

原文始发于微信公众号(安全客):【技术分享】《Chrome V8原理讲解》第十六篇 运行时辅助类,详解加载与调用过程

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...