本文是系列文章的第二部分。
如果您還沒有閱讀本文的前言,請先看一下,第一部分·引言
第一部分·引言
我們正在解構一個簡單的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處結束)。一級標題。
一級標題
一級標題
創建部分
現在,我們將深入研究代碼的創建部分。
圖1.解構BasicToken.sol的創建時EVM字節碼
這是本文中要理解的最重要的概念。創建代碼在事務中執行,該事務返回運行時代碼的副本,該副本是智能合約的實際代碼。正如我們將看到的,構造函數是創建代碼的一部分,而不是運行時代碼的一部分。智能合約的構造函數是創建代碼的一部分; 一旦部署,它將不會出現在智能合約的代碼中。
這種魔力是如何發生的?這就是我們現在要逐步分析的內容。
好的。所以現在我們的問題被簡化為理解這些與創建時代碼相對應的70條指令。
讓我們重新採用自上而下的方法,這次了解所有指令,而不是跳過任何指令。首先,讓我們關注使用PUSH1和MSTORE操作碼的指令0到2 。
圖2.空閒內存指針EVM字節碼結構
mstore(0x40, 0x80)
| |
| What to store.
Where to store.
(in memory)
PUSH1只需將一個字節壓入堆棧頂部,而MSTORE從堆棧中抓取最後兩個項並將其中一個存儲在內存中:
注意:上面的代碼片段是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則是停止執行。
if(msg.value!= 0)revert();
那麼這裡發生了什麼?在Solidity中,我們可以像這樣編寫這個彙編:
這段代碼實際上不是我們原始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代碼。
totalSupply_ = _initialSupply;
balances[msg.sender] = _initialSupply;
這些指令只不過是構造函數的主體:也就是Solidity代碼:
sstore(0x00, 0x2710)
| |
| What to store.
Where to store.
(in storage)
前四條指令非常明顯(38至42),首先,0被壓入堆棧,然後堆棧中的第二項被複製(這是我們的10000號碼),然後數字0被複製並被推送到堆棧,這是存儲中的位置槽totalSupply_。現在,SSTORE可以使用這些值,並且仍然保持10000個以下以備將來使用:
瞧!我們將數字10000存儲在變量中totalSupply_。是不是很神奇??
一定要在Remix的Debugger選項卡中可視化這個值。你可以在存儲完全加載的面板中找到它。
下一組指令(43到54)有點棘手,但基本上會處理在balances映射中存儲10000 的密鑰msg.sender。在繼續之前,請確保您了解Solidity文檔的這一部分,該文檔說明瞭如何在內存中保存映射。
簡而言之,它將連接映射值的槽(在這種情況下是數字1,因為它是智能合約中聲明的第二個變量)與使用的鍵(在這種情況下msg.sender,通過操作碼獲得CALLER),然後用SHA3操作碼取摘要並使用它作為在內存中的目標位置。最後,存儲只是一個簡單的字典或哈希表。
sstore(hash..., 0x2710)
| |
| What to store.
Where to store.
繼續執行指令43至45,將msg.sender地址存儲在內存中(此時在位置0x00),然後在指令46至50中,將值1(映射的槽)存儲在內存位置0x20。最後,SHA3操作碼計算從位置0x00到位置0x40的內存中的任何內容的Keccak256散列- 即映射的插槽/位置與所使用的鍵的串聯。這正是值10000將存儲在我們的映射中的位置:
此時,構造函數的主體已完全執行。
所有這些起初可能有點壓倒性,但它是存儲在Solidity中工作的基本部分。如果你沒有得到它,我建議你跟著Remix的調試器重複幾次,保持堆棧和內存面板。
另外,請隨時提出以下問題。此模式在Solidity生成的EVM字節碼中普遍使用,您將很快學會輕鬆識別它。最後,它只是計算在內存中保存映射的某個鍵的值的位置。
圖6.運行時代碼複製結構
在指令56至65中,我們再次執行代碼複製。只有這一次,我們不會將代碼的最後32個字節複製到內存中; 我們從位置0x0046(十進制70)開始復制0x01d1(十進制465)字節到位置0的內存。這是要復制的一大塊代碼!如果您再次將滑塊一直向右滑動,您將注意到位置70正好在我們的創建時EVM代碼之後,執行停止的地方。運行時字節碼包含在那些465個字節中。這是代碼的一部分,它將作為智能合約的運行時代碼保存在區塊鏈中,該代碼將是每次有人或某事與智能合約交互時執行的代碼。
(我們將在本系列的後續部分中介紹運行時代碼)。
這正是指令66到69所做的:返回我們複製到內存的代碼。
圖7.運行時代碼返回EVM字節碼結構。
RETURN抓取複製到內存的代碼並將其交給EVM。如果此創建代碼在對0x0地址的事務的上下文中執行,則EVM將執行代碼並將返回值存儲為創建的智能合約的運行時代碼。
獵豹區塊鏈安全以金山毒霸的技術為依托,結合人工智能、nlp等技術,為區塊鏈用戶提供合約審計、情感分析等生態安全服務。
獵豹區塊鏈安全以金山毒霸的技術為依托,結合人工智能、nlp等技術,為區塊鏈用戶提供合約審計、情感分析等生態安全服務。
*本文由Alejandro Santander首發於medium,由獵豹區塊鏈安全翻譯並整理*
獵豹區塊鏈安全以金山毒霸的技術為依托,結合人工智能、nlp等技術,為區塊鏈用戶提供合約審計、情感分析等生態安全服務。
Ratingtoken官網 https://www.ratingtoken.net/?from=z