EVM原理及其功能擴充
EVM運作機制概述
EVM即以太坊虛拟機,用于執行智能合約。智能合約可用進階開發語言Solidity進行開發,合約源代碼經過編譯得到可在EVM中運作的位元組碼。在部署合約、與合約互動的時候,位元組碼都是以16進制字元串形式傳遞和展現。
EVM運作過程中,其本身并不是一個獨立的協程、線程更不是程序,它隻是交易處理的一部分,在交易處理過程中以函數方式被調用。
調用路徑為:StateProcessor.Process --> core.ApplyTransaction(初始化evm對象) --> StateTransition.TransitionDb() --> 根據交易類型執行 evm.Create 或 evm.Call。
在evm.Create中會執行相關驗證、轉賬、初始化Contract對象并調用 run 函數開始執行合約代碼,執行成功後獲得傳回值也就是要存儲到鍊上的合約代碼,将傳回值存儲到鍊上。
而evm.Call既是調用入口,同時它本身也是一個可遞歸的函數,在合約位元組碼中指令 0xf1 即代表CALL操作,在CALL操作中會遞歸調用evm.Call。在evm.Call中會執行驗證及轉賬、從資料庫擷取合約代碼初始化Contract對象、并調用 run 函數開始執行合約代碼這些處理。
重要:上述"從資料庫擷取合約代碼初始化Contract對象"意味着,給合約賬戶進行轉賬時,合約賬戶所關聯的合約代碼将會被執行。
在合約代碼中,會固定包含檢查轉賬金額的邏輯,如果金額大于,則會執行合約的fallback函數。
如果合約沒有fallback函數,或者fallback函數沒有payable修飾符,則代碼執行會抛異常,進而交易失敗。
EVM是基于棧的虛拟機,另外會有一個記憶體空間用于臨時存儲資料,而最終大部分的指令都是對棧中的資料進行操作。
EVM資料存儲概述
合約狀态值(狀态變量、狀态常量,即需要持久化的内容)在底層資料庫中的存儲方式,可用幾條規則概括:
- 參照錄音帶存儲原理,可以認為一個合約對應了一條無限長的錄音帶,錄音帶上以32位元組為機關,擁有無數個存儲槽;每個存儲槽的位置就是它的key,也是用32位元組表示。
- 對于簡單的,大小在32位元組以内的變量,以定義變量的順序作為它的key來存儲變量值。即第一個變量的key為0,第二個變量的key為1,……
- 結構體和定長數組也是順序存儲(隻要每個值都是32位元組以内的),比如結構體變量定義在位置1,結構體内部要兩個成員,則這兩個成員的key依序為 1和2。數組類似,隻是在處理數組時編譯器會多加一些邊界檢查的代碼。
- 連續的若幹個小的值,可能被優化為存儲的同一個位置,比如:合約中前四個狀态變量都是uint64類型的,則四個狀态變量的值會被打包成一個32位元組的值存儲在0位置。
- map中内容的存儲,如果map中的value在32位元組以内,則會按以下公式得到資料庫中的key:keccak256(bytes32(map中的key)+bytes32(map變量的位置)); 例如,一個map變量在合約中最先定義,map中一個key為"abc",則其在資料庫中的存儲位置為:keccak256(bytes32("abc")+bytes32(0))
- 如果map中的value是一個複雜類型,存儲需求超過32位元組,則會按上述公式得到第一個存儲位置,然後依序加1得到後續資料的存儲位置;
- 可變長度數組,與map類似,但更複雜點:以數組變量所在位置為key,存儲數組的長度。然後從keccak256(bytes32(position))開始存儲數組中的元素;
- 可變長度位元組數組和字元串一樣:如果長度小于等于31位元組,則直接在變量位置處存儲字元串值,并用值的最後一個位元組存儲字元串的編碼長度。編碼長度 = 字元數 * 2 。比如,"abc"的存儲值為 "0x6162630000000000000000000000000000000000000000000000000000000006"
- 當可變長度位元組數組或字元串長度大于31位元組時,變量位置存儲的是 編碼長度,而此時編碼長度公式變為 編碼長度 = 字元數 * 2 + 1 。 然後,從 keccak256(bytes32(position))位置,使用連續的若幹個存儲槽存儲字元串值。
進而,對于字元串,如果編碼長度是奇數,則代表的是長字元串,如果是偶數則代表不超過31位元組的字元串。
EVM代碼結構
evm的代碼都在core包裡面,除了入口相關的一些代碼,具體運作合約的代碼都在core/vm包下
代碼檔案或結構體 | 說明 |
---|---|
evm.go | 定義了EVM運作環境結構體,并實作 轉賬處理 這些比較進階的,跟交易本身有關的功能 |
vm/evm.go | 定義了EVM結構體,提供Create和Call方法,作為虛拟機的入口,分别對應建立合約和執行合約代碼 |
vm/interpreter.go | 虛拟機的排程器,開始真正的解析執行合約代碼 |
vm/opcodes.go | 定義了虛拟機指令碼(操作碼) |
vm/instructions.go | 絕大部分的操作碼對應的實作都在這裡 |
vm/gas_table.go | 絕大部分操作碼所需的gas都在這裡計算 |
vm/jump_table.go | 定義了operation,就是将opcode和gas計算函數、具體實作函數等關聯起來 |
vm/stack.go | evm所需要的棧 |
vm/memory.go | evm的記憶體結構 |
vm/intpool.go | *big.Int的池子,主要是性能上的考慮,跟業務邏輯無關 |
evm的opcode,大體上可以粗略地分為兩類,一類是基礎操作,如壓棧、出棧、加減乘除等數學運算、邏輯比較、hash等等;另一類是跟交易業務密切相關的,可以稱為業務指令,比如BALANCE、ADDRESS、CALLER、CALL等等,這些指令有些對應了Solidity中的全局函數或屬性。
通過Solidity編寫的合約,需要進行編譯,編譯後變成可供虛拟機執行的二進制碼,這些二進制碼實際上在所有地方都按16進制位元組數組或字元串表示。
編譯後二進制碼的結構
以一個最簡單的無實際功能無構造器的合約為例,看看編譯後代碼的結構。 合約代碼:
pragma solidity ^0.4.11;
contract C {
}
編譯後得到這樣一串資料:
60606040523415600e57600080fd5b5b603680601c6000396000f30060606040525b600080fd00a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029
這串資料需要作為16進制格式顯示的位元組數組對待,這就是evm需要執行的代碼。
上面這串代碼可分為三部分,即:部署代碼、合約代碼、Auxdata。
- 部署代碼 在建立合約的時候,evm.Create會先建立合約賬戶,然後運作部署代碼,運作完成後,它會将 合約代碼+Auxdata 作為傳回值傳回,然後evm.Create函數中就會将傳回值跟合約賬戶關聯起來存儲在區塊鍊上,這樣就完成了合約的部署。 上述代碼中,部署代碼為前面的
60606040523415600e57600080fd5b5b603680601c6000396000f300
- 合約代碼 在這個合約中,合約代碼隻有11位元組:
這部分的代碼就是存儲在鍊上,供後續調用的代碼。
60606040525b600080fd00
- Auxdata 每個合約最後面的43位元組,就是Auxdata,會跟在合約代碼後面被存儲起來。即
由于後續跟合約互動,是需要知道合約的ABI(應用二進制接口,也就是合約接口的描述資訊)的,而在鍊上卻沒有存儲ABI資訊, 是以,要麼就隻能在部署合約時,使用者自己儲存好ABI以及合約位址(以太坊錢包就有這樣的功能,幫我們儲存了這些資訊),但這樣的話就隻有自己能調用這個合約,别人不知道ABI就不能調用合約。 想要讓别人也能調用我們部署的合約,在以太坊中提供了兩種機制,一個就是使用者直接将相關資訊上傳到etherscan.io這個網站跟合約關聯起來,另一個就是以太坊的swarm網絡。 而這裡的Auxdata,就是給swarm網絡使用的,可以認為就是swarm網絡的位址,也就是以太坊希望後續自動将合約的相關資訊包含ABI存儲到swarm中,這樣任何一個人從區塊上查詢到合約代碼後,就可以通過auxdata到swarm網絡中下載下傳合約的資訊。 Auxdata的固定格式為:
a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029
0xa1 0x65 'b' 'z' 'z' 'r' '0' 0x58 0x20 <32 bytes swarm hash> 0x00 0x29
- 構造函數參數 如果合約有構造函數參數,則建立合約時還需要跟上編碼之後的參數,代碼就變成如下結構:部署代碼+合約代碼+Auxdata+構造參數 構造參數的編碼方式,就是将參數值按順序編碼成32位元組的資料,連接配接起來。不同的類型有不同的規則,具體見Solidity文檔。
功能擴充
evm是以太坊合約的執行部分,想要從合約程式設計語言層面擴充功能,就需要同時在Solidity上和evm上實作擴充。Solidity上實作功能擴充,最重要的就是弄清楚它的編譯過程。 Solidity是面向使用者的進階程式設計語言,其中的一句代碼,可能編譯後就對應了一堆位元組碼。 真正要擴充功能,主要涉及以下幾點:
- 增加opcode,進而與已有的opcode做功能上的區分,也就是擴充了指令
- 需要在Solidity語言中,提供供使用者調用的接口,比如對Solidity中的address對象,增加address.balanceOf(bytes10 symbol)函數;
- 在Solidity編譯器中,支援上述擴充的接口的識别,并編譯成一段正确的指令組合。這段指令組合從邏輯上可以認為由三部分組成:新擴充指令所需參數的準備階段+新擴充的指令+對結果的處理
- 在EVM中增加新擴充的指令的功能支援。
以下幾點,有助于了解如何進行功能擴充:
- evm是基于棧的,用一個位元組值表示指令。是以evm中指令的個數最多隻有0x0~0xff共256個。是以任何一個指令,都有如下一些屬性:(1)指令碼;(2)該指令需要消耗棧頂多少個元素;(3)該指令執行完後,會往棧裡壓入幾個元素;(4)其他一些屬性,如針對PUSH指令,它所額外需要的memory中的元素數量,以及它是否除了對棧的操作外,還産生其他的影響等。
- 需要定義清楚指令所需棧頂的若幹個元素的順序,棧頂值代表什麼,第二個值代表什麼,在執行到這個指令之前,所有資料需要在棧中就緒;也就是,編譯過程不能将進階語言的一條語句,對應成底層的一個指令,而應該是一堆指令。
在evm中擴充功能,相對比較簡單,具體就是:
- 在opcode中定義指令碼
- 在instructions中實作指令的處理,處理就是按照指令定義從棧頂取需要的資料,然後将結果壓入棧頂。
- 在gas_table中提供gas函數
- 在jump_table中增加由上述内容組合成的operation
參考
深入了解以太坊虛拟機