Solidity匯編

翻譯原文

date:20170724

Solidity定義一種匯編語言,可脫離Solidity使用。該匯編語言可以被用來“內(nèi)聯(lián)編譯”。我們開始于描述如何使用內(nèi)聯(lián)編譯,它和獨(dú)立編譯的區(qū)別,最后闡述匯編。(?Solidity defines an assembly language that can also be used without Solidity. This assembly language can also be used as “inline assembly” inside Solidity source code. We start with describing how to use inline assembly and how it differs from standalone assembly and then specify assembly itself.)

//TODO:這里還要寫,行內(nèi)編譯的作用域有點(diǎn)區(qū)別,調(diào)用庫的內(nèi)部函數(shù)時(shí)編譯過程。更進(jìn)一步,寫寫編譯器定義的符號(hào)。

內(nèi)聯(lián)匯編

為了更細(xì)粒度的控制,尤其是通過寫庫來加強(qiáng)語言,可以在語言上引入內(nèi)聯(lián)匯編與虛擬機(jī)語言相交錯(cuò)。(?For more fine-grained control especially in order to enhance the language by writing libraries, it is possible to interleave Solidity statements with inline assembly in a language close to the one of the virtual machine.)由于EVM是堆棧機(jī),所以很難在堆棧中尋址正確的堆棧片和在正確的時(shí)間點(diǎn)為操作碼提供正確的操作數(shù)。Solidity內(nèi)聯(lián)編譯嘗試加強(qiáng)這個(gè)缺陷和其他通過以下命令手動(dòng)編譯出現(xiàn)的問題:

  • 函數(shù)類型的操作碼: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) } }
  • 標(biāo)簽: let x:= 10 repeat: x := sub(x, 1) jumpi(repeat,eq(x, 0))
  • 循環(huán):for { let i := 0} lt(i, x) { i := add(i, 1) } { y := mul(2, y)}
  • switch表達(dá)式: switch x case 0 {y := mul(x, 2)} default { y:= 0}
  • 函數(shù)調(diào)用:function f(x) -> y {switch x case 0 { y := 1} default {y := mul(x, f(sub(x, 1)))}}

現(xiàn)在我們將詳細(xì)描述行內(nèi)編譯語言。

警告:行內(nèi)編譯是訪問EVM底層的一種方式。這損失了Solidity的幾個(gè)安全特性。

例子

下面的例子提供了訪問其他合約的庫函數(shù)的代碼,并將它賦值到bytes變量。完全solidity是不可能的,現(xiàn)在的理念是匯編庫用來加強(qiáng)語言。

pragma solidity ^0.4.0;

library GetCode {
    function at(address _addr) returns (bytes o_code) {
        assembly {
            // 獲取代碼的大小,這需要匯編
            let size := extcodesize(_addr)
            // 分配輸出數(shù)組 - 這可以不需要匯編就能完成
            // 通過代碼 using o_code = new bytes(size)
            o_code := mload(0x40)
            //新的 "內(nèi)存端點(diǎn)" 包含填充
            mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
            // 內(nèi)存中的存儲(chǔ)長(zhǎng)度
            mstore(o_code, size)
            // 獲取代碼, 這需要匯編
            extcodecopy(_addr, add(o_code, 0x20), 0, size)
        }
    }
}

在優(yōu)化器無法提高代碼效率的時(shí)候,內(nèi)聯(lián)匯編就會(huì)顯得非常有用。請(qǐng)注意匯編比較難寫,因?yàn)榫幾g器不會(huì)檢查代碼,所以你在明白自己在做什么的時(shí)候才可以使用。

pragma solidity ^0.4.0;

library VectorSum {
    // 這個(gè)函數(shù)由于優(yōu)化器不能對(duì)數(shù)組進(jìn)行邊界檢查,所以比較低效。
    function sumSolidity(uint[] _data) returns (uint o_sum) {
        for (uint i = 0; i < _data.length; ++i)
            o_sum += _data[i];
    }

    // 我們知道我們只在一定范圍內(nèi)訪問數(shù)組,所以我們避免檢查邊界。
    // 0x20 需要添加到數(shù)組,因?yàn)榈谝粋€(gè)元素包含這樣的長(zhǎng)度
    function sumAsm(uint[] _data) 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))))
            }
        }
    }
}
語法

匯編跟solidity一樣,會(huì)解析注釋,字面量和標(biāo)識(shí)符。所以你可以使用///* */來寫注釋。內(nèi)聯(lián)編譯用assembly {}標(biāo)記,代碼在花括號(hào)中。會(huì)使用下面的規(guī)則(在以后的章節(jié)中會(huì)詳細(xì)描述):

  • 字面量,例如 0x123,42或者abc(字符串最多32個(gè)字符)
  • 操作符(指令類型),例如mload sload dup1 sstore,更多的操作符號(hào),查看下面的列表。
  • 函數(shù)類型的操作符,例如add(1, mlod(0))
  • 標(biāo)簽,如name
  • 變量聲明,例如let x := 7,let x := add(y, 3)或者let x(初始值為empty(0))
  • 標(biāo)識(shí)符(標(biāo)簽或者匯編的變量和內(nèi)聯(lián)匯編情況下的外部變量),例如jump(name)3 x add
  • 賦值(指令形式),例如 3 =: x
  • 函數(shù)形式的賦值,例如x := add(y, 3)
  • 包含局部變量的區(qū)塊,例如{ let x := 3 { let y := add(x, 1) } }
操作碼

本文檔不會(huì)完全描述以太坊虛擬機(jī),但是下面的列表是操作碼集合,這有必要了解。

如果操作碼包含參數(shù)(總是從棧頂取數(shù)據(jù)),會(huì)有圓括號(hào)。注意非函數(shù)類型的預(yù)留參數(shù)的順序(在下面闡述)。操作碼有-標(biāo)記的是不需要將數(shù)據(jù)推入堆棧的,有*標(biāo)記的會(huì)有些特別,其他的都會(huì)往堆棧中推入一個(gè)數(shù)據(jù)。

在下面的列表中,mem[a ... b)表明從a開始到b但是不包括b分配這么多字節(jié)的內(nèi)存。storage[p]表明p位置的數(shù)據(jù)。

操作碼push1jumpdest不能直接使用。

在語法中,操作碼相當(dāng)于預(yù)定義的標(biāo)識(shí)符。

操作碼 標(biāo)記 解釋
stop - 停止執(zhí)行,相當(dāng)于返回(0,0)
add(x,y) x + y
sub(x,y) x - y
mul(x,y) x * y
div(x,y) x / y
sdiv(x,y) x / y ,對(duì)于有符號(hào)數(shù),用補(bǔ)碼
mod(x,y) x % y
smod(x,y) x % y,對(duì)于有符號(hào)數(shù),用補(bǔ)碼
exp(x,y) x的y次方
not(x) ~x,按位取反
lt(x,y) 如果x < y,返回1,否則返回0
gt(x,y) 如果x > y,返回1,否則返回0
slt(x,y) 如果x < y,返回1,否則返回0,對(duì)于有符號(hào)數(shù),用補(bǔ)碼
sgt(x,y) 如果x > y,返回1,否則返回0,對(duì)于有符號(hào)數(shù),用補(bǔ)碼
eq(x,y) 如果x == y,返回1,否則返回0
iszero(x) 如果x == 0,返回1,否則返回0
and(x,y) x和y按位與
or(x,y) x和y按位或
xor(x,y) x和y按位異或
byte(n,x) x的第n位,最重要的是第0位
addmod(x,y,m) (x + y) % m,可以是任意精度的算術(shù)
mulmod(x,y,m) (x * y) % m,可以是任意精度的算術(shù)
signxtend(i,x) 從第(i * 8 + 7)位開始數(shù)最少簽名位數(shù),來擴(kuò)展簽名
keccak256(p,n) keccak(mem[p...(p+n)))
sha3(p,n) sha3(mem[p...(p+n)))
jump(label) - 跳轉(zhuǎn)到label標(biāo)簽/代碼位置
jumpi(label, cond) - 如果cond非零,就跳轉(zhuǎn)到label標(biāo)簽
pc 代碼的當(dāng)前位置
pop(x) - 刪除x推入的元素
dup1 ... dup16 拷貝第i位的元素到棧頂(從頂端開始數(shù))
swap1 ... swap16 * 交換棧頂和第i位的元素
mload(p) mem[p..(p+32))
mstore(p, v) - mem[p..(p+32)) := v
mstore8(p, v) - mem[p] := v & 0xff - 只分配一個(gè)字節(jié)
sload(p) storage[p]
sstore(p, v) - storage[p] := v
msize 內(nèi)存的大小,例如最大的內(nèi)存可訪問索引
gas 可用的gas數(shù)
address 當(dāng)前合約/執(zhí)行上下文的地址
balance(a) 地址a的余額,單位為wei
caller 調(diào)用者(不包含delegatecall)
callvalue 當(dāng)前調(diào)用發(fā)送的wei數(shù)
calldataload(p) 從位置p開始的調(diào)用數(shù)據(jù)(32位)
calldatasize 調(diào)用數(shù)據(jù)的大小,bytes為單位
calldatacopy(t,f,s) - 從數(shù)據(jù)位置f,拷貝s位數(shù)據(jù),到內(nèi)存位置t
codesize 當(dāng)前合約/執(zhí)行上下文的代碼大小
codecopy(t,f,s) - 從代碼位置f,拷貝s位數(shù)據(jù),到內(nèi)存位置t
extcodesize(a) 地址a處代碼的大小
extcodecopy(a, t, f, s) - 像codecopy(t,f,s),但是是位置a處的代碼
returndatasize 上個(gè)返回值的數(shù)據(jù)大小
returndatacopy(t, f, s) - 在返回?cái)?shù)據(jù)的位置f拷貝s位,到內(nèi)存位置t
create(v, p, s) 用 mem[p..(p+s))長(zhǎng)度的代碼,創(chuàng)建一個(gè)新的合約,發(fā)送v wei的以太幣,并返回合約地址
create2(v, n, p, s) 用 地址為keccak256(< address > . n . keccak256(mem[p..(p+s))),mem[p..(p+s))長(zhǎng)度的代碼,創(chuàng)建一個(gè)新的合約,發(fā)送v wei的以太幣,并返回合約地址
call(g, a, v, in, insize, out, outsize) 調(diào)用地址a上的合約,參數(shù)為mem[in..(in+insize)),提供g的gas,v wei的以太幣,輸出到mem[out..(out+outsize)),如果成功返回1,失?。ɡ鏶as不足)返回0
callcode(g, a, v, in, insize, out, outsize) call相同,但是使用a處的代碼,并只能在當(dāng)前合約的上下文中執(zhí)行
delegatecall(g, a, in, insize, out, outsize) callcode相同,但是保留callercallvalue
staticcall(g, a, in, insize, out, outsize) 與_call(g, a, 0, in, insize, out, outsize) _相同,但是不允許改變狀態(tài)
return(p, s) - 結(jié)束執(zhí)行,返回?cái)?shù)據(jù)mem[p..(p+s))
revert(p, s) - 結(jié)束執(zhí)行,恢復(fù)狀態(tài)變化,返回?cái)?shù)據(jù)mem[p..(p+s))
selfdestruct(a) - 結(jié)束執(zhí)行,銷毀當(dāng)前合約,并把余額發(fā)送給a地址
invalid - 用invalid指令結(jié)束執(zhí)行
log0(p,s) - 記錄日志,不包含主題,數(shù)據(jù)為mem[p..(p+s)
log1(p,s,t1) - 記錄日志,包含主題t1,數(shù)據(jù)為mem[p..(p+s)
log2(p,s,t1,t2) - 記錄日志,包含主題t1,t2,數(shù)據(jù)為mem[p..(p+s)
log3(p,s,t1,t2,t3) - 記錄日志,包含主題t1,t2,t3,數(shù)據(jù)為mem[p..(p+s)
log4(p,s,t1,t2,t3,t4) - 記錄日志,包含主題t1,t2,t3,t4,數(shù)據(jù)為mem[p..(p+s)
origin 交易發(fā)起方
gasprice 交易的gas價(jià)格
blockhash(b) 區(qū)塊序列為b的hash - 只能獲取到當(dāng)前塊的最近256塊
coinbase 當(dāng)前的礦工收益
timestamp 自創(chuàng)世紀(jì)區(qū)塊以來的時(shí)間戳,單位為秒
number 當(dāng)前區(qū)塊的序號(hào)
difficulty 當(dāng)前區(qū)塊的難度
gaslimit 當(dāng)前代碼塊的gas限制
字面量

你可以使用整形常量,可以用十進(jìn)制或十六進(jìn)制的寫法,一個(gè)合適的PUSHi指令會(huì)自動(dòng)的生成。下面的代碼執(zhí)行2加3等于5,然后計(jì)算位寬,然后與字符串"abc"相加。字符串是左對(duì)齊保存的,并且不能超過32位。

assembly { 2 3 add "abc" and }
函數(shù)類型

你可以用同樣的方式在操作碼之后輸入操作碼,它們會(huì)以字節(jié)碼結(jié)束。例如,給內(nèi)存0x80處的值加3的代碼是:

3 0x80 mload add 0x80 mstore

由于這種形式很難看出來操作碼的實(shí)際參數(shù),Solidity內(nèi)聯(lián)編譯也提供“函數(shù)形式”的寫法,如下代碼與上面的代碼功能相同:

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

函數(shù)形式的表達(dá)式內(nèi)部不能使用指令類型。例如,1 2 mstore(0x80, add)是不允許的。必須要寫成mstore(0x80, add(2, 1))。如果操作碼沒有參數(shù),那么圓括號(hào)就可以省略。
注意,參數(shù)順序是和函數(shù)類型的參數(shù)順序相反,如果你使用函數(shù)類型,第一個(gè)參數(shù)應(yīng)該在棧頂。

訪問外部變量和函數(shù)

Solidity的變量和其他標(biāo)識(shí)符可以通過使用名稱來簡(jiǎn)單訪問。對(duì)金錢變量,將會(huì)把地址,而不是金額推入堆棧。storage變量有些不同:storage的值可能不會(huì)占據(jù)一個(gè)完整的storage片,所以它們的地址是由一個(gè)片地址和位偏移組成。為了得到變量x的片地址,使用x_slot,獲取偏移使用x_offset

在賦值操作中(如下所示),我們可以使用Solidity變量去賦值。
內(nèi)聯(lián)編譯外部的函數(shù),也是可以訪問的:匯編會(huì)把它們的入口標(biāo)簽(使用虛擬函數(shù)解決方法)壓入堆棧。Solidity中的調(diào)用語法如下:

  • 調(diào)用者壓入標(biāo)簽,arg1,arg2,...,argn
  • 被調(diào)用返回ret1,ret2,...,retm

這個(gè)功能使用起來還是比較笨拙,因?yàn)闂F茣?huì)在調(diào)用中變化,所以引用的值也會(huì)出錯(cuò)。

pragma solidity ^0.4.11;

contract C {
    uint b;
    function f(uint x) returns (uint r) {
        assembly {
            r := mul(x, sload(b_slot)) // 這里忽略了偏移,因?yàn)槲覀冎?,偏移?
        }
    }
}
標(biāo)簽(labels)

EVM匯編的另一個(gè)問題是jumpjumpi使用的是絕對(duì)地址,可能會(huì)很容易變動(dòng)。Solidity內(nèi)聯(lián)編譯提供標(biāo)簽來使跳轉(zhuǎn)更加容易。注意,標(biāo)簽是底層特性,不用標(biāo)簽,只用匯編函數(shù),循環(huán)和switcht指令的效率可能會(huì)更高(看下面的例子)。下面的代碼是計(jì)算斐波那契數(shù)列。

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

請(qǐng)注意,自動(dòng)訪問棧變量只能在編譯器知道當(dāng)前棧的深度的情況有效。如果跳轉(zhuǎn)的深度和目標(biāo)的深度不同,就會(huì)失敗。但是使用jumps還是可以的,但是這種情況下你不能訪問棧變量了(即使是匯編變量)。

另外,棧深度分析器對(duì)opcode逐一分析(不是根據(jù)控制流),所以在下面的例子中,編譯器會(huì)對(duì)two標(biāo)簽的深度有錯(cuò)誤的分析。

{
    let x := 8
    jump(two)
    one:
        // 這里棧深度是2(因?yàn)槲覀儔喝肓藊和7)
        // 但是匯編器認(rèn)為深度只有1,因?yàn)樗亲皂斚蛳伦x取的。
        // 這里訪問變量x會(huì)導(dǎo)致錯(cuò)誤。
        x := 9
        jump(three)
    two:
        7 // 在堆棧中壓入數(shù)據(jù)
        jump(one)
    three:
}

這個(gè)問題可以手動(dòng)的調(diào)整棧深度來修復(fù)-你可以在標(biāo)簽之前提供一個(gè)棧深度偏移。注意,你不必關(guān)心這這些事情,如果你使用循環(huán)和匯編局部函數(shù)。

極端情況下的例子如下所示:

{
    let x := 8
    jump(two)
    0 // 這個(gè)代碼是可達(dá)的,但是會(huì)修正棧深度
    one:
        x := 9 // x是可訪問的
        jump(three)
        pop // 類似于負(fù)校正
    two:
        7 // 壓入數(shù)據(jù)
        jump(one)
    three:
    pop // 我們必須要手動(dòng)的彈出壓入的數(shù)據(jù)
}

聲明匯編局部變量(Declaring Assembly-Local Variables)

我們可以使用let關(guān)鍵字來聲明變量,但是這些變量只能在內(nèi)聯(lián)編譯代碼塊中可見。也就是只能在{...}塊中。這里的原理是,let指令會(huì)生成一個(gè)新的棧slot,保留給變量,并且當(dāng)代碼塊結(jié)束的時(shí)候自動(dòng)的移除。你需要為變量提供初始值,可以是0,但是也可以是復(fù)雜的函數(shù)表達(dá)式。

pragma solidity ^0.4.0;

contract C {
    function f(uint x) returns (uint b) {
        assembly {
            let v := add(x, 1)
            mstore(0x80, v)
            {
                let y := add(sload(v), 1)
                b := y
            } // y會(huì)被回收
            b := add(b, v)
        } // v被回收
    }
}
賦值

匯編局部變量和函數(shù)局部變量的賦值是可以實(shí)現(xiàn)的。注意對(duì)指向內(nèi)存或storage的變量賦值時(shí),你改變的是指針,而不是數(shù)據(jù)。

有兩種類型的賦值:函數(shù)形式的和指令形式的。對(duì)于函數(shù)形式的賦值(variable := value),你要在函數(shù)類型的表達(dá)式中提供值,并會(huì)返回一個(gè)棧值。對(duì)于指令形式的(=: variable),值從棧頂取。對(duì)于這兩種形式,冒號(hào)指向的是變量。賦值的操作是用新的值替換棧中的變量值。

{
    let v := 0 // 聲明變量,函數(shù)形式的賦值
    let g := add(v, 2)
    sload(10)
    =: v // 指令形式的賦值,把 sload(10)的結(jié)果賦值給v
}
switch

你可以像很基本的“if/else“一樣,來使用switch表達(dá)式。它會(huì)拿著表達(dá)式的值和多個(gè)條件對(duì)比。對(duì)應(yīng)的程序分支會(huì)被執(zhí)行。和一些容易出錯(cuò)的語言相比,這里的控制流不會(huì)接著執(zhí)行下一個(gè)分支。switch可以有一個(gè)默認(rèn)分支,稱為default

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

分支不需要包裹大括號(hào),但是分支需要大括號(hào)。

循環(huán)

匯編支持簡(jiǎn)單的for形式的循環(huán)。for形式的循環(huán),有一個(gè)頭部,包含初始條件,條件和遍歷結(jié)束條件。條件必須是函數(shù)形式的表達(dá)式。但是其他兩個(gè)是代碼塊。如果初始部分聲明了任何變量,這些變量的作用域可以延伸到函數(shù)體(包含條件和遍歷結(jié)束條件的部分)。

下面的例子會(huì)計(jì)算一個(gè)內(nèi)存區(qū)域的和:

{
    let x := 0
    for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
        x := add(x, mload(i))
    }
}
函數(shù)

匯編支持定義更底層的函數(shù)。函數(shù)從堆棧中獲取參數(shù)(和一個(gè)程序計(jì)數(shù)器PC),并且把結(jié)果返回到堆棧中。調(diào)用函數(shù)看起來只是執(zhí)行函數(shù)形式的操作碼。
函數(shù)可以定義在任何地方,并且在整個(gè)代碼塊中可見。在函數(shù)中,你不能訪問在函數(shù)外部定義的局部變量。函數(shù)也沒有明確的return表達(dá)式。

如果你調(diào)用一個(gè)函數(shù),并返回多個(gè)值,你可以把它們賦值給元祖。a,b := f(x)或者let a,b := f(x)。

下面的例子通過平方和乘法,實(shí)現(xiàn)了求冪功能。

{
    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) }
        }
    }
}
要避免的要點(diǎn)

內(nèi)聯(lián)編譯可能看起來像是高層次的,但是它確實(shí)是底層接口。函數(shù)調(diào)用,循環(huán)和switch分支會(huì)被轉(zhuǎn)變?yōu)楹?jiǎn)單的重寫規(guī)則,并且在那之后,編譯器為你做的事情只是重新整理函數(shù)形式的操作碼,管理跳轉(zhuǎn)標(biāo)簽,計(jì)算可訪問的棧深度,在代碼塊結(jié)束的時(shí)候移除匯編局部變量的棧內(nèi)存。尤其是最后兩個(gè)情況,必須要清楚,編譯器只會(huì)自頂向下的計(jì)算棧深度,而不是跟隨控制流。另外交換只會(huì)交換棧里的內(nèi)容,而不會(huì)交換變量的指向。

Solidity約定

和EVM的匯編相比,Solidity可以知道比256位更窄的類型,例如uint24。為了提高效率,很多算術(shù)操作符會(huì)把它們看成是256位的,并且高位只會(huì)在有需要的時(shí)候清除。例如,在寫入內(nèi)存的時(shí)候要縮短,或者在比較的時(shí)候。這意味這,如果你在內(nèi)聯(lián)編譯中訪問這些變量,那你要首先手動(dòng)的去除高位。
Solidity用一種很簡(jiǎn)單的方式來管理內(nèi)存:在0x40有一個(gè)空白內(nèi)存指針。如果你要分配內(nèi)存,只需要使用該指針指向的內(nèi)存,并相應(yīng)的更新指針。
Solidity中的內(nèi)存數(shù)組元素,只會(huì)占據(jù)32位的倍數(shù)(對(duì)的,對(duì)于byte[]也是一樣的,但是bytesstring就不一樣了)。多維內(nèi)存數(shù)組會(huì)指向內(nèi)存數(shù)組。動(dòng)態(tài)數(shù)組的長(zhǎng)度會(huì)保存在數(shù)組的第一個(gè)slot中,然后后面的都是數(shù)組的值。

警告:靜態(tài)大小的內(nèi)存數(shù)組不會(huì)有長(zhǎng)度的字段,但是會(huì)在以后的版本中加上,以提高靜態(tài)數(shù)組--動(dòng)態(tài)數(shù)組的可轉(zhuǎn)換性,所以現(xiàn)在不要做這樣的轉(zhuǎn)換。

獨(dú)立匯編(Standalone Assembly)

如上所說的匯編語言的內(nèi)聯(lián)編譯,也可以用獨(dú)立編譯。事實(shí)上,計(jì)劃用它作為solidity的中間語言。在這種情況下,它有如下幾個(gè)目的:

  1. 用它寫的代碼可讀,即使是solidity編譯器生成的代碼,也是可讀的。
  2. 從匯編到字節(jié)碼的轉(zhuǎn)換的黑魔法應(yīng)該越少越好。
  3. 控制流可以方便的監(jiān)測(cè)來有助格式檢測(cè)和優(yōu)化。

為了達(dá)到第一個(gè)和最后一個(gè)目標(biāo),匯編提供了高級(jí)的指令,像for循環(huán),switch表達(dá)式和函數(shù)調(diào)用。而不需要用到SWAP,DUP,JUMPJUMPI,因?yàn)榍皟蓚€(gè)會(huì)擾亂數(shù)據(jù)流,后兩個(gè)會(huì)擾亂控制流。另外,mul(add(x,y), 7)形式的函數(shù)表達(dá)式,好過操作碼形的7 y x add mul,因?yàn)榈谝环N形式更加直觀的看出操作碼用的操作數(shù)。
第二個(gè)目標(biāo)的實(shí)現(xiàn),通過引入去語法糖的短語--只是移除了高階構(gòu)指令--并允許檢測(cè)生成的低階的匯編碼。編譯器唯一的非局部操作是為用戶定義的標(biāo)識(shí)符(函數(shù),變量...)查找名稱,該過程遵循簡(jiǎn)單的作用域規(guī)則和清理堆棧中的局部變量。

作用域:標(biāo)識(shí)符(標(biāo)簽,變量,函數(shù),匯編(?assembly))只能在被聲明的代碼塊中可見(包含當(dāng)前塊的嵌入代碼塊)。不允許跨代碼塊訪問局部變量,即使它們?cè)谧饔糜蛑?。影子調(diào)用(?shadowing)是不允許的。局部變量必須先定義后使用。但是標(biāo)簽,函數(shù)和匯編可以。匯編是特殊的代碼塊,例如,用來返回運(yùn)行時(shí)操作碼或者創(chuàng)建合約。外部的標(biāo)識(shí)符是不能在內(nèi)部訪問。

如果控制流執(zhí)行到代碼塊結(jié)尾,會(huì)插入于本地變量數(shù)目相同的pop指令。局部變量無論在什么時(shí)候被引用,代碼生成器必須知道堆棧當(dāng)前的相對(duì)位置,并且要跟蹤棧的深度。由于所有的局部變量會(huì)在代碼結(jié)束的時(shí)候被移除,代碼執(zhí)行之前和執(zhí)行之后的棧深度是一致的。如果不是這種情況,會(huì)出現(xiàn)一個(gè)警告。

我們需要高階指令--例如switch,for和函數(shù)--的原因是:

使用switch,for和函數(shù),可以不用jumpjumpi來實(shí)現(xiàn)復(fù)雜的代碼。分析控制流也變得更加容易了,這會(huì)提高格式校驗(yàn)和優(yōu)化的效率。

另外,如果允許手動(dòng)跳轉(zhuǎn),計(jì)算棧的深度也是比較復(fù)雜的。所有局部變量的位置必須知道,另外還要正確引用變量,在代碼塊結(jié)束的時(shí)候正確的清理變量。如果不是連續(xù)的執(zhí)行流,去語法糖機(jī)制能夠在不可達(dá)的地方,正確的插入操作來較正棧深度。

例子:

我們分析一個(gè)例子從Solidity到去語法糖的匯編來說明原理。我們會(huì)分析下面Solidity代碼的字節(jié)碼:

pragma solidity ^0.4.0;

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

下面的是生成的匯編:

{
  mstore(0x40, 0x60) // 保存空白內(nèi)存指針
  // 函數(shù)調(diào)度
  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) }
  // 內(nèi)存分配
  function $allocate(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)
    }
  }
}

在去語法糖處理之后,代碼如下:

{
  mstore(0x40, 0x60)
  {
    let $0 := div(calldataload(0), exp(2, 226))
    jumpi($case1, eq($0, 0xb3de648b))
    jump($caseDefault)
    $case1:
    {
      // 函數(shù)調(diào)用- 我們把返回標(biāo)簽和參數(shù)壓入堆棧
      $ret1 calldataload(4) jump(f)
      // 這是不可達(dá)代碼,添加的操作碼用來調(diào)節(jié)棧深度:參數(shù)會(huì)移除,引入返回值
      pop pop
      let r := 0
      $ret1: // 真正的返回點(diǎn)
      $ret2 0x20 jump($allocate)
      pop pop let ret := 0
      $ret2:
      mstore(ret, r)
      return(ret, 0x20)
      // 盡管這是沒有用的,這個(gè)跳轉(zhuǎn)是自動(dòng)插入的。因?yàn)槿フZ法糖處理是存粹的語法操作,
      // 不會(huì)分析控制流
      jump($endswitch)
    }
    $caseDefault:
    {
      revert(0, 0)
      jump($endswitch)
    }
    $endswitch:
  }
  jump($afterFunction)
  allocate:
  {
    // 我們跳過不可達(dá)代碼
    jump($start)
    let $retpos := 0 let size := 0
    $start:
    // 輸出變量和參數(shù)有一樣的作用域,并且被分配
    let pos := 0
    {
      pos := mload(0x40)
      mstore(0x40, add(pos, size))
    }
    // 這個(gè)代碼用返回值和跳轉(zhuǎn)來替換參數(shù)
    swap1 pop swap1 jump
    // 這里也是不可達(dá)代碼,用來矯正棧的深度
    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:
    } // 這里會(huì)為i插入一個(gè)pop指令
    swap1 pop swap1 jump
    0 0
  }
  $afterFunction:
  stop
}

匯編會(huì)在四個(gè)階段發(fā)生:

  1. 解析
  2. 去語法糖(移除 switch,for和函數(shù))
  3. 操作碼流生成
  4. 字節(jié)碼生成

我們會(huì)用偽代碼的方式來分析第一步到第三步。下面是正規(guī)的闡述。(?More formal specifications will follow.)

解析/語法

解析器的任務(wù)如下:

  • 把字節(jié)碼流轉(zhuǎn)換為令牌流,移除c++格式的注釋(一種用于代碼引用的特殊注釋,但是我們?cè)谶@里不會(huì)再解釋)(?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).)
  • 根據(jù)如下的語法,將令牌流轉(zhuǎn)換為AST。
  • 在標(biāo)識(shí)符定義的代碼塊中注冊(cè)標(biāo)識(shí)符(注釋為AST節(jié)點(diǎn))并且注明指向的節(jié)點(diǎn),以及可以訪問的變量。

匯編詞法分析器會(huì)遵循Solidity自己定義的規(guī)則,

空白字符用來分離令牌,它包含空格鍵,Tab鍵和換行。注釋按照傳統(tǒng)的javascript/c++的方式,它們會(huì)被替換為空白字符。

語法:

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]+
去語法糖(desugaring)

AST變換會(huì)移除for,switch和函數(shù)指令。結(jié)果還是可以通過相同的分析器分析的。但是不會(huì)用到特定的指令。如果使用了跳轉(zhuǎn),不是連續(xù)執(zhí)行,那么就要加入關(guān)于堆棧的信息。除非沒有局部變量或者外部作用域的變量被訪問到,或者指令執(zhí)行之前和指令執(zhí)行之后的堆棧深度是相同的。

偽代碼:

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 // 不能在它自己的代碼塊里,因?yàn)槲覀兤谕兞康淖饔糜驍U(kuò)展到整個(gè)代碼體
    // 找到 I, 沒有 $forI_* 標(biāo)簽。
    $forI_begin:
    jumpi($forI_end, iszero(condition))
    { body }
    $forI_continue:
    { post }
    jump($forI_begin)
    $forI_end:
  }
'break' ->
  {
    // 找到標(biāo)簽$forI_end 最近的作用域
    // 彈出在這個(gè)點(diǎn)上,定義的所有變量,但是不是在 $forI_end 上
    jump($forI_end)
    0 (很多上面的變量被移除了)
  }
'continue' ->
  {
    // 找到標(biāo)簽$forI_continue 最近的作用域
    // 彈出在這個(gè)點(diǎn)上,定義的所有變量,但是不是在 $forI_continue
    jump($forI_continue)
    0 (很多上面的變量被移除了)
  }
AssemblySwitch(switch condition cases ( default: defaultBlock )? ) ->
  {
    // 找到I,直到?jīng)]有$switchI* 標(biāo)簽或者變量
    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 ->
      {
        // 找到 I 直到 $funcallI_* 標(biāo)簽不存在
        $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)
}
操作碼流生成(Opcode Stream Generation)

在操作碼流生成的過程中,我們用計(jì)數(shù)器來跟蹤堆棧的深度,來使得堆棧變量可以訪問。堆棧深度能夠被每個(gè)能夠改變堆棧的操作碼和用來注釋堆棧矯正的標(biāo)簽所改變。每當(dāng)新的變量被聲明,堆棧深度也會(huì)被改變。如果訪問一個(gè)變量(要么拷貝它的值,要么賦值),會(huì)依據(jù)當(dāng)前堆棧的深度和變量被引入點(diǎn)的深度來選擇使用合適的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 ->
      // 在生成字節(jié)碼過程中,引用會(huì)被解決
      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
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容