NeoVM 是执行 Neo 智能合约代码的虚拟机。本文所讲述的虚拟机概念比较狭义,并非是借助于操作系统对物理机器的一种模拟,与 vmware 或者 Hyper-V 不同,主要是针对具体语言所实现的虚拟机。
例如在 Java 的 JVM 或者 .Net 的 CLR 中,Java 或者 .Net 源码会被编译成相关的字节码,然后在对应虚拟机上运行, JVM 或 CLR 会对这些字节码进行取指令、译码、执行、结果回写等操作。这些步骤和真实物理机器上的概念都很相似。相对应的二进制指令仍然是在物理机器上运行,物理机器从内存中取指令,通过总线传输到 CPU,然后译码、执行、结果存储。
- 新增
- 删除
APPCALL
, TAILCALL
, SHA1
, SHA256
, HASH160
, HASH256
, CHECKSIG
, VERIFY
, CHECKMULTISIG
, CALL_I
, CALL_E
, CALL_ED
, CALL_ET
, CALL_EDT
上图是NEO虚拟机的系统架构图,其主要由执行引擎、存储器、互操作接口等部分构成。
一个完整的运行流程如下:
-
将编写好的C#、Java等智能合约源码通过编译器编译成字节码。
-
将字节码以及相关参数等作为一个运行上下文压入调用栈中。
-
执行引擎每次会根据当前上下文取出需要执行的操作码,然后针对不同的操作码去执行对应的操作,执行过程的数据将会存储在当前上下文的计算栈和临时栈。
-
如果需要访问外部数据等时,调用互操作接口。
-
所有脚本执行完后,将运行结果保存在结果栈中。
左侧部分是虚拟机执行引擎(相当于 CPU),它可以执行常见的指令,例如流程控制、栈操作、位操作、算数运算、逻辑运算、密码学方法等,还可以通过系统调用,与互操作服务层进行交互。虚拟机执行引擎具体支持的指令集将会在下文进行详细介绍。
NeoVM一共有四种状态,分别为 NONE
、HALT
、FAULT
、BREAK
。
-
NONE
为正常状态。
-
HALT
为停止状态,当调用栈为空,即所有脚本执行完毕后,会将虚拟机状态置为HALT。
-
FAULT
为错误状态,当指令操作出错时会将虚拟机状态置为FAULT。
-
BREAK
为中断状态,一般用于智能合约的调试过程中。
每次虚拟机启动时,执行引擎首先会检测虚拟机状态,只有当虚拟机状态为NONE时,才能开始运行。
NeoVM中一共有四种存储器,调用栈(InvocationStack)、计算栈(EvaluationStack)、临时栈(AltStack)和结果栈(ReaultStack)。
系统架构如图中所示:
- 调用栈,用于保存当前虚拟机的所有执行上下文(ExecutionContext),不同的上下文之间实现栈隔离。上下文切换依靠当前上下文(CurrentContext)和入口上下文(EntryContext)来完成。其中当前上下文指向调用栈的栈顶元素,在系统架构图中对应 ExecutionContext0,入口上下文指向调用栈的栈底元素,在图中对应ExecutionContextN。
- 计算栈,用于保存指令在相应执行过程中所用到的数据,每个执行上下文都有自己独立的计算栈;
- 临时栈,用于保存指令在相应执行过程中所用到的临时数据,每个执行上下文都有自己独立的临时栈;
- 结果栈,保存所有脚本执行完后产生的执行结果。
右侧部分是虚拟机的互操作服务层(相当于外设)。目前互操作服务层提供了智能合约所能访问区块链数据的一些 API,利用这些 API,可以访问区块信息、交易信息、合约信息、资产信息等。
除此之外互操作服务层还为每个合约提供了一个持久化存储区的功能。Neo 的每个智能合约在创建的时候都可选地启用一个私有存储区,存储区是 key-value 形式的,Neo 智能合约由合约的被调用者决定持久化存储区的上下文,而非调用者来决定。当然,调用者需要将自己的存储上下文传给被调用者(即完成授权)后,被调用者才可以执行读写操作。
关于互操作服务的详细介绍在智能合约部分。
NeoVM内置的数据类型一共有7种:
类型 |
描述 |
Boolean |
布尔类型,实现为一个bool值和两个字节数组TRUE和FALSE。 |
Integer |
整型,实现为一个BigInteger值。 |
ByteArray |
字节数组,实现为一个byte[]。 |
Array |
数组,实现为一个List<StackItem>,StackItem是一个抽象类,NeoVM内置的数据类型均继承自StackItem。 |
Struct |
结构体,继承自Array。结构与Array相同,只是添加了Clone方法和重写了Equals方法。 |
Map |
实现为一个键值对为StackItem的字典类型Dictionary<StackItem, StackItem> 。 |
InteropInterface |
互操作接口 |
// boolean 类型
private static readonly byte[] TRUE = { 1 };
private static readonly byte[] FALSE = new byte[0];
private bool value;
NeoVM虚拟机一共实现了173个指令,类别如下:
常数 |
流程控制 |
栈操作 |
字符串操作 |
逻辑运算 |
算术运算 |
高级数据结构 |
异常处理 |
98 |
7 |
17 |
5 |
5 |
25 |
14 |
2 |
下面将分别介绍各个指令的详细内容。
常数部分指令主要完成向计算栈中压入常数或者数组的功能。
指令 |
PUSH0 |
字节码: |
0x00 |
别名: |
PUSHF是PUSH0的别名 |
系统费: |
0.00000030 GAS |
功能: |
向计算栈中压入一个长度为0的字节数组。 |
指令: |
PUSHBYTES1~PUSHBYTES75 |
字节码: |
0x01~0x4B |
系统费: |
0.00000120 GAS |
功能: |
向计算栈中压入一个字节数组,其长度等于本指令字节码的数值。 |
指令: |
PUSHDATA1, PUSHDATA2, PUSHDATA4 |
字节码: |
0x4C, 0x4D, 0x4E |
系统费: |
0.00000180 GAS, 0.00013000 GAS, 0.00110000 GAS |
功能: |
向计算栈中压入一个字节数组,其长度由本指令后的 1|2|4 字节指定。 |
指令: |
PUSHNULL |
字节码: |
0x50 |
系统费: |
0.00000030 GAS |
功能: |
向计算栈中压入NULL值。 |
指令: |
PUSHM1 |
字节码: |
0x4F |
系统费: |
0.00000030 GAS |
功能: |
向计算栈中压入一个大整数,其数值等于-1。 |
指令: |
PUSH1~PUSH16 |
字节码: |
0x51~0x60 |
别名: |
PUSHT是PUSH1的别名 |
系统费: |
0.00000030 GAS |
功能: |
向计算栈中压入一个大整数,其数值等于1~16。 |
用于控制的虚拟机运行流程,包括跳转、调用等指令。
指令: |
NOP |
字节码: |
0x61 |
系统费: |
0.00000030 GAS |
功能: |
空操作,但是会使指令计步器加1。 |
指令: |
JMP |
字节码: |
0x62 |
系统费: |
0.00000070 GAS |
功能: |
无条件跳转到指定偏移位置,偏移量由本指令后的2字节指定。 |
指令: |
JMPIF |
字节码: |
0x63 |
系统费: |
0.00000070 GAS |
功能: |
当计算栈栈顶元素不等于0时,跳转到指定偏移位置, 偏移量由本指令后的2字节指定。不论条件判断成功与否,栈顶元素将被移除。 |
指令: |
JMPIFNOT |
字节码: |
0x64 |
系统费: |
0.00000070 GAS |
功能: |
当计算栈栈顶元素等于0时,跳转到指定偏移位置,偏移量由本指令后的2字节指定 |
指令: |
CALL |
字节码: |
0x65 |
系统费: |
0.00022000 GAS |
功能: |
调用指定偏移位置的函数,偏移量由本指令后的2字节指定。 |
指令: |
RET |
字节码: |
0x66 |
系统费: |
0.00000040 GAS |
功能: |
移除调用栈的顶部元素,并使程序在调用栈的下一帧中继续执行。 如果调用栈为空,则虚拟机进入停机状态。 |
指令: |
SYSCALL |
字节码: |
0x68 |
系统费: |
根据系统调用的具体互操作服务计费,参考互操作服务费用 GAS |
功能: |
调用指定的互操作函数,函数名称由本指令后的字符串指定。 |
实现对栈的元素做复制、移除、交换等功能。
NEO3 中新添加的指令
指令 |
DUPFROMALTSTACKBOTTOM |
字节码: |
0x6E |
系统费: |
0.00000060 GAS |
功能: |
复制备用栈栈底的元素,并将其压入计算栈。 |
指令 |
DUPFROMALTSTACK |
字节码: |
0x6A |
系统费: |
0.00000060 GAS |
功能: |
复制备用栈栈顶的元素,并将其压入计算栈。 |
指令: |
TOALTSTACK |
字节码: |
0x6B |
系统费: |
0.00000060 GAS |
功能: |
移除计算栈栈顶的元素,并将其压入备用栈。 |
指令: |
FROMALTSTACK |
字节码: |
0x6C |
系统费: |
0.00000060 GAS |
功能: |
移除备用栈栈顶的元素,并将其压入计算栈。 |
指令: |
ISNULL |
字节码: |
0x70 |
系统费: |
0.00000060 GAS |
功能: |
输入为NULL时,返回true;否则,返回false |
指令: |
XDROP |
字节码: |
0x6D |
系统费: |
0.00000400 GAS |
功能: |
移除计算栈栈顶的元素n,并移除剩余的索引为n的元素。 |
输入: |
Xn Xn-1 ... X2 X1 X0 n |
输出: |
Xn-1 ... X2 X1 X0 |
指令: |
XSWAP |
字节码: |
0x72 |
系统费: |
0.0000006 GAS |
功能: |
移除计算栈栈顶的元素n,并将剩余的索引为0的元素和索引为n的元素交换位置。 |
输入: |
Xn Xn-1 ... X2 X1 X0 n |
输出: |
X0 Xn-1 ... X2 X1 Xn |
指令: |
XTUCK |
字节码: |
0x73 |
系统费: |
0.000004 GAS |
功能: |
移除计算栈栈顶的元素n,并将剩余的索引为0的元素复制并插入到索引为n的位置。 |
输入: |
Xn Xn-1 ... X2 X1 X0 n |
输出: |
Xn X0 Xn-1 ... X2 X1 X0 |
指令: |
DEPTH |
字节码: |
0x74 |
系统费: |
0.0000006 GAS |
功能: |
将当前计算栈中的元素数量压入计算栈顶。 |
指令: |
DROP |
字节码: |
0x75 |
系统费: |
0.0000006 GAS |
功能: |
移除计算栈栈顶的元素。 |
指令: |
DUP |
字节码: |
0x76 |
系统费: |
0.0000006 GAS |
功能: |
复制计算栈栈顶的元素。 |
输入: |
X |
输出: |
X X |
指令: |
NIP |
字节码: |
0x77 |
系统费: |
0.0000006 GAS |
功能: |
移除计算栈栈顶的第2个元素。 |
输入: |
X1 X0 |
输出: |
X0 |
指令: |
OVER |
字节码: |
0x78 |
系统费: |
0.0000006 GAS |
功能: |
复制计算栈栈顶的第二个元素,并压入栈顶。 |
输入: |
X1 X0 |
输出: |
X1 X0 X1 |
指令: |
PICK |
字节码: |
0x79 |
系统费: |
0.0000006 GAS |
功能: |
移除计算栈栈顶的元素n,并将剩余的索引为n的元素复制到栈顶。 |
输入: |
Xn Xn-1 ... X2 X1 X0 n |
输出: |
Xn Xn-1 ... X2 X1 X0 Xn |
指令: |
ROLL |
字节码: |
0x7A |
系统费: |
0.000004 GAS |
功能: |
移除计算栈栈顶的元素n,并将剩余的索引为n的元素移动到栈顶。 |
输入: |
Xn Xn-1 ... X2 X1 X0 n |
输出: |
Xn-1 ... X2 X1 X0 Xn |
指令: |
ROT |
字节码: |
0x7B |
系统费: |
0.0000006 GAS |
功能: |
移除计算栈栈顶的第3个元素,并将其压入栈顶。 |
输入: |
X2 X1 X0 |
输出: |
X1 X0 X2 |
指令: |
SWAP |
字节码: |
0x7C |
系统费: |
0.0000006 GAS |
功能: |
交换计算栈栈顶两个元素的位置。 |
输入: |
X1 X0 |
输出: |
X0 X1 |
指令: |
TUCK |
字节码: |
0x7D |
系统费: |
0.0000006 GAS |
功能: |
复制计算栈栈顶的元素到索引为2的位置。 |
输入: |
X1 X0 |
输出: |
X0 X1 X0 |
指令: |
CAT |
字节码: |
0x7E |
系统费: |
0.0008 GAS |
功能: |
移除计算栈栈顶的两个元素,并将其拼接后压入栈顶。 |
输入: |
X1 X0 |
输出: |
Concat(X1,X0) |
指令: |
SUBSTR |
字节码: |
0x7F |
系统费: |
0.0008 GAS |
功能: |
移除计算栈栈顶的三个元素,取子串后压入栈顶。 |
输入: |
X index len |
输出: |
SubString(X,index,len) |
指令: |
LEFT |
字节码: |
0x80 |
系统费: |
0.0008 GAS |
功能: |
移除计算栈栈顶的两个元素,取子串后压入栈顶。 |
输入: |
X len |
输出: |
SubString(X,0,len) |
指令: |
RIGHT |
字节码: |
0x81 |
系统费: |
0.0008 GAS |
功能: |
移除计算栈栈顶的两个元素,取子串后压入栈顶。 |
输入: |
X len |
输出: |
SubString(X,X.Length - len,len) |
指令: |
SIZE |
字节码: |
0x82 |
系统费: |
0.0000006 GAS |
功能: |
将计算栈栈顶元素的长度压入栈顶。 |
输入: |
X |
输出: |
X len(X) |
指令: |
INVERT |
字节码: |
0x83 |
系统费: |
0.000001 GAS |
功能: |
对计算栈栈顶的元素按位取反。 |
输入: |
X |
输出: |
~X |
指令: |
AND |
字节码: |
0x84 |
系统费: |
0.000002 GAS |
功能: |
对计算栈栈顶的两个元素执行按位与运算。 |
输入: |
AB |
输出: |
A&B |
指令: |
OR |
字节码: |
0x85 |
系统费: |
0.000002 GAS |
功能: |
对计算栈栈顶的两个元素执行按位或运算。 |
输入: |
AB |
输出: |
A|B |
指令: |
XOR |
字节码: |
0x86 |
系统费: |
0.000002 GAS |
功能: |
对计算栈栈顶的两个元素执行按位异或运算。 |
输入: |
AB |
输出: |
A^B |
指令: |
EQUAL |
字节码: |
0x87 |
系统费: |
0.000002 GAS |
功能: |
对计算栈栈顶的两个元素执行逐字节的相等判断。 |
输入: |
AB |
输出: |
Equals(A,B) |
指令: |
INC |
字节码: |
0x8B |
系统费: |
0.000001 GAS |
功能: |
对计算栈栈顶的大整数执行递增运算。 |
输入: |
X |
输出: |
X+1 |
指令: |
DEC |
字节码: |
0x8C |
系统费: |
0.000001 GAS |
功能: |
对计算栈栈顶的大整数执行递减运算。 |
输入: |
X |
输出: |
X-1 |
指令: |
SIGN |
字节码: |
0x8D |
系统费: |
0.000001 GAS |
功能: |
获取计算栈栈顶的大整数的符号(负、正或零)。 |
输入: |
X |
输出: |
X.Sign() |
指令: |
NEGATE |
字节码: |
0x8F |
系统费: |
0.000001 GAS |
功能: |
求计算栈栈顶的大整数的相反数。 |
输入: |
X |
输出: |
-X |
指令: |
ABS |
字节码: |
0x90 |
系统费: |
0.000001 GAS |
功能: |
求计算栈栈顶的大整数的绝对值。 |
输入: |
X |
输出: |
Abs(X) |
指令: |
NOT |
字节码: |
0x91 |
系统费: |
0.000001 GAS |
功能: |
对计算栈栈顶的元素执行逻辑非运算。 |
输入: |
X |
输出: |
!X |
指令: |
NZ |
字节码: |
0x92 |
系统费: |
0.000001 GAS |
功能: |
判断计算栈栈顶的大整数是否为非0值。 |
输入: |
X |
输出: |
X!=0 |
指令: |
ADD |
字节码: |
0x93 |
系统费: |
0.000002 GAS |
功能: |
对计算栈栈顶的两个大整数执行加法运算。 |
输入: |
AB |
输出: |
A+B |
指令: |
SUB |
字节码: |
0x94 |
系统费: |
0.000002 GAS |
功能: |
对计算栈栈顶的两个大整数执行减法运算。 |
输入: |
AB |
输出: |
A-B |
指令: |
MUL |
字节码: |
0x95 |
系统费: |
0.000003 GAS |
功能: |
对计算栈栈顶的两个大整数执行乘法运算。 |
输入: |
AB |
输出: |
A*B |
指令: |
DIV |
字节码: |
0x96 |
系统费: |
0.000003 GAS |
功能: |
对计算栈栈顶的两个大整数执行除法运算。 |
输入: |
AB |
输出: |
A/B |
指令: |
MOD |
字节码: |
0x97 |
系统费: |
0.000003 GAS |
功能: |
对计算栈栈顶的两个大整数执行求余运算。 |
输入: |
AB |
输出: |
A%B |
指令: |
SHL |
字节码: |
0x98 |
系统费: |
0.000003 GAS |
功能: |
对计算栈中的大整数执行左移运算。 |
指令: |
Xn |
字节码: |
X<<n |
指令: |
SHR |
字节码: |
0x99 |
系统费: |
0.000003 GAS |
功能: |
对计算栈中的大整数执行右移运算。 |
输入: |
Xn |
输出: |
X>>n |
指令: |
BOOLAND |
字节码: |
0x9A |
系统费: |
0.000002 GAS |
功能: |
对计算栈栈顶的两个元素执行逻辑与运算。 |
输入: |
AB |
输出: |
A&&B |
指令: |
BOOLOR |
字节码: |
0x9D |
系统费: |
0.000002 GAS |
功能: |
对计算栈栈顶的两个元素执行逻辑或运算。 |
输入: |
AB |
输出: |
A||B |
指令: |
NUMEQUAL |
字节码: |
0x9C |
系统费: |
0.000002 GAS |
功能: |
对计算栈栈顶的两个大整数执行相等判断。 |
输入: |
AB |
输出: |
A==B |
指令: |
NUMNOTEQUAL |
字节码: |
0x9E |
系统费: |
0.000002 GAS |
功能: |
对计算栈栈顶的两个大整数执行不相等判断。 |
输入: |
AB |
输出: |
A!=B |
指令: |
LT |
字节码: |
0x9F |
系统费: |
0.000002 GAS |
功能: |
对计算栈栈顶的两个大整数执行小于判断。 |
输入: |
AB |
输出: |
A<B |
指令: |
GT |
字节码: |
0xA0 |
系统费: |
0.000002 GAS |
功能: |
对计算栈栈顶的两个大整数执行大于判断。 |
输入: |
AB |
输出: |
A>B |
指令: |
LTE |
字节码: |
0xA1 |
系统费: |
0.000002 GAS |
功能: |
对计算栈栈顶的两个大整数执行小于等于判断。 |
输入: |
AB |
输出: |
A<=B |
指令: |
GTE |
字节码: |
0xA2 |
系统费: |
0.000002 GAS |
功能: |
对计算栈栈顶的两个大整数执行大于等于判断。 |
输入: |
AB |
输出: |
A>=B |
指令: |
MIN |
字节码: |
0xA3 |
系统费: |
0.000002 GAS |
功能: |
取出计算栈栈顶的两个大整数中的最小值。 |
输入: |
AB |
输出: |
Min(A,B) |
指令: |
MAX |
字节码: |
0xA4 |
系统费: |
0.000002 GAS |
功能: |
取出计算栈栈顶的两个大整数中的最大值。 |
输入: |
AB |
输出: |
Max(A,B) |
指令: |
WITHIN |
字节码: |
0xA5 |
系统费: |
0.000002 GAS |
功能: |
判断计算栈中的大整数是否在指定的数值范围内。 |
输入: |
XAB |
输出: |
A<=X&&X<B |
实现对Array、Map、Struct等的常用操作。
指令: |
ARRAYSIZE |
字节码: |
0xC0 |
系统费: |
0.0000015 GAS |
功能: |
获取计算栈栈顶的数组的元素数量。 |
输入: |
[X0 X1 X2 ... Xn-1] |
输出: |
n |
指令: |
PACK |
字节码: |
0xC1 |
系统费: |
0.00007 GAS |
功能: |
将计算栈栈顶的n个元素打包成数组。 |
输入: |
Xn-1 ... X2 X1 X0 n |
输出: |
[X0 X1 X2 ... Xn-1] |
指令: |
UNPACK |
字节码: |
0xC2 |
系统费: |
0.00007 GAS |
功能: |
将计算栈栈顶的数组拆包成元素序列。 |
输入: |
[X0 X1 X2 ... Xn-1] |
输出: |
Xn-1 ... X2 X1 X0 n |
指令: |
PICKITEM |
字节码: |
0xC3 |
系统费: |
0.0027 GAS |
功能: |
获取计算栈栈顶的数组中的指定元素。 |
输入: |
[X0 X1 X2 ... Xn-1] i |
输出: |
Xi |
指令: |
SETITEM |
字节码: |
0xC4 |
系统费: |
0.0027 GAS |
功能: |
对计算栈栈顶的数组中的指定位置元素赋值。 |
输入: |
[X0 X1 X2 ... Xn-1] I V |
输出: |
[X0 X1 X2 Xi-1 V X i+1 ... Xn-1] |
指令: |
NEWARRAY |
字节码: |
0xC5 |
系统费: |
0.00015 GAS |
功能: |
在计算栈栈顶新建一个大小为n的Array |
输入: |
n |
输出: |
Array(n)值全为false的Array |
指令: |
NEWSTRUCT |
字节码: |
0xC6 |
系统费: |
0.00015 GAS |
功能: |
在计算栈栈顶新建一个大小为n的Struct |
输入: |
n |
输出: |
Struct(n)值全为false的Struct |
指令: |
NEWMAP |
字节码: |
0xC7 |
系统费: |
0.000002 GAS |
功能: |
在计算栈栈顶新建一个Map |
输入: |
无 |
输出: |
Map() |
指令: |
APPEND |
字节码: |
0xC8 |
系统费: |
0.00015 GAS |
功能: |
向Array中添加一个新项 |
输入: |
Array item |
输出: |
Array.add(item) |
指令: |
REVERSE |
字节码: |
0xC9 |
系统费: |
0.000005 GAS |
功能: |
将Array元素倒序排列 |
输入: |
[X0 X1 X2 ... Xn-1] |
输出: |
[Xn-1 ... X2 X1 X0] |
指令: |
REMOVE |
字节码: |
0xCA |
系统费: |
0.000005 GAS |
功能: |
从Array或Map中移除指定元素 |
输入: |
[X0 X1 X2 ... Xn-1] m |
输出: |
[X0 X1 X2 ... Xm-1 Xm+1 ... Xn-1] |
指令: |
HASKEY |
字节码: |
0xCB |
系统费: |
0.0027 GAS |
功能: |
判断Array或Map中是否包含Key指定元素 |
输入: |
[X0 X1 X2 ... Xn-1] key |
输出: |
true 或 false |
指令: |
KEYS |
字节码: |
0xCC |
系统费: |
0.000005 GAS |
功能: |
获取Map的所有Key,并放入新的Array中 |
输入: |
Map |
输出: |
[key1 key2 ... key n] |
指令: |
VALUES |
字节码: |
0xCD |
系统费: |
0.00007 GAS |
功能: |
获取Array或Map所有值,并放入新的Array中 |
输入: |
Map或Array |
输出: |
[Value1 Value2... Value n] |
指令: |
THROW |
字节码: |
0xF0 |
系统费: |
0.0000003 GAS |
功能: |
将虚拟机状态置为FAULT |
指令: |
THROWIFNOT |
字节码: |
0xF1 |
系统费: |
0.0000003 GAS |
功能: |
从计算栈栈顶读取一个布尔值,如果为False,则将虚拟机状态置为FAULT |
注:带 *
操作码表示该操作码的操作结果并未使用PUSH()放回计算栈。
点击此处查看NeoVM英文版