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