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

VSole2021-10-29 16:21:00

前言

本系列的前十三篇文章,讲解了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()无关。

好了,今天到这里,下次见。

恳请读者批评指正、提出宝贵意见

函数调用runtime
本作品采用《CC 协议》,转载必须注明作者和本文链接
今年初,Akamai 的研究人员发现了一个新型恶意僵尸网络,它以 Realtek SDK、华为路由器和 Hadoop YARN 服务器为目标,将设备引入到 DDoS 群中,有可能进行大规模攻击。这个新型僵尸网络是研究人员在自己的 HTTP 和 SSH 蜜罐上发现的,该僵尸网络利用了 CVE-2014-8361 和 CVE-2017-17215 等漏洞。
常规调试下watchpoint功能的受限及trace的低效是由于我们是使用软件方式在用户态进行操作,受到了CPU及操作系统的限制。但QEMU主要关注于仿真,对于安全分析来说并不友好。原因在于这个程序只是在控制台打印了HelloWorld,并没有涉及到JNI相关操作。Qiling的这种做法,以最小的成本保证了对各类各个版本的系统最大的适配性,并且也保证了程序运行状态与真实环境差异较小。
当线程从等待状态苏醒后,会自动检测自己得APC队列中是否存在APC过程。所以只需要将目标进程的线程的APC队列里面添加APC过程,当然为了提高命中率可以向进程的所有线程中添加APC过程。然后促使线程从休眠中恢复就可以实现APC注入。往线程APC队列添加APC,系统会产生一个软中断。第二个参数表示插入APC的线程句柄,要求线程句柄必须包含THREAD_SET_CONTEXT访问权限。第三个参数表示传递给执行函数的参数。如果直接传入shellcode不设置第三个函数,可以直接执行shellcode。
反射式DLL注入实现
2022-05-13 15:59:21
反射式dll注入与常规dll注入类似,而不同的地方在于反射式dll注入技术自己实现了一个reflective loader()函数来代替LoadLibaryA()函数去加载dll,示意图如下图所示。蓝色的线表示与用常规dll注入相同的步骤,红框中的是reflective loader()函数行为,也是下面重点描述的地方。
欺骗防御新方法:rMTD
2021-09-21 22:41:45
如果运行的操作系统和应用中存在漏洞,那攻击者大概率会找到一个利用它的方法。唯一确定的解决隐患方式就是从程序库中修复问题。但是,在安全补丁发布前,系统依然有被攻陷的风险。许多人不得不接受这种情况。 不过,事情可能出现了转机:轮换移动目标防御技术(rotational Moving Target Defense, rMTD)。
本系列将以官网资料为基础主要通过动态跟踪来解析DynamoRIO的源代码。因为如果不结合实例只是将各函数的作用写出来,实在无法很好的说明问题,我们将以代码覆盖工具drcov为例,分析DynamoRIO的执行流程。
前置知识分析Transformer接口及其实现类。transform()传入对象,进行反射调用。构造调用链调用链构造原则:找调用关系要找不同名的方法,如果找到同名,再通过find usages得到的还是一样的结果。找到InvokerTransformer类中的transform(),右键,点 Find Usages,找函数调用关系,最好找不同名的方法,调用了transform()。因为transform()调用transform()不能换到别的方法里,没有意义。如果有一个类的readObject()调用了get(),那我们就可能找到了调用链。最终选择TransformedMap这个类,因为TransformedMap类中有好几处都调用了transform()。
Thinkphp是一个国内轻量级的开发框架,采用php+apache,在更新迭代中,thinkphp也经常爆出各种漏洞,thinkphp一般有thinkphp2、thinkphp3、thinkphp5、thinkphp6版本,前两个版本已经停止更新
通过查阅相关资料得知虚幻引擎是通过'UNetDriver'进行网络交互的,而'UNetDriver'是在'UWorld'下,那么我们就需要对游戏先进行sdk dump拿到实例化对象。
前言本系列的前十三篇文章,讲解了V8执行Javascript时最基础的工作流程和原理,包括词法分析、语法分析、字节码生成、Builtins方法、ignition执行单元,等等,达到了从零做起,入门学习的目的。接下来的文章将以问题为导向讲解V8源码,例如:以闭包技术、或垃圾回收为专题讲解V8中的相关源码。V8代码过于庞大,以问题为导向可以使得学习主题更加明确、效果更好。
VSole
网络安全专家