1. 程式人生 > >Solidity語言學習(10) —— Solidity彙編(Solidity Assembly)

Solidity語言學習(10) —— Solidity彙編(Solidity Assembly)

solidity定義一個組合語言,這個語言可以在沒有Solidity下使用。該組合語言也能在Solidity原始碼中被用作“內聯”。我們從這樣使用內聯彙編以及怎樣區分其與離線彙編開始介紹,然後接下來詳細介紹彙編。

內聯彙編(Inline Assembly)

為了更細膩的控制,尤其是通過寫庫來提升語言,在一個接近虛擬即的語言中插入包含內聯彙編的Solidity指令是完全可能的。因為EVM是一個堆疊機器,通常定位正確的堆疊槽以及在堆疊中正確的點提供引數給操作程式碼都是比較困難的。Solidity的內聯彙編嘗試使這些問題以及手動編寫彙編時產生的其他爭議簡單化,通過下面的特徵:

  • 函式風格 的操作程式碼: 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)
  • 訪問external變數:function f(uint x) public { 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) }
  • if指令: if slt(x, 0) { x := sub(0, x) }
  • 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))) } }

    現在我們想要介紹更多在內聯組合語言的細節

warning:
內聯彙編是訪問EVM的一種低階方法。它丟棄了許多Solidity的重要的安全特性。
Note:
TODO:以下資訊包括關於內聯彙編的範圍規則如何不同,以及當比如說使用庫的internal函式時產生的複雜情況。以及更多的有關於編譯器定義的符號。

示例

下面的示例提供了訪問另一個合約程式碼的庫程式碼,並且將其裝載到一個 bytes 型變數中。這在“樸素Solidity(plain Solidity)”中是根本不可能的,它的核心思路是使用匯編庫提升語言(效能)。

pragma solidity ^0.4.0;

library GetCode {
    function at(address _addr) public view returns (Bytes memory 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(ox1f))))
            // store length in memory
            mstore(o_code, size)
            // actually retrieve the code, this needs assembly
            extcodecopy(_addr, add(o_code, 0x20), 0, size)
        }
    }
}

當最優化器無法產生高效的程式碼,內聯彙編就顯得非常方便。請注意彙編是更難編寫的,因為編譯器並不會檢查(彙編程式碼),所以當且僅當你真的瞭解你需要做什麼時,你可以將彙編用於複雜的事情上。

pragma solidity ^0.4.16;

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

     // 如果我們知道我們僅僅訪問陣列範圍內的數,我們可以避免邊界檢查
     // 0x20 需要被加入一個數列,因為數列的第一個槽包含了數列的長度
     function sumAsm(uint[] _data) public view returns (uint ox-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 {
             // Load the length (first 32 bytes)
             let len := mload(_data)

             // 跳過長度欄位(skip over the length field.)
             //
             // 保留一個臨時變數,再合適的時候增加它
             //
             // Note:如果增加 _data, 將導致在該元件阻塞後,_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))
             }
         }
     }
}

語法結構(Syntax)

Solidity中彙編 從語法上分析 恰好可分為評註(comment),直接量(literal)和 識別符號(identify)。所有你可以使用常見的//或者/* */評註。內聯彙編由assembly { ... }來標示,在花括號裡面,以下的都可以使用(可以參見後面的章節瞭解更多細節):

  • literals(直接量),比如 0x123,42 ,"abc" (最多32個文字的字串)
  • 操作程式碼opcode(in “instruction style”)比如:mload sload dupl sstore,可以看下面的列表
  • opcode in functional style 比如 add(1, mlod(0))
  • labels標籤,比如 name:
  • 變數宣告,比如:let x := 7,let x := add(y, 3),或者let x(賦予初始化值——空值(0))
  • 識別符號(標籤或者本地元件變數(assembly-local variable)以及被用作內聯彙編的externals)比如 jump(name),3 x add
  • 賦值assignments ( in “imstructrion style”) ,比如 3 =: x
  • assignments in functional style 比如 x := add(y, 3)
  • 本地變數被管轄在內的塊 比如 { let x := 3 { let y:= add(x, 1) } }

操作程式碼(Opcodes)

本文件並不希望成為EVM的一個完全介紹,但是下面的列表能夠被用作它的操作程式碼的一個索引。

如果一個操作程式碼使用了引數(總是從棧的頂端),它們會在圓括號中給出。注意引數的順序在非函式風格的呼叫中可能反向(下面會解釋)。用_標記的操作程式碼不會向棧中推入任何專案,而用×標記的操作碼是特別的,而其他的所用操作碼都會向棧中推入正好一個專案。標記有FHB或者C的操作碼錶示分別來自於Frontier, Homestead, Byzantium or Constantinople時期。其中Constantinople版本尚在規劃中,使用任何標記有該資訊的指令都會引起無效指令異常。

在下面的表格中,mem[a....b)表示記憶體(memory)位元組從a位置開始直到位置b(但不包含位置b),storage[p]表示在p位置的儲存(storage)內容。

操作碼pushijumpdest無法被直接使用。

在語法(檢查)中,操作碼被表示為預定義的識別符號。
Instruction Explanation
stop - F stop execution, identical to return(0,0)
add(x, y) F x + y
sub(x, y) F x - y
mul(x, y) F x * y
div(x, y) F x / y
sdiv(x, y) F x / y, for signed numbers in two’s complement
mod(x, y) F x % y
smod(x, y) F x % y, for signed numbers in two’s complement
exp(x, y) F x to the power of y
not(x) F ~x, every bit of x is negated
lt(x, y) F 1 if x < y, 0 otherwise
gt(x, y) F 1 if x > y, 0 otherwise
slt(x, y) F 1 if x < y, 0 otherwise, for signed numbers in two’s complement
sgt(x, y) F 1 if x > y, 0 otherwise, for signed numbers in two’s complement
eq(x, y) F 1 if x == y, 0 otherwise
iszero(x) F 1 if x == 0, 0 otherwise
and(x, y) F bitwise and of x and y
or(x, y) F bitwise or of x and y
xor(x, y) F bitwise xor of x and y
byte(n, x) F nth byte of x, where the most significant byte is the 0th byte
shl(x, y) C logical shift left y by x bits
shr(x, y) C logical shift right y by x bits
sar(x, y) C arithmetic 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) F sign extend from (i*8+7)th bit counting from least significant
keccak256(p, n) F keccak(mem[p…(p+n)))
sha3(p, n) F keccak(mem[p…(p+n)))
jump(label) - F jump to label / code position
jumpi(label, cond) - F jump to label if cond is nonzero
pc F current position in code
pop(x) - F remove the element pushed by x
dup1 … dup16 F copy ith stack slot to the top (counting from top)
swap1 … swap16 * F swap topmost and ith stack slot below it
mload(p) F mem[p..(p+32))
mstore(p, v) - F mem[p..(p+32)) := v
mstore8(p, v) - F mem[p] := v & 0xff (only modifies a single byte)
sload(p) F storage[p]
sstore(p, v) - F storage[p] := v
msize F size of memory, i.e. largest accessed memory index
gas F gas still available to execution
address F address of the current contract / execution context
balance(a) F wei balance at address a
caller F call sender (excluding delegatecall)
callvalue F wei sent together with the current call
calldataload(p) F call data starting from position p (32 bytes)
calldatasize F size of call data in bytes
calldatacopy(t, f, s) - F copy s bytes from calldata at position f to mem at position t
codesize F size of the code of the current contract / execution context
codecopy(t, f, s) - F copy s bytes from code at position f to mem at position t
extcodesize(a) F size of the code at address a
extcodecopy(a, t, f, s) - F like codecopy(t, f, s) but take code at address a
returndatasize B size of the last returndata
returndatacopy(t, f, s) - B copy s bytes from returndata at position f to mem at position t
create(v, p, s) F create new contract with code mem[p..(p+s)) and send v wei and return the new address
create2(v, n, p, s) C create 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) F call 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) F identical 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) H identical to callcode but also keep caller and callvalue
staticcall(g, a, in, insize, out, outsize) B identical to call(g, a, 0, in, insize, out, outsize) but do not allow state modifications
return(p, s) - F end execution, return data mem[p..(p+s))
revert(p, s) - B end execution, revert state changes, return data mem[p..(p+s))
selfdestruct(a) - F end execution, destroy current contract and send funds to a
invalid - F end execution with invalid instruction
log0(p, s) - F log without topics and data mem[p..(p+s))
log1(p, s, t1) - F log with topic t1 and data mem[p..(p+s))
log2(p, s, t1, t2) - F log with topics t1, t2 and data mem[p..(p+s))
log3(p, s, t1, t2, t3) - F log with topics t1, t2, t3 and data mem[p..(p+s))
log4(p, s, t1, t2, t3, t4) - F log with topics t1, t2, t3, t4 and data mem[p..(p+s))
origin F transaction sender
gasprice F gas price of the transaction
blockhash(b) F hash of block nr b - only for last 256 blocks excluding current
coinbase F current mining beneficiary
timestamp F timestamp of the current block in seconds since the epoch
number F current block number
difficulty F difficulty of the current block
gaslimit F block gas limit of the current block

直接量(Literals)

你可以通過以十進位制或十六進位制標記輸入整型常數來使用它們,這樣一個適當的 PUSHi指令會自動被生成。下面的創造程式碼將2 + 3 得到 5 ,然後按位和字串”abc”進行與計算。字串將被左對齊儲存,並且長度不能超過32字元。

assembly { 2 3 add "abc" and }

函式風格(Function Style)

你可以在操作碼緊挨操作碼的輸入,就像他們最終在位元組碼中那樣。比如你要把3加入記憶體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變數和其他識別符號可以通過簡單使用其名稱來訪問。對於記憶體變數,這會將地址而非值推入棧中。儲存(storage)變數就不一樣了:儲存中的值不會佔據一整個儲存槽,所以它們的“address”是一個槽的一部分以及槽的一個位元組偏移。要檢索變數x指向的槽,你可以使用x_slot;如果要檢索字元偏移量,可以用x_offset

在賦值中,我們甚至可以使用本地Solidity變數去賦值。(見下面)

內聯彙編以外的函式也能被訪問:組合語言將放入它們的整個標籤(with virtual function resolution applied也應用虛擬函式解決方案)(入棧)。solidity中呼叫語法如下:

  • 呼叫者入棧return label,arg1,arg2….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))  // 這裡我們知道偏移量是0,遂無視它
         }
      }
}

Note:
如果你訪問一個跨度小於256位元的型別的變數(比如 uint64,address,bytes16 或者 byte),你不能假定位元數不是該型別的編碼的一部分。特別的,不要假定它們為0.安全起見,在你將它們用於一個非常重要的程式碼中之前,總是適當的清理資料:uint32 x = f(); assembly { x := and(x, 0xffffffff) /* now use x */ }。要清理帶符號的型別,可以使用signextend操作碼

標籤(Label)

Note:
標籤已經被放棄。請使用函式,迴圈,if 或者 switch指令來替代它

EVM彙編的另一個問題在於,jumpjumpi都使用的 絕對地址,而該地址很容易改變。Solidity內聯彙編提供label是來使jumps的使用更為簡便。注意labes是低階特徵,編寫沒有label的彙編是完全可能的,只需要使用匯編函式,迴圈,if和switch指令(見下面)。下面的程式碼計算斐波拉契數列中的一個元素:

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

請注意自動訪問棧變數僅僅當彙編器只當當前棧高時才會工作。如果jump的源以及目標的棧高不同時,將會執行失敗。使用這樣的jumps依然是可行的,但在這種情況下你不應該訪問任何棧變數(即使是彙編變數)。

此外,棧高統計器一個操作碼接一個操作碼的檢查整個程式碼(且不被包含在控制流中),所以在下面這種情況,彙編器在標籤two位置將對棧高有一個錯誤認識:

{
     let x := 8
     jump(two)
     one:
         // 在這裡棧高為2(因為我們推入了x和7)但是彙編器會認為棧高為1,因為它是從頂向下讀的
         // 這裡訪問棧變數x會引起錯誤
         x := 9
         jump(three)
     two:
         7// 將一些東西放入棧
         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被回收
               b := add(b, v)
           }// v在這裡被回收
      }
}

賦值(Assignments)

給本地彙編變數和本地函式變數賦值都是可行的。注意當你給一個指向記憶體(memory)或者儲存(storage)的變數賦值時,你將僅僅改變它們的指向,而非資料。

有兩種型別的賦值:函式風格(functional-styel)和指令風格(instruction-style)。對於函式風格賦值(variable := value),你需要提供一個使用函式風格表示式的值,這正好會作為一個棧值。對於指令風格(=: variable)值會從棧頂取出。對於兩種方式,冒號對在變數名字的一邊。賦值操作實際是通過用新值來替換棧中變數的值來完成的。

{
      let v := 0 // 函式風格賦值,作為變數宣告的一部分
      let g := add(v, 2)
      sload(10)
      =: v // 指令風格賦值,將sload(10)的結果放入v
}

Note:
指令風格賦值以遭到摒棄

If

if指令能夠被用於條件執行程式碼。這裡沒有else部分,如果你需要多種情況選擇,請使用“switch”(下面會講)

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

if條件後面的花括號是必須要的。

Switch

你可以使用switch宣告作為一個基礎版本的“if/else”。它會獲取一個表示式的值,然後將其與一些常數對比。每個匹配的常數的分支將進行響應。與許多自頂向下的程式語言不同,(Solidity)的控制流並不會從一種情況繼續到下一個情況。這裡可以有撤回,也可以有預設情況宣告為default

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

情況列表可以不用花括號括起來,但是每個情況的執行體必須括起來。

Loops

組合語言支援一個簡單的 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)
       }
}

函式(Function)

組合語言允許定義低階函式(low-level functions)。它們從棧中取它們的引數(and a return PC),也把結果放回到棧中。呼叫一個函式和執行一個函式風格操作碼看起來是一樣的。

函式能夠在任何地方定義,並且在它們宣告的塊中可見。在函式內,你不能訪問定義在函式外面的本地變數,也沒有顯式的return指令。

如果你要呼叫一個返回多個值的函式,你必須使用 a, b := f(x)或者let a, b := f(x)將它們放入一個元組中。

下面的例子展示了一個power函式(乘方和相加):

{
      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)

內聯元件可能會看起來很高階(have a quite high-level look),但實際上它是十分低階的。函式呼叫,迴圈,if和switch都被通過簡單的改寫規則轉化,在那之後,彙編器做的就是重新排列函式風格的操作碼,管理標籤跳轉,計算變數訪問的棧高以及當塊執行到底時移除彙編本地變數。尤其是對最後兩個例子,知道彙編器僅僅從頂向下計算棧高而非必須跟隨控制流,是非常重要的。此外,像交換(swap)這樣的控制將僅僅交換兩個棧的內容而非變數位置。

Solidity慣例(Conventions in Solidity)

與EVM彙編相反,Solidity有比256bits更小的型別,比如uint24。為了使算術操作更加高效,大部分操作都將它們視作256位元數,並且更高順位的位元(指超過其本身位數的那些位)將僅在需要時被清理出來,比如說當在資料要寫入記憶體時,或者要執行比較時。這意味著如果你在內聯彙編訪問這樣的一個變數,你必須先手動清理較高位位元。

Solidity以一種非常簡單的方法管理記憶體:在記憶體的0x40位置有一個“自由記憶體指向器”(free memory pointer)。如果你想要分配記憶體,只用使用該點的記憶體並更新相關的(記憶體)指向即可。

記憶體的頭64個字元被用於短期儲存的“擦除空間(scratch space)”。在free memory pointer後面的32位元組(從0x60開始)暫時預定為0,它被用於空的動態記憶體陣列的初始值。

Solidity中記憶體陣列的元素總是佔據多個32位元組(即使對於byte[]也是如此,但是對bytes和tring並非如此)。多維記憶體陣列是指向記憶體陣列的指標。動態陣列的長度被存在陣列的第一個槽中,其後只有陣列元素可以跟隨。

Warning:
靜態規模記憶體陣列並沒有長度域,但它將很快會被加入,以增加動態和靜態陣列之間更好的轉變性,所有不要依賴於這一點,

離線彙編

上面介紹的內聯元件也能夠離線使用,而且實際上目前計劃是將其用作一個Solidity編譯器的中間語言。在這個規劃下,它將嘗試達到幾個目標:

  • 寫在其中的函式應該是可讀的,即使該程式碼是由Solidity編譯器生成的
  • 從組合語言到字元碼的轉譯應該包括儘量少的“驚喜(surprises)”
  • 控制流應該能被簡單的讀取,以便於正式化的檢查和優化

為了實現第一個和最後一個目標,組合語言提供了高階指令,比如for迴圈,if和switch指令以及函式呼叫。這樣我們可以實現不使用諸如SWAPDUPJUMPJUMPI之類的指令也能寫彙編程式,因為前兩個之類使得資料流迷糊不清,而後兩個則混淆了控制流。此外,mul(add(x, y), 7)這種形式的函式宣告也將比純粹的操作碼宣告,比如7 y x add mul這種更受青睞,因為在第一種形式中,可以很清楚的看到每個操作物件是用於哪個操作碼中的。

第二個目標通過將高階指令以一種很常規的方式編譯為字元碼來實現。唯一一個彙編器執行的非區域性操作名為使用者定義識別符號(函式,變數。。。)檢索,跟隨該操作的是非常簡單和常規的轄域規則,以及從棧中清理區域性變數。

轄域(scoping):一個聲明瞭的識別符號(標籤,變數,函式,彙編元件)僅僅在其宣告的塊內可見(包括在當前塊中的塊)。跨越函式邊界去範根區域性變數是非法的,即使他們在轄域內。影子(shadowing)是不允許的。區域性變數不能夠在其宣告之前被訪問,但是標籤,函式以及彙編元件可以。彙編是一個用於比如說返回執行程式碼或者生成合約的特殊的塊。來自外部彙編的識別符號在子彙編中不可見。

如果控制流經過了塊尾,匹配在塊中宣告的區域性變數數量的出棧指令將被插入。無論一個區域性變數在何時被引用,程式碼生成器需要知道它在棧中的當前位置,因此它需要了解當前聲稱的棧高。因為所有區域性變數在塊尾會被移除,塊之前或之後的棧高應該是一樣的。如果不一樣,那麼就會丟擲一個警告。

是用switchfor和函式可以不使用jumpjumpi而寫出複雜的程式碼。這使得管理控制流變得簡便許多,且允許了更加正式的檢驗和優化。

此外,如果允許手動跳轉,計算棧高將變得更加複雜。棧中所有區域性變數的位置都需要知道,否則既無法引用區域性變數,也無法自動從棧中移除區域性變數。

示例:

我們將展示一個簡單的從Solidity轉為彙編的例子。我們來考察下面這個Solidity程式的執行時字元碼:

pragam solidity ^0.4.16;

contract C {
   function f(uint x) public pure returns (uint y) {
      y = 1;
      for (uint i = 0; i < x; i ++)
          y = 2 * y;
   }
}

就會生成下面的組合語言:

{ 
    mstore (0x40, 0x60) // 儲存“free memory pointer”
    // 函式排程員
    switch div(calldataload(0), exp(2, 226))
    case 0xb3de648b {
      let r := f(calldataload(4))
      let ret := $allocate(0x20)
      mstore(ret, r)
      return(ret, 0x20)
    }
    default { revert(0, 0)  }
    // 記憶體分配
    funtion $allocaate(size) -> pos {
       pos := mload(0x40)
       mstore(0x40, add(pos, size))
    }
    // 合約函式
    function f(x) -> y {
       y := 1
       for { let i := 0 } lt(i, x) { i :=add(i, 1) } { 
          y := mul(2, y)
       }
    }
}

合約語法檢查(Assembly Grammar)

該分析程式的任務為:(這段實在不懂)

  • Turn the byte stream into a token stream, discarding C++-style comments (a special comment exists for source references, but we will not explain it here).
  • Turn the token stream into an AST(抽象語法樹) according to the grammar below
  • Register identifiers with the block they are defined in (annotation to the AST node) and note from which point on, variables can be accessed.

組合語言的詞法分析器(lexer)遵從Solidity自身定義的詞法分析器。

空白符用來劃定符號範圍,它包括普通的空白符,製表符(Tab)和換行符(Linefeed,LF).評註為普通JavaScript/C++評註,且和空白符解釋相同。

Grammar:

AssemblyBlock = '{' AssemblyItem* '}'
AssemblyItem =
    Identifier |
    AssemblyBlock |
    AssemblyExpression |
    AssemblyLocalDefinition |
    AssemblyAssignment |
    AssemblyStackAssignment |
    LabelDefinition |
    AssemblyIf |
    AssemblySwitch |
    AssemblyFunctionDefinition |
    AssemblyFor |
    'break' |
    'continue' |
    SubAssembly
AssemblyExpression = AssemblyCall | Identifier | AssemblyLiteral
AssemblyLiteral = NumberLiteral | StringLiteral | HexLiteral
Identifier = [a-zA-Z_$] [a-zA-Z_0-9]*
AssemblyCall = Identifier '(' ( AssemblyExpression ( ',' AssemblyExpression )* )? ')'
AssemblyLocalDefinition = 'let' IdentifierOrList ( ':=' AssemblyExpression )?
AssemblyAssignment = IdentifierOrList ':=' AssemblyExpression
IdentifierOrList = Identifier | '(' IdentifierList ')'
IdentifierList = Identifier ( ',' Identifier)*
AssemblyStackAssignment = '=:' Identifier
LabelDefinition = Identifier ':'
AssemblyIf = 'if' AssemblyExpression AssemblyBlock
AssemblySwitch = 'switch' AssemblyExpression AssemblyCase*
    ( 'default' AssemblyBlock )?
AssemblyCase = 'case' AssemblyExpression AssemblyBlock
AssemblyFunctionDefinition = 'function' Identifier '(' IdentifierList? ')'
    ( '->' '(' IdentifierList ')' )? AssemblyBlock
AssemblyFor = 'for' ( AssemblyBlock | AssemblyExpression )
    AssemblyExpression ( AssemblyBlock | AssemblyExpression ) AssemblyBlock
SubAssembly = 'assembly' Identifier AssemblyBlock
NumberLiteral = HexNumber | DecimalNumber
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+