解構智慧合約:創造與執行時間程式碼解析
本文是系列文章的第二部分。
如果您還沒有閱讀本文的前言,請先看一下, ofollow,noindex" target="_blank">第一部分·引言
我們正在解構一個簡單的solidity智慧合約的EVM位元組碼。
今天,讓我們開始用“分而治之”的策略來拆解智慧合約的複雜程式碼吧。我在介紹性的前言中說過,這個反彙編的程式碼其實非常低階,但與原始位元組碼相比會比較易讀。
請確保已在遵循了我在前言中介紹的操作,把BasicToken的程式碼在remix編譯器中進行了部署。
免責宣告:本文中提供的所有說明均受我自己對transaction運作方式的解釋,不代表以太坊官方意見。
現在,讓我們聚焦在JUMP、JUMPI、JUMPDES、RETURN和STOP操作嗎,並忽略所有其他的操作。每當我們發現不是其中之一的操作碼時,我們就忽略它,並跳到下一條指令,不要被他們干預。
當EVM執行程式碼時,是自上而下的順序,程式碼中沒有其他入口點,始終從頂部開始執行。JUMP和JUMPI可以讓程式碼跳轉。JUMP獲取堆疊中的最上面的值,並將執行移動到該位置的指令。但是,目標位置必須包含JUMPDEST操作碼,否則執行將失敗。這樣做的唯一目的是:JUMPDEST將位置標記為有效的跳轉目標。JUMPI也完全相同,但堆疊的第二個位置一定不能有“0”,否則就沒有跳轉。所以這是一個有條件的跳轉,STOP是讓智慧合約完全停止的指令,RETURN則是暫停智慧合約的執行,但返回EVM記憶體的一部分資料,這很方便。
所以,讓我們開始解釋程式碼時考慮到所有這些。在Remix的偵錯程式中,將“ transaction”的滑塊滑到最左邊。你可以使用Step Into按鈕(看起來像一個向下的小箭頭)並按照說明進行操作。
前面的指令可以忽略,直接到第11條指令,我們找到了第一條JUMPI。如果它沒有跳轉,它將繼續通過指令12到15並最終進入REVERT,接著將停止執行。但如果跳轉,它將跳過這些指令到位置16(十六進位制0x0010,它在指令8被壓入堆疊)。指令16是一個JUMPDEST。
繼續單步執行操作碼,直到“ transaction”滑塊一直向右。剛剛發生了很多等等,但只有在68的位置才能找到RETURN操作碼(以及STOP指令69中的操作碼,以防萬一)。這很奇怪。如果您考慮一下,本智慧合約的控制流程將始終在指令15或68結束。我們剛剛完成它並確定沒有其他可能的流程,那麼剩下的指令是什麼?(如果您滑動“ 指令”面板,您將看到程式碼在位置566處結束)。
我們剛剛遍歷的指令集(0到69)就是所謂的合約的“建立程式碼”。它永遠不會成為智慧合約程式碼本身的一部分,但僅在建立智慧合約的交易期間由EVM執行一次。我們很快就會發現,這段程式碼負責設定建立的合約的初始狀態,以及返回其執行時程式碼的副本。剩下的497條指令(70到566),正如我們所見,執行流程永遠不會達到的,正是這些程式碼將成為已部署智慧合約的一部分。
接下來,我們對程式碼進行第一次拆分:把建立時的程式碼與執行時的程式碼區分開。
建立部分
現在,我們將深入研究程式碼的建立部分。
圖1.解構BasicToken.sol的建立時EVM位元組碼
這是本文中要理解的最重要的概念。建立程式碼在事務中執行,該事務返回執行時程式碼的副本,該副本是智慧合約的實際程式碼。正如我們將看到的,建構函式是建立程式碼的一部分,而不是執行時程式碼的一部分。智慧合約的建構函式是建立程式碼的一部分; 一旦部署,它將不會出現在智慧合約的程式碼中。
這種魔力是如何發生的?這就是我們現在要逐步分析的內容。
好的。所以現在我們的問題被簡化為理解這些與建立時程式碼相對應的70條指令。
讓我們重新採用自上而下的方法,這次瞭解所有指令,而不是跳過任何指令。首先,讓我們關注使用PUSH1和MSTORE操作碼的指令0到2 。
圖2.空閒記憶體指標EVM位元組碼結構
PUSH1只需將一個位元組壓入堆疊頂部,而MSTORE從堆疊中抓取最後兩個項並將其中一個儲存在記憶體中:
mstore(0x40, 0x80) | | | What to store. Where to store. (in memory)
注意:上面的程式碼片段是Yul-ish程式碼。注意它是如何從左到右消耗堆疊中的元素,總是首先消耗堆疊頂部的元素。
這是將數字0x80(十進位制128)儲存在位置0x40(十進位制64)的位置。
在我們現在討論的問題中,不用去管它,如果必須有一個原因,我後面會解釋。
現在,在Remix的Debugger選項卡中開啟Stack 以及Memory的面板,以便在逐步檢視這些指令時可以視覺化。
你可能想知道:指令1和3發生了什麼?PUSH是唯一由兩個或多個位元組組成的EVM指令。所以,PUSH 80是兩條指令。所以我們揭開了謎底:指令1是0x80,而指令3的 0x40。
接下來我會說明從5到15的指令。
圖3.non-payable檢查EVM位元組碼結構。
在這裡,又有一大堆的新的操作碼:CALLVALUE,DUP1,ISZERO,PUSH2,和REVERT。CALLVALUE推送建立事務中涉及的wei的數量,DUP1複製堆疊中的第一個元素,如果堆疊的最高值為零,ISZERO則將1推送到堆疊,PUSH2就像PUSH1,但它將兩個位元組推送到堆疊,而REVERT則是停止執行。
那麼這裡發生了什麼?在Solidity中,我們可以像這樣編寫這個彙編:
if(msg.value!= 0)revert();
這段程式碼實際上不是我們原始Solidity源的一部分,而是由編譯器注入的,因為我們沒有將建構函式宣告為payable。在Solidity的最新版本中,未明確宣告為payable的函式不能接收以太。返回到彙編程式碼,在指令11的JUMPI將跳過指令12到15,如果沒有相關的以太幣,則跳轉到16。否則,REVERT將以兩個引數執行為0(意味著不會返回有用的資料)。
好的!讓我們中場休息一下,來杯咖啡。
(下一部分會有點棘手,所以最好休息幾分鐘。在你再次集中專注力之前,為自己準備一杯好咖啡。確保你理解我們到目前為止看到的內容,因為下一部分有點複雜。)
如果您想要另一種方式來視覺化我們剛剛完成的工作,請嘗試使用我構建的這個簡單工具:solmap。它允許您實時編譯Solidity程式碼,然後單擊EVM操作碼以突出顯示相關的Solidity程式碼。反彙編與Remix有點不同,但你應該能夠通過比較來理解它。
咖啡時間到!
準備繼續前進了嗎?接下來是指令16到37。請繼續使用Remix的偵錯程式。(記住,remix是你的好朋友^ ^)。
圖4. EVM位元組碼結構,用於從智慧合約位元組碼末尾附加的程式碼中檢索建構函式引數
前四個指令(17到20)讀取位置在儲存器中的任何內容0x40,並將其推送到堆疊。如果你能回憶起來,那應該是數字0x80。下面是推0x20(十進位制32)到堆疊(指令21),並拷貝該值(指令23),壓棧0x0217(十進位制535)(指令24),最後拷貝第四個值(指令27),這應該是0x80。
在檢視這樣的EVM指令時,可以暫時不瞭解發生了什麼。別擔心,它會時不時出現在你的腦海。
在指令28,執行了 CODECOPY,它接受三個引數:目標記憶體位置,用來儲存複製程式碼,從中複製的指令編號,以及要複製的程式碼的位元組數。因此,在這種情況下,0x80從位於程式碼中的位元組位置(535,32位元組程式碼長度的目標位置)開始。
如果檢視整個反彙編程式碼,有566條指令。為什麼這段程式碼試圖複製最後32個位元組的程式碼呢?實際上,在部署包含引數的建構函式的合約時,引數作為原始十六進位制資料附加到程式碼的末尾(向下滾動“說明”面板可以檢視此內容)。在這種情況下,建構函式接受一個uint256引數,因此所有這些程式碼所做的就是將引數從附加在程式碼末尾的值複製到記憶體中。
這些32條指令作為反彙編程式碼沒有意義,但是它們用原始的十六進位制表示:0x0000000000000000000000000...0000000000000000000002710。當然,這是我們在部署智慧合約時傳遞給建構函式的十進位制值10000!
你可以一步一步地在Remix中重複這一部分,確保您瞭解剛剛發生的事情。最終結果應該是0x00..002710的位置,看到在記憶體中的數字0x80。
好,開始下一部分之前,我建議來一杯威士忌休息一下。
威士忌時光!
為什麼建議你來一杯威士忌,因為從這裡開始,都是下坡路了。
下一組指令是29到35,更新記憶體地址0x40的值0x80到值0xa0,可以看到,它們將值偏移了0x20(32)位元組。
現在我們可以開始理解指令0到2了。Solidity追蹤稱為“空記憶體指標”的東西:即記憶體中我們可以用來儲存東西的地方,保證沒有人會覆蓋它(除非我們犯了錯誤)。因此,由於我們將數字10000儲存在舊的空閒記憶體位置,我們通過向前移動32個位元組來更新空閒儲存器指標。
即使是經驗豐富的Solidity開發人員在看到“空閒記憶體指標”或程式碼時也會感到困惑,mload(0x40, 0x80),這些只是說,“每當我們寫一個新條目時,我們將從這一點開始寫入記憶體並保留偏移記錄”。
Solidity中的每個函式,當編譯為EVM位元組碼時,將初始化此指標。
在0x00到0x40之間的記憶體有什麼,你可能不知道。沒有。Solidity保留的一段記憶體,計算雜湊值,我們很快就會看到,這對於對映和其他型別的動態資料是必需的。
現在,在指令37中,MLOAD從儲存器讀取位置0x40並基本上將我們10000的值從記憶體下載到堆疊中,在那裡它將是新的,並且可以在下一組指令中使用的。
這是由Solidity生成的EVM位元組碼中的常見模式:在執行函式體之前,函式的引數被載入到堆疊中(只要有可能),以便即將到來的程式碼可以使用它們 - 這正是接下來會發生的事情。
讓我們繼續說明38至55。
圖5.建構函式的主體EVM程式碼。
這些指令只不過是建構函式的主體:也就是Solidity程式碼:
totalSupply_ = _initialSupply; balances[msg.sender] = _initialSupply;
前四條指令非常明顯(38至42),首先,0被壓入堆疊,然後堆疊中的第二項被複制(這是我們的10000號碼),然後數字0被複制並被推送到堆疊,這是儲存中的位置槽totalSupply_。現在,SSTORE可以使用這些值,並且仍然保持10000個以下以備將來使用:
sstore(0x00, 0x2710) | | | What to store. Where to store. (in storage)
瞧!我們將數字10000儲存在變數中totalSupply_。是不是很神奇??
一定要在Remix的Debugger選項卡中視覺化這個值。你可以在儲存完全載入的面板中找到它。
下一組指令(43到54)有點棘手,但基本上會處理在balances對映中儲存10000 的金鑰msg.sender。在繼續之前,請確保您瞭解Solidity文件的這一部分,該文件說明了如何在記憶體中儲存對映。
簡而言之,它將連線對映值的槽(在這種情況下是數字1,因為它是智慧合約中宣告的第二個變數)與使用的鍵(在這種情況下msg.sender,通過操作碼獲得CALLER),然後用SHA3操作碼取摘要並使用它作為在記憶體中的目標位置。最後,儲存只是一個簡單的字典或雜湊表。
繼續執行指令43至45,將msg.sender地址儲存在記憶體中(此時在位置0x00),然後在指令46至50中,將值1(對映的槽)儲存在記憶體位置0x20。最後,SHA3操作碼計算從位置0x00到位置0x40的記憶體中的任何內容的Keccak256雜湊- 即對映的插槽/位置與所使用的鍵的串聯。這正是值10000將儲存在我們的對映中的位置:
sstore(hash..., 0x2710) | | | What to store. Where to store.
此時,建構函式的主體已完全執行。
所有這些起初可能有點壓倒性,但它是儲存在Solidity中工作的基本部分。如果你沒有得到它,我建議你跟著Remix的偵錯程式重複幾次,保持堆疊和記憶體面板。
另外,請隨時提出以下問題。此模式在Solidity生成的EVM位元組碼中普遍使用,您將很快學會輕鬆識別它。最後,它只是計算在記憶體中儲存對映的某個鍵的值的位置。
圖6.執行時程式碼複製結構
在指令56至65中,我們再次執行程式碼複製。只有這一次,我們不會將程式碼的最後32個位元組複製到記憶體中; 我們從位置0x0046(十進位制70)開始複製0x01d1(十進位制465)位元組到位置0的記憶體。這是要複製的一大塊程式碼!
如果您再次將滑塊一直向右滑動,您將注意到位置70正好在我們的建立時EVM程式碼之後,執行停止的地方。執行時位元組碼包含在那些465個位元組中。這是程式碼的一部分,它將作為智慧合約的執行時程式碼儲存在區塊鏈中,該程式碼將是每次有人或某事與智慧合約互動時執行的程式碼。(我們將在本系列的後續部分中介紹執行時程式碼)。
這正是指令66到69所做的:返回我們複製到記憶體的程式碼。
圖7.執行時程式碼返回EVM位元組碼結構。
RETURN抓取複製到記憶體的程式碼並將其交給EVM。如果此建立程式碼在對0x0地址的事務的上下文中執行,則EVM將執行程式碼並將返回值儲存為建立的智慧合約的執行時程式碼。
到現在為止,我們的BasicToken程式碼將建立和部署智慧合約例項,並準備好使用其初始狀態和執行時程式碼。如果退後一步並檢視圖2,您將看到我們分析的所有EVM位元組碼結構都是通用的,除了以紫色突出顯示的那個:也就是說,它們將是由Solidity編譯器生成的建立時位元組碼。建構函式與建構函式的區別僅在於紫色部分 - 建構函式的實際體。獲取嵌入在位元組碼末尾的引數的結構,以及複製執行時程式碼並將其返回的結構,可以被認為是樣板程式碼和通用EVM操作碼結構。您現在應該能夠檢視任何建構函式,在按指令學習之前,您應該對構成它的元件有一個大概的瞭解。
在本系列的下一篇文章中,我們將介紹實際的執行時程式碼,首先介紹如何在不同的入口點與智慧合約的EVM程式碼進行互動。現在,給自己一個當之無愧的輕拍,因為你剛剛消化了系列中最困難的部分。你還應該具有強大的能力來讀取和除錯EVM位元組碼,理解通用結構,最重要的是,瞭解建立時和執行時EVM位元組碼之間的區別。這就是使得合約的建構函式在Solidity中如此特殊的原因。
我們將在下一篇的系列文章中繼續解構!