连载《Chrome V8 原理讲解》第二篇 鸟瞰V8运行过程,形成大局观

阅读量347663

发布时间 : 2021-09-16 10:30:51

 

V8是chrome核心组件,重要程度不用多言。本系列文章,讲解V8源码,力求做到全面覆盖知识点、有理论高度,做到细致讲解代码、有实践依据。

 

本篇内容

本次是第二篇,主要内容是从宏观上概述V8的运行过程,包括:初始化、编译代码、运行、退出。面对V8这个庞大的系统工程,本文尽力为读者构建一个全面的大局观,从程序源码的角度揭示V8代码的主要脉络,达到快速入手的目的。本文想给读者的“大局观”是:知道V8是怎么运行的,了解V8运行过程中的几个重要中间阶段、重要中间结果、以及相应的重要数据结构。为此,本文从Javascript代码的两个主要阶段编译和执行的知识点出发,以V8源码中的重要数据结构抽象语法树(AST)和字节表(Bytecode)为抓手展开全面、宏观的讲解。

 

1 V8运行过程

以v8\samples\hello-world.c为例,这个例子我曾反复说过,它是运行V8功能的最小代码集合,只包含了V8的最重要最基本的功能,适合入门。例如,徒增学习难度的优化编译功能,在这个例子中就没出现,优化编译是V8最重要的部分,是提升性能的关键,但对初学者并没有用,所以说只有基础功能的hello-world最适合入门。

int main(int argc, char* argv[]) {
  // Initialize V8.
  v8::V8::InitializeICUDefaultLocation(argv[0]);
  v8::V8::InitializeExternalStartupData(argv[0]);
  std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
  v8::V8::InitializePlatform(platform.get());
  v8::V8::Initialize();

  // Create a new Isolate and make it the current one.
  v8::Isolate::CreateParams create_params;
  create_params.array_buffer_allocator =
      v8::ArrayBuffer::Allocator::NewDefaultAllocator();
  v8::Isolate* isolate = v8::Isolate::New(create_params);
  {
    v8::Isolate::Scope isolate_scope(isolate);

    // Create a stack-allocated handle scope.
    v8::HandleScope handle_scope(isolate);

    // Create a new context.
    v8::Local<v8::Context> context = v8::Context::New(isolate);

    // Enter the context for compiling and running the hello world script.
    v8::Context::Scope context_scope(context);

    {
      // Create a string containing the JavaScript source code.
      v8::Local<v8::String> source =
          v8::String::NewFromUtf8Literal(isolate, "'Hello' + ', World!'");

      // Compile the source code.
      v8::Local<v8::Script> script =
          v8::Script::Compile(context, source).ToLocalChecked();

      // Run the script to get the result.
      v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();
//...      
//省略部分代码
//...

上面这段代码是hello-world.cc中最重要的部分,V8的初始化是v8::V8::Initialize(),Isolate的创建v8::Isolate::New,编译v8::Script::Compile,执行script->Run。handle和context没有提及,因为对初学者不重要。图1说明了每个阶段的重要数据结构,读者可以利用VS2019的debug跟踪查看详细过程(跟踪方法见上一篇文章)。

图1中可以看到hello-world.cc程序的主要主体结构(工作流程),还有与之对应的方法和数据结构。V8代码量很大,通过重要数据结构和中间结果入手学习,可以很好地抓住V8的主线功能,把主线功能理解透彻之后,再学习旁支功能可达到事半功倍的效果。接下来,以图1中主要方法为主,结合主要数据结构进行单独讲解。

 

2 V8启动时的内存申请

InitReservation负责为V8申请内存,代码位置src\utils\allocation.cc。

// Reserve a region of twice the size so that there is an aligned address
// within it that's usable as the cage base.
VirtualMemory padded_reservation(params.page_allocator,
                               params.reservation_size * 2,
                               reinterpret_cast<void*>(hint));
Address address =
          VirtualMemoryCageStart(padded_reservation.address(), params);
...省略了很多行代码...
// Now free the padded reservation and immediately try to reserve an
// exact region at aligned address. We have to do this dancing because
// the reservation address requirement is more complex than just a
// certain alignment and not all operating systems support freeing parts
// of reserved address space regions.
padded_reservation.Free();
VirtualMemory reservation(params.page_allocator,
                          params.reservation_size,
                          reinterpret_cast<void*>(address));

申请内存时为了保证内存对齐,它的做法是先申请两倍的内存,然后从中找一个适合做内存对齐的地址,再把两倍内存释放,从刚找到的地址申请需要的4G大小。具体做法是:padded_reservation申请一个两倍大小的内存(8G),再利用padded_reservation.Free()释放,再用reservation申请的4G内存则是V8真正的内存。下面讲解V8管理内存的主要数据结构:VirtualMemoryCage。V8向操作系统申请4G内存,用于后续的所有工作,例如创新Isolate,等等。V8的内存方式采用的段页式,和操作系统(OS)的方法类似,但不像OS有多个段,V8只有一个段,但有很多页。VirtualMemeoryCage的定义在allocation.h中,我们对它的结构进行说明。

// +------------+-----------+-----------  ~~~  -+
// |     ...    |    ...    |   ...             |
// +------------+-----------+------------ ~~~  -+
// ^            ^           ^
// start        cage base   allocatable base
//
// <------------>           <------------------->
// base bias size              allocatable size
// <-------------------------------------------->
//             reservation size

“a VirtualMemory reservation”是V8源码中的叫法,reservation size是4G,也就是v8申请内存的总大小,start是这个内存的基址,cage base是v8用于管理的基址,可以先理解为cage base是页表位置,allocatable开始是v8可以分配的,用于创新isolate。VirtualMemoryCage中的成员reservation_负责指向这个4G内存。另一个重要的结构是ReservationParams,申请内存大小(4G),对齐方式,指针压缩等参数都在这个结构中定义。

 

3 Isolate

Isolate是一个完整的V8实例,有着完整的堆栈和Heap。V8是虚拟机,isolate才是运行javascript的宿主。一个Isolate是一个独立的运行环境, 包括但不限于堆管理器(heap)、垃圾回收器(gc)等。在一个时间,有且只有一个线程能在isolate中运行代码,也就是说同一时刻,只有一个线程能进入isolate,多个线程可以通过切换来共享同一个isolate。

/**
 * Isolate represents an isolated instance of the V8 engine.  V8 isolates have
 * completely separate states.  Objects from one isolate must not be used in
 * other isolates.  The embedder can create multiple isolates and use them in
 * parallel in multiple threads.  An isolate can be entered by at most one
 * thread at any given time.  The Locker/Unlocker API must be used to
 * synchronize.
 */
class V8_EXPORT Isolate {
 public:
  /**
   * Initial configuration parameters for a new Isolate.
   */
  struct V8_EXPORT CreateParams {
    CreateParams();
    ~CreateParams();

    /**
     * Allows the host application to provide the address of a function that is
     * notified each time code is added, moved or removed.
     */
    JitCodeEventHandler code_event_handler = nullptr;

    /**
     * ResourceConstraints to use for the new Isolate.
     */
    ResourceConstraints constraints;

    /**
     * Explicitly specify a startup snapshot blob. The embedder owns the blob.
     */
    StartupData* snapshot_blob = nullptr;

    /**
     * Enables the host application to provide a mechanism for recording
     * statistics counters.
     */
    CounterLookupCallback counter_lookup_callback = nullptr;
//.....
//省略多行
//.....

上面这段代码是isolate对外(export)提供的接口,方便其它程序的使用。 这个isolate可以理解为javascript的运行单元,多个线程也就是多个任务可以共享一个运行单元,这种共享类似操作系统中的调度机制,涉及到几个重要的概念:任务切换(亦称任务调度)、中断、上下文(context)的切换方法,由此我们引出V8中几个重要的概念。
a.Context:上下文,所有的JS代码都是在某个V8 Context中运行的。
b.Handle,一个指定JS对象的索引,它指向此JS对象在V8堆中的位置。
c.Handle Scope,包含很多handle的集合,用于对多个handle进行统一管理,当Scope被移出堆时,它所管理的handle集合也就被释放了。
其它的重要概念,本文不涉及,暂不深究。
Isolate还有一个对内的isolate数据结构,代码如下。它可以与对外接口进行无差别转换,在转换过程中,数据不会丢失。

class V8_EXPORT_PRIVATE Isolate final : private HiddenFactory {
  // These forward declarations are required to make the friend declarations in
  // PerIsolateThreadData work on some older versions of gcc.
  class ThreadDataTable;
  class EntryStackItem;

 public:
  Isolate(const Isolate&) = delete;
  Isolate& operator=(const Isolate&) = delete;

  using HandleScopeType = HandleScope;
  void* operator new(size_t) = delete;
  void operator delete(void*) = delete;

  // A thread has a PerIsolateThreadData instance for each isolate that it has
  // entered. That instance is allocated when the isolate is initially entered
  // and reused on subsequent entries.
  class PerIsolateThreadData {
   public:
    PerIsolateThreadData(Isolate* isolate, ThreadId thread_id)
        : isolate_(isolate),
          thread_id_(thread_id),
          stack_limit_(0),
          thread_state_(nullptr)
//省略很多代码...

这个isolate是V8内部使用的,不对外开放。这个结构贯穿了V8虚拟机的始终,是我们必须要掌握的,从入门的角度来讲,这个结构不是我们最先需要掌握的,我们只需要知道它,用到相关的成员再来查找,下文讲解的几个重要结构,其实都是i::isolate的成员。

 

3 编译

编译涉及到的概念包括:词法分析,用来生成token字;语法分析(parse),最后生成抽象语法树(AST)。下面介绍重要的数据结构

// A container for the inputs, configuration options, and outputs of parsing.
class V8_EXPORT_PRIVATE ParseInfo {
 public:
  ParseInfo(Isolate* isolate, const UnoptimizedCompileFlags flags,
            UnoptimizedCompileState* state);

  // Creates a new parse info based on parent top-level |outer_parse_info| for
  // function |literal|.
  static std::unique_ptr<ParseInfo> ForToplevelFunction(
      const UnoptimizedCompileFlags flags,
      UnoptimizedCompileState* compile_state, const FunctionLiteral* literal,
      const AstRawString* function_name);

  ~ParseInfo();

  template <typename IsolateT>
  EXPORT_TEMPLATE_DECLARE(V8_EXPORT_PRIVATE)
  Handle<Script> CreateScript(IsolateT* isolate, Handle<String> source,
                              MaybeHandle<FixedArray> maybe_wrapped_arguments,
                              ScriptOriginOptions origin_options,
                              NativesFlag natives = NOT_NATIVES_CODE);

  // Either returns the ast-value-factory associcated with this ParseInfo, or
  // creates and returns a new factory if none exists.
  AstValueFactory* GetOrCreateAstValueFactory();

这个结构的作用是接收javascript源码(U16格式),输出就是AST,在它输出AST过程中,会临时生成Token,AST生成后销毁,不做永久保存,生成token过程中还用到了缓存(cache)机制来提高效率。下面先来看一段JS的AST语法树,如图2。

图2左侧是js源码,右侧是通过VS2019的调试工具看到的AST树,借助d8工具也能打印AST树,如图3所示,一屏显示不全,只截取了部分。

下面是AST树的主要数据结构。

class FunctionLiteral final : public Expression {
 public:
  enum ParameterFlag : uint8_t {
    kNoDuplicateParameters,
    kHasDuplicateParameters
  };
  enum EagerCompileHint : uint8_t { kShouldEagerCompile, kShouldLazyCompile };

  // Empty handle means that the function does not have a shared name (i.e.
  // the name will be set dynamically after creation of the function closure).
  template <typename IsolateT>
  MaybeHandle<String> GetName(IsolateT* isolate) const {
    return raw_name_ ? raw_name_->AllocateFlat(isolate) : MaybeHandle<String>();
  }
  bool has_shared_name() const { return raw_name_ != nullptr; }
  const AstConsString* raw_name() const { return raw_name_; }
  void set_raw_name(const AstConsString* name) { raw_name_ = name; }
  DeclarationScope* scope() const { return scope_; }
  ZonePtrList<Statement>* body() { return &body_; }
  void set_function_token_position(int pos) { function_token_position_ = pos; }
  int function_token_position() const { return function_token_position_; }
  int start_position() const;
  int end_position() const;
  bool is_anonymous_expression() const {
    return syntax_kind() == FunctionSyntaxKind::kAnonymousExpression;
  }
  //省略很多代码... ...

Abstract Syntax Tree抽象语法树(AST)是精简版的解析树(parse tree),在编译过程中,解析树是包含javascript源码所有语法信息的树型表示结构,它是代码在编译阶段的等价表示。抽象语法树概念是相对于解析树而言,对解析树进行裁剪,去掉一些语法信息和一些不重要的细节,所以叫抽象语法树。
V8编译的第一个阶段是扫描(scanner)js源代码文本,把文本拆成一些单词,再传入分词器,经过一系列的类型识别,根据词的类型识别单词的含义,进而产生token序列,单词识别过程有一个预先定义好的识别器类型模板,如图4。

图4中只截取了部分,读者可根据文件名自行查阅。

 

4 代码执行

图5中给出来了执行Javascript代码的关键位置,从此处debug跟踪,将最终进入字节码(bytecode)的执行过程。

下面来看Exectuion这个数据结构,这个结构承载着运行过程前后的相关信息。

  class Execution final : public AllStatic {
 public:
  // Whether to report pending messages, or keep them pending on the isolate.
  enum class MessageHandling { kReport, kKeepPending };
  enum class Target { kCallable, kRunMicrotasks };

  // Call a function, the caller supplies a receiver and an array
  // of arguments.
  //
  // When the function called is not in strict mode, receiver is
  // converted to an object.
  //
  V8_EXPORT_PRIVATE V8_WARN_UNUSED_RESULT static MaybeHandle<Object> Call(
      Isolate* isolate, Handle<Object> callable, Handle<Object> receiver,
      int argc, Handle<Object> argv[]);
  //省略很多

从图5中标记的位置跟踪进入能看到执行的细节,目前我们还没提到AST如何生成字节码,但已基本梳理了v8从启动到运行的关建过程,和关键的数据结构。我们已经能看到Javascript源码对应的AST树是什么样子,它在执行期时的字节码又是什么样。

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

微信:qq9123013 备注:v8交流学习 邮箱:v8blink@outlook.com

本文由灰豆原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/253298

安全客 - 有思想的安全新媒体

分享到:微信
+13赞
收藏
灰豆
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66