標籤:情況下 變數 定位字元 intro lock height blog 根據 嵌套
簡介:上一節,我們講過在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 | HexLiteralIdentifier = [a-zA-Z_$] [a-zA-Z_0-9]*FunctionalAssemblyExpression = Identifier ‘(‘ ( AssemblyItem ( ‘,‘ AssemblyItem )* )? ‘)‘AssemblyLocalDefinition = ‘let‘ IdentifierOrList ‘:=‘ FunctionalAssemblyExpressionFunctionalAssemblyAssignment = IdentifierOrList ‘:=‘ FunctionalAssemblyExpressionIdentifierOrList = Identifier | ‘(‘ IdentifierList ‘)‘IdentifierList = Identifier ( ‘,‘ Identifier)*AssemblyAssignment = ‘=:‘ IdentifierLabelDefinition = Identifier ‘:‘AssemblySwitch = ‘switch‘ FunctionalAssemblyExpression AssemblyCase*( ‘default‘ AssemblyBlock )?AssemblyCase = ‘case‘ FunctionalAssemblyExpression AssemblyBlockAssemblyFunctionDefinition = ‘function‘ Identifier ‘(‘ IdentifierList? ‘)‘( ‘->‘ ‘(‘ IdentifierList ‘)‘ )? AssemblyBlockAssemblyFor = ‘for‘ ( AssemblyBlock | FunctionalAssemblyExpression)FunctionalAssemblyExpression ( AssemblyBlock | FunctionalAssemblyExpression) AssemblyBlockSubAssembly = ‘assembly‘ Identifier AssemblyBlockLinkerSymbol = ‘linkerSymbol‘ ‘(‘ StringLiteral ‘)‘NumberLiteral = HexNumber | DecimalNumberHexLiteral = ‘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 stackjump0 (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_endpop all local variables that are defined at the current pointbut not at $forI_endjump($forI_end)0 (as many as variables were removed above)}‘continue‘ ->{// find nearest enclosing scope with label $forI_continuepop all local variables that are defined at the current pointbut not at $forI_continuejump($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 variablelet $switchI_value := conditionfor 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 variablesintroduced by labels)warn if the stack height at this point is not the same as at the start of the blockIdentifier(id) ->lookup id in the syntactic stack of blocksmatch type of idLocal 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 heightcodegen(expr) - assert that expr returns n items to the stackFunctionalAssemblyAssignment((id1, ..., idn) := expr) ->lookup id1, ..., idn in the syntactic stack of blocks, assert that they are variablescodegen(expr)for j = n, ..., i:SWAPi where i = 1 + stack_height - stack_height_of_identifier(idj)POPAssemblyAssignment(=: id) ->look up id in the syntactic stack of blocks, assert that it is a variableSWAPi where i = 1 + stack_height - stack_height_of_identifier(id)POPLabelDefinition(name:) ->JUMPDESTNumberLiteral(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 codedataSize(<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組合語言