資安

Solidity彙編開發中的動態數組使用

我們始終建議在開發Solidity智能合約時儘量不要使用匯編。但在少數情況下可能並沒有其他選擇,因此還是需要學習一些Solidity彙編開發的知識。在這個教程中,我們將學習如何在Solidity彙編開發中使用動態字節數組。

用自己熟悉的語言學習 以太坊DApp開發
Java | Php | Python | .Net / C# | Golang | Node.JS | Flutter / Dart

1、使用Remix編輯器

首先,讓我們將這個簡單的合約粘貼到remix編輯器中:

pragma solidity ^0.5.10;

contract AssemblyArrays {
  
  bytes testArray;
  
  function getLength() public view returns (uint256) {
      return testArray.length;
  }
  
  function getElement(uint256 index) public view returns (bytes1) {
      return testArray[index];
  }
  
  function pushElement(bytes1 value) public {
      testArray.push(value);
  }
  
  function updateElement(bytes1 value, uint256 index) public {
      testArray[index] = value;
  }
}

首先熟悉一下Remix編輯器。我們首先需要選擇編譯器版本,然後編譯合約、部署合約,執行一些功能,然後調試。

2、第一行彙編代碼

現在,讓我們修改getLength函數來編寫第一行彙編代碼:

function getLength() public view returns (uint256) {
  bytes memory memoryTestArray = testArray;
  uint256 result;
  assembly {
    result := mload(memoryTestArray)
  }
  return result;
}

上面幾行代碼中發生了很多事情。彙編就是這樣,要實現一個非常簡單的功能也需要很多代碼。我們將testArray從存儲複製到內存,因為這是本文的重點。以後我們可以再談一談存儲插槽。

在深入探討彙編語言塊之前,請注意彙編指令是對32字節的字進行操作。因此,mload指令會將
memoryTestArray指向的內存位置的32個字節壓入棧。。

3、Solidity彙編代碼的斷點設置與單步執行

現在調試一下。在Remix中,你可以通過單擊行號來設置一個斷點。讓我們在第11行上設置一個斷點,所以它看起來像這樣:
在這裡插入圖片描述

更新getLength功能後,請確保已再次編譯並重新部署了合約。現在,讓我們調用pushElement函數將字節0x05插入數組,然後調用getLength,該函數應返回1。

調用getLength後我們可以對其進行調試。在底部面板的最後一個調用中單擊“調試”按鈕,這將在左側欄中打開調試器。有一個用於快進的按鈕(如:fast_forward:),它跳到下一個斷點。讓我們單擊那個。如果你使用不同的編譯器或不同的設置,那麼對於你來說可能並不完全相同,但是其核心將是相同的。基本的思路是在mload執行之前獲取調試器,在我的環境中是#0871指令。
在這裡插入圖片描述

4、查看Solidity彙編代碼對棧的影響

現在讓我們看一下調試器側欄的棧/stack內容:

在這裡插入圖片描述

在堆棧頂部,位置0處為0x0...80。這是在內存中memoryTestArray的位置,該位置將作為mload指令的參數。

5、查看Solidity彙編代碼對內存的影響

現在,讓我們看一下調試器側邊欄的“ 內存”部分,從地址0x0...80開始:

在這裡插入圖片描述

這裡有31個字節0x00,後跟1個字節0x01,然後是1個字節0x05,後跟31個字節0x00。這可能有點令人困惑,所以讓我們退後一點,注意1個字節(8位)由2個十六進制數字表示(1個十六進制數字表示4位)。同樣,0x10十六進制的十進制等於16。因此,在內存中,位置0x80保存16個字節,位置0x90(0x80+ 0x10)保存隨後的16個字節,然後位置0xa0(0x90+ 0x10)保存以下16個字節,而位置0xb0保存最後的16個字節。因為彙編中的指令以32字節為單位操作,所以如果我們調用mload(0x80),它將從內存位置0x80開始取32字節放入棧。

6、單步執行mload指令

讓我們看看實際執行情況。讓我們單擊調試器中的“單步進入”按鈕(即向下的箭頭)來執行mload指令。現在看一下棧頂:

在這裡插入圖片描述

mload指令取棧頂內容:0x0...80,然後將內存中位置0x0...1的32個字節壓入,這是瞭解內存中的字節數組最重要的一點:前32個字節存儲數組的長度。

嘗試調用pushElement函數將元素0x06插入數組。然後調用getLength並再次調試。同樣,mload將從內存位置0x80開始載入32個字節,但是這次內存的內容為0x0...2。當我們追加新元素時,Solidity為我們更新了數組的大小。

內存中發生變化的另一件事是,現在位置0xa0是0x050600...00。因此,在內存中,一個字節數組變量在前32個字節存儲其長度,然後開始存儲具體的成員。首先我們壓入0x05,然後又壓入0x06。

在這裡插入圖片描述

7、用Solidity彙編重寫getLength方法

嘗試再壓入一些元素,調用getLength並調試,以查看內存中的新字節。如果我們將getElement 轉換為彙編,這個過程將變得更加清晰:

function getElement(uint256 index) public view returns (bytes1) {
    uint256 length = getLength();
    require(index < length);
    bytes memory memoryTestArray = testArray;
    bytes1 result;
    assembly {
      let wordIndex := div(index, 32)
      let initialElement := add(memoryTestArray, 32)
      let resultWord := mload(add(initialElement, mul(wordIndex, 32)))
      let indexInWord := mod(index, 32)
      result := shl(mul(indexInWord, 8), resultWord)
    }
    return result;
}

好吧,這可能有點嚇到你了!讓我們​​慢慢地捋一下。

第一件超級重要的事情是,我們添加了require語句來檢查index並沒有超出範圍。這在調用mload時是至關重要的,我們需要確保要載入的內存位置是正確的,否則可能就會洩漏調用者不應該訪問的信息,這可能會讓我們的合約存在嚴重的受攻擊風險。

接下來,讓我們看一下彙編代碼塊。由於mload一次讀取32個字節,因此僅讀取1個字節並不容易。如果我們把index除以32並取整,這將得到要查找的成員所在的32字節的序號。例如:

div(0, 32) = 0
div(18, 32) = 0
div(32, 32) = 1
div(65, 32) = 2

看起來還不錯。但是請記住,內存中memoryTestArray指向的內置的第一個字(32字節)是存儲數組長度的。因此,我們需要加上32個字節來查找第一個數組成員。考慮到所有這些因素後,我們就可以載入包含我們需要的1個字節的字(32字節):

memoryTestArray的內存位置加上32個字節以跳過數組長度,再加wordIndex上乘以32,因為每個字都有32個字節。

但是還沒有完成。現在我們需要從這個字中恰好提取1個字節。為此,我們需要在字中找到該字節的索引。這是字索引除以32的餘數部分,可以通過mod指令獲得。例如:

mod(0, 32) = 0
mod(18, 32) = 18
mod(32, 32) = 0
mod(65, 32) = 1

不錯,下面讓我們完成最後一步,提取該字節。為了讓字節在最前面,我們向左移動需要的位數。shl指令一次移動一位,所以為了移動指定的位數,我們要把indexInWord乘以8。

一旦我們將以這個字節開頭的32個字節的字賦值給result變量,它就會刪除所有其他字節,因為
我們將其類型聲明為bytes1


原文鏈接:Solidity彙編開發之動態數組 — 匯智網

Leave a Reply

Your email address will not be published. Required fields are marked *