a) 以太坊虚拟机(EVM)是以太坊中智能合约的运行环境。它不仅被沙箱封装起来,事实上它被完全隔离,也就是说运行在EVM内部的代码不能接触到网络、文件系统或者其它进程。甚至智能合约与其它智能合约只有有限的接触。
b) 编程语言支持:为了兼容尚未实现的应用程序,虚拟机应该支持编程语言,而不是特定的应用程序,应用程序的业务逻辑可以用这种语言实现
c) 高级语言实现:
i. 开发人员不想在二进制EVM程序中编程,用较高级语言编写代码,编译器编译为EVM代码
ii. 高级语言包括:Serpent,LLL,Solidity(最流行)
d) EVM要求:
i. 代码量较小(使得许多用户的很多合同可以由一个节点存储);
ii. 禁止无限循环(必需确定完成; 不能超时);
iii. 多种语言实现,缓解公共链中的开发人员集中化
2、 虚拟机实现
a) 指令集
EVM的指令集被刻意保持在最小规模,以尽可能避免可能导致共识问题的错误实现。所有的指令都是针对256比特这个基本的数据类型的操作。具备常用的算术,位,逻辑和比较操作。也可以做到条件和无条件跳转。此外,合约可以访问当前区块的相关属性,比如它的编号和时间戳。
i. 暂停执行
关键字 |
操作码 |
输入 |
输出 |
描述 |
STOP |
0x00 |
0 |
0 |
停止执行 |
ii. 算术运算
关键字 |
操作码 |
输入 |
输出 |
描述 |
ADD |
0x01 |
2 |
1 |
加法操作 |
MUL |
0x02 |
2 |
1 |
乘法操作 |
SUB |
0x03 |
2 |
1 |
减法操作 |
DIV |
0x04 |
2 |
1 |
除法操作 |
SDIV |
0x05 |
2 |
1 |
有符号除法 |
MOD |
0x06 |
2 |
1 |
求模操作 |
SMOD |
0x07 |
2 |
1 |
有符号求模操作 |
ADDMOD |
0x08 |
3 |
1 |
先加再求模 |
MULMOD |
0x09 |
3 |
1 |
先乘再求模 |
EXP |
0x0a |
2 |
1 |
指数运算 |
SIGNEXTEND |
0x0b |
2 |
1 |
扩展有符号整数的长度 |
iii. 按位逻辑与比较运算
关键字 |
操作码 |
输入 |
输出 |
描述 |
LT |
0X10 |
2 |
1 |
小于操作 |
GT |
0X11 |
2 |
1 |
大于操作 |
SLT |
0X12 |
2 |
1 |
有符号小于操作 |
SGT |
0X13 |
2 |
1 |
有符号大于操作 |
EQ |
0X14 |
2 |
1 |
等于操作 |
ISZERO |
0x15 |
1 |
1 |
否定操作 |
AND |
0x16 |
2 |
1 |
按位与运算 |
OR |
0x17 |
2 |
1 |
按位或运算 |
XOR |
0x18 |
2 |
1 |
按位异或运算 |
NOT |
0x19 |
1 |
1 |
按位非运算 |
BYTE |
0x1a |
2 |
1 |
从字检索单个字节 |
iv. 加密操作
关键字 |
操作码 |
输入 |
输出 |
描述 |
SHA3 |
0x20 |
2 |
1 |
计算SHA3-256散列 |
v. 环境信息
关键字 |
操作码 |
输入 |
输出 |
描述 |
ADDRESS |
0x30 |
0 |
1 |
获取当前执行帐户的地址 |
BALANCE |
0x31 |
1 |
1 |
获取给定帐户的余额 |
ORIGIN |
0x32 |
0 |
1 |
获取执行起始地址 |
CALLER |
0x33 |
0 |
1 |
获取调用者地址 |
CALLVALUE |
0x34 |
0 |
1 |
通过负责此执行的指令/事务获取存储值 |
CALLDATALOAD |
0x35 |
1 |
1 |
获取当前环境的输入数据 |
CALLDATASIZE |
0x36 |
0 |
1 |
获取当前环境中的输入数据的大小 |
CALLDATACOPY |
0x37 |
3 |
0 |
将当前环境中的输入数据复制到内存 |
CODESIZE |
0x38 |
0 |
1 |
获取在当前环境中运行的代码的大小 |
CODECOPY |
0x39 |
3 |
0 |
将当前环境中运行的代码复制到内存 |
GASPRICE |
0x3a |
0 |
1 |
获取当前环境中的气体价格 |
EXTCODESIZE |
0x3b |
1 |
1 |
获取在当前环境中使用给定偏移量运行的代码大小 |
EXTCODECOPY |
0x3c |
4 |
0 |
将在当前环境中运行的代码复制到具有给定偏移量的内存中 |
vi. 块信息
关键字 |
操作码 |
输入 |
输出 |
描述 |
BLOCKHASH |
0x40 |
1 |
1 |
获取最近完成块的哈希值 |
COINBASE |
0x41 |
0 |
1 |
获取块的硬币基地址 |
TIMESTAMP |
0x42 |
0 |
1 |
获取块的时间戳 |
NUMBER |
0x43 |
0 |
1 |
获取块的编号 |
DIFFICULTY |
0x44 |
0 |
1 |
获得块的难度 |
GASLIMIT |
0x45 |
0 |
1 |
获取块的气体限制 |
vii. 内存,存储和流操作
关键字 |
操作码 |
输入 |
输出 |
描述 |
POP |
0x50 |
1 |
0 |
出栈 |
MLOAD |
0x51 |
1 |
1 |
从内存加载字 |
MSTORE |
0x52 |
2 |
0 |
将word保存到内存 |
MSTORE8 |
0x53 |
2 |
0 |
将byte保存到存储器 |
SLOAD |
0x54 |
1 |
1 |
从存储器加载word |
SSTORE |
0x55 |
2 |
0 |
将word保存到存储 |
JUMP |
0x56 |
1 |
0 |
跳转 |
JUMPI |
0x57 |
2 |
0 |
有条件跳转 |
PC |
0x58 |
0 |
1 |
获取程序计数器 |
MSIZE |
0x59 |
0 |
1 |
获取活动内存的大小 |
GAS |
0x5a |
0 |
1 |
获取可用气体的量 |
JUMPDEST |
0x5b |
0 |
0 |
无操作 |
viii. Push操作
关键字 |
操作码 |
输入 |
输出 |
描述 |
PUSH1 |
0x60 |
0 |
1 |
将1字节项放在堆栈上 |
PUSH2 |
0x61 |
0 |
1 |
将2字节项放在堆栈上 |
PUSH3 |
0x62 |
0 |
1 |
将3字节项放在堆栈上 |
PUSH4 |
0x63 |
0 |
1 |
将4字节项放在堆栈上 |
PUSH5 |
0x64 |
0 |
1 |
将5字节项放在堆栈上 |
PUSH6 |
0x65 |
0 |
1 |
将6字节项放在堆栈上 |
PUSH7 |
0x66 |
0 |
1 |
将7字节项放在堆栈上 |
PUSH8 |
0x67 |
0 |
1 |
将8字节项放在堆栈上 |
PUSH9 |
0x68 |
0 |
1 |
将9字节项放在堆栈上 |
PUSH10 |
0x69 |
0 |
1 |
将10字节项放在堆栈上 |
PUSH11 |
0x6a |
0 |
1 |
将11字节项放在堆栈上 |
PUSH12 |
0x6b |
0 |
1 |
将12字节项放在堆栈上 |
PUSH13 |
0x6c |
0 |
1 |
将13字节项放在堆栈上 |
PUSH14 |
0x6d |
0 |
1 |
将14字节项放在堆栈上 |
PUSH15 |
0x6e |
0 |
1 |
将15字节项放在堆栈上 |
PUSH16 |
0x6f |
0 |
1 |
将16字节项放在堆栈上 |
PUSH17 |
0x70 |
0 |
1 |
将17字节项放在堆栈上 |
PUSH18 |
0x71 |
0 |
1 |
将18字节项放在堆栈上 |
PUSH19 |
0x72 |
0 |
1 |
将19字节项放在堆栈上 |
PUSH20 |
0x73 |
0 |
1 |
将20字节项放在堆栈上 |
PUSH21 |
0x74 |
0 |
1 |
将21字节项放在堆栈上 |
PUSH22 |
0x75 |
0 |
1 |
将22字节项放在堆栈上 |
PUSH23 |
0x76 |
0 |
1 |
将23字节项放在堆栈上 |
PUSH24 |
0x77 |
0 |
1 |
将24字节项放在堆栈上 |
PUSH25 |
0x78 |
0 |
1 |
将25字节项放在堆栈上 |
PUSH26 |
0x79 |
0 |
1 |
将26字节项放在堆栈上 |
PUSH27 |
0x7a |
0 |
1 |
将27字节项放在堆栈上 |
PUSH28 |
0x7b |
0 |
1 |
将28字节项放在堆栈上 |
PUSH29 |
0x7c |
0 |
1 |
将29字节项放在堆栈上 |
PUSH30 |
0x7d |
0 |
1 |
将30字节项放在堆栈上 |
PUSH31 |
0x7e |
0 |
1 |
将31字节项放在堆栈上 |
PUSH32 |
0x7f |
0 |
1 |
将32字节项放在堆栈上 |
ix. 从堆栈复制第N个项目
关键字 |
操作码 |
输入 |
输出 |
描述 |
DUP1 |
0x80 |
1 |
2 |
在堆栈上复制第1条数据 |
DUP2 |
0x81 |
2 |
3 |
在堆栈上复制第2条数据 |
DUP3 |
0x82 |
3 |
4 |
在堆栈上复制第3条数据 |
DUP4 |
0x83 |
4 |
5 |
在堆栈上复制第4条数据 |
DUP5 |
0x84 |
5 |
6 |
在堆栈上复制第5条数据 |
DUP6 |
0x85 |
6 |
7 |
在堆栈上复制第6条数据 |
DUP7 |
0x86 |
7 |
8 |
在堆栈上复制第7条数据 |
DUP8 |
0x87 |
8 |
9 |
在堆栈上复制第8条数据 |
DUP9 |
0x88 |
9 |
10 |
在堆栈上复制第9条数据 |
DUP10 |
0x89 |
10 |
11 |
在堆栈上复制第10条数据 |
DUP11 |
0x8a |
11 |
12 |
在堆栈上复制第11条数据 |
DUP12 |
0x8b |
12 |
13 |
在堆栈上复制第12条数据 |
DUP13 |
0x8c |
13 |
14 |
在堆栈上复制第13条数据 |
DUP14 |
0x8d |
14 |
15 |
在堆栈上复制第14条数据 |
DUP15 |
0x8e |
15 |
16 |
在堆栈上复制第15条数据 |
DUP16 |
0x8f |
16 |
17 |
在堆栈上复制第16条数据 |
x. 使用顶部数据交换堆栈中的第N项数据
关键字 |
操作码 |
输入 |
输出 |
描述 |
SWAP1 |
0x90 |
2 |
2 |
堆栈的顶部数据和第2项数据交换 |
SWAP2 |
0x91 |
3 |
3 |
堆栈的顶部数据和第3项数据交换 |
SWAP3 |
0x92 |
4 |
4 |
堆栈的顶部数据和第4项数据交换 |
SWAP4 |
0x93 |
5 |
5 |
堆栈的顶部数据和第5项数据交换 |
SWAP5 |
0x94 |
6 |
6 |
堆栈的顶部数据和第6项数据交换 |
SWAP6 |
0x95 |
7 |
7 |
堆栈的顶部数据和第7项数据交换 |
SWAP7 |
0x96 |
8 |
8 |
堆栈的顶部数据和第8项数据交换 |
SWAP8 |
0x97 |
9 |
9 |
堆栈的顶部数据和第9项数据交换 |
SWAP9 |
0x98 |
10 |
10 |
堆栈的顶部数据和第10项数据交换 |
SWAP10 |
0x99 |
11 |
11 |
堆栈的顶部数据和第11项数据交换 |
SWAP11 |
0x9a |
12 |
12 |
堆栈的顶部数据和第12项数据交换 |
SWAP12 |
0x9b |
13 |
13 |
堆栈的顶部数据和第13项数据交换 |
SWAP13 |
0x9c |
14 |
14 |
堆栈的顶部数据和第14项数据交换 |
SWAP14 |
0x9d |
15 |
15 |
堆栈的顶部数据和第15项数据交换 |
SWAP15 |
0x9e |
16 |
16 |
堆栈的顶部数据和第16项数据交换 |
SWAP16 |
0x9f |
17 |
17 |
堆栈的顶部数据和第17项数据交换 |
i. 使用0..n标记记录一些地址的一些数据
关键字 |
操作码 |
输入 |
输出 |
描述 |
LOG0 |
0xa0 |
2 |
0 |
写日志 |
LOG1 |
0xa1 |
3 |
0 |
写日志 |
LOG2 |
0xa2 |
4 |
0 |
写日志 |
LOG3 |
0xa3 |
5 |
0 |
写日志 |
LOG4 |
0xa4 |
6 |
0 |
写日志 |
ii. 系统操作
关键字 |
操作码 |
输入 |
输出 |
描述 |
CREATE |
0xf0 |
3 |
1 |
创建具有关联代码的新帐户 |
CALL |
0xf1 |
7 |
1 |
消息呼叫到帐户 |
CALLCODE |
0xf2 |
7 |
1 |
调用自己,但是从TO参数而不是从自己的地址获取代码 |
RETURN |
0xf3 |
2 |
0 |
暂停执行返回输出数据 |
DELEGATECALL |
0xf4 |
6 |
1 |
在理念上类似于CALLCODE,除了它将发送者和值从父作用域传播到子作用域 |
SUICIDE |
0xff |
1 |
0 |
暂停执行并注册帐户以便稍后删除 |
3、 执行流程
a) 总体技术架构,借用李赫的图片
钱包客户端可以编写智能合约代码,通过本地solc程序编译成evm字节码,然后通过rpc接口发送到以太坊节点
各个以太坊节点通过本地evm虚拟机执行智能合约的二进制代码,得到运算结果后,就可以写入区块链数据。
b) Evm解析原理
解析指令使用的方法是译码分派(decode-and-dispatch)方式,它是围绕一个主循环来组织的,要解析一条指令,就将其分配到属于该指令类型的解析程序。其流程如下:
先创建个虚拟机vm,然后创建个程序program,举个例子,对应evm代码"6002600201",program指向的就是这段16进制数据,虚拟机执行的pc指针对应字符串"6002600301"的第一个字节60,在while循环里面,先读取一个操作码op,这里是60,含义是push1,操作就是压栈一个字节,代码实现就是执行step,pc指针指向02,通过sweep函数读取一个字节的数据,得到02,然后把02压栈;下一个循环中,读取了下一个字节60,含义还是push1,同理读取到数据03并压栈;再下一个循环中,读取了下一个字节01,含义是add,操作就是两个数相加,代码实现是连续出栈两个数,然后相加,然后压栈。因此程序"6002600301"实现的操作就是2+3=5,最终堆栈里面保存了数据5。总体来说就是一个循环,先读取一条指令,根据指令类型,继续读取数据或者操作堆栈数据,然后继续循环读取指令,做新的操作,最终执行完整个程序。
4、 问题
a) Solidity不是常用的高级语言,入门门槛高,相关代码和文档也很少
b) 支持图灵完备就面临死循环,递归调用问题,导致复杂度提升,是否有必要
5、 参考文档:
a) 以太坊(三)
b) 比特币脚本
c) 以太坊虚拟机与执行环境概述(英文).pdf