1. 程式人生 > >智能合約從入門到精通:Solidity匯編語言

智能合約從入門到精通:Solidity匯編語言

man revert scope mov dup 步驟 ali less pen

簡介:上一節,我們講過在JUICE平臺開發智能合約的開發規範,本節我們將繼續就Solidity定義的匯編語言進行更加深入的討論。
Solidity定義的匯編語言可以達到下述的目標:
1.使用它編寫的代碼要可讀,即使代碼是從Solidity編譯得到的。
2.從匯編語言轉為字節碼應該盡可能的少坑。
3.控制流應該容易檢測來幫助進行形式驗證與優化。
為了達到第一條和最後一條的目標,Solidity匯編語言提供了高層級的組件比如,for循環,switch語句和函數調用。這樣的話,可以不直接使用SWAP,DUP,JUMP,JUMPI語句,因為前兩個有混淆的數據流,後兩個有混淆的控制流。此外,函數形式的語句如mul(add(x, y), 7)比純的指令碼的形式7 y x add num更加可讀。
第二個目標是通過引入一個絕對階段來實現,該階段只能以非常規則的方式去除較高級別的構造,並且仍允許檢查生成的低級匯編代碼。Solidity匯編語言提供的非原生的操作是用戶定義的標識符的命名查找(函數名,變量名等),這些都遵循簡單和常規的作用域規則,會清理棧上的局部變量。
作用域:一個標識符(標簽,變量,函數,匯編)在定義的地方,均只有塊級作用域(作用域會延伸到,所在塊所嵌套的塊)。跨函數邊界訪問局部變量是不合法的,即使可能在作用域內(譯者註:這裏可能說的是,函數內定義多個函數的情況,JavaScript有這種語法)。不允許shadowing。局部變量不能在定義前被訪問,但標簽,函數和匯編可以。匯編是非常特殊的塊結構可以用來,如,返回運行時的代碼,或創建合約。外部定義的匯編變量在子匯編內不可見。
如果控制流來到了塊的結束,局部變量數匹配的pop指令會插入到棧底(譯者註:移除局部變量,因為局部變量失效了)。無論何時引用局部變量,代碼生成器需要知道其當前在堆棧中的相對位置,因此需要跟蹤當前所謂的堆棧高度。由於所有的局部變量在塊結束時會被移除,因此在進入塊之前和之後的棧高應該是不變的,如果不是這樣的,將會拋出一個警告。
我們為什麽要使用高層級的構造器,比如switch,for和函數。
使用switch,for和函數,可以在不用jump和jumpi的情況下寫出來復雜的代碼。這會讓分析控制流更加容易,也可以進行更多的形式驗證及優化。
此外,如果手動使用jumps,計算棧高是非常復雜的。棧內所有的局部變量的位置必須是已知的,否則指向本地變量的引用,或者在塊結束時自動刪除局部變量都不會正常工作。脫機處理機制正確的在塊內不可達的地方插入合適的操作以修正棧高來避免出現jump時非連續的控制流帶來的棧高計算不準確的問題。
示例:
我們從一個例子來看一下Solidity到這種中間的脫機匯編結果。我們可以一起來考慮下下述Soldity程序的字節碼:

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

它將生成下述的匯編內容:

{
  mstore(0x40, 0x60) // store the "free memory pointer"
  // function dispatcher
  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) }
  // memory allocator
  function $allocate(size) -> pos {
    pos := mload(0x40)
    mstore(0x40, add(pos, size))
  }
  // the contract function
  function f(x) -> y {
    y := 1
    for { let i := 0 } lt(i, x) { i := add(i, 1) } {
      y := mul(2, y)
    }
  }
}

在經過脫機匯編階段,它會編譯成下述的內容:

{
  mstore(0x40, 0x60)
  {
    let $0 := div(calldataload(0), exp(2, 226))
    jumpi($case1, eq($0, 0xb3de648b))
    jump($caseDefault)
    $case1:
    {
      // the function call - we put return label and arguments on the stack
      $ret1 calldataload(4) jump(f)
      // This is unreachable code. Opcodes are added that mirror the
      // effect of the function on the stack height: Arguments are
      // removed and return values are introduced.
      pop pop
      let r := 0
      $ret1: // the actual return point
      $ret2 0x20 jump($allocate)
      pop pop let ret := 0
      $ret2:
      mstore(ret, r)
      return(ret, 0x20)
      // although it is useless, the jump is automatically inserted,
      // since the desugaring process is a purely syntactic operation that
      // does not analyze control-flow
      jump($endswitch)
    }
    $caseDefault:
    {
      revert(0, 0)
      jump($endswitch)
    }
    $endswitch:
  }
  jump($afterFunction)
  allocate:
  {
    // we jump over the unreachable code that introduces the function arguments
    jump($start)
    let $retpos := 0 let size := 0
    $start:
    // output variables live in the same scope as the arguments and is
    // actually allocated.
    let pos := 0
    {
      pos := mload(0x40)
      mstore(0x40, add(pos, size))
    }
    // This code replaces the arguments by the return values and jumps back.
    swap1 pop swap1 jump
    // Again unreachable code that corrects stack height.
    0 0
  }
  f:
  {
    jump($start)
    let $retpos := 0 let x := 0
    $start:
    let y := 0
    {
      let i := 0
      $for_begin:
      jumpi($for_end, iszero(lt(i, x)))
      {
        y := mul(2, y)
      }
      $for_continue:
      { i := add(i, 1) }
      jump($for_begin)
      $for_end:
    } // Here, a pop instruction will be inserted for i
    swap1 pop swap1 jump
    0 0
  }
  $afterFunction:
  stop
}

匯編有下面四個階段:
1.解析
2.脫匯編(移除switch,for和函數)
3.生成指令流
4.生成字節碼
我們將簡單的以步驟1到3指定步驟。更加詳細的步驟將在後面說明。
解析、語法
解析的任務如下:

  • 將字節流轉為符號流,去掉其中的C++風格的註釋(一種特殊的源代碼引用的註釋,這裏不打算深入討論)。
  • 將符號流轉為下述定義的語法結構的AST。
  • 註冊塊中定義的標識符,標註從哪裏開始(根據AST節點的註解),變量可以被訪問。
    組合詞典遵循由Solidity本身定義的詞組。
    空格用於分隔標記,它由空格,制表符和換行符組成。 註釋是常規的JavaScript / C ++註釋,並以與Whitespace相同的方式進行解釋。
    語法:
    AssemblyBlock = ‘{‘ AssemblyItem* ‘}‘
    AssemblyItem =
    Identifier |
    AssemblyBlock |
    FunctionalAssemblyExpression |
    AssemblyLocalDefinition |
    FunctionalAssemblyAssignment |
    AssemblyAssignment |
    LabelDefinition |
    AssemblySwitch |
    AssemblyFunctionDefinition |
    AssemblyFor |
    ‘break‘ | ‘continue‘ |
    SubAssembly | ‘dataSize‘ ‘(‘ Identifier ‘)‘ |
    LinkerSymbol |
    ‘errorLabel‘ | ‘bytecodeSize‘ |
    NumberLiteral | StringLiteral | HexLiteral
    Identifier = [a-zA-Z_$] [a-zA-Z_0-9]*
    FunctionalAssemblyExpression = Identifier ‘(‘ ( AssemblyItem ( ‘,‘ AssemblyItem )* )? ‘)‘
    AssemblyLocalDefinition = ‘let‘ IdentifierOrList ‘:=‘ FunctionalAssemblyExpression
    FunctionalAssemblyAssignment = IdentifierOrList ‘:=‘ FunctionalAssemblyExpression
    IdentifierOrList = Identifier | ‘(‘ IdentifierList ‘)‘
    IdentifierList = Identifier ( ‘,‘ Identifier)*
    AssemblyAssignment = ‘=:‘ Identifier
    LabelDefinition = Identifier ‘:‘
    AssemblySwitch = ‘switch‘ FunctionalAssemblyExpression AssemblyCase*
    ( ‘default‘ AssemblyBlock )?
    AssemblyCase = ‘case‘ FunctionalAssemblyExpression AssemblyBlock
    AssemblyFunctionDefinition = ‘function‘ Identifier ‘(‘ IdentifierList? ‘)‘
    ( ‘->‘ ‘(‘ IdentifierList ‘)‘ )? AssemblyBlock
    AssemblyFor = ‘for‘ ( AssemblyBlock | FunctionalAssemblyExpression)
    FunctionalAssemblyExpression ( AssemblyBlock | FunctionalAssemblyExpression) AssemblyBlock
    SubAssembly = ‘assembly‘ Identifier AssemblyBlock
    LinkerSymbol = ‘linkerSymbol‘ ‘(‘ StringLiteral ‘)‘
    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]+

    脫匯編
    一個AST轉換,移除其中的for,switch和函數構建。結果仍由同一個解析器,但它不確定使用什麽構造。如果添加僅跳轉到並且不繼續的jumpdests,則添加有關堆棧內容的信息,除非沒有局部變量訪問到外部作用域或棧高度與上一條指令相同。偽代碼如下:

    desugar item: AST -> AST =
    match item {
    AssemblyFunctionDefinition(‘function‘ name ‘(‘ arg1, ..., argn ‘)‘ ‘->‘ ( ‘(‘ ret1, ..., retm ‘)‘ body) ->
    <name>:
    {
    jump($<name>_start)
    let $retPC := 0 let argn := 0 ... let arg1 := 0
    $<name>_start:
    let ret1 := 0 ... let retm := 0
    { desugar(body) }
    swap and pop items so that only ret1, ... retm, $retPC are left on the stack
    jump
    0 (1 + n times) to compensate removal of arg1, ..., argn and $retPC
    }
    AssemblyFor(‘for‘ { init } condition post body) ->
    {
    init // cannot be its own block because we want variable scope to extend into the body
    // find I such that there are no labels $forI_*
    $forI_begin:
    jumpi($forI_end, iszero(condition))
    { body }
    $forI_continue:
    { post }
    jump($forI_begin)
    $forI_end:
    }
    ‘break‘ ->
    {
    // find nearest enclosing scope with label $forI_end
    pop all local variables that are defined at the current point
    but not at $forI_end
    jump($forI_end)
    0 (as many as variables were removed above)
    }
    ‘continue‘ ->
    {
    // find nearest enclosing scope with label $forI_continue
    pop all local variables that are defined at the current point
    but not at $forI_continue
    jump($forI_continue)
    0 (as many as variables were removed above)
    }
    AssemblySwitch(switch condition cases ( default: defaultBlock )? ) ->
    {
    // find I such that there is no $switchI* label or variable
    let $switchI_value := condition
    for each of cases match {
      case val: -> jumpi($switchI_caseJ, eq($switchI_value, val))
    }
    if default block present: ->
      { defaultBlock jump($switchI_end) }
    for each of cases match {
      case val: { body } -> $switchI_caseJ: { body jump($switchI_end) }
    }
    $switchI_end:
    }
    FunctionalAssemblyExpression( identifier(arg1, arg2, ..., argn) ) ->
    {
    if identifier is function <name> with n args and m ret values ->
      {
        // find I such that $funcallI_* does not exist
        $funcallI_return argn  ... arg2 arg1 jump(<name>)
        pop (n + 1 times)
        if the current context is `let (id1, ..., idm) := f(...)` ->
          let id1 := 0 ... let idm := 0
          $funcallI_return:
        else ->
          0 (m times)
          $funcallI_return:
          turn the functional expression that leads to the function call
          into a statement stream
      }
    else -> desugar(children of node)
    }
    default node ->
    desugar(children of node)
    }

    生成操作碼流
    在操作碼流生成期間,我們在一個計數器中跟蹤當前的棧高,所以通過名稱訪問棧的變量是可能的。棧高在會修改棧的操作碼後或每一個標簽後進行棧調整。當每一個新局部變量被引入時,它都會用當前的棧高進行註冊。如果要訪問一個變量(或者拷貝其值,或者對其賦值),會根據當前棧高與變量引入時的當時棧高的不同來選擇合適的DUP或SWAP指令。
    偽代碼:

    codegen item: AST -> opcode_stream =
    match item {
    AssemblyBlock({ items }) ->
    join(codegen(item) for item in items)
    if last generated opcode has continuing control flow:
    POP for all local variables registered at the block (including variables
    introduced by labels)
    warn if the stack height at this point is not the same as at the start of the block
    Identifier(id) ->
    lookup id in the syntactic stack of blocks
    match type of id
    Local Variable ->
      DUPi where i = 1 + stack_height - stack_height_of_identifier(id)
    Label ->
      // reference to be resolved during bytecode generation
      PUSH<bytecode position of label>
    SubAssembly ->
      PUSH<bytecode position of subassembly data>
    FunctionalAssemblyExpression(id ( arguments ) ) ->
    join(codegen(arg) for arg in arguments.reversed())
    id (which has to be an opcode, might be a function name later)
    AssemblyLocalDefinition(let (id1, ..., idn) := expr) ->
    register identifiers id1, ..., idn as locals in current block at current stack height
    codegen(expr) - assert that expr returns n items to the stack
    FunctionalAssemblyAssignment((id1, ..., idn) := expr) ->
    lookup id1, ..., idn in the syntactic stack of blocks, assert that they are variables
    codegen(expr)
    for j = n, ..., i:
    SWAPi where i = 1 + stack_height - stack_height_of_identifier(idj)
    POP
    AssemblyAssignment(=: id) ->
    look up id in the syntactic stack of blocks, assert that it is a variable
    SWAPi where i = 1 + stack_height - stack_height_of_identifier(id)
    POP
    LabelDefinition(name:) ->
    JUMPDEST
    NumberLiteral(num) ->
    PUSH<num interpreted as decimal and right-aligned>
    HexLiteral(lit) ->
    PUSH32<lit interpreted as hex and left-aligned>
    StringLiteral(lit) ->
    PUSH32<lit utf-8 encoded and left-aligned>
    SubAssembly(assembly <name> block) ->
    append codegen(block) at the end of the code
    dataSize(<name>) ->
    assert that <name> is a subassembly ->
    PUSH32<size of code generated from subassembly <name>>
    linkerSymbol(<lit>) ->
    PUSH32<zeros> and append position to linker table
    }

    參考內容:https://open.juzix.net/doc
    智能合約開發教程視頻:區塊鏈系列視頻課程之智能合約簡介

智能合約從入門到精通:Solidity匯編語言