Solidity提供了不少高級語言的抽象概念,可是這些特性讓人很難明白在運行程序的時候到底發生了什麼。我閱讀了Solidity的文檔,但依舊存在着幾個基本的問題沒有弄明白。git
string, bytes32, byte[], bytes之間的區別是什麼?github
該在什麼地方使用哪一個類型?數據庫
將 string 轉換成bytes時會怎麼樣?能夠轉換成byte[]嗎?編程
它們的存儲成本是多少?數組
EVM是如何存儲映射( mappings)的?數據結構
爲何不能刪除一個映射?app
能夠有映射的映射嗎?(能夠,可是怎樣映射?)編程語言
爲何存在存儲映射,可是卻沒有內存映射?函數
編譯的合約在EVM看來是什麼樣子的?工具
合約是如何建立的?
到底什麼是構造器?
什麼是 fallback 函數?
我以爲學習在以太坊虛擬機(EVM)上運行的相似Solidity 高級語言是一種很好的投資,有幾個緣由:
Solidity不是最後一種語言。更好的EVM語言正在到來。(拜託?)
EVM是一個數據庫引擎。要理解智能合約是如何以任意EVM語言來工做的,就必需要明白數據是如何被組織的,被存儲的,以及如何被操做的。
知道如何成爲貢獻者。以太坊的工具鏈還處於早期,理解EVM能夠幫助你實現一個超棒的工具給本身和其餘人使用。
智力的挑戰。EVM可讓你有個很好的理由在密碼學、數據結構、編程語言設計的交集之間進行翱翔。
在這個系列的文章中,我會拆開一個簡單的Solidity合約,來讓你們明白它是如何以EVM字節碼(bytecode)來運行的。
我但願可以學習以及會書寫的文章大綱:
EVM字節碼的基礎認識
不一樣類型(映射,數組)是如何表示的
當一個新合約建立以後會發生什麼
當一個方法被調用時會發生什麼
ABI如何橋接不一樣的EVM語言
個人最終目標是總體的理解一個編譯的Solidity合約。讓咱們從閱讀一些基本的EVM字節碼開始。
咱們的第一個合約有一個構造器和一個狀態變量:
// c1.solpragma solidity ^0.4.11; contract C { uint256 a; function C() { a = 1; } }
用solc來編譯此合約:
$ solc --bin --asm c1.sol ======= c1.sol:C ======= EVM assembly: /* "c1.sol":26:94 contract C {... */ mstore(0x40, 0x60) /* "c1.sol":59:92 function C() {... */ jumpi(tag_1, iszero(callvalue)) 0x0 dup1 revert tag_1: tag_2: /* "c1.sol":84:85 1 */ 0x1 /* "c1.sol":80:81 a */ 0x0 /* "c1.sol":80:85 a = 1 */ dup2 swap1 sstore pop /* "c1.sol":59:92 function C() {... */ tag_3: /* "c1.sol":26:94 contract C {... */ tag_4: dataSize(sub_0) dup1 dataOffset(sub_0) 0x0 codecopy 0x0 return stop sub_0: assembly { /* "c1.sol":26:94 contract C {... */ mstore(0x40, 0x60) tag_1: 0x0 dup1 revert auxdata: 0xa165627a7a72305820af3193f6fd31031a0e0d2de1ad2c27352b1ce081b4f3c92b5650ca4dd542bb770029 } Binary:60606040523415600e57600080fd5b5b60016000819055505b5b60368060266000396000f30060606040525b600080fd00a165627a7a72305820af3193f6fd31031a0e0d2de1ad2c27352b1ce081b4f3c92b5650ca4dd542bb770029
6060604052...這串數字就是EVM實際運行的字節碼。
上面一半的編譯彙編是大多數Solidity程序中都會存在的樣板語句。咱們稍後再來看這些。如今,咱們來看看合約中獨特的部分,簡單的存儲變量賦值:
a = 1
表明這個賦值的字節碼是6001600081905550。咱們把它拆成一行一條指令:
60 01 60 00 81 90 55 50
EVM本質上就是一個循環,從上到下的執行每一條命令。讓咱們用相應的字節碼來註釋彙編代碼(縮進到標籤tag_2下),來更好的看看他們之間的關聯:
tag_2: // 60 01 0x1 // 60 00 0x0 // 81 dup2 // 90 swap1 // 55 sstore // 50 pop
注意0x1在彙編代碼中其實是push(0x1)的速記。這條指令將數值1壓入棧中。
只是盯着它依然很難明白到底發生了什麼,不過不用擔憂,一行一行的模擬EVM是比較簡單的。
EVM是個堆棧機器。指令可能會使用棧上的數值做爲參數,也會將值做爲結果壓入棧中。讓咱們來思考一下add操做。
假設棧上有兩個值:
[1 2]
當EVM看見了add,它會將棧頂的2項相加,而後將答案壓入棧中,結果是:
[3]
接下來,咱們用[]符號來標識棧:
// 空棧 stack: [] // 有3個數據的棧,棧頂項爲3,棧底項爲1 stack: [3 2 1] 用{}符號來標識合約存儲器: // 空存儲 store: {} // 數值0x1被保存在0x0的位置上 store: { 0x0 => 0x1 } 如今讓咱們來看看真正的字節碼。咱們將會像EVM那樣來模擬6001600081905550字節序列,並打印出每條指令的機器狀態: // 60 01:將1壓入棧中 0x1 stack: [0x1] // 60 00: 將0壓入棧中 0x0 stack: [0x0 0x1] // 81: 複製棧中的第二項 dup2 stack: [0x1 0x0 0x1] // 90: 交換棧頂的兩項數據 swap1 stack: [0x0 0x1 0x1] // 55: 將數值0x01存儲在0x0的位置上 // 這個操做會消耗棧頂兩項數據 sstore stack: [0x1] store: { 0x0 => 0x1 } // 50: pop (丟棄棧頂數據) pop stack: [] store: { 0x0 => 0x1 }
最後,棧就爲空棧,而存儲器裏面有一項數據。
值得注意的是Solidity已經決定將狀態變量uint256 a保存在0x0的位置上。其餘語言徹底能夠選擇將狀態變量存儲在其餘的任何位置上。
6001600081905550字節序列在本質上用EVM的操做僞代碼來表示就是:
// a = 1 sstore(0x0, 0x1)
仔細觀察,你就會發現dup2,swap1,pop都是多餘的,彙編代碼能夠更簡單一些:
0x1 0x0 sstore
你能夠模擬上面的3條指令,而後會發現他們的機器狀態結果都是同樣的:
stack: [] store: { 0x0 => 0x1 }
讓咱們再額外的增長一個相同類型的存儲變量:
// c2.solpragma solidity ^0.4.11; contract C { uint256 a; uint256 b; function C() { a = 1; b = 2; } }
編譯以後,主要來看tag_2:
$ solc --bin --asm c2.sol //前面的代碼忽略了 tag_2: /* "c2.sol":99:100 1 */ 0x1 /* "c2.sol":95:96 a */ 0x0 /* "c2.sol":95:100 a = 1 */ dup2 swap1 sstore pop /* "c2.sol":112:113 2 */ 0x2 /* "c2.sol":108:109 b */ 0x1 /* "c2.sol":108:113 b = 2 */ dup2 swap1 sstore pop
彙編的僞代碼:
// a = 1 sstore(0x0, 0x1)// b = 2 sstore(0x1, 0x2)
咱們能夠看到兩個存儲變量的存儲位置是依次排列的,a在0x0的位置而b在0x1的位置。
每一個存儲槽均可以存儲32個字節。若是一個變量只須要16個字節可是使用所有的32個字節會很浪費。Solidity爲了高效存儲,提供了一個優化方案:若是能夠的話,就將兩個小一點的數據類型進行打包而後存儲在一個存儲槽中。
咱們將a和b修改爲16字節的變量:
pragma solidity ^0.4.11; contract C { uint128 a; uint128 b; function C() { a = 1; b = 2; } }
編譯此合約:
$ solc --bin --asm c3.sol
產生的彙編代碼如今更加的複雜一些:
tag_2: // a = 1 0x1 0x0 dup1 0x100 exp dup2 sload dup2 0xffffffffffffffffffffffffffffffff mul not and swap1 dup4 0xffffffffffffffffffffffffffffffff and mul or swap1 sstore pop // b = 2 0x2 0x0 0x10 0x100 exp dup2 sload dup2 0xffffffffffffffffffffffffffffffff mul not and swap1 dup4 0xffffffffffffffffffffffffffffffff and mul or swap1 sstore pop
上面的彙編代碼將這兩個變量打包放在一個存儲位置(0x0)上,就像這樣:
[ b ][ a ] [16 bytes / 128 bits][16 bytes / 128 bits]
進行打包的緣由是由於目前最昂貴的操做就是存儲的使用:
sstore指令第一次寫入一個新位置須要花費20000 gas
sstore指令後續寫入一個已存在的位置須要花費5000 gas
sload指令的成本是500 gas
大多數的指令成本是3~10 gas
經過使用相同的存儲位置,Solidity爲存儲第二個變量支付5000 gas,而不是20000 gas,節約了15000 gas。
應該能夠將兩個128位的數打包成一個數放入內存中,而後使用一個'sstore'指令進行存儲操做,而不是使用兩個單獨的sstore命令來存儲變量a和b,這樣就額外的又省了5000 gas。
你能夠經過添加optimize選項來讓Solidity實現上面的優化:
$ solc --bin --asm --optimize c3.sol
這樣產生的彙編代碼只有一個sload指令和一個sstore指令:
tag_2: /* "c3.sol":95:96 a */ 0x0 /* "c3.sol":95:100 a = 1 */ dup1 sload /* "c3.sol":108:113 b = 2 */ 0x200000000000000000000000000000000 not(sub(exp(0x2, 0x80), 0x1)) /* "c3.sol":95:100 a = 1 */ swap1 swap2 and /* "c3.sol":99:100 1 */ 0x1 /* "c3.sol":95:100 a = 1 */ or sub(exp(0x2, 0x80), 0x1) /* "c3.sol":108:113 b = 2 */ and or swap1 sstore
字節碼是:
600080547002000000000000000000000000000000006001608060020a03199091166001176001608060020a0316179055 將字節碼解析成一行一指令: // push 0x0 60 00 // dup1 80 // sload 54 // push17 將下面17個字節做爲一個32個字的數值壓入棧中 70 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 /* not(sub(exp(0x2, 0x80), 0x1)) */ // push 0x1 60 01 // push 0x80 (32) 60 80 // push 0x80 (2) 60 02 // exp 0a // sub 03 // not 19 // swap1 90 // swap2 91 // and 16 // push 0x1 60 01 // or 17 /* sub(exp(0x2, 0x80), 0x1) */ // push 0x1 60 01 // push 0x80 60 80 // push 0x02 60 02 // exp 0a // sub 03 // and 16 // or 17 // swap1 90 // sstore 55
上面的彙編代碼中使用了4個神奇的數值:
* 0x1(16字節),使用低16字節 // 在字節碼中表示爲0x01 16:32 0x00000000000000000000000000000000 00:16 0x00000000000000000000000000000001 * 0x2(16字節),使用高16字節 //在字節碼中表示爲0x200000000000000000000000000000000 16:32 0x00000000000000000000000000000002 00:16 0x00000000000000000000000000000000 * not(sub(exp(0x2, 0x80), 0x1)) // 高16字節的掩碼 16:32 0x00000000000000000000000000000000 00:16 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF * sub(exp(0x2, 0x80), 0x1) // 低16字節的掩碼 16:32 0x00000000000000000000000000000000 00:16 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
代碼將這些數值進行了一些位的轉換來達到想要的結果:
16:32 0x00000000000000000000000000000002 00:16 0x00000000000000000000000000000001
最後,該32字節的數值被保存在了0x0的位置上。
600080547002000000000000000000000000000000006001608060020a03199091166001176001608060020a0316179055
注意0x200000000000000000000000000000000被嵌入到了字節碼中。可是編譯器也可能選擇使用exp(0x2, 0x81)指令來計算數值,這會致使更短的字節碼序列。
但結果是0x200000000000000000000000000000000比exp(0x2, 0x81)更便宜。讓咱們看看與gas費用相關的信息:
一筆交易的每一個零字節的數據或代碼費用爲 4 gas
一筆交易的每一個非零字節的數據或代碼的費用爲 68 gas
來計算下兩個表示方式所花費的gas成本:
0x200000000000000000000000000000000字節碼包含了不少的0,更加的便宜。 (1 * 68) + (32 * 4) = 196
608160020a字節碼更短,可是沒有0。 5 * 68 = 340
更長的字節碼序列有不少的0,因此實際上更加的便宜!
EVM的編譯器實際上不會爲字節碼的大小、速度或內存高效性進行優化。相反,它會爲gas的使用進行優化,這間接鼓勵了計算的排序,讓以太坊區塊鏈能夠更高效一點。
咱們也看到了EVM一些奇特的地方:
EVM是一個256位的機器。以32字節來處理數據是最天然的
持久存儲是至關昂貴的
Solidity編譯器會爲了減小gas的使用而作出相應的優化選擇
Gas成本的設置有一點武斷,也許將來會改變。當成本改變的時候,編譯器也會作出不一樣的優化選擇。
內容來源:簡書
原文做者: Lilymoana
Blockathon|48小時極客競賽,區塊鏈馬拉松等你挑戰(成都)
時間:2018年9月14-16日
地點:成都高新區天府五街200號菁蓉國際廣場2號樓A座12樓中韓互聯網+新技術孵化器
招募50名開發者(識別下圖二維碼或點擊「閱讀原文」便可報名)
報名費100元爲參賽押金,參賽者我的緣由不能到場參加活動概不退款;參賽者全程參與活動,待活動結束後現場退還。9月14日18:00開始第一次簽到,9月15日和16日天天早上都要記得簽到哦。
主辦方免費提供2天的食物、飲料,併爲每一位參會者準備一件文化衫
如下是咱們的社區介紹,歡迎各類合做、交流、學習:)