以太坊虚拟机EVM执行原理

以太坊虚拟机简介

  以太坊虚拟机(environment virtual machine,简称EVM),作用是将智能合约代码编译成可在以太坊上执行的机器码,并提供智能合约的运行环境。它是一个对外完全隔离的沙盒环境,在运行期间不能访问网络、文件,即使不同合约之间也有有限的访问权限。
  尽管区块链是可编程的,但是由于其脚本系统支持的功能有限,基于区块链做应用开发是一件很有难度的事情。而以太坊是基于区块链底层技术进行封装,完善,其中很重要的一个革新就是以太坊虚拟机及面向合约的高级编程语言solidity,这使得开发者可以专注于应用本身,更方便、快捷的开发去中心化应用程序,同时也大大降低了开发难度。

以太坊EVM的特点

  • EVM是一种基于栈的虚拟机(区别于基于寄存器的虚拟机),用于编译、执行智能合约
  • EVM是图灵完备的(图灵完备是指:具有无限存储能力的通用物理机器或编程语言,简单来说就是可以解决一切可计算的问题)
  • EVM是一个完全隔离的环境,在运行期间不能访问网络、文件,即使不同合约之间也有有限的访问权限
  • 操作数栈调用深度为1024
  • 机器码长度一个字节,最多可以有256个操作码

什么是基于栈的虚拟机

  以太坊虚拟机是一种基于栈的虚拟机,所以要弄清以太坊虚拟机原理,我们就必须了解什么是基于栈的虚拟机。首先我们来介绍下虚拟机需要实现的功能:

  • 取指令,其中指令来源于内存
  • 译码,决定指令类型(执行何种操作)。另外译码的过程要包括从内存中取操作数
  • 执行。指令译码后,被虚拟机执行(其实最终都会借助于物理机资源)

虚拟机分为两种:基于栈的虚拟机和基于寄存器的虚拟机。基于栈的虚拟机有几个重要的特性:实现简单、可移植,这也是为什么以太坊选择了基于栈的虚拟机。

在基于栈的虚拟机中,有个重要的概念:操作数栈,数据存取为先进先出。所有的操作都是直接与操作数栈直接交互,例如:取数据、存数据、执行操作等。这样有一个好处:可以无视具体的物理机器架构,特别是寄存器,但是缺点也很明显,速度慢,无论什么操作都需要经过操作数栈。

我们举个简单的例子来说明基于栈的虚拟机是如何执行操作的,例如我们需要执行a = b + c的运算,那么在基于栈的虚拟机上会编译生成类似于下面的字节码指令:

1
2
3
4
I2: LOAD b
I1: LOAD c
I3: ADD
I4: STORE a

具体的执行流程为:

  1. 从内存中加载变量b到操作数栈
  2. 从内存中加载变量c到操作数栈
  3. 从操作数栈弹出前两个元素,执行累加
  4. 将计算后的数值压入栈顶
  5. 将栈顶的数值取出放入内存中

以太坊虚拟机如何执行智能合约

上面我们简单介绍了基于栈的虚拟机是如何执行操作的,以太坊虚拟机的执行过程也是类似,我们来详细介绍下。以如下的智能合约为例

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.0;

contract test {

uint public c;

function add(uint a) public returns (uint){
uint b = 100;
c = a + b;
return c;
}

}

使用solc编译编译该文件,执行命令为:solc --asm test.sol,生成的字节码如下,示例使用的solc版本为0.4.25,solc版本不同编译后的字节码也可能会有所差异

接下来我们使用stack:[]表示操作数栈,左侧是栈顶,store:{}表示局部变量表。这里我们以add函数为例,来解析EVM执行过程,由于编译后的内容很多,我们只截取add函数对应的字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
======= test.sol:test =======
EVM assembly:
/* "test.sol":25:177 contract test {... */
mstore(0x40, 0x80)
callvalue
...
tag_1:
/* "test.sol":25:177 contract test {... */
pop
dataSize(sub_0)
dup1
dataOffset(sub_0)
0x0
codecopy
0x0
return
stop

sub_0: assembly {
...
/* "test.sol":66:174 function add(uint a) public returns (uint){... */
tag_6:
/* "test.sol":103:107 uint */
// push 0 到栈的第1位,stack:[0]
0x0
/* "test.sol":118:124 uint b */
// 复制栈的第1位压入栈顶,stack:[0, 0]
dup1
/* "test.sol":127:130 100 */
// push 100 到栈的第1位,stack:[100, 0, 0]
0x64
/* "test.sol":118:130 uint b = 100 */
// 将第2位和栈顶元素互换,stack:[0, 100, 0]
swap1
// 弹出栈顶元素,stack:[100, 0]
pop
/* "test.sol":148:149 b */
// 复制栈的第1位压入栈顶,stack:[100, 100, 0]
dup1
/* "test.sol":144:145 a */
// a的值在运行时才能确定
// 复制栈的第4位压入栈顶,stack:[x, 100, 100, 0]
dup4
/* "test.sol":144:149 a + b */
// 取出栈顶的2个元素执行add操作,将结果压入栈顶,stack:[x+100, 100, 0]
add
/* "test.sol":140:141 c */
// push 0 到栈的第1位,stack:[0, x+100, 100, 0]
0x0
/* "test.sol":140:149 c = a + b */
// 复制栈的第2位压入栈顶,stack:[x+100, 0, x+100, 100, 0]
dup2
// 将第2位和栈顶元素互换,stack:[0, x+100, x+100, 100, 0]
swap1
// 取出栈顶前2位放入局部变量表中,stack:[x+100, 100, 0], store:{0: x+100}
sstore
// 弹出栈顶元素,stack:[100, 0]
pop
/* "test.sol":166:167 c */
// 从局部变量表中取出第1个元素压入栈顶,stack:[x+100, 100, 0]
sload(0x0)
/* "test.sol":159:167 return c */
// 将第3位和栈顶元素互换,stack:[0, 100, x+100]
swap2
// 弹出栈顶元素,stack:[100, x+100]
pop
/* "test.sol":66:174 function add(uint a) public returns (uint){... */
// 弹出栈顶元素,stack:[x+100]
pop
swap2
swap1
pop
jump // out
/* "test.sol":46:59 uint public c */
tag_9:
sload(0x0)
dup2
jump // out

auxdata: 0xa165627a7a723058208ca38ce847598da0c3a86f7e...
}

以太坊虚拟机字节码

在以太坊EVM中,字节码长度被限定在一个字节以内,也就是说最多可以有256个操作码,目前已经定义了144个操作码,还有100多个操作码可以扩展。
完整的操作码可以查看 以太坊EVM操作码
各操作码对应的指令可以查看 以太坊EVM操作码详细指令

为了方便查看,将部分指令的弹栈数、压栈数、Gas消耗整理为一行,左侧是操作码的字节码,对应的数组第一个值是操作码,第二个为弹栈数,第三个为压栈数,第四个为Gas消耗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
opcodes = {
0x00: ['STOP', 0, 0, 0],
0x01: ['ADD', 2, 1, 3],
0x02: ['MUL', 2, 1, 5],
0x03: ['SUB', 2, 1, 3],
0x04: ['DIV', 2, 1, 5],
0x05: ['SDIV', 2, 1, 5],
0x06: ['MOD', 2, 1, 5],
0x07: ['SMOD', 2, 1, 5],
0x08: ['ADDMOD', 3, 1, 8],
0x09: ['MULMOD', 3, 1, 8],
0x0a: ['EXP', 2, 1, 10],
0x15: ['ISZERO', 1, 1, 3],
0x16: ['AND', 2, 1, 3],
0x17: ['OR', 2, 1, 3],
0x18: ['XOR', 2, 1, 3],
0x19: ['NOT', 1, 1, 3],
0x1a: ['BYTE', 2, 1, 3],
0x20: ['SHA3', 2, 1, 30],
0x30: ['ADDRESS', 0, 1, 2],
0x31: ['BALANCE', 1, 1, 20], # now 400
0x32: ['ORIGIN', 0, 1, 2],
0x33: ['CALLER', 0, 1, 2],
0x34: ['CALLVALUE', 0, 1, 2],
0x35: ['CALLDATALOAD', 1, 1, 3],
0x36: ['CALLDATASIZE', 0, 1, 2],
0x37: ['CALLDATACOPY', 3, 0, 3],
0x38: ['CODESIZE', 0, 1, 2],
0x39: ['CODECOPY', 3, 0, 3],
0x3a: ['GASPRICE', 0, 1, 2],
0x3d: ['RETURNDATASIZE', 0, 1, 2],
0x3e: ['RETURNDATACOPY', 3, 0, 3],
0x40: ['BLOCKHASH', 1, 1, 20],
0x41: ['COINBASE', 0, 1, 2],
0x42: ['TIMESTAMP', 0, 1, 2],
0x43: ['NUMBER', 0, 1, 2],
0x44: ['DIFFICULTY', 0, 1, 2],
0x45: ['GASLIMIT', 0, 1, 2],
0x50: ['POP', 1, 0, 2], // 从栈顶弹出一个元素
0x51: ['MLOAD', 1, 1, 3],
0x52: ['MSTORE', 2, 0, 3],
0x53: ['MSTORE8', 2, 0, 3],
0x54: ['SLOAD', 1, 1, 50],
0x55: ['SSTORE', 2, 0, 0],
0x56: ['JUMP', 1, 0, 8],
0x57: ['JUMPI', 2, 0, 10],
0x58: ['PC', 0, 1, 2],
0x59: ['MSIZE', 0, 1, 2],
0x5a: ['GAS', 0, 1, 2],
0x5b: ['JUMPDEST', 0, 0, 1],
0x60: ['PUSH1', 0, 1, 3], // 把第i个元素压入栈顶
......
0x7f: ['PUSH32', 0, 1, 3],
0x80: ['DUP1', 1, 2, 3], // 把第i个元素复制一份压入栈顶
...... z
0x8f: ['DUP32', 16, 17, 3],
0x90: ['SWAP1', 2, 2, 3], // 将栈顶的元素和第i+1个元素进行交换
......
0x9f: ['SWAP32', 17, 17, 3],
0xa0: ['LOG0', 2, 0, 375],
0xa1: ['LOG1', 3, 0, 750],
0xa2: ['LOG2', 4, 0, 1125],
0xa3: ['LOG3', 5, 0, 1500],
0xa4: ['LOG4', 6, 0, 1875],
}

以操作码add(0x1)为例:0x01: [‘ADD’, 2, 1, 3]`,具体含义如下

1
2
3
4
0x01 表示操作码对应的数值,
2 表示从栈顶弹出的元素个数
1 表示计算完之后压栈数
3 表示执行该操作需要花费gas数

JouyPub wechat
欢迎订阅「K叔区块链」 - 专注于区块链技术学习