连载《Chrome V8 原理讲解》第八篇 解释器Ignition

阅读量491581

|评论2

发布时间 : 2021-09-30 10:00:20

 

1 摘要

本次是第八篇,讲解v8解释器Ignition的工作流程。Ignition是基于寄存器的解释器,本过通过分析Ignition重要源码和核心数据结构、讲解bytecode的加载和执行过程,详细阐述Ignition的工作流程。
本文内容的组织方式:讲解Ignition的先导知识—Builtin是什么、具体实现以及调试方法(章节2);Ignition工作流程、原理讲解、源码分析(章节3)。

关键字: bytecode handler(字节码处理程序),dispatch,Builtin,Ignition

 

2 Builtin

学习Ignition,绕不开Builtin,因为Ignition的大部分功能由Builtin实现。Builtin(built in function)是V8的内建功能,它是V8运行时可执行的代码块,实现Builtin功能的方式主要有:Javascript、C++、汇编、CodeStubAssembler四种方式。其中,CodeStubAssembler是一种平台无关(platform-independent)的抽象语言,由TurbFan编译生成。Builtin有很多种,以TF_BUILTIN举例说明,下面是它的宏定义模板:

#define TF_BUILTIN(Name, AssemblerBase)                                     \
  class Name##Assembler : public AssemblerBase {                            \
   public:                                                                  \
    using Descriptor = Builtin_##Name##_InterfaceDescriptor;                \
                                                                            \
    explicit Name##Assembler(compiler::CodeAssemblerState* state)           \
        : AssemblerBase(state) {}                                           \
    void Generate##Name##Impl();                                            \
                                                                            \
    template <class T>                                                      \
    TNode<T> Parameter(                                                     \
        Descriptor::ParameterIndices index,                                 \
        cppgc::SourceLocation loc = cppgc::SourceLocation::Current()) {     \
      return CodeAssembler::Parameter<T>(static_cast<int>(index), loc);     \
    }                                                                       \
                                                                            \
    template <class T>                                                      \
    TNode<T> UncheckedParameter(Descriptor::ParameterIndices index) {       \
      return CodeAssembler::UncheckedParameter<T>(static_cast<int>(index)); \
    }                                                                       \
  };                                                                        \
  void Builtins::Generate_##Name(compiler::CodeAssemblerState* state) {     \
    Name##Assembler assembler(state);                                       \
    state->SetInitialDebugInformation(#Name, __FILE__, __LINE__);           \
    if (Builtins::KindOf(Builtin::k##Name) == Builtins::TFJ) {              \
      assembler.PerformStackCheck(assembler.GetJSContextParameter());       \
    }                                                                       \
    assembler.Generate##Name##Impl();                                       \
  }                                                                         \
  void Name##Assembler::Generate##Name##Impl()

上述代码中,AssemblerBase是Builtin功能的父类,功能不同,其父类也不同,通过下面的代码举例说明:

TF_BUILTIN(CloneFastJSArrayFillingHoles, ArrayBuiltinsAssembler) {
  auto context = Parameter<Context>(Descriptor::kContext);
  auto array = Parameter<JSArray>(Descriptor::kSource);

  CSA_ASSERT(this,
             Word32Or(Word32BinaryNot(IsHoleyFastElementsKindForRead(
                          LoadElementsKind(array))),
                      Word32BinaryNot(IsNoElementsProtectorCellInvalid())));

  Return(CloneFastJSArray(context, array, base::nullopt,
                          HoleConversionMode::kConvertToUndefined));
}

上述代码是一个具体的Builtin功能实现,CloneFastJSArrayFillingHoles是Builtin功能的名字,ArrayBuiltinsAssembler是它的父类。名字不同,功能不同,其父类自然也不同。但是,所有Builtin均继承自同一个顶层父类CodeStubAssembler,代码如下:

class V8_EXPORT_PRIVATE CodeStubAssembler
    : public compiler::CodeAssembler,
      public TorqueGeneratedExportedMacrosAssembler {
 public:
  using ScopedExceptionHandler = compiler::ScopedExceptionHandler;

  template <typename T>
  using LazyNode = std::function<TNode<T>()>;

  explicit CodeStubAssembler(compiler::CodeAssemblerState* state);

  enum AllocationFlag : uint8_t {
    kNone = 0,
    kDoubleAlignment = 1,
    kPretenured = 1 << 1,
    kAllowLargeObjectAllocation = 1 << 2,
  };

  enum SlackTrackingMode { kWithSlackTracking, kNoSlackTracking };

  using AllocationFlags = base::Flags<AllocationFlag>;

  TNode<IntPtrT> ParameterToIntPtr(TNode<Smi> value) { return SmiUntag(value); }
  TNode<IntPtrT> ParameterToIntPtr(TNode<IntPtrT> value) { return value; }
  TNode<IntPtrT> ParameterToIntPtr(TNode<UintPtrT> value) {
    return Signed(value);
  }

  enum InitializationMode {
    kUninitialized,
    kInitializeToZero,
    kInitializeToNull
  };
//........................
//代码近4000行,以下部分省略......................
//........................

代码太多,请自行查阅。下面给出Builtin列表,它包含了所有的Builtin,是一个宏模板,里面又嵌套了不同子类型的Builtin宏模板。

#define BUILTIN_LIST(CPP, TFJ, TFC, TFS, TFH, BCH, ASM)  \
  BUILTIN_LIST_BASE(CPP, TFJ, TFC, TFS, TFH, ASM)        \
  BUILTIN_LIST_FROM_TORQUE(CPP, TFJ, TFC, TFS, TFH, ASM) \
  BUILTIN_LIST_INTL(CPP, TFJ, TFS)                       \
  BUILTIN_LIST_BYTECODE_HANDLERS(BCH)

Builtin的编写规则,本文不做介绍,想学习的读者可以留言联系我,或查阅官方文档。
下面讲debug跟踪Builtin功能的方法,分析Ignition工作流程时离不开debug调试。无论Builtin的实现方式是js亦或C++,都只能做汇编调试,因为Builtin的实现与V8分离,单独生成snapshot_blob.bin文件,保存在磁盘上,V8启动时将其进行反序列化,读取到内存中,这样做为了提升V8的启动速度。7.9版之前的V8,支持Builtin的C++调试,请读者自行分析,有问题可以联系我。
Ignition执行字节码的入口是InterpreterEntryTrampoline,它是一个Builtin,它的功能和具体实现稍后讲解,下面看如何debug跟踪它。

enum class Builtin : int32_t {
  kNoBuiltinId = -1,
#define DEF_ENUM(Name, ...) k##Name,
  BUILTIN_LIST(DEF_ENUM, DEF_ENUM, DEF_ENUM, DEF_ENUM, DEF_ENUM, DEF_ENUM,
               DEF_ENUM)
#undef DEF_ENUM
#define EXTRACT_NAME(Name, ...) k##Name,
  // Define kFirstBytecodeHandler,
  kFirstBytecodeHandler =
      FirstFromVarArgs(BUILTIN_LIST_BYTECODE_HANDLERS(EXTRACT_NAME) 0)
#undef EXTRACT_NAME
};

首先,看上面的Builtin类结构,每一个Builtin功能都有一个枚举编号,根据BUILTIN_LIST宏模板的定义顺序,可以计算出InterpreterEntryTrampoline的枚举编号,用这个编码做数组下标在图1中找到对应的数组成员,这个isolate->isolate_data_.builtins_成员是BUILTIN数组。

根据数组成员中存储的内存地址,进行汇编级调试。此外,另一个跟踪方法是从i::Excetuion::Call()方法进行跟踪,最终也是进入汇编代码,不再赘述。开始跟踪之前,一定要先分析重要的数据结构,学习相关原理,例如V8的堆栈布局(stack layout)等,这会使调试Builtin事半功倍。V8是一个庞大的系统,涉及了编译技术、体系结构、操作系统等众多知识领域,有相应的知识储备可以使学习V8的过程更容易。

 

3 Ignition解释器

前面介绍了Ignition的调试方法,本节详细讲解Ignition源码的具体实现和工作流程,Ignition是V8解释器,负责执行字节码,它的输入一个字节码列表(bytecode array),输出是程序的执行结果。先给出几个重要约定:

(1) bytecode handler,字节码处理程序,每个字节码对应一个处理程序,Ignition解释执行字节码的本质就是执行对应的处理程序。

(2) bytecode array,字节码列表,一个Javascript功能编译完后生字节码列表。执行字节码之前,需要做预先的准备,包括构建堆栈,参数入压等等,具体工作由InterpreterEntryTrampoline负责。

(3) 每一条字节码执行完后,都要调用Dispatch(),这个函数负责进入下一条字节码开始执行。

(4) Ignition是一个基于寄存器的解释器,这些寄存器是V8维护的虚拟寄存器,用栈实现,不是物理寄存器。但有一个例外,Ignition有一个累加器寄存器,它被很多字节码作为隐式的输入输出寄存器,它是物理寄存器。

(5) dispatch table,字节码分发表,每个isolate都包含一个全局的字节码分发表,分发表以字节码的枚举值作为索引,表项是字节码处理程序的对象指针。

以上五点约定的功能均写入了snapshot_blob.bin文件,在V8启动时通过反序列化方式加载。
InterpreterEntryTrampoline的源码如下:

void Builtins::Generate_InterpreterEntryTrampoline(MacroAssembler* masm) {
  Register closure = rdi;
  Register feedback_vector = rbx;

  // Get the bytecode array from the function object and load it into
  // kInterpreterBytecodeArrayRegister.
  __ LoadTaggedPointerField(
      kScratchRegister,
      FieldOperand(closure, JSFunction::kSharedFunctionInfoOffset));
  __ LoadTaggedPointerField(
      kInterpreterBytecodeArrayRegister,
      FieldOperand(kScratchRegister, SharedFunctionInfo::kFunctionDataOffset));

  Label is_baseline;
  GetSharedFunctionInfoBytecodeOrBaseline(
      masm, kInterpreterBytecodeArrayRegister, kScratchRegister, &is_baseline);

  // The bytecode array could have been flushed from the shared function info,
  // if so, call into CompileLazy.
  Label compile_lazy;
  __ CmpObjectType(kInterpreterBytecodeArrayRegister, BYTECODE_ARRAY_TYPE,
                   kScratchRegister);
  __ j(not_equal, &compile_lazy);

  // Load the feedback vector from the closure.
  __ LoadTaggedPointerField(
      feedback_vector, FieldOperand(closure, JSFunction::kFeedbackCellOffset));
  __ LoadTaggedPointerField(feedback_vector,
                            FieldOperand(feedback_vector, Cell::kValueOffset));

  Label push_stack_frame;
  // Check if feedback vector is valid. If valid, check for optimized code
  // and update invocation count. Otherwise, setup the stack frame.
  __ LoadMap(rcx, feedback_vector);
  __ CmpInstanceType(rcx, FEEDBACK_VECTOR_TYPE);
  __ j(not_equal, &push_stack_frame);

  // Check for an optimization marker.
  Label has_optimized_code_or_marker;
  Register optimization_state = rcx;
  LoadOptimizationStateAndJumpIfNeedsProcessing(
      masm, optimization_state, feedback_vector, &has_optimized_code_or_marker);
//........................
//代码太长,以下部分省略......................
//........................

}

InterpreterEntryTrampoline的作用是构建调用堆栈,分配部局变量等,图2给出了一种InterpreterEntryTrampoline构建的栈布局,不同类型函数有不同的堆栈,第七篇文章中讲的堆栈也是由这个函数构建的。上述代码中GetSharedFunctionInfoBytecodeOrBaseline是取得bytecode array,通过每一个Label可以看出要执行的功能,__的具体实现是#define __ ACCESS_MASM(masm),之后会调用bytecode array的第一条bytecode,开始执行。

Builtins类中还定义了其它一些重要的函数,见下面源码:

class Builtins {
//........................
//代码太长,省略很多.......
//........................
  static void Generate_CallFunction(MacroAssembler* masm,
                                    ConvertReceiverMode mode);

  static void Generate_CallBoundFunctionImpl(MacroAssembler* masm);

  static void Generate_Call(MacroAssembler* masm, ConvertReceiverMode mode);

  enum class CallOrConstructMode { kCall, kConstruct };
  static void Generate_CallOrConstructVarargs(MacroAssembler* masm,
                                              Handle<Code> code);
  static void Generate_CallOrConstructForwardVarargs(MacroAssembler* masm,
                                                     CallOrConstructMode mode,
                                                     Handle<Code> code);

  static void Generate_InterpreterPushArgsThenCallImpl(
      MacroAssembler* masm, ConvertReceiverMode receiver_mode,
      InterpreterPushArgsMode mode);

  static void Generate_InterpreterPushArgsThenConstructImpl(
      MacroAssembler* masm, InterpreterPushArgsMode mode);

  template <class Descriptor>
  static void Generate_DynamicCheckMapsTrampoline(MacroAssembler* masm,
                                                  Handle<Code> builtin_target);

#define DECLARE_ASM(Name, ...) \
  static void Generate_##Name(MacroAssembler* masm);
#define DECLARE_TF(Name, ...) \
  static void Generate_##Name(compiler::CodeAssemblerState* state);

  BUILTIN_LIST(IGNORE_BUILTIN, DECLARE_TF, DECLARE_TF, DECLARE_TF, DECLARE_TF,
               IGNORE_BUILTIN, DECLARE_ASM)
//........................
//代码太长,以下部分省略......................
//........................

Builtins类中的#define DECLARE_TF(Name, ...)#define DECLARE_ASM(Name, ...)是所有Builtin的生成函数,它们由Turbofan生成,每一条bytecode的执行,由一个具体的bytecode handler负责。注意:bytecode handler只是一种Builtin,还有其它的Builtin,byteocde是Builtin,Builtin并不都是bytecode!

下面是生成bytecode handler的功能代码:

#define IGNITION_HANDLER(Name, BaseAssembler)                         \
  class Name##Assembler : public BaseAssembler {                      \
   public:                                                            \
    explicit Name##Assembler(compiler::CodeAssemblerState* state,     \
                             Bytecode bytecode, OperandScale scale)   \
        : BaseAssembler(state, bytecode, scale) {}                    \
    Name##Assembler(const Name##Assembler&) = delete;                 \
    Name##Assembler& operator=(const Name##Assembler&) = delete;      \
    static void Generate(compiler::CodeAssemblerState* state,         \
                         OperandScale scale);                         \
                                                                      \
   private:                                                           \
    void GenerateImpl();                                              \
  };                                                                  \
  void Name##Assembler::Generate(compiler::CodeAssemblerState* state, \
                                 OperandScale scale) {                \
    Name##Assembler assembler(state, Bytecode::k##Name, scale);       \
    state->SetInitialDebugInformation(#Name, __FILE__, __LINE__);     \
    assembler.GenerateImpl();                                         \
  }                                                                   \
  void Name##Assembler::GenerateImpl()
//=======================================================
//=====================分隔线==================================
//=======================================================
// LdaZero
//
// Load literal '0' into the accumulator.
IGNITION_HANDLER(LdaZero, InterpreterAssembler) {
  TNode<Number> zero_value = NumberConstant(0.0);
  SetAccumulator(zero_value);
  Dispatch();
}

IGNITION_HANDLER是宏模板,Name是字节码名字,BaseAssembler是字节码的父类,IGNITION_HANDLER(LdaZero, InterpreterAssembler)这条语句是成生LdaZero的handler。Dispatch()功能是查询“dispatch table”,它的作用是执行下一条字节码,可以理解为寄存器eip++,下面是Dispatch()的具体实现:

void InterpreterAssembler::Dispatch() {
  Comment("========= Dispatch");
  DCHECK_IMPLIES(Bytecodes::MakesCallAlongCriticalPath(bytecode_), made_call_);
  TNode<IntPtrT> target_offset = Advance();
  TNode<WordT> target_bytecode = LoadBytecode(target_offset);
  DispatchToBytecodeWithOptionalStarLookahead(target_bytecode);
}

void InterpreterAssembler::DispatchToBytecodeWithOptionalStarLookahead(
    TNode<WordT> target_bytecode) {
  if (Bytecodes::IsStarLookahead(bytecode_, operand_scale_)) {
    StarDispatchLookahead(target_bytecode);
  }
  DispatchToBytecode(target_bytecode, BytecodeOffset());
}

LoadBytecode(target_offset)获取下一条字节码,DispatchToBytecodeWithOptionalStarLookahead(target_bytecode)负责进入到下一条字节码并执行。
上面讲了字节码的生成方式,以及程序运行期进入下一条字节码的方式(dispatch),下面的代码是生成所有的字节码处理程序。

Handle<Code> GenerateBytecodeHandler(Isolate* isolate, const char* debug_name,
                                     Bytecode bytecode,
                                     OperandScale operand_scale,
                                     Builtin builtin,
                                     const AssemblerOptions& options) {
  Zone zone(isolate->allocator(), ZONE_NAME, kCompressGraphZone);
  compiler::CodeAssemblerState state(
      isolate, &zone, InterpreterDispatchDescriptor{},
      CodeKind::BYTECODE_HANDLER, debug_name,
      builtin);

  switch (bytecode) {
#define CALL_GENERATOR(Name, ...)                     \
  case Bytecode::k##Name:                             \
    Name##Assembler::Generate(&state, operand_scale); \
    break;
    BYTECODE_LIST_WITH_UNIQUE_HANDLERS(CALL_GENERATOR);
#undef CALL_GENERATOR
    case Bytecode::kIllegal:
      IllegalAssembler::Generate(&state, operand_scale);
      break;
    case Bytecode::kStar0:
      Star0Assembler::Generate(&state, operand_scale);
      break;
    default:
      UNREACHABLE();
  }
//............省略代码...................
}

GenerateBytecodeHandler()函数是生成字节码处理程序的入口,由它负责调用上面的IGNITION_HANDLER(XXX,YYY)宏模板,完成所有字节码处理程序的生成,GenerateBytecodeHandler()由TurbFan启动,一句话总结:每一个字节码处理程序由Turbofan独立生成且作为Handle<Code>存在,最终写进snapshot_blob.bin文件中。
好了,今天到这里,下次见。
恳请读者批评指正、提出宝贵意见
微信:qq9123013 备注:v8交流 邮箱:v8blink@outlook.com

本文由灰豆原创发布

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

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

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

发表评论

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