1. 程式人生 > >智慧合約基礎語言(十)——Solidity內聯彙編

智慧合約基礎語言(十)——Solidity內聯彙編

智慧合約基礎語言(十)——Solidity內聯彙編

一、目錄

☞概念

☞語法

☞操作碼

☞字面量

☞函式風格

☞訪問外部函式與變數

☞標籤

☞定義區域性變數

☞賦值

☞Switch

☞迴圈

☞函式

☞內聯彙編中注意事項

☞Solidity中的慣例

二、概念

通常我們通過庫程式碼,來增強語言,實現一些精細化的控制,Solidity為我們提供了一種接近於EVM底層的語言,內聯彙編,允許與Solidity結合使用。由於EVM是棧式的,所以有時定位棧比較麻煩,Solidty的內聯彙編為我們提供了下述的特性,來解決手寫底層程式碼帶來的各種問題:

• 允許函式風格的操作碼:mul(1, add(2, 3))等同於push1 3 push1 2 add push1 1 mul

• 內聯區域性變數:let x := add(2, 3) let y := mload(0x40) x := add(x, y)

• 可訪問外部變數:function f(uint x) { assembly { x := sub(x, 1) } }

• 標籤:let x := 10 repeat: x := sub(x, 1) jumpi(repeat, eq(x, 0))

• 迴圈:for { let i := 0 } lt(i, x) { i := add(i, 1) } { y := mul(2, y) }

• switch語句:switch x case 0 { y := mul(x, 2) } default { y := 0 }

• 函式呼叫:function f(x) -> y { switch x case 0 { y := 1 } default { y := mul(x, f(sub(x, 1))) } }

需要注意的是內聯彙編是一種非常底層的方式來訪問EVM虛擬機器。他沒有Solidity提供的多種安全機制。

2.1 示例

下面的例子提供了一個庫函式來訪問另一個合約,並把它寫入到一個bytes變數中。有一些不能通過常規的Solidity語言完成,內聯庫可以用來在某些方面增強語言的能力。

內聯彙編在當編譯器沒辦法得到有效率的程式碼時非常有用。但需要留意的是內聯組合語言寫起來是比較難的,因為編譯器不會進行一些檢查,所以你應該只在複雜的,且你知道你在做什麼的事情上使用它。

三、語法

內聯組合語言也會像Solidity一樣解析註釋,字面量和識別符號。所以你可以使用//和/**/的方式註釋。內聯彙編的在Solidity中的語法是包裹在assembly { ... },下面是可用的語法,更詳細的語法請參考官方API https://solidity.readthedocs.io/en/v0.4.21/assembly.html。

• 字面量。如0x123,42或abc(字串最多是32個字元)

• 操作碼(指令的方式),如mload sload dup1 sstore,後面有可• 支援的指令列表

• 函式風格的操作碼,如add(1, mlod(0))

• 標籤,如name:

• 變數定義,如let x := 7 或 let x := add(y, 3)

• 識別符號(標籤或內聯區域性變數或外部),如jump(name),3 x add

• 賦值(指令風格),如,3 =: x。

• 函式風格的賦值,如x := add(y, 3)

• 支援塊級的區域性變數,如{ let x := 3 { let y := add(x, 1) } }

四、操作碼

如果一個操作碼有引數(通過在棧頂),那麼他們會放在括號。需要注意的是引數的順序可以顛倒(非函式風格,後面會詳細說明)。用-標記的操作碼不會將一個引數推到棧頂,而標記為*的是非常特殊的,所有其它的將且只將一個推到棧頂。

在後面的例子中,mem[a...b)表示成位置a到位置b(不包含)的memory位元組內容,storage[p]表示在位置p的strorage內容。

操作碼pushi和jumpdest不能被直接使用。在語法中,操作碼被表示為預先定義的識別符號。

五、字面常量

你可以直接鍵入十進位制或十六進位制符號來作為整型常量使用,這會自動生成相應的 PUSHi 指令。 下面的程式碼將計算 2 加 3(等於 5),然後計算其與字串 “abc” 的按位與。字串在儲存時為左對齊,且長度不能超過 32 位元組。

六、函式風格

你可以像使用位元組碼那樣在操作碼之後鍵入操作碼。例如,把 3 與記憶體位置 0x80 處的資料相加就是:

由於通常很難看到某些操作碼的實際引數是什麼,所以 Solidity 內聯彙編還提供了一種“函式風格”表示法,同樣功能的程式碼可以寫做:

函式風格表示式內不能使用指令風格的寫法,即 1 2 mstore(0x80, add) 是無效彙編語句, 它必須寫成 mstore(0x80, add(2, 1)) 這種形式。對於不帶引數的操作碼,括號可以省略。

注意,在函式風格寫法中引數的順序與指令風格相反。如果使用函式風格寫法,第一個引數將會位於棧頂。

七、訪問外部變數和函式

通過簡單使用它們名稱就可以訪問 Solidity 變數和其他識別符號。對於記憶體變數,這會將地址而不是值壓入棧中。 儲存變數是不同的,因為儲存變數的值可能不佔用完整的儲存槽,因此其“地址”由儲存槽和槽內的位元組偏移量組成。 為了獲取變數 x 所使用的儲存槽,你可以使用 x_slot,並用的 x_offset 獲取其位元組偏移量。

在賦值語句中(見下文),我們甚至可以使用 Solidity 區域性變數來賦值。

對於內聯彙編而言的外部函式也可以被訪問:彙編會將它們的入口標籤(帶有虛擬函式解析)壓入棧中。Solidity 中的呼叫語義為:

• 呼叫者壓入 return label、arg1、arg2、...、argn

• 被呼叫方返回 ret1、ret2、...、retm

這個特性使用起來還是有點麻煩,因為在呼叫過程中堆疊偏移量發生了根本變化,因此對區域性變數的引用將會出錯。

八、標籤

EVM 彙編的另一個問題是 jump 和 jumpi 函式使用絕對地址,這些絕對地址很容易改變。 Solidity 內聯彙編提供了標籤,以便更容易地使用 jump。注意,標籤具有底層特徵,使用迴圈、if 和 switch 指令(參見下文)而不使用標籤也能寫出高效彙編程式碼。 以下程式碼用來計算斐波那契數列中的一個元素。

請注意:只有彙編程式知道當前棧高度時,才能自動訪問堆疊變數。如果 jump 源和目標的棧高度不同,訪問將失敗。 雖然我們可以這麼使用 jump,但在這種情況下,你不應該去訪問任何棧裡的變數(即使是彙編變數)。

此外,棧高度分析器還可以通過操作碼(而不是根據控制流)檢查程式碼操作碼,因此在下面的情況下,彙編程式對標籤 two 處的堆疊高度會產生錯誤的印象:

九、彙編區域性變數宣告

你可以使用 let 關鍵字來宣告只在內聯彙編中可見的變數,實際上只在當前的 {...} 塊中可見。 下面發生的事情應該是:let 指令將建立一個為變數保留的新資料槽,並在到達塊末尾時自動刪除。 你需要為變數提供一個初始值,它可以只是 0,但它也可以是一個複雜的函式風格表示式。

十、賦值

可以給彙編區域性變數和函式區域性變數賦值。請注意:當給指向記憶體或儲存的變數賦值時,你只是更改指標而不是資料。

有兩種賦值方式:函式風格和指令風格。對於函式風格賦值(變數 := 值),你需要在函式風格表示式中提供一個值,它恰好可以產生一個棧裡的值; 對於指令風格賦值(=: 變數),則僅從棧頂部獲取資料。對於這兩種方式,冒號均指向變數名稱。賦值則是通過用新值替換棧中的變數值來實現的。

十一、If

if 語句可以用於有條件地執行程式碼,且沒有“else”部分;如果需要多種選擇,你可以考慮使用“switch”(見下文)。

十二、Swicth

作為“if/else”的非常初級的版本,你可以使用 switch 語句。它計算表示式的值並與幾個常量進行比較。選出與匹配常數對應的分支。 與某些程式語言容易出錯的情況不同,控制流不會從一種情形繼續執行到下一種情形。我們可以設定一個 fallback 或稱為 default 的預設情況。

十三、迴圈

組合語言支援一個簡單的 for-style 迴圈。For-style 迴圈有一個頭,它包含初始化部分、條件和迭代後處理部分。 條件必須是函式風格表示式,而另外兩個部分都是語句塊。如果起始部分聲明瞭某個變數,這些變數的作用域將擴充套件到迴圈體中(包括條件和迭代後處理部分)。

下面例子是計算某個記憶體區域中的數值總和。

For 迴圈也可以寫成像 while 迴圈一樣:只需將初始化部分和迭代後處理兩部分留空。

十四、函式

組合語言允許定義底層函式。底層函式需要從棧中取得它們的引數(和返回 PC),並將結果放入棧中。呼叫函式的方式與執行函式風格操作碼相同。

函式可以在任何地方定義,並且在宣告它們的語句塊中可見。函式內部不能訪問在函式之外定義的區域性變數。這裡沒有嚴格的 return 語句。

如果呼叫會返回多個值的函式,則必須使用 a,b:= f(x) 或 let a,b:= f(x) 的方式把它們賦值到一個元組。

下面例子通過平方和乘法實現了冪運算函式。

十五、注意事項

內聯組合語言可能具有相當高階的外觀,但實際上它是非常低階的程式語言。函式呼叫、迴圈、if 語句和 switch 語句通過簡單的重寫規則進行轉換, 然後,彙編程式為你做的唯一事情就是重新組織函式風格操作碼、管理 jump 標籤、計算訪問變數的棧高度,還有在到達語句塊末尾時刪除區域性彙編變數的棧資料。 特別是對於最後兩種情況,彙編程式僅會按照程式碼的順序計算棧的高度,而不一定遵循控制流程;瞭解這一點非常重要。此外,swap 等操作只會交換棧內的資料,而不是變數位置。

十六、Solidity慣例

與 EVM 組合語言相比,Solidity 能夠識別小於 256 位的型別,例如 uint24。為了提高效率,大多數算術運算只將它們視為 256 位數字, 僅在必要時才清除未使用的資料位,即在將它們寫入記憶體或執行比較之前才會這麼做。這意味著,如果從內聯彙編中訪問這樣的變數,你必須先手工清除那些未使用的資料位。

Solidity 以一種非常簡單的方式管理記憶體:在 0x40 的位置有一個“空閒記憶體指標”。如果你打算分配記憶體,只需從此處開始使用記憶體,然後相應地更新指標即可。

記憶體的開頭 64 位元組可以用來作為臨時分配的“暫存空間”。“空閒記憶體指標”之後的 32 位元組位置(即從 0x60 開始的位置)將永遠為 0,可以用來初始化空的動態記憶體陣列。

在 Solidity 中,記憶體陣列的元素總是佔用 32 個位元組的倍數(是的,甚至對於 byte[] 都是這樣,只有 bytes 和 string 不是這樣)。 多維記憶體陣列就是指向記憶體陣列的指標。動態陣列的長度儲存在陣列的第一個槽中,其後才是陣列元素。

-END-