做爲一門面向智能合約的語言,Solidity與其餘經典語言既有差別也有類似之處。java
一方面,服務於區塊鏈的屬性使其與其餘語言存在差別。例如,合約的部署與調用均要通過區塊鏈網絡確認;執行成本須要被嚴格控制,以防止惡意代碼消耗節點資源。mysql
另外一方面,身爲編程語言,Solidity的實現並未脫離經典語言,好比Solidity中包含相似棧、堆的設計,採用棧式虛擬機來進行字節碼處理。git
本系列前幾篇文章介紹瞭如何開發Solidity程序,爲了讓讀者知其然更知其因此然,本文將進一步介紹Solidity的內部運行原理,聚焦於Solidity程序的生命週期和EVM工做機制。github
與其餘語言同樣,Solidity的代碼生命週期離不開編譯、部署、執行、銷燬這四個階段。下圖整理展示了Solidity程序的完整生命週期: 算法
經編譯後,Solidity文件會生成字節碼。這是一種相似jvm字節碼的代碼。部署時,字節碼與構造參數會被構建成交易,這筆交易會被打包到區塊中,經由網絡共識過程,最後在各區塊鏈節點上構建合約,並將合約地址返還用戶。sql
當用戶準備調用該合約上的函數時,調用請求一樣也會經歷交易、區塊、共識的過程,最終在各節點上由EVM虛擬機來執行。編程
下面是一個示例程序,咱們經過remix探索它的生命週期。數組
pragma solidity ^0.4.25; contract Demo{ uint private _state; constructor(uint state){ _state = state; } function set(uint state) public { _state = state; } }
源代碼編譯完後,能夠經過ByteCode按鈕獲得它的二進制:緩存
608060405234801561001057600080fd5b506040516020806100ed83398101806040528101908080519060200190929190
還能夠獲得對應的字節碼(OpCode):微信
PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0x10 JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x40 MLOAD PUSH1 0x20 DUP1 PUSH2 0xED DUP4 CODECOPY DUP2 ADD DUP1 PUSH1 0x40 MSTORE DUP2 ADD SWAP1 DUP1 DUP1 MLOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP DUP1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP POP PUSH1 0xA4 DUP1 PUSH2 0x49 PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN STOP PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x4 CALLDATASIZE LT PUSH1 0x3F JUMPI PUSH1 0x0 CALLDATALOAD PUSH29 0x100000000000000000000000000000000000000000000000000000000 SWAP1 DIV PUSH4 0xFFFFFFFF AND DUP1 PUSH4 0x60FE47B1 EQ PUSH1 0x44 JUMPI JUMPDEST PUSH1 0x0 DUP1 REVERT JUMPDEST CALLVALUE DUP1 ISZERO PUSH1 0x4F JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x6C PUSH1 0x4 DUP1 CALLDATASIZE SUB DUP2 ADD SWAP1 DUP1 DUP1 CALLDATALOAD SWAP1 PUSH1 0x20 ADD SWAP1 SWAP3 SWAP2 SWAP1 POP POP POP PUSH1 0x6E JUMP JUMPDEST STOP JUMPDEST DUP1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP POP JUMP STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 0x4e 0xd9 MOD DIFFICULTY 0x4c 0xc4 0xc9 0xaa 0xbd XOR EXTCODECOPY MSTORE 0xb2 0xd4 DUP7 0xdf 0xc5 0xde 0xa9 DUP1 SLT PUSH1 0xC3 CALLDATACOPY XOR 0x5d 0xad KECCAK256 0xe1 0x1f DUP2 SHL STOP 0x29
其中下述指令集爲set函數對應的代碼,後面會解釋set函數如何運行。
JUMPDEST DUP1 PUSH1 0x0 DUP2 SWAP1 SSTORE POP POP JUMP STOP
編譯完後,便可在remix上對代碼進行部署,構造參數傳入0x123:
部署成功後,可獲得一條交易回執:
點開input,能夠看到具體的交易輸入數據:
上面這段數據中,標黃的部分正好是前文中的合約二進制;而標紫的部分,則對應了傳入的構造參數0x123。
這些都代表,合約部署以交易做爲介質。結合區塊鏈交易知識,咱們能夠還原出整個部署過程:
客戶端將部署請求(合約二進制,構造參數)做爲交易的輸入數據,以此構造出一筆交易
交易通過rlp編碼,而後由發送者進行私鑰簽名
已簽名的交易被推送到區塊鏈上的節點
區塊鏈節點驗證交易後,存入交易池
輪到該節點出塊時,打包交易構建區塊,廣播給其餘節點
其餘節點驗證區塊並取得共識。不一樣區塊鏈可能採用不一樣共識算法,FISCO BCOS中採用PBFT取得共識,這要求經歷三階段提交(pre-prepare,prepare, commit)
節點執行交易,結果就是智能合約Demo被建立,狀態字段_state的存儲空間被分配,並被初始化爲0x123
根據是否帶有修飾符view,咱們可將函數分爲兩類:調用與交易。因爲在編譯期就肯定了調用不會引發合約狀態的變動,故對於這類函數調用,節點直接提供查詢便可,無需與其餘區塊鏈節點確認。而因爲交易可能引發狀態變動,故會在網絡間確認。
下面將以用戶調用了set(0x10)爲假設,看看具體的運行過程。
首先,函數set沒有配置view/pure修飾符,這意味着其可能更改合約狀態。因此這個調用信息會被放入一筆交易,經由交易編碼、交易簽名、交易推送、交易池緩存、打包出塊、網絡共識等過程,最終被交由各節點的EVM執行。
在EVM中,由SSTORE字節碼將參數0xa存儲到合約字段_state中。該字節碼先從棧上拿到狀態字段_state的地址與新值0xa,隨後完成實際存儲。
下圖展現了運行過程:
這裏僅粗略介紹了set(0xa)是如何運行,下節將進一步展開介紹EVM的工做機制以及數據存儲機制。
因爲合約上鍊後就沒法篡改,因此合約生命可持續到底層區塊鏈被完全關停。若要手動銷燬合約,可經過字節碼selfdestruct。銷燬合約也須要進行交易確認,在此很少做贅述。
在前文中,咱們介紹了Solidity程序的運行原理。通過交易確認後,最終由EVM執行字節碼。對EVM,上文只是一筆帶過,這一節將具體介紹其工做機制。
EVM是棧式虛擬機,其核心特徵就是全部操做數都會被存儲在棧上。下面咱們將經過一段簡單的Solidity語句代碼看看其運行原理:
uint a = 1; uint b = 2; uint c = a + b;
這段代碼通過編譯後,獲得的字節碼以下:
PUSH1 0x1 PUSH1 0x2 ADD
爲了讀者更好了解其概念,這裏精簡爲上述3條語句,但實際的字節碼可能更復雜,且會摻雜SWAP和DUP之類的語句。
咱們能夠看到,在上述代碼中,包含兩個指令:PUSH1和ADD,它們的含義以下:
PUSH1:將數據壓入棧頂。
ADD:POP兩個棧頂元素,將它們相加,並壓回棧頂。
這裏用半動畫的方式解釋其執行過程。下圖中,sp表示棧頂指針,pc表示程序計數器。當執行完push1 0x1後,pc和sp均往下移:
相似地,執行push1 0x2後,pc和sp狀態以下:
最後,當add執行完後,棧頂的兩個操做數都被彈出做爲add指令的輸入,二者的和則會被壓入棧:
在開發過程當中,咱們常會遇到使人迷惑的memory修飾符;閱讀開源代碼時,也會看到各類直接針對內存進行的assembly操做。不瞭解存儲機制的開發者遇到這些狀況就會一頭霧水,因此,這節將探究EVM的存儲原理。
在前文《智能合約編寫之Solidity的基礎特性》中咱們介紹過,一段Solidity代碼,一般會涉及到局部變量、合約狀態變量。
而這些變量的存儲方式存在差異,下面代碼代表了變量與存儲方式之間的關係。
contract Demo{ //狀態存儲 uint private _state; function set(uint state) public { //棧存儲 uint i = 0; //內存存儲 string memory str = "aaa"; } }
棧用於存儲字節碼指令的操做數。在Solidity中,局部變量如果整型、定長字節數組等類型,就會隨着指令的運行入棧、出棧。
例如,在下面這條簡單的語句中,變量值1會被讀出,經過PUSH操做壓入棧頂:
uint i = 1;
對於這類變量,沒法強行改變它們的存儲方式,若是在它們以前放置memory修飾符,編譯會報錯。
內存相似java中的堆,它用於儲存"對象"。在Solidity編程中,若是一個局部變量屬於變長字節數組、字符串、結構體等類型,其一般會被memory修飾符修飾,以代表存儲在內存中。
本節中,咱們將以字符串爲例,分析內存如何存儲這些對象。
1. 對象存儲結構
下面將用assembly語句對複雜對象的存儲方式進行分析。
assembly語句用於調用字節碼操做。mload指令將被用於對這些字節碼進行調用。mload(p)表示從地址p讀取32字節的數據。開發者可將對象變量看做指針直接傳入mload。
在下面代碼中,通過mload調用,data變量保存了字符串str在內存中的前32字節。
string memory str = "aaa"; bytes32 data; assembly{ data := mload(str) }
掌握mload,便可用此分析string變量是如何存儲的。下面的代碼將揭示字符串數據的存儲方式:
function strStorage() public view returns(bytes32, bytes32){ string memory str = "你好"; bytes32 data; bytes32 data2; assembly{ data := mload(str) data2 := mload(add(str, 0x20)) } return (data, data2); }
data變量表示str的0~31字節,data2表示str的32~63字節。運行strStorage函數的結果以下:
0: bytes32: 0x0000000000000000000000000000000000000000000000000000000000000006 1: bytes32: 0xe4bda0e5a5bd0000000000000000000000000000000000000000000000000000
能夠看到,第一個數據字獲得的值爲6,正好是字符串"你好"經UTF-8編碼後的字節數。第二個數據字則保存的是"你好"自己的UTF-8編碼。
熟練掌握了字符串的存儲格式以後,咱們就能夠運用assembly修改、拷貝、拼接字符串。讀者可搜索Solidity的字符串庫,瞭解如何實現string的concat。
2. 內存分配方式
既然內存用於存儲對象,就必然涉及到內存分配方式。
memory的分配方式很是簡單,就是順序分配。下面咱們將分配兩個對象,並查看它們的地址:
function memAlloc() public view returns(bytes32, bytes32){ string memory str = "aaa"; string memory str2 = "bbb"; bytes32 p1; bytes32 p2; assembly{ p1 := str p2 := str2 } return (p1, p2); }
運行此函數後,返回結果將包含兩個數據字:
0: bytes32: 0x0000000000000000000000000000000000000000000000000000000000000080 1: bytes32: 0x00000000000000000000000000000000000000000000000000000000000000c0
這說明,第一個字符串str1的起始地址是0x80,第二個字符串str2的起始地址是0xc0,之間64字節,正好是str1自己佔據的空間。此時的內存佈局以下,其中一格表示32字節(一個數據字,EVM採用32字節做爲一個數據字,而非4字節):
0x40~0x60:空閒指針,保存可用地址,本例中是0x100,說明新的對象將從0x100處分配。能夠用mload(0x40)獲取到新對象的分配地址。
0x80~0xc0:對象分配的起始地址。這裏分配了字符串aaa
0xc0~0x100:分配了字符串bbb
0x100~...:由於是順序分配,新的對象將會分配到這裏。
顧名思義,狀態存儲用於存儲合約的狀態字段。
從模型而言,存儲由多個32字節的存儲槽構成。在前文中,咱們介紹了Demo合約的set函數,裏面0x0表示的是狀態變量_state的存儲槽。全部固定長度變量會依序放到這組存儲槽中。
對於mapping和數組,存儲會更復雜,其自身會佔據1槽,所包含數據則會按相應規則佔據其餘槽,好比mapping中,數據項的存儲槽位由鍵值k、mapping自身槽位p經keccak計算得來。
從實現而言,不一樣的鏈可能採用不一樣實現,比較經典的是以太坊所採用的MPT樹。因爲MPT樹性能、擴展性等問題,FISCO BCOS放棄了這一結構,而採用了分佈式存儲,經過rocksdb或mysql來存儲狀態數據,使存儲的性能、可擴展性獲得提升。
本文介紹了Solidity的運行原理,運行原理總結以下。
首先,Solidity源碼會被編譯爲字節碼,部署時,字節碼會以交易爲載體在網絡間確認,並在節點上造成合約;合約函數調用,若是是交易類型,會通過網絡確認,最終由EVM執行。
EVM是棧式虛擬機,它會讀取合約的字節碼並執行。
在執行過程當中,會與棧、內存、合約存儲進行交互。其中,棧用於存儲普通的局部變量,這些局部變量就是字節碼的操做數;內存用於存儲對象,採用length+body進行存儲,順序分配方式進行內存分配;狀態存儲用於存儲狀態變量。
理解Solidity的運行方式及其背後原理,是成爲Solidity編程高手必經之路。
咱們鼓勵機構成員、開發者等社區夥伴參與開源共建事業,有你在一塊兒,會更了不得。多樣參與方式:
一、進入微信社羣,隨時隨地與圈內最活躍、最頂尖的團隊暢聊技術話題(進羣請添加小助手微信,
微信ID:FISCOBCOS010);
二、訂閱咱們的公衆號:「FISCO BCOS開源社區」,咱們爲你準備了開發資料庫、最新FISCO BCOS動態、活動、大賽等信息;
三、來Meetup與開發團隊面對面交流,FISCO BCOS正在全國舉辦巡迴Meetup,深圳、北京、上海、成都……歡迎您公衆號在菜單欄【找活動】中找到附近的Meetup,前往結識技術大咖,暢聊硬核技術;
四、參與代碼貢獻,您能夠在Github提交Issue進行問題交流,歡迎向FISCO BCOS提交Pull Request,包括但不限於文檔修改、修復發現的bug、提交新的功能特性。
代碼貢獻指引:
https://github.com/FISCO-BCOS/FISCO-BCOS/blob/master/docs/CONTRIBUTING_CN.md