solidity高级编程——内联汇编

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/bondsui/article/details/88537922

文档https://solidity-cn.readthedocs.io/zh/develop/assembly.html

Solidity定义了一个汇编语言,可以不同Solidity一起使用,也可以嵌入到Solidity源码中,以内联汇编的方式使用。

1 内联汇编(Inline Assembly)

内联汇编,一种接近于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))) } }

2 示例合约字节编码

pragma solidity >=0.4.22 <0.6.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)
        }
    }
}

contract A{
    address public owner;
    constructor() public{
        owner = msg.sender;
    }
    
}

contract B{
    address public owner;
    constructor() public{
        owner = msg.sender;
    }
    
    function getSize(address _addr) public view returns (bytes){
        return GetCode.at(_addr);
    }
}

部署A\B后调用B.getSize()传入A的地址。

得到:

0x608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680638da5cb5b146044575b600080fd5b348015604f57600080fd5b5060566098565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff16815600a165627a7a7230582045e9440f7298080f79b54079f1f0648fdfc85261140f85661a906fcbcc122c2e0029

查看编辑器,对比bytecode

在这里插入图片描述

在优化器无法生成高效代码的情况下,内联汇编也可能更有好处。请注意,由于编译器无法对汇编语句进行相关的检查,所以编写汇编代码肯定更加困难; 因此只有在处理一些相对复杂的问题时才需要使用它,并且你需要明确知道自己要做什么。

3 示例内联汇编降低gas消费

所有部署合约,或者调用合约执行指令的操作,都以消耗gas为代价。内联汇编,类底层虚拟机语言,可节省部分gas消费。

例子:计算uint数组 之和

pragma solidity >=0.4.22 <0.6.0;

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

    // 我们知道我们只能在数组范围内访问数组元素,所以我们可以在内联汇编中不做边界检查。
    // 由于 ABI 编码中数组数据的第一个字(32 字节)的位置保存的是数组长度,
    // 所以我们在访问数组元素时需要加入 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)

           // 略过长度字段。
           //
           // 保持临时变量以便它可以在原地增加。
           //
           // 注意:对 _data 数值的增加将导致 _data 在这个汇编语句块之后不再可用。
           //      因为无法再基于 _data 来解析后续的数组数据。
           let data := add(_data, 0x20)

           // 迭代到数组数据结束
           for
               { let end := add(data, mul(len, 0x20)) }
               lt(data, end)
               { data := add(data, 0x20) }
           {
               o_sum := add(o_sum, mload(data))
           }
        }
    }
}

contract Demo2{
    uint[] public numbers;
    constructor() public {
        for(uint i=0;i<10;i++){
            numbers.push(i);
        }
    }
    // solidity
    function sum1() public view returns(uint){
       return VectorSum.sumSolidity(numbers);
    }      
    // solidity+ 汇编
    function sum2() public view returns(uint){
       return VectorSum.sumAsm(numbers);
    } 
    // 存内联汇编
    function sum3() public view returns(uint){
       return VectorSum.sumPureAsm(numbers);
    }
}

部署合约后分别执行3个函数

在这里插入图片描述

分别查看下交易信息:debug3个输出

在这里插入图片描述

sum1:28024-6752=21272

transaction cost 	28024 gas (Cost only applies when called by a contract)
execution cost 	6752 gas (Cost only applies when called by a contract)

sum2: 27600-6328=21272

transaction cost 	27600 gas (Cost only applies when called by a contract)
execution cost 	6328 gas (Cost only applies when called by a contract)

sum3: 27414-6142=21272

transaction cost 	27414 gas (Cost only applies when called by a contract)
execution cost 	6142 gas (Cost only applies when called by a contract)

说明

transaction cost指的是将交易送至ethereum blockchain所耗费的cost,是基于data size的大小,部署合约时就是基于合约內容的大小.
execution cost指的是虚拟机(VM)执行所需的cost,而在部署合约时,会去执行建構子以及一些初始化的工作

28024-6752=21272

 var getGasUsedOutput = function (result, vmResult) {
        var $gasUsed = $('<div class="gasUsed">');
        var caveat = lookupOnly ? '<em>(<span class="caveat" title="Cost only applies when called by a contract">caveat</span>)</em>' : '';
        if (result.gasUsed) {
            var gas = result.gasUsed.toString(10);//Transaction cost
            $gasUsed.html('<strong>Transaction cost:</strong> ' + gas + ' gas. ' + caveat);
        }
        if (vmResult.gasUsed) {
            var $callGasUsed = $('<div class="gasUsed">');
            var gas = vmResult.gasUsed.toString(10);//Execution cost
            $callGasUsed.append('<strong>Execution cost:</strong> ' + gas + ' gas.');
            $gasUsed.append($callGasUsed);
        }
        return $gasUsed;
};

部署合约:122780

 transaction cost 	545747 gas 
 execution cost 	422967 gas 
 transaction cost 	269488 gas 
 execution cost 	174408 gas 

4 内联汇编语法

和 Solidity 一样,Assembly 也会解析注释、文字和标识符,所以你可以使用通常的 ///* */来进行注释。 内联汇编程序由 assembly { ... } 来标记,在这些大括号内可以使用以下内容(更多详细信息请参阅后面部分)。

  • 字面常数,也就是 0x12342"abc" (不超过 32 个字符的字符串)
  • 操作码(在“instruction style”内),比如 mload sload dup1 sstore,操作码列表请看后面
  • 函数风格操作码,比如 add(1,mlod(0))
  • 标签,比如 name:
  • 变量声明,比如 let x := 7let x := add(y, 3) 或者 let x (初始值将被置为 empty(0))
  • 标识符(标签或者汇编局部变量以及用作内联汇编时的外部变量),比如 jump(name)3 x add
  • 赋值(在“instruction style”内),比如 3 =: x
  • 函数风格赋值,比如 x := add(y,3)
  • 一些控制局部变量作用域的语句块,比如 {let x := 3 { let y := add(x,1) }}

操作码

如果一个操作码需要参数(总是来自堆栈顶部)。请注意:参数顺序可以看作是在非函数风格中逆序

标有 - 的操作码不会向栈中压入(push)数据

标有 * 的操作码有特殊操作,而所有其他操作码都只会将一个数据压入(push)栈中。

在下表中,mem[a...b) 表示从位置 a 开始至(不包括)位置 b 的内存字节数,storage[p] 表示位置 p 处的存储内容。

FHBC 标记的操作码代表它们从 Frontier、Homestead、Byzantium 或 Constantinople 开始被引入。 Constantinople 目前仍在计划中,所以标记为 C 的指令目前都会导致一个非法指令异常。

Instruction Explanation
stop - F 停止执行,与 return(0,0) 等价
add(x, y) F x + y
sub(x, y) F x - y
mul(x, y) F x * y
div(x, y) F x / y
sdiv(x, y) F x / y,以二进制补码作为符号
mod(x, y) F x % y
smod(x, y) F x % y,以二进制补码作为符号
exp(x, y) F x 的 y 次幂
not(x) F ~x,对 x 按位取反
lt(x, y) F 如果 x < y 为 1,否则为 0
gt(x, y) F 如果 x > y 为 1,否则为 0
slt(x, y) F 如果 x < y 为 1,否则为 0,以二进制补码作为符号
sgt(x, y) F 如果 x > y 为 1,否则为 0,以二进制补码作为符号
eq(x, y) F 如果 x == y 为 1,否则为 0
iszero(x) F 如果 x == 0 为 1,否则为 0
and(x, y) F x 和 y 的按位与
or(x, y) F x 和 y 的按位或
xor(x, y) F x 和 y 的按位异或
byte(n, x) F x 的第 n 个字节,这个索引是从 0 开始的
shl(x, y) C 将 y 逻辑左移 x 位
shr(x, y) C 将 y 逻辑右移 x 位
sar(x, y) C 将 y 算术右移 x 位
addmod(x, y, m) F 任意精度的 (x + y) % m
mulmod(x, y, m) F 任意精度的 (x * y) % m
signextend(i, x) F 对 x 的最低位到第 (i * 8 + 7) 进行符号扩展
keccak256(p, n) F keccak(mem[p…(p + n)))
jump(label) - F 跳转到标签 / 代码位置
jumpi(label, cond) - F 如果条件为非零,跳转到标签
pc F 当前代码位置
pop(x) - F 删除(弹出)栈顶的 x 个元素
dup1 … dup16 F 将栈内第 i 个元素(从栈顶算起)复制到栈顶
swap1 … swap16 * F 将栈顶元素和其下第 i 个元素互换
mload§ F mem[p…(p + 32))
mstore(p, v) - F mem[p…(p + 32)) := v
mstore8(p, v) - F mem[p] := v & 0xff (仅修改一个字节)
sload§ F storage[p]
sstore(p, v) - F storage[p] := v
msize F 内存大小,即最大可访问内存索引
gas F 执行可用的 gas
address F 当前合约 / 执行上下文的地址
balance(a) F 地址 a 的余额,以 wei 为单位
caller F 调用发起者(不包括 delegatecall
callvalue F 随调用发送的 Wei 的数量
calldataload§ F 位置 p 的调用数据(32 字节)
calldatasize F 调用数据的字节数大小
calldatacopy(t, f, s) - F 从调用数据的位置 f 的拷贝 s 个字节到内存的位置 t
codesize F 当前合约 / 执行上下文地址的代码大小
codecopy(t, f, s) - F 从代码的位置 f 开始拷贝 s 个字节到内存的位置 t
extcodesize(a) F 地址 a 的代码大小
extcodecopy(a, t, f, s) - F 和 codecopy(t, f, s) 类似,但从地址 a 获取代码
returndatasize B 最后一个 returndata 的大小
returndatacopy(t, f, s) - B 从 returndata 的位置 f 拷贝 s 个字节到内存的位置 t
create(v, p, s) F 用 mem[p…(p + s)) 中的代码创建一个新合约、发送 v wei 并返回 新地址
create2(v, n, p, s) C 用 mem[p…(p + s)) 中的代码,在地址 keccak256(
. n . keccak256(mem[p…(p + s))) 上 创建新合约、发送 v wei 并返回新地址
call(g, a, v, in, insize, out, outsize) F 使用 mem[in…(in + insize)) 作为输入数据, 提供 g gas 和 v wei 对地址 a 发起消息调用, 输出结果数据保存在 mem[out…(out + outsize)), 发生错误(比如 gas 不足)时返回 0,正确结束返回 1
callcode(g, a, v, in, insize, out, outsize) F call 等价,但仅使用地址 a 中的代码 且保持当前合约的执行上下文
delegatecall(g, a, in, insize, out, outsize) F callcode 等价且保留 callercallvalue
staticcall(g, a, in, insize, out, outsize) F call(g, a, 0, in, insize, out, outsize) 等价 但不允许状态修改
return(p, s) - F 终止运行,返回 mem[p…(p + s)) 的数据
revert(p, s) - B 终止运行,撤销状态变化,返回 mem[p…(p + s)) 的数据
selfdestruct(a) - F 终止运行,销毁当前合约并且把资金发送到地址 a
invalid - F 以无效指令终止运行
log0(p, s) - F 以 mem[p…(p + s)) 的数据产生不带 topic 的日志
log1(p, s, t1) - F 以 mem[p…(p + s)) 的数据和 topic t1 产生日志
log2(p, s, t1, t2) - F 以 mem[p…(p + s)) 的数据和 topic t1、t2 产生日志
log3(p, s, t1, t2, t3) - F 以 mem[p…(p + s)) 的数据和 topic t1、t2、t3 产生日志
log4(p, s, t1, t2, t3, t4) - F 以 mem[p…(p + s)) 的数据和 topic t1、t2、t3 和 t4 产生日志
origin F 交易发起者地址
gasprice F 交易所指定的 gas 价格
blockhash(b) F 区块号 b 的哈希 - 目前仅适用于不包括当前区块的最后 256 个区块
coinbase F 当前的挖矿收益者地址
timestamp F 从当前 epoch 开始的当前区块时间戳(以秒为单位)
number F 当前区块号
difficulty F 当前区块难度
gaslimit F 当前区块的 gas 上限

字面常量

你可以直接键入十进制或十六进制符号来作为整型常量使用,这会自动生成相应的 PUSHi 指令。 下面的代码将计算 2 加 3(等于 5),然后计算其与字符串 “abc” 的按位与。字符串在存储时为左对齐,且长度不能超过 32 字节。

assembly { 2 3 add "abc" and }

函数风格

你可以像使用字节码那样在操作码之后键入操作码。例如,把 3 与内存位置 0x80 处的数据相加就是

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

等价于

3 0x80 mload add 0x80 mstore

访问外部变量和函数

通过简单使用它们名称就可以访问 Solidity 变量和其他标识符。

对于内存变量,这会将地址而不是值压入栈中。 存储变量是不同的,因为存储变量的值可能不占用完整的存储槽,因此其“地址”由存储槽和槽内的字节偏移量组成。 为了获取变量 x 所使用的存储槽,你可以使用 x_slot,并用的 x_offset 获取其字节偏移量。

这个特性使用起来还是有点麻烦,因为在调用过程中堆栈偏移量发生了根本变化,因此对局部变量的引用将会出错。

pragma solidity ^0.4.11;

contract C {
    uint b;
    function f(uint x) public returns (uint r) {
        assembly {
            r := mul(x, sload(b_slot)) // 因为偏移量为 0,所以可以忽略
        }
    }
}

注解:

如果你访问一个实际数据位数小于 256 位的数据类型(比如 uint64addressbytes16byte), 不要对这种类型经过编码后未使用的数据位上的数值做任何假设。尤其是不要假设它们肯定为 0。 安全起见,在某个上下文中使用这种数据之前,请一定先将其数据清空为 0,这非常重要:uint32 x = f(); assembly { x := and(x, 0xffffffff) /* now use x */ } 要清空有符号类型,你可以使用 signextend 操作码。

汇编局部变量声明

let 指令将创建一个为变量保留的新数据槽,并在到达块末尾时自动删除。 你需要为变量提供一个初始值,它可以只是 0,也可以是一个复杂的函数表达式。

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 会在这里被“清除”
            // b := add(y, v) // 编译错误
            b := add(b, v)
        } // v 会在这里被“清除”
    }
}

赋值

{
    let v := 0 // 作为变量声明的函数风格赋值
    let g := add(v, 2)
}

控制结构

if没有“else”部分;如果需要多种选择,你可以考虑使用“switch”

function testif(uint x) public pure returns (uint b) {
    b=x+1;
    assembly {
        if eq(x, 0) { 
            b := 99
        }
    } 
}

switch:Case 列表里面不需要大括号,但 case 主体需要。

contract Demo4{
     uint public a=1;
     uint public b=11;
     uint public c=22;
     uint public d=33;
    function testswitch(uint x) public view returns(uint r){
        assembly{
            x := add(x,sload(0)) // 输入参数加第0个storage的值
            switch mod(x,3)
            case 1 {
                r := sload(1)
            }
            case 2 {
                r := sload(2)
            }  
            default {
                r := sload(3)
            }
        }
    }
}

for

// 和上面一样,但在内联汇编内完成整个代码。
function sumPureAsm(uint[] _data) public view returns (uint o_sum) {
    assembly {
       let len := mload(_data)
       let data := add(_data, 0x20)
       for { let end := add(data, mul(len, 0x20)) }
           lt(data, end)
           { data := add(data, 0x20) }
       {
           o_sum := add(o_sum, mload(data))
       }
    }
}

函数

函数可以在任何地方定义,并且在声明它们的语句块中可见。函数内部不能访问在函数之外定义的局部变量。这里没有严格的 return 语句。

如果调用会返回多个值的函数,则必须使用 a,b:= f(x)let a,b:= f(x) 的方式把它们赋值到一个元组。

将输入的值都加1后返回

 function testfunc(uint a1,uint a2) public pure returns (uint r1,uint r2) {
        assembly {         
           r1,r2 := addOne(a1,a2)      
            function addOne(a, b) -> aa,bb {
                aa := add(a,1)
                bb := add(b,1)
            }           
        }
 }
//幂运算函数。
{
    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) }
        }
    }
}

5 内联汇编注意

与 EVM 汇编语言相比,Solidity 能够识别小于 256 位的类型,例如 uint24。为了提高效率,大多数算术运算只将它们视为 256 位数字, 仅在必要时才清除未使用的数据位,即在将它们写入内存或执行比较之前才会这么做。这意味着,如果从内联汇编中访问这样的变量,你必须先手工清除那些未使用的数据位。

Solidity 以一种非常简单的方式管理内存:在 0x40 的位置有一个“空闲内存指针”。如果你打算分配内存,只需从此处开始使用内存,然后相应地更新指针即可。

内存的开头 64 字节可以用来作为临时分配的“暂存空间”。“空闲内存指针”之后的 32 字节位置(即从 0x60 开始的位置)将永远为 0,可以用来初始化空的动态内存数组。

在 Solidity 中,内存数组的元素总是占用 32 个字节的倍数(是的,甚至对于 byte[] 都是这样,只有 bytesstring 不是这样)。 多维内存数组就是指向内存数组的指针。动态数组的长度存储在数组的第一个槽中,其后才是数组元素。

静态内存数组没有长度字段,但很快就会增加,这是为了可以更好地进行静态数组和动态数组之间的转换,所以请不要依赖这点。

猜你喜欢

转载自blog.csdn.net/bondsui/article/details/88537922