以太坊虚拟机(environment virtual machine,简称EVM),做用是将智能合约代码编译成可在以太坊上执行的机器码,并提供智能合约的运行环境。它是一个对外彻底隔离的沙盒环境,在运行期间不能访问网络、文件,即便不一样合约之间也有有限的访问权限。
尽管区块链是可编程的,可是因为其脚本系统支持的功能有限,基于区块链作应用开发是一件颇有难度的事情。而以太坊是基于区块链底层技术进行封装,完善,其中很重要的一个革新就是以太坊虚拟机及面向合约的高级编程语言solidity,这使得开发者能够专一于应用自己,更方便、快捷的开发去中心化应用程序,同时也大大下降了开发难度。git
以太坊虚拟机是一种基于栈的虚拟机,因此要弄清以太坊虚拟机原理,咱们就必须了解什么是基于栈的虚拟机。首先咱们来介绍下虚拟机须要实现的功能:github
虚拟机分为两种:基于栈的虚拟机和基于寄存器的虚拟机。基于栈的虚拟机有几个重要的特性:实现简单、可移植,这也是为何以太坊选择了基于栈的虚拟机。编程
在基于栈的虚拟机中,有个重要的概念:操做数栈,数据存取为先进先出。全部的操做都是直接与操做数栈直接交互,例如:取数据、存数据、执行操做等。这样有一个好处:能够无视具体的物理机器架构,特别是寄存器,可是缺点也很明显,速度慢,不管什么操做都须要通过操做数栈。segmentfault
咱们举个简单的例子来讲明基于栈的虚拟机是如何执行操做的,例如咱们须要执行a = b + c
的运算,那么在基于栈的虚拟机上会编译生成相似于下面的字节码指令:数组
I2: LOAD b I1: LOAD c I3: ADD I4: STORE a
具体的执行流程为:网络
上面咱们简单介绍了基于栈的虚拟机是如何执行操做的,以太坊虚拟机的执行过程也是相似,咱们来详细介绍下。以以下的智能合约为例架构
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函数对应的字节码函数
======= 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消耗
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]`,具体含义以下
0x01 表示操做码对应的数值, 2 表示从栈顶弹出的元素个数 1 表示计算完以后压栈数 3 表示执行该操做须要花费gas数