智慧合約從入門到精通:Solidity組合語言

區塊鏈技術發表於2018-07-06

簡介:上一節,我們講過在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)
    }
  }
}
複製程式碼

在經過離線彙編階段,它會編譯成下述的內容:

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

智慧合約開發教程視訊:區塊鏈系列視訊課程之智慧合約簡介

相關文章