什么是字节码解释器?
解释器是一种顺序执行源代码的引擎。
在过去的V8中,源代码被立即编译到汇编程序并执行,但与之不同,解释器将源代码转换为高级字节指令并按顺序执行字节指令。感觉就像一个 高级汇编程序。
Ignition概要
Ignition是一个基于寄存器的字节码解释器。与Java的堆栈基础不同,它实际上将值分配给CPU的寄存器并执行它们。在Ignition中,预先生成一个名为BytecodeHandler的字节码处理函数,从字节码中获取数组索引,
并将生成的处理函数分配给索引,一个接一个地循环Bytecode数组,并使用相应索引的函数执行调用代码。
简化版本的js代码如下:1
2
3
4
5
6
7
8
9
10
11var 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生成步骤。
- 由于BytecodeGenerator实现了AstVisitor,我们将在Javascript AST中运行时创建相应的字节码。
- BytecodeGenerator位于src / interpreter / bytecode - generator.h中
- 字节码生成方法是BytecodeGenerator :: GenerateBytecode。
- InterpreterCompilationJob :: ExecuteJobImpl(src / interpreter / interpreter.cc)中调用BytecodeGenerator :: GenerateBytecode。
- InterpreterCompilationJob :: ExecuteJobImpl由静态Interpreter :: NewCompilationJob执行。
Interpreter :: NewCompilationJob的层次结构如下。1
2
3
4
5Interpreter::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
21ScriptCompiler::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 | ScriptCompiler::Compile |
字节码生成
现在我们知道调用层次结构,我们将看看如何生成字节码。
由于字节码生成继承自前面编写的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
250 [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
24 * 8 = 32。
2 Frame size 32
每个字节串是当前地址的偏移字节码的数字字节码名称操作数。3 0x3f5e20aafdf6 @ 0 : 09 00 LdaConstant [0]
这是常量值池的内容。
在此示例中,汇总了变量名称a。1
2
3
4
5
619 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
73 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 | //将常量池中索引1(变量名a)的值加载到累加器中。 |
顺便说一下,在CallRuntime的情况下,需要为每个运行时确定调用约定,因此有必要相应地分配寄存器。
InitializeVarGlobal运行时调用需要以下寄存器。
- r0 =要绑定的变量名称
- r1 = LaunguageMode SLOPPY(正常)STRICT(严格模式)LAUNGUAGE_END(未知)
- r2 =要绑定的值
因此,上面的代码:
- 将值加载到累加器中
- 将值加载到寄存器中
- loop
字节码执行
BytecodeHandler
字节码处理由BytecodeHandler完成。
BytecodeHandler在v8初始化时将被生成。
以下是BytecodeHandler的示例。1
2
3
4
5IGNITION_HANDLER(LdaZero, InterpreterAssembler) {
Node* zero_value = NumberConstant(0.0);
SetAccumulator(zero_value);
Dispatch();
}
在BytecoeHandler load zero的过程中,将累加器设置为0。
实际上,每组字节码都将有一个这样的BytecodeHandler。
每个BytecodeHandler通过depatch直接调用下一个BytecodeHandler。
下图显示了BytecodeHandler的生成
InterpreterEntryTrampoline
在Ignition终于生成了BytecodeArray之后,从使用InterpreterEntryTrampoline代码构建的代码中点燃DispatchTable of Ignition,
它从BytecodeArray中检索字节码并执行相应DispatchTable的处理并转发。
下图显示了Ignition的执行方式