Solidity語言學習筆記————38、Solidity彙編

FLy_鵬程萬里發表於2018-07-08

Solidity彙編(Solidity Assembly)

Solidity定義了一個組合語言,可以不同Solidity一起使用。這個組合語言還可以嵌入到Solidity原始碼中,以內聯彙編的方式使用。下面我們將從內聯彙編如何使用著手,介紹其與獨立使用的組合語言的不同,最後再介紹這門組合語言。

內聯彙編(Inline Assembly)

通常我們通過庫程式碼,來增強語言我,實現一些精細化的控制,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))) } }

下面將詳細介紹內聯編譯(inline assembly)語言。

警告
需要注意的是內聯編譯是一種非常底層的方式來訪問EVM虛擬機器。他沒有Solidity提供的多種安全機制。
註解
TODO:待補充內聯彙編的變數作用域的不同,尤其是使用含internal的函式的庫時所引入的複雜度。另外,還需補充,編譯器定義的符號(symbols)。

示例

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

pragma solidity ^0.4.0;

library GetCode {
    function at(address _addr) public view returns (bytes o_code) {
        assembly {
            // 取得程式碼的大小,這需要彙編
            let size := extcodesize(_addr)
            // 分配輸出位元組陣列
            // 也可以不使用匯編完成——o_code = new bytes(size)
            o_code := mload(0x40)
            // new "memory end" including padding
            mstore(0x40,到add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
            // 儲存長度在記憶體中
            mstore(o_code, size)
            // 實際取得程式碼,這需要彙編
            extcodecopy(_addr, add(o_code, 0x20), 0, size)
        }
    }
}
內聯彙編在當編譯器沒辦法得到有效率的程式碼時非常有用。但需要留意的是內聯彙編寫起來是比較難的,因為編譯器不會進行一些檢查,所以你應該只在複雜的,且你知道你在做什麼的事情上使用它。
pragma solidity ^0.4.16;

library VectorSum {
    // 此函式效率較低,因為優化器當前無法
    // 移除陣列訪問中的邊界檢查。
    function sumSolidity(uint[] _data) public view returns (uint o_sum) {
        for (uint i = 0; i < _data.length; ++i)
            o_sum += _data[i];
    }

    // 我們知道我們只訪問陣列,所以我們可以避免檢查。
    // 0x20需要新增到一個陣列中,因為第一個位置包含
    // 陣列長度。
    function sumAsm(uint[] _data) public view returns (uint o_sum) {
        for (uint i = 0; i < _data.length; ++i) {
            assembly {
                o_sum := add(o_sum, mload(add(add(_data, 0x20), mul(i, 0x20))))
            }
        }
    }

    // 功能與上述相同,但完全使用內聯彙編。
    function sumPureAsm(uint[] _data) public view returns (uint o_sum) {
        assembly {
           // 載入長度(前32位元組)
           let len := mload(_data)

           // 跳過長度欄位。
           //
           // 保持臨時變數,以便可以在適當位置增加。
           //
           // 注意:在這個彙編程式塊之後,遞增的資料會導致一個不可用的資料變數。
           let data := add(_data, 0x20)

           // Iterate until the bound is not met.
           for
               { let end := add(data, len) }
               lt(data, end)
               { data := add(data, 0x20) }
           {
               o_sum := add(o_sum, mload(data))
           }
        }
    }
}

語法

內聯編譯語言也會像Solidity一樣解析註釋,字面量和識別符號。所以你可以使用///**/的方式註釋。內聯編譯的在Solidity中的語法是包裹在assembly { ... },下面是可用的語法,後續有更詳細的內容。

  • 字面量。如0x12342"abc"(字串最多是32個字元)
  • 操作碼(指令的方式),如mload sload dup1 sstore,後面有可支援的指令列表
  • 函式風格的操作碼,如add(1, mlod(0)
  • 標籤,如name:
  • 變數定義,如let x := 7 、let x := add(y, 3)let x(初始值為0)
  • 識別符號(標籤或內聯區域性變數或外部),如jump(name)3 x add
  • 賦值(指令風格),如,3 =: x
  • 函式風格的賦值,如x := add(y, 3)
  • 支援塊級的區域性變數,如{ let x := 3 { let y := add(x, 1) } }

操作碼

這個文件不想介紹EVM虛擬機器的完整描述,但後面的列表可以做為EVM虛擬機器的指令碼的一個參考。

如果一個操作碼有引數(通過在棧頂),那麼他們會放在括號。需要注意的是引數的順序可以顛倒(非函式風格,後面會詳細說明)。用-標記的操作碼不會將一個引數推到棧頂,而標記為*的是非常特殊的,所有其它的將且只將一個推到棧頂。用FHBC標記的操作碼分別來自Frontier、Homestead、Byzantium或Constantinople。Constantinople仍在計劃中,所有這樣的指令都會導致無效的指令異常。

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

操作碼pushijumpdest不能被直接使用。

在語法中,操作碼被表示為預先定義的識別符號。

操作碼  說明
stop-F停止執行,等同於return(0,0)
add(x, y) Fx + y
sub(x, y) Fx - y
mul(x, y) Fx * y
div(x, y) Fx / y
sdiv(x, y) Fx / y, for signed numbers in two’s complement
mod(x, y) Fx % y
smod(x, y) Fx % y, for signed numbers in two’s complement
exp(x, y) Fx to the power of y
not(x) F~x, every bit of x is negated
lt(x, y) F1 if x < y, 0 otherwise
gt(x, y) F1 if x > y, 0 otherwise
slt(x, y) F1 if x < y, 0 otherwise, for signed numbers in two’s complement
sgt(x, y) F1 if x > y, 0 otherwise, for signed numbers in two’s complement
eq(x, y) F1 if x == y, 0 otherwise
iszero(x) F1 if x == 0, 0 otherwise
and(x, y) Fbitwise and of x and y
or(x, y) Fbitwise or of x and y
xor(x, y) Fbitwise xor of x and y
byte(n, x) Fnth byte of x, where the most significant byte is the 0th byte
shl(x, y) Clogical shift left y by x bits
shr(x, y) Clogical shift right y by x bits
sar(x, y) Carithmetic shift right y by x bits
addmod(x, y, m) F(x + y) % m with arbitrary precision arithmetics
mulmod(x, y, m) F(x * y) % m with arbitrary precision arithmetics
signextend(i, x) Fsign extend from (i*8+7)th bit counting from least significant
keccak256(p, n) Fkeccak(mem[p…(p+n)))
sha3(p, n) Fkeccak(mem[p…(p+n)))
jump(label)-Fjump to label / code position
jumpi(label, cond)-Fjump to label if cond is nonzero
pc Fcurrent position in code
pop(x)-Fremove the element pushed by x
dup1 … dup16 Fcopy ith stack slot to the top (counting from top)
swap1 … swap16*Fswap topmost and ith stack slot below it
mload(p) Fmem[p..(p+32))
mstore(p, v)-Fmem[p..(p+32)) := v
mstore8(p, v)-Fmem[p] := v & 0xff (only modifies a single byte)
sload(p) Fstorage[p]
sstore(p, v)-Fstorage[p] := v
msize Fsize of memory, i.e. largest accessed memory index
gas Fgas still available to execution
address Faddress of the current contract / execution context
balance(a) Fwei balance at address a
caller Fcall sender (excluding delegatecall)
callvalue Fwei sent together with the current call
calldataload(p) Fcall data starting from position p (32 bytes)
calldatasize Fsize of call data in bytes
calldatacopy(t, f, s)-Fcopy s bytes from calldata at position f to mem at position t
codesize Fsize of the code of the current contract / execution context
codecopy(t, f, s)-Fcopy s bytes from code at position f to mem at position t
extcodesize(a) Fsize of the code at address a
extcodecopy(a, t, f, s)-Flike codecopy(t, f, s) but take code at address a
returndatasize Bsize of the last returndata
returndatacopy(t, f, s)-Bcopy s bytes from returndata at position f to mem at position t
create(v, p, s) Fcreate new contract with code mem[p..(p+s)) and send v wei and return the new address
create2(v, n, p, s) Ccreate new contract with code mem[p..(p+s)) at address keccak256(
. n . keccak256(mem[p..(p+s))) and send v wei and return the new address
call(g, a, v, in, insize, out, outsize) Fcall contract at address a with input mem[in..(in+insize)) providing g gas and v wei and output area mem[out..(out+outsize)) returning 0 on error (eg. out of gas) and 1 on success
callcode(g, a, v, in, insize, out, outsize) Fidentical to call but only use the code from a and stay in the context of the current contract otherwise
delegatecall(g, a, in, insize, out, outsize) Hidentical to callcode but also keep caller and callvalue
staticcall(g, a, in, insize, out, outsize) Bidentical to call(g, a, 0, in, insize, out, outsize) but do not allow state modifications
return(p, s)-Fend execution, return data mem[p..(p+s))
revert(p, s)-Bend execution, revert state changes, return data mem[p..(p+s))
selfdestruct(a)-Fend execution, destroy current contract and send funds to a
invalid-Fend execution with invalid instruction
log0(p, s)-Flog without topics and data mem[p..(p+s))
log1(p, s, t1)-Flog with topic t1 and data mem[p..(p+s))
log2(p, s, t1, t2)-Flog with topics t1, t2 and data mem[p..(p+s))
log3(p, s, t1, t2, t3)-Flog with topics t1, t2, t3 and data mem[p..(p+s))
log4(p, s, t1, t2, t3, t4)-Flog with topics t1, t2, t3, t4 and data mem[p..(p+s))
origin Ftransaction sender
gasprice Fgas price of the transaction
blockhash(b) Fhash of block nr b - only for last 256 blocks excluding current
coinbase Fcurrent mining beneficiary
timestamp Ftimestamp of the current block in seconds since the epoch
number Fcurrent block number
difficulty Fdifficulty of the current block
gaslimit Fblock gas limit of the current block

字面量(Literals)

你可以使用整數常量,通過直接以十進位制或16進位制的表示方式,將會自動生成恰當的pushi指令。

assembly { 2 3 add "abc" and }

上面的例子中,將會先加2,3得到5,然後再與字串abc進行與運算。字串按左對齊儲存,且不能超過32位元組。

函式風格(Functional Style)

你可以在操作碼後接著輸入操作碼,它們最終都會生成正確的位元組碼。比如下面將會新增3到memory中位置0x80

3 0x80 mload add 0x80 mstore
由於經常很難直觀的看到某個操作碼真正的引數,Solidity內聯編譯提供了一個函式風格的表示式,上面的程式碼與下述等同:

mstore(0x80, add(mload(0x80), 3))

函式風格的表示式不能在內部使用指令風格,如1 2 mstore(0x80, add)將不是合法的,必須被寫為mstore(0x80, add(2, 1))。那些不帶引數的操作碼,括號可以忽略。

需要注意的是函式風格的引數與指令風格的引數是反的。如果使用函式風格,第一個引數將會出現在棧頂。

訪問外部函式與變數(Access to External Variables and Functions)

Solidity中的變數和其它識別符號,可以簡單的通過名稱引用。對於memory變數,這將會把地址而不是值推到棧上。Storage的則有所不同,由於對應的值不一定會佔滿整個storage槽位,所以它的地址由槽和實際儲存位置相對起始位元組偏移。要搜尋變數x指向的槽位,使用x_slot,得到變數相對槽位起始位置的偏移使用x_offset

在賦值中(見下文),我們甚至可以直接向Solidity變數賦值。

還可以訪問內聯編譯的外部函式:內聯編譯會推入整個的入口的label(應用虛擬函式解析的方式)。Solidity中的呼叫語義如下:

  • 呼叫者推入return labelarg1arg2, … argn
  • 呼叫返回ret1ret2,…, retm

這個功能使用起來還是有點麻煩,因為堆疊偏移量在呼叫過程中基本上有變化,因此對區域性變數的引用將是錯誤的。


pragma solidity ^0.4.11;

contract C {
    uint b;
    function f(uint x) public returns (uint r) {
        assembly {
            r := mul(x, sload(b_slot)) // ignore the offset, we know it is zero
        }
    }
}

註解
如果訪問小於256位的型別的變數(例如uint64addressbytes16 或 byte),則不能對不屬於該型別編碼的位進行任何假設。特別是,不要假定它們為零。為了安全起見,在使用該資料之前,請始終正確清除資料:uint32 x = f(); assembly { x := and(x, 0xffffffff) /* now use x */ }。清除有符號的型別,您可以使用signextend操作碼。

標籤(Labels)

註解
標籤被棄用。請使用函式、迴圈、if或switch語句來代替。

另一個在EVM的彙編的問題是jumpjumpi使用了絕對地址,可以很容易的變化。Solidity內聯彙編提供了標籤來讓jump跳轉更加容易。需要注意的是標籤是非常底層的特性,儘量使用內聯彙編函式,迴圈,Switch指令來代替。下面是一個求Fibonacci的例子:

{
    let n := calldataload(4)
    let a := 1
    let b := a
loop:
    jumpi(loopend, eq(n, 0))
    a add swap1
    n := sub(n, 1)
    jump(loop)
loopend:
    mstore(0, a)
    return(0, 0x20)
}

需要注意的是自動訪問棧元素需要內聯者知道當前的棧高。這在跳轉的源和目標之間有不同棧高時將失敗。當然你也仍然可以在這種情況下使用jump,但你最好不要在這種情況下訪問棧上的變數(即使是內聯變數)。

此外,棧高分析器會一個操作碼接著一個操作碼的分析程式碼(而不是根據控制流),所以在下面的情況下,彙編程式將對標籤two的堆疊高度產生錯誤的判斷:

{
    let x := 8
    jump(two)
    one:
        // Here the stack height is 2 (because we pushed x and 7),
        // but the assembler thinks it is 1 because it reads
        // from top to bottom.
        // Accessing the stack variable x here will lead to errors.
        x := 9
        jump(three)
    two:
        7 // push something onto the stack
        jump(one)
    three:
}

宣告彙編-區域性變數(Declaring Assembly-Local Variables)

你可以通過let關鍵字來定義在內聯彙編中有效的變數,實際上它只是在{...}中有效。內部實現上是,在let指令出現時會在棧上建立一個新槽位,來儲存定義的臨時變數,在塊結束時,會自動在棧上移除對應變數。你需要為變數提供一個初始值,比如0,但也可以是複雜的函式表示式:

pragma solidity ^0.4.16;

contract C {
    function f(uint x) public view returns (uint b) {
        assembly {
            let v := add(x, 1)
            mstore(0x80, v)
            {
                let y := add(sload(v), 1)
                b := y
            } // y is "deallocated" here
            b := add(b, v)
        } // v is "deallocated" here
    }
}

賦值(Assignments)

你可以向內聯區域性變數賦值,或者函式區域性變數。需要注意的是當你向一個指向memory或storage賦值時,你只是修改了對應指標而不是對應的資料。

有兩種方式的賦值方式:函式風格和指令風格。函式風格,比如variable := value,你必須在函式風格的表示式中提供一個變數,最終將得到一個棧變數。指令風格=: variable,值則直接從棧底取。以於兩種方式冒號指向的都是變數名稱。賦值的效果是將棧上的變數值替換為新值。

{
    let v := 0 // functional-style assignment as part of variable declaration
    let g := add(v, 2)
    sload(10)
    =: v // instruction style assignment, puts the result of sload(10) into v
}
註解
指令風格賦值被deprecated。

If

if語句可用於有條件地執行程式碼。沒有“else”部分,如果需要多個備選方案,請考慮使用“switch”(見下文)

{
    if eq(value, 0) { revert(0, 0) }
}

body的花括號是必須的。

Switch

你可以使用switch語句來作為一個基礎版本的if/else語句。它需要取一個值,用它來與多個常量進行對比。每個分支對應的是對應切爾西到的常量。與某些語言容易出錯的行為相反,控制流不會自動從一個判斷情景到下一個場景(即預設是break的)。最後有個叫default的兜底。

{
    let x := 0
    switch calldataload(4)
    case 0 {
        x := calldataload(0x24)
    }
    default {
        x := calldataload(0x44)
    }
    sstore(0, div(x, 2))
}

可以有的case不需要包裹到大括號中,但每個case需要用大括號的包裹。

迴圈

內聯彙編支援一個簡單的for風格的迴圈。for風格的迴圈的頭部有三個部分,一個是初始部分,一個條件和一個後疊加部分。條件必須是一個函式風格的表示式,而其它兩個部分用大括號包裹。如果在初始化的塊中定義了任何變數,這些變數的作用域會被預設擴充套件到迴圈體內(條件,與後面的疊加部分定義的變數也類似。因為預設是塊作用域,所以這裡是一種特殊情況)。

下面的示例計算記憶體中的一個區域的和。

{
    let x := 0
    for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
        x := add(x, mload(i))
    }
}
for迴圈也可以類似於while迴圈:簡單地保留初始化和迭代後的部分空。
{
    let x := 0
    let i := 0
    for { } lt(i, 0x100) { } {     // while(i < 0x100)
        x := add(x, mload(i))
        i := add(i, 0x20)
    }
}

函式

組合語言允許定義底層的函式。這些需要在棧上取引數(以及一個返回的程式碼行),也會將結果存到棧上。呼叫一個函式與執行一個函式風格的操作碼看起來是一樣的。

函式可以在任何地方定義,可以在定義的塊中可見。在函式內,你不能訪問一個在函式外定義的一個區域性變數。同時也沒有明確的return語句。

如果你呼叫一個函式,並返回了多個值,你可以將他們賦值給一個元組,使用a, b := f(x)let a, b := f(x)

下面的例子中通過平方乘來實現一個指數函式。

{
    function power(base, exponent) -> result {
        switch exponent
        case 0 { result := 1 }
        case 1 { result := base }
        default {
            result := power(mul(base, base), div(exponent, 2))
            switch mod(exponent, 2)
                case 1 { result := mul(base, result) }
        }
    }
}

要注意的事(Things to Avoid)

內聯彙編使用中需要一個比較高的視野,但它又是非常底層的語法。函式呼叫,迴圈,switch被轉換為簡單的重寫規則,另外一個語言提供的是重安排函式風格的操作碼,管理了jump標籤,計算了棧高以方便變數的訪問,同時在塊結束時,移除塊內定義的塊內的區域性變數。特別需要注意的是最後兩個情況。你必須清醒的知道,組合語言只提供了從開始到結束的棧高計算,它沒有根據你的邏輯去計算棧高(譯者注:這常常導致錯誤)。此外,像交換這樣的操作,僅僅交換棧裡的內容,並不是變數的位置。

Solidity中的慣例

與EVM彙編不同,Solidity知道型別少於256位元組,如,uint24。為了讓他們更高效,大多數的數學操作僅僅是把也們當成是一個256位元組的數字進行計算,高位的位元組只在需要的時候才會清理,比如在寫入記憶體前,或者在需要比較時。這意味著如果你在內聯彙編中訪問這樣的變數,你必須要手動清除高位的無效位元組。

Solidity以非常簡單的方式來管理記憶體:內部存在一個空間記憶體的指標在記憶體位置0x40。如果你想分配記憶體,可以直接使用從那個位置的記憶體,並相應的更新指標。

記憶體的前64個位元組可以用作短期分配的“劃痕空間”。空閒記憶體指標(即0x60開始)後的32位元組永遠為0,並用作空動態記憶體陣列的初始值。

Solidity中的記憶體陣列元素,總是佔用多個32位元組的記憶體(也就是說byte[]也是這樣,但是bytesstring不是這樣)。多維的memory的陣列是指向memory的陣列。一個動態陣列的長度儲存在資料的第一個槽位,緊接著就是陣列的元素。

警告
靜態大小的記憶體陣列沒有長度欄位,但是它在未來很快會被新增,以便在靜態和動態大小的陣列之間允許更好的可互換性,所以請不要依賴於此。


相關文章