Solidity彙編開發簡明教程
By ChainNode·
在用solidity開發以太坊智慧合約時,使用匯編可以直接與evm互動,降低 gas開銷成本,更精細的控制智慧合約的行為,因此值得solidity開發者學習 並加以利用。本文是solidity彙編開發的簡明教程,旨在幫助你快速熟悉 如何在solidity智慧合約程式碼中嵌入彙編程式碼。
## 以太坊虛擬機器和堆疊結構機器
以太坊虛擬機器evm有自己的指令集,該指令集中目前包含了 144個操作碼,詳情參考geth原始碼
這些指令是solidity抽象出來的,可以在solidity內聯使用。例如:
```
contract assembler {
function do_something_cpu() public {
assembly {
// start writing evm assembler language
}
}
}
```
evm是一個棧虛擬機器,棧這種資料結構只允許兩個操作:壓入(push)或彈出(pop)資料。 最後壓入的資料位於棧頂,因此將被第一個彈出,這被稱為後進先出 (lifo:last in, first out):
![image.png](https://appserversrc.8btc.com/post/2020030215443080366.png)
棧虛擬機器將所有的運算元儲存在棧上,關於棧虛擬機器的詳細資訊 可以參考stack machine 基礎
## 堆疊結構機器的操作碼
為了能夠解決實際問題,棧結構機器需要實現一些額外的指令,例如 add、substract等等。指令執行時通常會先從堆疊彈出一個或多個值作為引數, 再將執行結果壓回堆疊。這通常被稱為逆波蘭表示法(rpn:reverse polish notation):
```
a + b // 標準表示法infix
a b add // 逆波蘭表示法rpn
```
## 在solidity合約中使用內聯彙編
可以在solidity中使用assembly{}來嵌入彙編程式碼段,這被稱為內聯彙編:
```
assembly {
// some assembly code here
}
```
在assembly塊內的程式碼開發語言被稱為yul,為了簡化我們稱其為 彙編或evm彙編。
另一個需要注意的問題時,彙編程式碼塊之間不能通訊,也就是說在 一個彙編程式碼塊裡定義的變數,在另一個彙編程式碼塊中不可以訪問。 例如:
```
assembly {
let x := 2
}
assembly {
let y := x // error
}
```
上面的程式碼編譯時會報如下錯誤:
```
// declarationerror: identifier not found
// let y := x
// ^
```
下面的程式碼使用內聯彙編程式碼計算函式的兩個引數的和並返回結果:
```
function addition(uint x, uint y) public pure returns (uint) {
assembly {
let result := add(x, y) // x + y
mstore(0x0, result) // 在記憶體中儲存結果
return(0x0, 32) // 從記憶體中返回32位元組
}
}
```
讓我們重寫上面的程式碼,補充一些更詳細的註釋,以便說明每個指令 在evm內部的執行原理。
```
function addition(uint x, uint y) public pure returns (uint) {
assembly {
// 建立一個新的變數result
// -> 使用add操作碼計算x+y
// -> 將計算結果賦值給變數result
let result := add(x, y) // x + y
// 使用mstore操作碼
// -> 將result變數的值存入記憶體
// -> 指定記憶體地址 0x0
mstore(0x0, result) // 將結果存入記憶體
// 從記憶體地址0x返回32位元組
return(0x0, 32)
}
}
```
## solidity彙編中的變數定義與賦值
在yul中,使用let關鍵字定義變數。使用:=運算子給變數賦值:
```
assembly {
let x := 2
}
```
如果沒有使用:=運算子給變數賦值,那麼該變數自動初始化為0值:
```
assembly {
let x // 自動初始化為 x = 0
x := 5 // x 現在的值是5
}
```
你可以使用複雜的表示式為變數賦值,例如:
```
assembly {
let x := 7
let y := add(x, 3)
let z := add(keccak256(0x0, 0x20), div(slength, 32))
let n
}
```
## solidity彙編中let指令的執行機制
在evm的內部,let指令執行如下任務:
- 建立一個新的堆疊槽位
- 為變數保留該槽位
- 當到達程式碼塊結束時自動銷燬該槽位
因此,使用let指令在彙編程式碼塊中定義的變數,在該程式碼塊 外部是無法訪問的。
## solidity彙編中的註釋
在yul彙編中註釋的寫法和solidity一樣,可以使用單行註釋// 或多行註釋/* */。例如:
```
assembly {
// single line comment
/*
multi
line
comment
*/
}
```
## solidity彙編中的字面量
在solidity彙編中字面量的寫法與solidity一致。不過,字串字面量 最多可以包含32個字元。
```
assembly {
let a := 0x123 // 16進位制
let b := 42 // 10進位制
let c := "hello world" // 字串
let d := "very long string more than 32 bytes" // 超長字串,錯誤!
}
```
## solidity彙編中的塊和作用範圍
在solidity彙編中,變數的作用範圍遵循標準規則。一個塊的範圍使用 一對大括號標識。
在下面的示例中,y和z僅在定義所在塊範圍內有效。因此y變數的作用 範圍是scope 1,z變數的作用範圍是scope 2。
```
assembly {
let x := 3 // x在各處可見
// scope 1
{
let y := x // ok
} // 到此處會銷燬y
// scope 2
{
let z := y // error
} // 到此處會銷燬z
}
// declarationerror: identifier not found
// let z := y
// ^
```
作用範圍的唯一例外是函式和for迴圈,我們將在下面解釋。
## 在solidity彙編中使用函式的區域性變數
在solidity彙編中,只需要使用變數名就可以訪問區域性變數, 無論該變數是定義在彙編塊中,還是solidity程式碼中,不過 變數必須是函式的區域性變數:
```
function assembly_local_var_access() public pure {
uint b = 5;
assembly { // defined inside an assembly block
let x := add(2, 3)
let y := 10
z := add(x, y)
}
assembly { // defined outside an assembly block
let x := add(2, 3)
let y := mul(x, b)
}
}
```
## 在solidity彙編中使用for迴圈
先看一下solidity中迴圈的使用。下面的solidity函式程式碼中 計算變數的倍數n次,其中value和n是函式的引數:
```
function for_loop_solidity(uint n, uint value) public pure returns(uint) {
for ( uint i = 0; i < n; i++ ) {
value = 2 * value;
}
return value;
}
```
等效的solidity彙編程式碼如下:
```
function for_loop_assembly(uint n, uint value) public pure returns (uint) {
assembly {
for { let i := 0 } lt(i, n) { i := add(i, 1) } {
value := mul(2, value)
}
mstore(0x0, value)
return(0x0, 32)
}
}
```
類似於其他開發語言中的for迴圈,在solidity彙編中,for迴圈也包含 3個元素:
```
初始化:let i := 0
執行條件:lt(i, n) ,必須是函式風格表示式
迭代後續步驟:add(i, 1)
注意:for迴圈中變數的作用範圍略有不同。在初始化部分定義的變數 在迴圈的其他部分都有效。
```
## 在solidity彙編中使用while迴圈
在solidity彙編中實際上是沒有while迴圈關鍵字的,但是可以使用 for迴圈實現同樣的功能:只要留空for迴圈的初始化部分和迭代後續步驟即可。
```
assembly {
let x := 0
let i := 0
for { } lt(i, 0x100) { } { // 等價於:while(i < 0x100)
x := add(x, mload(i))
i := add(i, 0x20)
}
}
```
## 在solidity彙編中使用if語句
solidity內聯彙編支援使用if語句來設定程式碼執行的條件,但是 沒有其他語言中的else部分。
```
assembly {
if slt(x, 0) { x := sub(0, x) } // ok
if eq(value, 0) revert(0, 0) // error, 需要大括號
}
```
if語句強制要求程式碼塊使用大括號,即使需要保護的程式碼只有一行, 也需要使用大括號。這和solidity不同。
如果需要在solidity內聯彙編中檢查多種條件,可以考慮使用 switch語句。
## 在solidity彙編中使用switch語句
evm彙編中也有switch語句,它將一個表示式的值於多個常量 進行對比,並選擇相應的程式碼分支來執行。switch語句支援 一個預設分支default,當表示式的值不匹配任何其他分支條件時,將 執行預設分支的程式碼。
```
assembly {
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
```
switch語句有一些限制:
分支列表不需要大括號,但是分支的程式碼塊需要大括號
所有的分支條件值必須:1)具有相同的型別 2)具有不同的值
如果分支條件已經涵蓋所有可能的值,那麼不允許再出現default條件
```
assembly {
let x := 34
switch lt(x, 30)
case true {
// do something
}
case false {
// do something els
}
default {
// 不允許
}
}
```
## 在solidity彙編中使用函式
也可以在solidity內聯彙編中定義底層函式。呼叫這些自定義的函式 和使用內建的操作碼一樣。
下面的彙編函式用來分配指定長度的記憶體,並返回記憶體指標pos:
```
assembly {
function allocate(length) -> pos {
pos := mload(0x40)
mstore(0x40, add(pos, length))
}
let free_memory_pointer := allocate(64)
}
```
彙編函式的執行機制如下:
- 從堆疊提取引數
- 將結果壓入堆疊
- 和solidity函式不同,不需要指定彙編函式的可見性,例如public或private, 因為彙編函式僅在定義所在的彙編程式碼塊內有效。
## solidity彙編中的操作碼
evm操作碼可以分為以下幾類:
- 算數和比較操作
- 位操作
- 密碼學操作,目前僅包含keccak256
- 環境操作,主要指與區塊鏈相關的全域性資訊,例如blockhash或coinbase收款賬號
- 儲存、記憶體和棧操作
- 交易與合約呼叫操作
- 停機操作
- 日誌操作
詳細的操作碼可以檢視[solidity文件](https://solidity.readthedocs.io/en/v0.6.2/yul.html#evm-dialect)。
原文連結:[solidity tutorial : all about assembly](https://medium.com/@jeancvllr/solidity-tutorial-all-about-assembly-5acdfefde05c)
匯智網翻譯#資訊
免責聲明:
- 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
- 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
- 鏈報僅提供相關項目信息,不構成任何投資建議。
推荐阅读