P1umerのStudio.

V8 Iginition Interpreter

字数统计: 1,716阅读时长: 8 min
2018/07/10 Share

什么是字节码解释器?

解释器是一种顺序执行源代码的引擎。
在过去的V8中,源代码被立即编译到汇编程序并执行,但与之不同,解释器将源代码转换为高级字节指令并按顺序执行字节指令。感觉就像一个 高级汇编程序。

Ignition概要

Ignition是一个基于寄存器的字节码解释器。与Java的堆栈基础不同,它实际上将值分配给CPU的寄存器并执行它们。在Ignition中,预先生成一个名为BytecodeHandler的字节码处理函数,从字节码中获取数组索引,
并将生成的处理函数分配给索引,一个接一个地循环Bytecode数组,并使用相应索引的函数执行调用代码。
简化版本的js代码如下:

1
2
3
4
5
6
7
8
9
10
11
var Bytecodes = [0,1,2,3,4,5];
var index = 0;
function dispatch(next) {BytecodeHandlers[next]();}
const BytecodeHandlers = {
['0']() {...; dispatch(Bytecodes[index++])},
['1']() {...; dispatch(Bytecodes[index++])},
['2']() {...; dispatch(Bytecodes[index++])},
['3']() {...; dispatch(Bytecodes[index++])},
['4']() {...; dispatch(Bytecodes[index++])},
['5']() {...; dispatch(Bytecodes[index++])},
}

Ignition结构

字节码生成的函数调用

Ignition 从Javascript AST 生成 bytecodes

我们将检查这个bytecodes生成步骤。

  1. 由于BytecodeGenerator实现了AstVisitor,我们将在Javascript AST中运行时创建相应的字节码。
  2. BytecodeGenerator位于src / interpreter / bytecode - generator.h中
  3. 字节码生成方法是BytecodeGenerator :: GenerateBytecode。
  4. InterpreterCompilationJob :: ExecuteJobImpl(src / interpreter / interpreter.cc)中调用BytecodeGenerator :: GenerateBytecode。
  5. InterpreterCompilationJob :: ExecuteJobImpl由静态Interpreter :: NewCompilationJob执行。

Interpreter :: NewCompilationJob的层次结构如下。

1
2
3
4
5
Interpreter::NewCompilationJob
|
InterpreterCompilationJob::ExecuteJobImpl
|
BytecodeGenerator::GenerateBytecode

由于这个静态Interpreter :: NewCompilationJob是一个在编译器管道中生成作业的方法,让我们看一下compiler.cc(src / compiler.cc)。
compiler.cc(src / compiler.cc)有一个非常复杂且难以理解的调用层次结构,并且与可选的设置解析器设置一起读取也很困难。
调用堆栈直到调用静态Interpreter :: NewCompilationJob如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ScriptCompiler::Compile
|
ScriptCompiler::CompileUnboundInternal
|
Compiler::GetSharedFunctionInfoForScript
|
Compiler::CompileToplevel
|
CompileUnoptimizedCode(compiler.cc)
|
CompileUnoptimizedInnerFunctions
|
GenerateUnoptimizedCode
|
GetUnoptimizedCompilationJob
|
---- Iginitionオプションによってfullcodegenと分岐
| |
Interpreter::NewCompilationJob
|
FullCodeGenerator::NewCompilationJob

ScriptCompiler :: Compile是V8的Javascript编译器的入口点,并按顺序调用该函数,最后创建Job of Interpreter。

到最终的BytecodeGenerator :: GenerateBytecode的调用堆栈如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ScriptCompiler::Compile
|
ScriptCompiler::CompileUnboundInternal
|
Compiler::GetSharedFunctionInfoForScript
|
Compiler::CompileToplevel
|
CompileUnoptimizedCode(compiler.cc)
|
CompileUnoptimizedInnerFunctions
|
GenerateUnoptimizedCode
|
GetUnoptimizedCompilationJob
|
---- Branch with fullcodegen by Iginition option
| |
| FullCodeGenerator::NewCompilationJob
|
Interpreter::NewCompilationJob
|
InterpreterCompilationJob::ExecuteJobImpl
|
BytecodeGenerator::GenerateBytecode

字节码生成

现在我们知道调用层次结构,我们将看看如何生成字节码。
由于字节码生成继承自前面编写的AstVisitor,因此有Visit必要实现各种方法。
你应该能够通过查看各种实现来了解您正在做的事情。或者直接通过trace_bytecode来运行d8查看输出。

JavaScript代码:

1
var a = 1;

字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
0  [generating bytecode for function: ]
1 Parameter count 1
2 Frame size 32
3 0x3f5e20aafdf6 @ 0 : 09 00 LdaConstant [0]
4 0x3f5e20aafdf8 @ 2 : 1f f9 Star r1
5 0x3f5e20aafdfa @ 4 : 02 LdaZero
6 0x3f5e20aafdfb @ 5 : 1f f8 Star r2
7 0x3f5e20aafdfd @ 7 : 20 fe f7 Mov <closure>, r3
8 0x3f5e20aafe00 @ 10 : 55 aa 01 f9 03 CallRuntime [DeclareGlobalsForInterpreter], r1-r3
9 0 E> 0x3f5e20aafe05 @ 15 : 92 StackCheck
10 116 S> 0x3f5e20aafe06 @ 16 : 09 01 LdaConstant [1]
11 0x3f5e20aafe08 @ 18 : 1f f9 Star r1
12 0x3f5e20aafe0a @ 20 : 02 LdaZero
13 0x3f5e20aafe0b @ 21 : 1f f8 Star r2
14 0x3f5e20aafe0d @ 23 : 03 01 LdaSmi [1]
15 0x3f5e20aafe0f @ 25 : 1f f7 Star r3
16 0x3f5e20aafe11 @ 27 : 55 ab 01 f9 03 CallRuntime [InitializeVarGlobal], r1-r3
17 0x3f5e20aafe16 @ 32 : 04 LdaUndefined
18 118 S> 0x3f5e20aafe17 @ 33 : 96 Return
19 Constant pool (size = 2)
20 0x3f5e20aafda1: [FixedArray]
21 - map = 0x1cfd2a282309 <Map(FAST_HOLEY_ELEMENTS)>
22 - length: 2
23 0: 0x3f5e20aafd71 <FixedArray[4]>
24 1: 0x2315b1a87ef9 <String[1]: a>

你得到这个结果。

这里,函数名称输入到函数字节码中。

0 [generating bytecode for function: ]

这是堆栈的参数数量。
这个字节码是全局的,所以忽略它。

1 Parameter count 1

FrameSize是已分配寄存器的数量*指针大小
指针大小大致相同,32位为4字节,64位为8字节。
在这种情况下,由于分配的寄存器数是4 64位环境,因此指针大小为8字节

1
2
4 * 8 = 32。
2 Frame size 32

每个字节串是当前地址的偏移字节码的数字字节码名称操作数。
3 0x3f5e20aafdf6 @ 0 : 09 00 LdaConstant [0]
这是常量值池的内容。
在此示例中,汇总了变量名称a。

1
2
3
4
5
6
19 Constant pool (size = 2)
20 0x3f5e20aafda1: [FixedArray]
21 - map = 0x1cfd2a282309 <Map(FAST_HOLEY_ELEMENTS)>
22 - length: 2
23 0: 0x3f5e20aafd71 <FixedArray[4]>
24 1: 0x2315b1a87ef9 <String[1]: a>

现在,让我们先看看基于这些信息的源代码和字节码。
以下部分可能会被扫除。 因为这是编译准备。

1
2
3
4
5
6
7
3           0x3f5e20aafdf6 @    0 : 09 00             LdaConstant [0]
4 0x3f5e20aafdf8 @ 2 : 1f f9 Star r1
5 0x3f5e20aafdfa @ 4 : 02 LdaZero
6 0x3f5e20aafdfb @ 5 : 1f f8 Star r2
7 0x3f5e20aafdfd @ 7 : 20 fe f7 Mov <closure>, r3
8 0x3f5e20aafe00 @ 10 : 55 aa 01 f9 03 CallRuntime [DeclareGlobalsForInterpreter], r1-r3
9 0 E> 0x3f5e20aafe05 @ 15 : 92 StackCheck

从这里真正的produce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//将常量池中索引1(变量名a)的值加载到累加器中。
10 116 S> 0x3f5e20aafe06 @ 16:09 01 LdaConstant [1]
11 //将累加器的值(变量名a)加载到r1寄存器中。
12 0x3f5e20aafe08 @ 18:1f f9 Star r1
13 //加载累加器0。
14 0x3f5e20aafe0a @ 20:02 LdaZero
15 //将值从累加器(0)加载到r2寄存器中。
16 0x3f5e20aafe0b @ 21:1 f f 8 Star r 2
17 //将立即值1加载到累加器中。
18 0x3f5e20aafe0d @ 23:03 01 Lda Smi [1]
19 //将累加器(1)的值加载到r3寄存器中。
20 0x3f5e20aafe0f @ 25:1 f f 7 Star r 3
21 //使用r1寄存器中的r3寄存器值(a,0,1)调用InitializeVarGlobal运行时。
22 0x3f5e20aafe11 @ 27:55 ab 01 f9 03 CallRuntime [InitializeVarGlobal],r1-r3
23 //将undefined设置为accumulator
24 0x3f5e20aafe16 @ 32:04 LdaUndefined
25 / /完成
26 118 S> 0x3f5e20aafe17 @ 33:96 Return

顺便说一下,在CallRuntime的情况下,需要为每个运行时确定调用约定,因此有必要相应地分配寄存器。

InitializeVarGlobal运行时调用需要以下寄存器。

  • r0 =要绑定的变量名称
  • r1 = LaunguageMode SLOPPY(正常)STRICT(严格模式)LAUNGUAGE_END(未知)
  • r2 =要绑定的值

因此,上面的代码:

  • 将值加载到累加器中
  • 将值加载到寄存器中
  • loop

字节码执行

BytecodeHandler

字节码处理由BytecodeHandler完成。
BytecodeHandler在v8初始化时将被生成。

以下是BytecodeHandler的示例。

1
2
3
4
5
IGNITION_HANDLER(LdaZero, InterpreterAssembler) {
Node* zero_value = NumberConstant(0.0);
SetAccumulator(zero_value);
Dispatch();
}

在BytecoeHandler load zero的过程中,将累加器设置为0。
实际上,每组字节码都将有一个这样的BytecodeHandler。
每个BytecodeHandler通过depatch直接调用下一个BytecodeHandler。

下图显示了BytecodeHandler的生成
mark

InterpreterEntryTrampoline

在Ignition终于生成了BytecodeArray之后,从使用InterpreterEntryTrampoline代码构建的代码中点燃DispatchTable of Ignition,
它从BytecodeArray中检索字节码并执行相应DispatchTable的处理并转发。

下图显示了Ignition的执行方式

mark

CATALOG
  1. 1. 什么是字节码解释器?
  2. 2. Ignition概要
  3. 3. Ignition结构
    1. 3.1. 字节码生成的函数调用
    2. 3.2. 字节码生成
    3. 3.3. 字节码执行
      1. 3.3.1. BytecodeHandler
      2. 3.3.2. InterpreterEntryTrampoline