4

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

 2 years ago
source link: https://zhuanlan.zhihu.com/p/427676465
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

chrome v8连载,3~4天一篇,持续更新中...

本次是第八篇,讲解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交流 邮箱:[email protected]


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK