以太坊虛擬機(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數