解構智慧合約:手把手教你拆解深入Solidity神祕世界
第一部分·前言
想象一下,你正在駕駛著1969年的Mustang Mach在美國西部的公路上快速行駛,陽光照耀在華麗的鍍金輪輞上,整條道路只有你和沙漠,一望無際的地平線見證著你和落日的追逐... ...
心曠神怡間,突然一身巨響,你335馬力的快馬被滾滾的白煙吞沒,瞬間變成了蒸汽機車,於是你被迫停在路邊。
你打算看看出現了什麼問題,當你翻開前車蓋,發現自己完全看不懂。你根本就不知道這該死的機器是怎麼工作的,於是你拿起手機準備求救,發現附近沒有信號... ...
上面描述的情形是不是和你正在做的DApp開發很像?在開發Dapp的過程中,在類比中,豪車是你的智慧合約,輪輞和改造過的地方是那些經過深思熟慮的小細節。而一旦出現問題,你就需要在智慧合約EVM位元組碼中尋找答案,大部分情況下你都完全不知道發生了什麼。
如果你是Dapp的開發者,並且遇到過上面的尷尬狀況的話,那麼以後再也不用擔心了!
因為,本系列文章的目的就是解構一個簡單的Solidity合約,檢視其位元組碼,並將其分解為可識別的結構,直至最低級別。我們將開啟Solidity這臺跑車的引擎蓋。在本系列的最後,您在檢視或除錯EVM位元組碼時應該感覺很舒服。本系列的重點是揭開Solidity編譯器生成的EVM位元組碼的神祕面紗,它真的比它看起來簡單得多。
以下是我們解構時要使用到的智慧合約程式碼:
pragma solidity ^0.4.24; contract BasicToken { uint256 totalSupply_; mapping(address => uint256) balances; constructor(uint256 _initialSupply) public { totalSupply_ = _initialSupply; balances[msg.sender] = _initialSupply; } function totalSupply() public view returns (uint256) { return totalSupply_; } function transfer(address _to, uint256 _value) public returns (bool) { require(_to != address(0)); require(_value <= balances[msg.sender]); balances[msg.sender] = balances[msg.sender] - _value; balances[_to] = balances[_to] + _value; return true; } function balanceOf(address _owner) public view returns (uint256) { return balances[_owner]; } }
注意:此合約容易受到溢位攻擊,我們只是為了說明問題,所以力求簡潔。
編譯合約
為了編譯合約,我們將使用Remix(地址:https://remix.ethereum.org)。
當你開啟Remix編譯器,單擊檔案瀏覽器區域上方左上角的+按鈕,建立新的智慧合約。將檔名設定為BasicToken.sol。建立好之後,將上面的程式碼貼上到編輯器上。
在右側,轉到“ Setting”的選項中,確保選中了“ Enable Personal Mode”。另外,要注意選擇的Solidity編譯器版本是
“ version:0.4.24 +commit.e67f0147.Emscripten.clang ”。
這兩個細節非常重要,否則你將無法檢視文中討論的位元組碼。
接下來,你可以進入Compile選項並單擊Details按鈕,你會看到一個彈出視窗,裡面包含Solidity編譯器生成的所有東西,其中一個是名為BYTECODE的JSON物件,它具有“object”屬性,這個就是編譯的合約程式碼,它的程式碼是這樣的:
608060405234801561001057600080fd5b5060405160208061021783398101604090815290516000818155338152600160205291909120556101d1806100466000396000f3006080604052600436106100565763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166318160ddd811461005b57806370a0823114610082578063a9059cbb146100b0575b600080fd5b34801561006757600080fd5b506100706100f5565b60408051918252519081900360200190f35b34801561008e57600080fd5b5061007073ffffffffffffffffffffffffffffffffffffffff600435166100fb565b3480156100bc57600080fd5b506100e173ffffffffffffffffffffffffffffffffffffffff60043516602435610123565b604080519115158252519081900360200190f35b60005490565b73ffffffffffffffffffffffffffffffffffffffff1660009081526001602052604090205490565b600073ffffffffffffffffffffffffffffffffffffffff8316151561014757600080fd5b3360009081526001602052604090205482111561016357600080fd5b503360009081526001602081905260408083208054859003905573ffffffffffffffffffffffffffffffffffffffff85168352909120805483019055929150505600a165627a7a72305820a5d999f4459642872a29be93a490575d345e40fc91a7cccb2cf29c88bcdaf3be0029
沒錯,這些程式碼完全沒有可讀性(至少對於正常人而言)。
部署合約
接下來,轉到Remix中的Run部分。首先,確保您使用的是Javascript VM。這基本上是一個嵌入式Javascript EVM +網路,理想的以太坊訓練場。確保在ComboBox中選擇了BasicToken ,並在Deploy輸入框中輸入數字10000 。接下來,單擊“ Deploy”按鈕進行部署。這部署的是我們建立的BasicToken智慧合約例項,最初提供的10000個token由當前在ComboBox帳戶頂部選擇的帳戶擁有,該帳戶會保留我們設定的所有token供應。
在“ Run”選項卡的“Deployed Contracts”中,您可以看到已部署的智慧合約,其中包含與合約進行互動的三個欄位:transfer,balanceOf和totalSupply。在這裡,我們能夠與剛剛部署的智慧合約例項進行互動。
但在此之前,讓我們來看看合同的“Deploy”(部署)究竟是什麼意思:
在頁面底部的控制檯區域中,你可以看到一條日誌“creation of BasicToken pending ...”,然後是一個包含各種欄位的事務條目:from,to,value,data,logs和hash。單擊此條目以展開交易信息,你應該看到transaction的date、input,以及我們上面提到的位元組碼。所以,建立一個智慧合約例項,其中會包含自己的地址和程式碼。
我們將在下一篇文章中詳細研究這transaction個過程。
反彙編位元組碼
在控制檯中心,transaction框框的右側,有個“ debug”(除錯)按鈕。點選這個按鈕,你將啟用Remix右側區域中的Debugger選項。我們可以一起看看Instructions部分,如果向下滾動,應該是出現以下內容:
000 PUSH1 80
002 PUSH1 40
004 MSTORE
005 CALLVALUE
006 DUP1
007 ISZERO
008 PUSH2 0010
011 JUMPI
012 PUSH1 00
014 DUP1
015 REVERT
016 JUMPDEST
017 POP
018 PUSH1 40
020 MLOAD
021 PUSH1 20
023 DUP1
024 PUSH2 0217
027 DUP4
028 CODECOPY
029 DUP2
030 ADD
031 PUSH1 40
033 SWAP1
034 DUP2
035 MSTORE
036 SWAP1
037 MLOAD
038 PUSH1 00
040 DUP2
041 DUP2
042 SSTORE
043 CALLER
044 DUP2
045 MSTORE
046 PUSH1 01
048 PUSH1 20
050 MSTORE
051 SWAP2
...(縮寫)
為了確保你前面的操作沒有出錯,你可以把你所操作的Remix編譯器中看到的內容與上面進行比較。
這其實就是合約的反彙編位元組碼。如果您按位元組掃描原始位元組碼(一次兩個字元),則EVM會識別與特定操作關聯的特定操作碼。例如:
0x60 => PUSH
0x01 => ADD
0x02 => MUL
0x00 => STOP
...
反彙編的程式碼非常低階並且很難看懂,但是我們可以通過這種方式可以開始理解它。
Opcode在解構智慧合約程式碼開始之前,你將需要一個基本的工具集理解單個opcode,如PUSH,ADD,SWAP,DUP等的操作碼,到最後,每個操作嗎只能從EVM的堆疊,記憶體或屬於合約的儲存中壓入一個項或消費一個項。
要檢視EVM可以處理的所有可用操作碼,可以檢視Pyethereum,上面顯示了操作碼列表。要了解每個操作碼的工作原理,Solidity官方的彙編文件也是一個很好的參考。即使它不是與原始操作碼一一對應,但是非常接近(它實際上是Yul,介於Solidity和EVM位元組碼之間的中間語言)。如果你能讀懂技術文件,可以閱讀以太坊黃皮書,其實歸根結底都是上面的內容。
雖然和大家推薦了這麼多文件,現在從頭到尾閱讀這些資源沒有什麼意義,你只要記住有這麼個資料就行,我們將在需要的時候使用到它們。
指令
上面反彙編程式碼中的每一行都是EVM執行的操作指令,每條指令都包含一個操作碼,例如,讓我們採用其中一條指令,指令88,將數字4推送到堆疊。這個特殊的反彙編程式解釋說明如下:
88 PUSH1 0x04
| | |
| | Hex value for push.
| Opcode
Instruction number
儘管反彙編的程式碼能讓我們更加理解底層的東西,但它還是讓人摸不著頭腦。我們需要一個能夠解構所有問題的方法。
策略
任何一開始看上去不可能完成的任務,其實都可以通過不斷的拆解,分解成可以解決的任務,我們遇到的問題也不例外,面對這個問題,我所採取的策略就是“ 分而治之 ”。
我們可以試圖找到反彙編程式碼的分叉點,並逐漸分解,直到分解成很小的塊,我們將在Remix的偵錯程式中逐步完成。
在下圖中,我們可以看到我們對反彙編程式碼進行的第一次拆分(我將在下一篇文章中對其進行全面分析)。
如果你不瞭解圖表,也不要擔心,你不用一開始就什麼都會,我們的系列文章將會循序漸進的介紹。現在就跟著我們的節奏,不斷深入你的豪車的內部結構吧。