概述
方舟字节码(Ark Bytecode),是由方舟编译器编译ArkTS/TS/JS生成的,提供给方舟运行时解释执行的二进制文件,字节码中的主要内容是方舟字节码指令。
本文旨在介绍方舟字节码指令相关的设计,将在后续章节中对构成指令的重要概念和具体的指令格式及含义进行说明,帮助开发者了解方舟字节码指令,指导开发者进行指令相关的特性开发工作。
一条方舟字节码指令,由操作码(指令的名称)和指令入参列表组成。操作码包含无前缀的操作码和有前缀的操作码两种情况。寄存器、立即数以及string id/method id/literal id,均可以作为指令的入参,除此之外,部分指令中使用累加器作为默认参数。
方舟字节码中,除寄存器和累加器之外,还存在全局变量、模块(module)命名空间和模块变量、词法环境和词法变量、补丁变量4种值存储方式。指令可以使用这4种储值位置中的值作为入参。
术语和约束
术语
本文涉及的术语清单:
术语 | 说明 |
---|---|
acc | accumulator,累加器,方舟字节码中一个特殊的寄存器 |
bit | 一个比特,本文中用位表示 |
hole | 还未进行初始化的对象或变量 |
id | index,索引,是string id/method id/literal id的总称 |
string id | string index,16位的数字,用于索引到对应的字符串 |
method id | method index,16位的数字,用于索引到对应的方法 |
literal id | literal index,16位的数字,用于索引到对应的字面量数组 |
lexical environment | 词法环境,用来存放闭包变量的语义环境 |
lexical variable | 词法变量,词法环境中所存的闭包变量 |
字节码构成
操作码与前缀
方舟字节码中的操作码通常被编码为一个8位的值,因此至多只能有256个操作码。随着方舟编译器运行时功能的演进,字节码的数量也在逐步增加,已经超过了256个。因此,方舟字节码引入了前缀(prefix),将操作码最大宽度从8位扩展到16位。8位操作码(无前缀的)用于表示频繁出现的指令,16位操作码(有前缀的)用于表示出现频率不高的指令。
带前缀的操作码为小端法存储的16位值,由8位操作码和8位前缀组成,编码规则为:操作码左移8位,再与前缀相或。
前缀操作码 | 助记符 | 描述 |
---|---|---|
0xfe | throw | 有条件/无条件的throw指令 |
0xfd | wide | 含有更宽编码宽度的立即数、id或寄存器索引的指令 |
0xfc | deprecated | 方舟编译器不再会产生的指令,仅用于维护运行时兼容性; 本文后续章节中将省略对这些指令的说明 |
0xfb | callruntime | 调用运行时方法的指令 |
前缀操作码的助记符的形式为前缀助记符.操作码助记符, 例如,wide.stlexvar。stlexvar指令的操作码是0x0d,前缀wide是0xfd,则此带前缀的指令(wide.stlexvar)的操作码是0x0dfd。
寄存器与累加器
方舟虚拟机模型基于寄存器,所有的寄存器均是虚拟寄存器。当寄存器中存放原始类型的值时,寄存器的宽度是64位;当寄存器中存放对象类型的值时,寄存器的宽度适应为足够宽,以存放对该对象的引用。
方舟字节码中,存在一个名为累加器(accumulator,也简称作acc)的不可见寄存器。acc是许多指令的默认目标寄存器,也是许多指令的默认参数。acc不占用编码宽度,有助于产生更为紧凑的字节码。
示例代码:
function foo(): number {
return 1;
}
字节码中的相关指令:
.function any .foo(any a0, any a1, any a2) {
ldai 0x1
return
}
指令ldai 0x1:将整型字面量1加载到acc中;
指令return:将acc中的值返回。
立即数
方舟字节码中部分指令采用常数形式来表示整型数值、双精度浮点型数值、跳转偏移量等数据。这类常数被称为立即数,可以是8位、16位、32位或64位。
方法索引、字符串索引、字面量索引
方舟字节码中存放着源文件中使用到的所有方法、字符串和字面量数组的偏移量。其中,字面量数组中存放着各种字面量数据,例如整型数字、字符串偏移量和方法偏移量。在方舟字节码指令中,这些方法、字符串以及字面量数组的索引都是16位的,分别被称作方法索引(method id)、字符串索引(string id)以及字面量索引(literal id)。这些索引被编码在指令中,以引用方法、字符串和字面量数组。
值存储方式
全局变量
在Script编译模式下,全局变量是一个存储在全局唯一的映射中的变量,其键值为全局变量的名称,值为全局变量的值。全局变量可通过全局(global)相关的指令进行访问。
示例代码:
function foo(): void {
a += 2;
b = 5;
}
字节码中的相关指令:
.function any .foo(any a0, any a1, any a2) {
tryldglobalbyname 0x0, a
sta v4
ldai 0x2
add2 0x1, v4
trystglobalbyname 0x2, a
ldai 0x5
trystglobalbyname 0x3, b
...
}
指令tryldglobalbyname 0x0, a:将名称为a的全局变量加载进acc,不存在名称为a的全局变量时,抛出异常;
指令trystglobalbyname 0x2, a:将acc中的值存放到名称为a的全局变量上,不存在名称为a的全局变量时,抛出异常;
指令trystglobalbyname 0x3, b:将acc中的值存放到名称为b的全局变量上,不存在名称为b的全局变量时,抛出异常。
注意:
上述指令中出现的0x0,0x2,0x3是方舟运行时内部使用的保留数字,开发者无需关注。
模块命名空间和模块变量
源文件中所有使用到的模块命名空间(module namespace)都会被编译进一个数组中,指令中使用索引来引用一个模块命名空间。例如,指令getmodulenamespace 0x1引用了索引0x1处的模块命名空间。
源文件中所有使用到的模块变量(module variable)都会被编译进一个数组中,指令中使用索引来引用一个模块变量。例如,指令stmodulevar 0x1引用了索引0x1处的模块变量。
在函数中,如果一个模块变量的声明和这个函数在同一个源文件中,则将这个变量称为局部模块变量;否则称为外部模块变量。例如,指令ldlocalmodulevar和ldexternalmodulevar分别用于加载局部模块变量和外部模块变量。
产生模块指令的相关场景,包括import和export,主要场景列举如下:
- import * as:module namespace
- import { }:module variable
- export:local export
注意:
模块相关的逻辑是编译器的内部实现,随着方舟编译器的后续演进,可能会出现新的涉及模块指令的场景;另一方面,现有的模块命名空间和模块变量指令的相关场景,也可能会随着需求演进和代码重构,不再涉及产生模块相关指令。
示例代码:
import { a, b } from "./module_foo"
import * as c from "./module_bar"
export let d: number = 3;
a + b + d;
字节码中的相关指令:
.function any .func_main_0(any a0, any a1, any a2) {
getmodulenamespace 0x1
ldai 0x3
stmodulevar 0x0
ldexternalmodulevar 0x0
sta v0
throw.undefinedifholewithname a
ldexternalmodulevar 0x1
sta v1
throw.undefinedifholewithname b
lda v1
add2 0x0, v0
sta v0
ldlocalmodulevar 0x0
sta v1
throw.undefinedifholewithname d
lda v1
add2 0x1, v0
...
}
指令getmodulenamespace 0x1:获取1号槽位上的模块命名空间(c),存放到acc中;
指令stmodulevar 0x0:将acc中的值存放到当前模块的0号槽位上;
指令ldexternalmodulevar 0x0:加载外部模块的0号槽位上的值(a),存放到acc中;
指令ldlocalmodulevar 0x0:加载当前局部模块的0号槽位上的值(d),存放到acc中。
词法环境和词法变量
方舟字节码中,词法环境(lexical environment)可以看作是一个具有多个槽位的数组,每个槽位对应一个词法变量(lexical variable),一个方法中可能会存在多个词法环境。指令中使用词法环境的相对层级编号和槽位索引,来表示一个词法变量。例如,指令ldlexvar 0x1, 0x2的含义是:将1个层次外的词法环境的2号槽位上的值存放到acc中。
|xxx|xxx|xxx|xxx| <-- 当前词法环境外的第1个词法环境
^
|------------ ldlexvar 0x1, 0x2
|xxx|xxx|xxx|xxx| <-- 当前词法环境
注意:
lexical相关的逻辑是编译器的内部实现。随着方舟编译器的后续演进,可能会出现新的涉及lexical指令的场景;另一方面,现有的lexical指令的相关场景,也可能会随着需求演进和代码重构,不再涉及产生lexical相关指令。
示例代码:
function foo(): void {
let a: number = 1;
function bar(): number {
return a;
}
}
字节码中的相关指令:
.function any .foo(any a0, any a1, any a2) {
newlexenv 0x1
...
definefunc 0x0, .bar, 0x0
sta v3
ldai 0x1
...
stlexvar 0x0, 0x0
...
}
.function any .bar(any a0, any a1, any a2) {
...
ldlexvar 0x0, 0x0
...
}
指令newlexenv 0x1:创建一个槽位数为1的词法环境,将其存放到acc中,并进入该词法环境;
指令stlexvar 0x0, 0x0:将acc中的值存放到0个层次外的词法环境的0号槽位上;
指令ldlexvar 0x0, 0x0:将0个层次外的词法环境的0号槽位上的值存放到acc中。
补丁变量
方舟编译器支持补丁模式的编译,当源文件发生修改时,经过补丁模式编译,生成一个补丁字节码,配合原字节码,完成功能的更新。方舟编译器在补丁模式下编译时,产生的补丁变量会被存放在一个特殊的补丁词法环境中。方舟字节码中使用补丁词法环境上的槽位编号来引用补丁变量。例如,指令ldpatchvar 0x1加载的是槽位号为1的补丁变量。
示例代码:
function bar(): void {} // 新增语句,编译补丁
function foo(): void {
bar(); // 新增语句,编译补丁
}
字节码中的相关指令:
.function any foo(...) {
...
wide.ldpatchvar 0x0
sta v4
lda v4
callarg0 0x0
...
}
.function any patch_main_0(...) {
newlexenv 0x1
definefunc 0x1, bar:(any,any,any), 0x0
wide.stpatchvar 0x0
...
}
指令wide.stpatchvar 0x0:将函数bar存放到补丁词法环境的0号槽位;
指令wide.ldpatchvar 0x0:将补丁词法环境上0号槽位的值存放到acc中。
函数调用规范
对于一个包含了N个形参的方法,该方法所使用的寄存器中的最后N+3个会被用于传递参数。其中,前三个寄存器固定表示函数本身(FunctionObject)、new.target(NewTarget)和函数所在的词法环境中的this(this),后续的N个寄存器依次对应这N个形参。
示例代码:
function foo(a: number, b: number): void {}
字节码中的相关指令:
.function any .foo(any a0, any a1, any a2, any a3, any a4) {
// a0: FunctionObject
// a1: NewTarget
// a2: this
// a3: a
// a4: b
}
字节码格式说明
助记符 | 语义说明 |
---|---|
ID16 | 8位操作码,16位id |
IMM16 | 8位操作码,16位立即数 |
IMM16_ID16 | 8位操作码,16位立即数,16位id |
IMM16_ID16_ID16_IMM16_V8 | 8位操作码,16位立即数,2个16位id,16位立即数,8位寄存器 |
IMM16_ID16_IMM8 | 8位操作码,16位立即数,16位id,8位立即数 |
IMM16_ID16_V8 | 8位操作码,16位立即数,16位id,8位寄存器 |
IMM16_IMM16 | 8位操作码,2个16位立即数 |
IMM16_IMM8_V8 | 8位操作码,16位立即数,8位立即数,8位寄存器 |
IMM16_V8 | 8位操作码,16位立即数,8位寄存器 |
IMM16_V8_IMM16 | 8位操作码,16位立即数,8位寄存器,16位立即数 |
IMM16_V8_V8 | 8位操作码,16位立即数,2个8位寄存器 |
IMM32 | 8位操作码,32位立即数 |
IMM4_IMM4 | 8位操作码,2个4位立即数 |
IMM64 | 8位操作码,64位立即数 |
IMM8 | 8位操作码,8位立即数 |
IMM8_ID16 | 8位操作码,8位立即数,16位id |
IMM8_ID16_ID16_IMM16_V8 | 8位操作码,8位立即数,2个16位id,16位立即数,8位寄存器 |
IMM8_ID16_IMM8 | 8位操作码,8位立即数,16位id,8位立即数 |
IMM8_ID16_V8 | 8位操作码,8位立即数,16位id,8位寄存器 |
IMM8_IMM16 | 8位操作码,8位立即数,16位立即数 |
IMM8_IMM8 | 8位操作码,2个8位立即数 |
IMM8_IMM8_V8 | 8位操作码,2个8位立即数,8位寄存器 |
IMM8_V8 | 8位操作码,8位立即数,8位寄存器 |
IMM8_V8_IMM16 | 8位操作码,8位立即数,8位寄存器,16位立即数 |
IMM8_V8_V8 | 8位操作码,8位立即数,2个8位寄存器 |
IMM8_V8_V8_V8 | 8位操作码,8位立即数,3个8位寄存器 |
IMM8_V8_V8_V8_V8 | 8位操作码,8位立即数,4个8位寄存器 |
NONE | 8位操作码 |
PREF_IMM16 | 16位前缀操作码,16位立即数 |
PREF_IMM16_ID16 | 16位前缀操作码,16位立即数,16位id |
PREF_IMM16_V8 | 16位前缀操作码,16位立即数,8位寄存器 |
PREF_IMM16_V8_V8 | 16位前缀操作码,16位立即数,2个8位寄存器 |
PREF_IMM8 | 16位前缀操作码,8位立即数 |
PREF_NONE | 16位前缀操作码 |
PREF_V8 | 16位前缀操作码,8位寄存器 |
PREF_V8_ID16 | 16位前缀操作码,8位寄存器,16位id |
PREF_V8_IMM32 | 16位前缀操作码,8位寄存器,32位立即数 |
V16_V16 | 8位操作码,2个16位寄存器 |
V4_V4 | 8位操作码,2个4位寄存器 |
V8 | 8位操作码,8位寄存器 |
V8_IMM16 | 8位操作码,8位寄存器,16位立即数 |
V8_IMM8 | 8位操作码,8位寄存器,8位立即数 |
V8_V8 | 8位操作码,2个8位寄存器 |
V8_V8_V8 | 8位操作码,3个8位寄存器 |
V8_V8_V8_V8 | 8位操作码,4个8位寄存器 |
字节码汇总集合
下表中汇总了当前版本的所有方舟字节码,寄存器索引、立即数和id通过每四位宽度使用一个字符替代的形式来描述。
以指令defineclasswithbuffer RR, @AAAA, @BBBB, +CCCC, vDD为例:
- defineclasswithbuffer:指示操作的操作码助记符
- RR:方舟运行时内部使用的8位保留数字,此处提及仅为完整展示指令格式,开发者无需关注
- @AAAA,@BBBB:16位id
- +CCCC:16位立即数
- vDD:8位寄存器索引
操作码 | 格式 | 助记符/语法 | 参数 | 说明 |
---|---|---|---|---|
0x00 | NONE | ldundefined | 将undefined加载进acc。 | |
0x01 | NONE | ldnull | 将null加载进acc。 | |
0x02 | NONE | ldtrue | 将true加载进acc。 | |
0x03 | NONE | ldfalse | 将false加载进acc。 | |
0x04 | NONE | createemptyobject | 创建一个空对象,并将其存放到acc中。 | |
0x05 | IMM8 | createemptyarray RR | R:方舟运行时内部使用的8位保留数字 | 创建一个空数组,并将其存放到acc中。 |
0x06 | IMM8_ID16 | createarraywithbuffer RR, @AAAA | R:方舟运行时内部使用的8位保留数字 A:16位的literal id |
使用索引A对应的字面量数组,创建一个数组对象,并将其存放到acc中。 |
0x07 | IMM8_ID16 | createobjectwithbuffer RR, @AAAA | R:方舟运行时内部使用的8位保留数字 A:16位的literal id |
使用索引A对应的字面量数组,创建一个对象,并将其存放到acc中。 |
0x08 | IMM8_IMM8_V8 | newobjrange RR, +AA, vBB | R:方舟运行时内部使用的8位保留数字 A:参数数量 B:类对象 B + 1, ..., B + A - 1:传递给构造函数的参数 |
以B + 1, ..., B + A - 1作为参数,创建一个B类的实例,并将其存放到acc中。 |
0x09 | IMM8 | newlexenv +AA | A:词法环境中的槽位数目 | 创建一个槽位数为A的词法环境,将其存放到acc中,并进入该词法环境。 |
0x0a | IMM8_V8 | add2 RR, vAA | 默认入参:acc:操作数 R:方舟运行时内部使用的8位保留数字 A:操作数 |
计算A + acc,并将计算结果存放到acc中。 |
0x0b | IMM8_V8 | sub2 RR, vAA | 默认入参:acc:操作数 R:方舟运行时内部使用的8位保留数字 A:操作数 |
计算A - acc,并将计算结果存放到acc中。 |
0x0c | IMM8_V8 | mul2 RR, vAA | 默认入参:acc:操作数 R:方舟运行时内部使用的8位保留数字 A:操作数 |
计算A * acc,并将计算结果存放到acc中。 |
0x0d | IMM8_V8 | div2 RR, vAA | 默认入参:acc:操作数 R:方舟运行时内部使用的8位保留数字 A:操作数 |
计算A / acc,并将计算结果存放到acc中。 |
0x0e | IMM8_V8 | mod2 RR, vAA | 默认入参:acc:操作数 R:方舟运行时内部使用的8位保留数字 A:操作数 |
计算A % acc,并将计算结果存放到acc中。 |
0x0f | IMM8_V8 | eq RR, vAA | 默认入参:acc:操作数 R:方舟运行时内部使用的8位保留数字 A:操作数 |
计算A == acc,并将计算结果存放到acc中。 |
0x10 | IMM8_V8 | noteq RR, vAA | 默认入参:acc:操作数 R:方舟运行时内部使用的8位保留数字 A:操作数 |
计算A != acc,并将计算结果存放到acc中。 |
0x11 | IMM8_V8 | less RR, vAA | 默认入参:acc:操作数 R:方舟运行时内部使用的8位保留数字 A:操作数 |
计算A < acc,并将计算结果存放到acc中。 |
0x12 | IMM8_V8 | lesseq RR, vAA | 默认入参:acc:操作数 R:方舟运行时内部使用的8位保留数字 A:操作数 |
计算A <= acc,并将计算结果存放到acc中。 |
0x13 | IMM8_V8 | greater RR, vAA | 默认入参:acc:操作数 R:方舟运行时内部使用的8位保留数字 A:操作数 |
计算A > acc,并将计算结果存放到acc中。 |
0x14 | IMM8_V8 | greatereq RR, vAA | 默认入参:acc:操作数 R:方舟运行时内部使用的8位保留数字 A:操作数 |
计算A >= acc,并将计算结果存放到acc中。 |
0x15 | IMM8_V8 | shl2 RR, vAA | 默认入参:acc:操作数 R:方舟运行时内部使用的8位保留数字 A:操作数 |
计算A << acc,并将计算结果存放到acc中。 |
0x16 | IMM8_V8 | shr2 RR, vAA |