深入理解Solidity——Solidity汇编

Solidity汇编(Solidity Assembly)

Solidity定义了一个汇编语言,可以不同Solidity一起使用。这个汇编语言还可以嵌入到Solidity源码中,以内联汇编的方式使用。下面我们将从内联汇编如何使用着手,介绍其与独立使用的汇编语言的不同,最后再介绍这门汇编语言。

内联汇编(Inline Assembly)

通常我们通过库代码,来增强语言我,实现一些精细化的控制,Solidity为我们提供了一种接近于EVM底层的语言,内联汇编,允许与Solidity结合使用。由于EVM是栈式的,所以有时定位栈比较麻烦,Solidty的内联汇编为我们提供了下述的特性,来解决手写底层代码带来的各种问题:

  • 允许函数风格的操作码: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) } }
  • 标签: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) }
  • 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))) } }

下面将详细介绍内联编译(inline assembly)语言。

警告
需要注意的是内联编译是一种非常底层的方式来访问EVM虚拟机。他没有Solidity提供的多种安全机制。
注解
TODO:待补充内联汇编的变量作用域的不同,尤其是使用含internal的函数的库时所引入的复杂度。另外,还需补充,编译器定义的符号(symbols)。

示例

下面的例子提供了一个库函数来访问另一个合约,并把它写入到一个bytes变量中。有一些不能通过常规的Solidity语言完成,内联库可以用来在某些方面增强语言的能力。

pragma solidity ^0.4.0;

library GetCode {
    function at(address _addr) public view returns (bytes 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(0x1f))))
            // 存储长度在内存中
            mstore(o_code, size)
            // 实际取得代码,这需要汇编
            extcodecopy(_addr, add(o_code, 0x20), 0, size)
        }
    }
}

内联汇编在当编译器没办法得到有效率的代码时非常有用。但需要留意的是内联汇编写起来是比较难的,因为编译器不会进行一些检查,所以你应该只在复杂的,且你知道你在做什么的事情上使用它。

pragma solidity ^0.4.16;

library VectorSum {
    // 此函数效率较低,因为优化器当前无法
    // 移除数组访问中的边界检查。
    function sumSolidity(uint[] _data) public view returns (uint o_sum) {
        for (uint i = 0; i < _data.length; ++i)
            o_sum += _data[i];
    }

    // 我们知道我们只访问数组,所以我们可以避免检查。
    // 0x20需要添加到一个数组中,因为第一个位置包含
    // 数组长度。
    function sumAsm(uint[] _data) public view 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))))
            }
        }
    }

    // 功能与上述相同,但完全使用内联汇编。
    function sumPureAsm(uint[] _data) public view returns (uint o_sum) {
        assembly {
           // 加载长度(前32字节)
           let len := mload(_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))
           }
        }
    }
}

语法

内联编译语言也会像Solidity一样解析注释,字面量和标识符。所以你可以使用///**/的方式注释。内联编译的在Solidity中的语法是包裹在assembly { ... },下面是可用的语法,后续有更详细的内容。

  • 字面量。如0x12342"abc"(字符串最多是32个字符)
  • 操作码(指令的方式),如mload sload dup1 sstore,后面有可支持的指令列表
  • 函数风格的操作码,如add(1, mlod(0)
  • 标签,如name:
  • 变量定义,如let x := 7let x := add(y, 3)let x(初始值为0)
  • 标识符(标签或内联局部变量或外部),如jump(name)3 x add
  • 赋值(指令风格),如,3 =: x
  • 函数风格的赋值,如x := add(y, 3)
  • 支持块级的局部变量,如{ let x := 3 { let y := add(x, 1) } }

操作码

这个文档不想介绍EVM虚拟机的完整描述,但后面的列表可以做为EVM虚拟机的指令码的一个参考。

如果一个操作码有参数(通过在栈顶),那么他们会放在括号。需要注意的是参数的顺序可以颠倒(非函数风格,后面会详细说明)。用-标记的操作码不会将一个参数推到栈顶,而标记为*的是非常特殊的,所有其它的将且只将一个推到栈顶。用FHBC标记的操作码分别来自Frontier、Homestead、Byzantium或Constantinople。Constantinople仍在计划中,所有这样的指令都会导致无效的指令异常。

在后面的例子中,mem[a...b)表示成位置a到位置b(不包含)的memory字节内容,storage[p]表示在位置p的strorage内容。

操作码pushijumpdest不能被直接使用。

在语法中,操作码被表示为预先定义的标识符。

操作码说明
stop-F停止执行,等同于return(0,0)
add(x, y)Fx + y
sub(x, y)Fx - y
mul(x, y)Fx * y
div(x, y)Fx / y
sdiv(x, y)Fx / y, for signed numbers in two’s complement
mod(x, y)Fx % y
smod(x, y)Fx % y, for signed numbers in two’s complement
exp(x, y)Fx to the power of y
not(x)F~x, every bit of x is negated
lt(x, y)F1 if x < y, 0 otherwise
gt(x, y)F1 if x > y, 0 otherwise
slt(x, y)F1 if x < y, 0 otherwise, for signed numbers in two’s complement
sgt(x, y)F1 if x > y, 0 otherwise, for signed numbers in two’s complement
eq(x, y)F1 if x == y, 0 otherwise
iszero(x)F1 if x == 0, 0 otherwise
and(x, y)Fbitwise and of x and y
or(x, y)Fbitwise or of x and y
xor(x, y)Fbitwise xor of x and y
byte(n, x)Fnth byte of x, where the most significant byte is the 0th byte
shl(x, y)Clogical shift left y by x bits
shr(x, y)Clogical shift right y by x bits
sar(x, y)Carithmetic 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)Fsign extend from (i*8+7)th bit counting from least significant
keccak256(p, n)Fkeccak(mem[p…(p+n)))
sha3(p, n)Fkeccak(mem[p…(p+n)))
jump(label)-Fjump to label / code position
jumpi(label, cond)-Fjump to label if cond is nonzero
pcFcurrent position in code
pop(x)-Fremove the element pushed by x
dup1 … dup16Fcopy ith stack slot to the top (counting from top)
swap1 … swap16*Fswap topmost and ith stack slot below it
mload(p)Fmem[p..(p+32))
mstore(p, v)-Fmem[p..(p+32)) := v
mstore8(p, v)-Fmem[p] := v & 0xff (only modifies a single byte)
sload(p)Fstorage[p]
sstore(p, v)-Fstorage[p] := v
msizeFsize of memory, i.e. largest accessed memory index
gasFgas still available to execution
addressFaddress of the current contract / execution context
balance(a)Fwei balance at address a
callerFcall sender (excluding delegatecall)
callvalueFwei sent together with the current call
calldataload(p)Fcall data starting from position p (32 bytes)
calldatasizeFsize of call data in bytes
calldatacopy(t, f, s)-Fcopy s bytes from calldata at position f to mem at position t
codesizeFsize of the code of the current contract / execution context
codecopy(t, f, s)-Fcopy s bytes from code at position f to mem at position t
extcodesize(a)Fsize of the code at address a
extcodecopy(a, t, f, s)-Flike codecopy(t, f, s) but take code at address a
returndatasizeBsize of the last returndata
returndatacopy(t, f, s)-Bcopy s bytes from returndata at position f to mem at position t
create(v, p, s)Fcreate new contract with code mem[p..(p+s)) and send v wei and return the new address
create2(v, n, p, s)Ccreate 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)Fcall 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)Fidentical 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)Hidentical to callcode but also keep caller and callvalue
staticcall(g, a, in, insize, out, outsize)Bidentical to call(g, a, 0, in, insize, out, outsize) but do not allow state modifications
return(p, s)-Fend execution, return data mem[p..(p+s))
revert(p, s)-Bend execution, revert state changes, return data mem[p..(p+s))
selfdestruct(a)-Fend execution, destroy current contract and send funds to a
invalid-Fend execution with invalid instruction
log0(p, s)-Flog without topics and data mem[p..(p+s))
log1(p, s, t1)-Flog with topic t1 and data mem[p..(p+s))
log2(p, s, t1, t2)-Flog with topics t1, t2 and data mem[p..(p+s))
log3(p, s, t1, t2, t3)-Flog with topics t1, t2, t3 and data mem[p..(p+s))
log4(p, s, t1, t2, t3, t4)-Flog with topics t1, t2, t3, t4 and data mem[p..(p+s))
originFtransaction sender
gaspriceFgas price of the transaction
blockhash(b)Fhash of block nr b - only for last 256 blocks excluding current
coinbaseFcurrent mining beneficiary
timestampFtimestamp of the current block in seconds since the epoch
numberFcurrent block number
difficultyFdifficulty of the current block
gaslimitFblock gas limit of the current block

字面量(Literals)

你可以使用整数常量,通过直接以十进制或16进制的表示方式,将会自动生成恰当的pushi指令。

assembly { 2 3 add "abc" and }

上面的例子中,将会先加2,3得到5,然后再与字符串abc进行与运算。字符串按左对齐存储,且不能超过32字节。

函数风格(Functional Style)

你可以在操作码后接着输入操作码,它们最终都会生成正确的字节码。比如下面将会添加3到memory中位置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中的变量和其它标识符,可以简单的通过名称引用。对于memory变量,这将会把地址而不是值推到栈上。Storage的则有所不同,由于对应的值不一定会占满整个storage槽位,所以它的地址由槽和实际存储位置相对起始字节偏移。要搜索变量x指向的槽位,使用x_slot,得到变量相对槽位起始位置的偏移使用x_offset

在赋值中(见下文),我们甚至可以直接向Solidity变量赋值。

还可以访问内联编译的外部函数:内联编译会推入整个的入口的label(应用虚函数解析的方式)。Solidity中的调用语义如下:

  • 调用者推入return labelarg1arg2, … 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)) // ignore the offset, we know it is zero
        }
    }
}
注解
如果访问小于256位的类型的变量(例如uint64, address, bytes16byte),则不能对不属于该类型编码的位进行任何假设。特别是,不要假定它们为零。为了安全起见,在使用该数据之前,请始终正确清除数据:uint32 x = f(); assembly { x := and(x, 0xffffffff) /* now use x */ }。清除有符号的类型,您可以使用signextend操作码。

标签(Labels)

注解
标签被弃用。请使用函数、循环、if或switch语句来代替。

另一个在EVM的汇编的问题是jumpjumpi使用了绝对地址,可以很容易的变化。Solidity内联汇编提供了标签来让jump跳转更加容易。需要注意的是标签是非常底层的特性,尽量使用内联汇编函数,循环,Switch指令来代替。下面是一个求Fibonacci的例子:

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

需要注意的是自动访问栈元素需要内联者知道当前的栈高。这在跳转的源和目标之间有不同栈高时将失败。当然你也仍然可以在这种情况下使用jump,但你最好不要在这种情况下访问栈上的变量(即使是内联变量)。

此外,栈高分析器会一个操作码接着一个操作码的分析代码(而不是根据控制流),所以在下面的情况下,汇编程序将对标签two的堆栈高度产生错误的判断:

{
    let x := 8
    jump(two)
    one:
        // Here the stack height is 2 (because we pushed x and 7),
        // but the assembler thinks it is 1 because it reads
        // from top to bottom.
        // Accessing the stack variable x here will lead to errors.
        x := 9
        jump(three)
    two:
        7 // push something onto the stack
        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 is "deallocated" here
            b := add(b, v)
        } // v is "deallocated" here
    }
}

赋值(Assignments)

你可以向内联局部变量赋值,或者函数局部变量。需要注意的是当你向一个指向memory或storage赋值时,你只是修改了对应指针而不是对应的数据。

有两种方式的赋值方式:函数风格和指令风格。函数风格,比如variable := value,你必须在函数风格的表达式中提供一个变量,最终将得到一个栈变量。指令风格=: variable,值则直接从栈底取。以于两种方式冒号指向的都是变量名称。赋值的效果是将栈上的变量值替换为新值。

{
    let v := 0 // functional-style assignment as part of variable declaration
    let g := add(v, 2)
    sload(10)
    =: v // instruction style assignment, puts the result of sload(10) into v
}
注解
指令风格赋值被deprecated。

If

if语句可用于有条件地执行代码。没有“else”部分,如果需要多个备选方案,请考虑使用“switch”(见下文)。

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

body的花括号是必须的。

Switch

你可以使用switch语句来作为一个基础版本的if/else语句。它需要取一个值,用它来与多个常量进行对比。每个分支对应的是对应切尔西到的常量。与某些语言容易出错的行为相反,控制流不会自动从一个判断情景到下一个场景(即默认是break的)。最后有个叫default的兜底。

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

可以有的case不需要包裹到大括号中,但每个case需要用大括号的包裹。

循环

内联汇编支持一个简单的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)
    }
}

函数

汇编语言允许定义底层的函数。这些需要在栈上取参数(以及一个返回的代码行),也会将结果存到栈上。调用一个函数与执行一个函数风格的操作码看起来是一样的。

函数可以在任何地方定义,可以在定义的块中可见。在函数内,你不能访问一个在函数外定义的一个局部变量。同时也没有明确的return语句。

如果你调用一个函数,并返回了多个值,你可以将他们赋值给一个元组,使用a, b := f(x)let a, b := f(x)

下面的例子中通过平方乘来实现一个指数函数。

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

内联汇编使用中需要一个比较高的视野,但它又是非常底层的语法。函数调用,循环,switch被转换为简单的重写规则,另外一个语言提供的是重安排函数风格的操作码,管理了jump标签,计算了栈高以方便变量的访问,同时在块结束时,移除块内定义的块内的局部变量。特别需要注意的是最后两个情况。你必须清醒的知道,汇编语言只提供了从开始到结束的栈高计算,它没有根据你的逻辑去计算栈高(译者注:这常常导致错误)。此外,像交换这样的操作,仅仅交换栈里的内容,并不是变量的位置。

Solidity中的惯例

与EVM汇编不同,Solidity知道类型少于256字节,如,uint24。为了让他们更高效,大多数的数学操作仅仅是把也们当成是一个256字节的数字进行计算,高位的字节只在需要的时候才会清理,比如在写入内存前,或者在需要比较时。这意味着如果你在内联汇编中访问这样的变量,你必须要手动清除高位的无效字节。

Solidity以非常简单的方式来管理内存:内部存在一个空间内存的指针在内存位置0x40。如果你想分配内存,可以直接使用从那个位置的内存,并相应的更新指针。

内存的前64个字节可以用作短期分配的“划痕空间”。空闲内存指针(即0x60开始)后的32字节永远为0,并用作空动态内存数组的初始值。

Solidity中的内存数组元素,总是占用多个32字节的内存(也就是说byte[]也是这样,但是bytesstring不是这样)。多维的memory的数组是指向memory的数组。一个动态数组的长度存储在数据的第一个槽位,紧接着就是数组的元素。

警告
静态大小的内存数组没有长度字段,但是它在未来很快会被添加,以便在静态和动态大小的数组之间允许更好的可互换性,所以请不要依赖于此。

上一篇:深入理解Solidity——Using for

下一篇:深入理解Solidity——独立汇编

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值