1. 程式人生 > >圖解以太坊虛擬機器EVM

圖解以太坊虛擬機器EVM

今天聊一聊以太坊虛擬機器的原理。

以太坊虛擬機器,簡稱EVM,是用來執行以太坊上的交易的。業務流程參見下圖:
在這裡插入圖片描述
輸入一筆交易,內部會轉換成一個Message物件,傳入EVM執行。

如果是一筆普通轉賬交易,那麼直接修改StateDB中對應的賬戶餘額即可。

如果是智慧合約的建立或者呼叫,則通過EVM中的直譯器載入和執行位元組碼,執行過程中可能會查詢或者修改StateDB。

1.固定油費(Intrinsic Gas)

每筆交易過來,不管三七二十一先需要收取一筆固定油費,計算方法如下:
在這裡插入圖片描述
如果你的交易不帶額外資料(Payload),比如普通轉賬,那麼需要收取21000的油費。

如果你的交易攜帶額外資料,那麼這部分資料也是需要收費的,具體來說是按位元組收費:位元組為0的收4塊,位元組不為0收68塊,所以你會看到很多做合約優化的,目的就是減少資料中不為0的位元組數量,從而降低油費消耗。

2.生成Contract物件

交易會被轉換成一個Message物件傳入EVM,而EVM則會根據Message生成一個Contract物件以便後續執行:
在這裡插入圖片描述
可以看到,Contract中會根據合約地址,從StateDB中載入對應的程式碼,後面就可以送入直譯器執行了。

另外,執行合約能夠消耗的油費有一個上限,就是節點配置的每個區塊能夠容納的GasLimit。

3.送入直譯器執行

程式碼跟輸入都有了,就可以送入直譯器執行了。EVM是基於棧的虛擬機器,直譯器中需要操作四大元件:

  • PC:類似於CPU中的PC暫存器,指向當前執行的指令
  • Stack:執行堆疊,位寬為256 bits,最大深度為1024
  • Memory:記憶體空間
  • Gas:油費池,耗光郵費則交易執行失敗

在這裡插入圖片描述
具體解釋執行的流程參見下圖:
在這裡插入圖片描述
EVM的每條指令稱為一個OpCode,佔用一個位元組,所以指令集最多不超過256,具體描述參見:https://ethervm.io。比如下圖就是一個示例(PUSH1=0x60, MSTORE=0x52):
在這裡插入圖片描述
首先PC會從合約程式碼中讀取一個OpCode,然後從一個JumpTable中檢索出對應的operation,也就是與其相關聯的函式集合。接下來會計算該操作需要消耗的油費,如果油費耗光則執行失敗,返回ErrOutOfGas錯誤。如果油費充足,則呼叫execute()執行該指令,根據指令型別的不同,會分別對Stack、Memory或者StateDB進行讀寫操作。

4.呼叫合約函式

前面分析完了EVM解釋執行的主要流程,可能有些同學會問:那麼EVM怎麼知道交易想呼叫的是合約裡的哪個函式呢?別急,前面提到跟合約程式碼一起送到直譯器裡的還有一個Input,而這個Input資料是由交易提供的。
在這裡插入圖片描述
Input資料通常分為兩個部分:

前面4個位元組被稱為“4-byte signature”,是某個函式簽名的Keccak雜湊值的前4個位元組,作為該函式的唯一標識。(可以在該網站查詢目前所有的函式簽名:https://www.4byte.directory

後面跟的就是呼叫該函式需要提供的引數了,長度不定。

舉個例子:我在部署完A合約後,呼叫add(1)對應的Input資料是0x87db03b70000000000000000000000000000000000000000000000000000000000000001

而在我們編譯智慧合約的時候,編譯器會自動在生成的位元組碼的最前面增加一段函式選擇邏輯:

首先通過CALLDATALOAD指令將“4-byte signature”壓入堆疊中,然後依次跟該合約中包含的函式進行比對,如果匹配則呼叫JUMPI指令跳入該段程式碼繼續執行。

這麼講可能有點抽象,我們可以看一看上圖中的合約對應的反彙編程式碼就一目瞭然了:
在這裡插入圖片描述
在這裡插入圖片描述
這裡提到了CALLDATALOAD,就順便講一下資料載入相關的指令,一共有4種:

  • CALLDATALOAD:把輸入資料載入到Stack中
  • CALLDATACOPY:把輸入資料載入到Memory中
  • CODECOPY:把當前合約程式碼拷貝到Memory中
  • EXTCODECOPY:把外部合約程式碼拷貝到Memory中

最後一個EXTCODECOPY不太常用,一般是為了審計第三方合約的位元組碼是否符合規範,消耗的gas一般也比較多。這些指令對應的操作如下圖所示:
在這裡插入圖片描述

5.合約呼叫合約

合約內部呼叫另外一個合約,有4種呼叫方式:

  • CALL
  • CALLCODE
  • DELEGATECALL
  • STATICALL

後面會專門寫篇文章比較它們的異同,這裡先以最簡單的CALL為例,呼叫流程如下圖所示:
在這裡插入圖片描述
可以看到,呼叫者把呼叫引數儲存在記憶體中,然後執行CALL指令。

CALL指令執行時會建立新的Contract物件,並以記憶體中的呼叫引數作為其Input。

直譯器會為新合約的執行建立新的Stack和Memory,從而不會破環原合約的執行環境。

新合約執行完成後,通過RETURN指令把執行結果寫入之前指定的記憶體地址,然後原合約繼續向後執行。

6.建立合約

前面都是討論的合約呼叫,那麼建立合約的流程時怎麼樣的呢?

如果某一筆交易的to地址為nil,則表明該交易是用於建立智慧合約的。

首先需要建立合約地址,採用下面的計算公式:Keccak(RLP(call_addr, nonce))[:12]。也就是說,對交易發起人的地址和nonce進行RLP編碼,再算出Keccak雜湊值,取後20個位元組作為該合約的地址。

下一步就是根據合約地址建立對應的stateObject,然後儲存交易中包含的合約程式碼。該合約的所有狀態變化會儲存在一個storage trie中,最終以Key-Value的形式儲存到StateDB中。程式碼一經儲存則無法改變,而storage trie中的內容則是可以通過呼叫合約進行修改的,比如通過SSTORE指令。
在這裡插入圖片描述

7.油費計算

最後囉嗦一下油費的計算,計算公式基本上是根據以太坊黃皮書中的定義:http://gavwood.com/paper.pdf
在這裡插入圖片描述
當然你可以直接read the fucking code,程式碼位於core/vm/gas.go和core/vm/gas_table.go中。

好,今天就聊到這裡吧。

更多文章歡迎關注“鑫鑫點燈”專欄:https://blog.csdn.net/turkeycock
或關注飛久微信公眾號:
在這裡插入圖片描述