侧边栏壁纸
博主头像
秋码记录

一个游离于山间之上的Java爱好者 | A Java lover living in the mountains

  • 累计撰写 29 篇文章
  • 累计创建 40 个标签
  • 累计创建 185 个分类

玩以太坊链上项目的必备技能(内联汇编 [inline assembly]-Solidity之旅十八)

在 Solidity 中使用 Assembly 的主要好处之一是节省 gas。 让我们尝试通过创建一个将 2 个值 x 和 y 相加并返回结果的函数来比较 Solidity 和 Assembly 之间的 gas 成本。

概要

大抵是讲到汇编,身为编程开发者的我们脑瓜子早就嗡嗡作响了。看那晦涩难懂的低级汇编代码,敢断言,那一行不是我写的,其他行也不是我写的。

自从C语言问世,而后类C语言犹如雨后春笋般地搅动着IT界,而这些语言有别于汇编语言那样。它们就是更贴切自然语言的高级编程语言,可这些高级编程语言最终还是要编译成机器语言(汇编语言)。

EVM(Ethereum Virtual Machine)是一种栈(Stack)结构,我们知道是一种先进后出(LIFO)的数据结构。

那为什么要用汇编来编写呢?

借您所问,既然 Solidity 可以编写出优秀的智能合约,那为什么还要使用低级地汇编语言呢?

在回答这个问题之前,我们来看看每个新的编程语言诞生都是为了解决当前编程语言无法解决,或者说使用当前编程语言解决起来比较麻烦,那么,新的编程语言就在这样的环境下应运而生,当然咯,并不是所有新编程语言都是为了解决当前编程语言不能解决的问题,才被开发出来,而是……(此处不便说出缘由,毕竟它也不是本文的重点)。

细粒度控制

Assembly允许您执行一些仅仅靠 Solidity 无法实现的逻辑,比如,指向特定的内存插槽(Memory Slot)

当我们在编写库(library)时,细粒度控制特别有用,因为它们会被重复使用。

节省 gas

在 Solidity 中使用 Assembly 的主要好处之一是节省 gas。 让我们尝试通过创建一个将 2 个值 x 和 y 相加并返回结果的函数来比较 Solidity 和 Assembly 之间的 gas 成本。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract AssemblyExample {

    function addAssembly(uint x, uint y) public pure returns (uint) {
        assembly {
            let result := add(x, y)
            mstore(0x0, result)
            return(0x0, 32)
        }
    }

    function addSolidity(uint x, uint y) public pure returns (uint) {
        return x + y;
    }

}

img

Solidity 中两种方式实现 Assembly

  • 1、 内联汇编:也可以在 Solidity 代码中使用。
  • 2、 独立程序集:无需编写 Solidity 代码即可使用。

怎么使用 Assembly?

正如上面的例子那样,汇编代码运行在assembly { ...}汇编块中的。

汇编代码是使用YUL语言来编写的!

内联汇编块不共享命名空间,即不能在一个汇编块调用另一个汇编块中定义的变量。

assembly {
   // some assembly code here
}

img

以下是一个简单的示例,函数接受两个参数,并将它们的和作为返回值,看看使用 Assembly是怎么实现的?了解它们在 EVM的工作方式。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract AssemblyExample {

   function addition(uint x, uint y) public pure returns (uint) {

    assembly {

        //声明一个 result 变量,并将 x,y之和赋值给它
        let result := add(x, y)   // x + y

        //使用 mstore 操作码将 result存在 memory 中,地址是 0x0
        mstore(0x0, result)       // store result in memory

        //返回 32 字节的 memory 地址
        return(0x0, 32)          

    }
}

}

img

数据存储

让我们来看看一个简单的例子。我们将数据存放在storage(存储)中,然后再去调用它。

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract StorageDataExample {

    function setData(uint256 newValue) public {
        assembly {
            sstore(0, newValue)
        }
    }

    function getData() public view returns(uint256) {
        assembly {
            let v := sload(0)
            mstore(0x80, v)
            return(0x80, 32)
        }
    }
}

img

setData函数使用了sstore操作码将变量newValue写入storage(存储)中。

getData函数先是用了sload操作码来加载storage(存储)中的数据,它并不能从storage中直接返回。所以才需要mstore操作码将其写入memory(内存)中,最后我们返回引用memory(内存)中存放数据的地址和 32 字节长度的数据。

https://docs.soliditylang.org/en/latest/yul.html

指令 解释
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 或 如果 y == 0,则为 0
sdiv(x, y) F x / y,对于有符号的二进制补数,如果 y == 0,则为 0
mod(x, y) F x % y, 如果 y == 0,则为 0
smod(x, y) F x % y, 对于有符号的二进制补数, 如果 y == 0,则为 0
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,采用任意精度算术,如果m == 0则为0
mulmod(x, y, m) F (x * y) % m,采用任意精度算术,如果m == 0则为0
signextend(i, x) F 从第 (i*8+7) 位开始进行符号扩展,从最低符号位开始计算
keccak256(p, n) F keccak(mem[p…(p+n)))
pc() F 代码中的当前位置
pop(x) - F 丢弃值 x
mload(p) F mem[p…(p+32))
mstore(p, v) - F mem[p…(p+32)) := v
mstore8(p, v) - F mem[p] := v & 0xff ((只修改了一个字节))
sload(p) F storage[p]
sstore(p, v) - F storage[p] := v
msize() F 内存的大小,即最大的访问内存索引
gas() F 仍可以执行的气体值
address() F 当前合约/执行环境的地址
balance(a) F 地址为A的余额,以wei为单位
selfbalance() I 相当于balance(address()),但更便宜
caller() F 消息调用者(不包括 delegatecall 调用)。
callvalue() F 与当前调用一起发送的wei的数量
calldataload(p) F 从位置p开始的调用数据(32字节)
calldatasize() F 调用数据的大小,以字节为单位
calldatacopy(t, f, s) - F 从位置f的calldata复制s字节到位置t的内存中
codesize() F 当前合约/执行环境的代码大小
codecopy(t, f, s) - F 从位置f的code中复制s字节到位置t的内存中
extcodesize(a) F 地址为a的代码的大小
extcodecopy(a, t, f, s) - F 像codecopy(t, f, s)一样,但在地址a处取代码
returndatasize() B 最后返回数据的大小
returndatacopy(t, f, s) - B 从位置f的returndata复制s字节到位置t的内存中
extcodehash(a) C 地址a的代码哈希值
create(v, p, n) F 用代码mem[p…(p+n))创建新的合约,发送v数量的wei并返回新地址; 错误时返回0
create2(v, p, n, s) C 在keccak256(0xff . this . s . keccak256(mem[p…(p+n)))地址处 创建代码为mem[p…(p+n)]的新合约 并发送v 数量个wei和返回新地址, 其中 0xff 是一个1字节的值, this 是当前合约的地址, 是一个20字节的值, s 是一个256位的大端的值; 错误时返回0
call(g, a, v, in, insize, out, outsize) F 调用地址 a 上的合约,以 mem[in..(in+insize)) 作为输入 一并发送 g 数量的 gas 和 v 数量的 wei, 以 mem[out..(out+outsize)) 作为输出空间。 若错误,返回 0 (比如,gas 用光) 若成功,返回 1
callcode(g, a, v, in, insize, out, outsize) F 相当于 call 但仅仅使用地址 a 上的代码, 执行时留在当前合约的上下文当中
delegatecall(g, a, in, insize, out, outsize) H 相当于 callcode, 但同时保留 callercallvalue
staticcall(g, a, in, insize, out, outsize) B 相当于 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 产生日志
chainid() I 执行链的ID(EIP-1344)
basefee() L 当前区块的基本费用(EIP-3198和EIP-1559)
origin() F 交易发送者
gasprice() F 交易的气体价格n
blockhash(b) F 区块编号b的哈希值—只针对最近的256个区块,不包括当前区块。
coinbase() F 目前的挖矿的受益者
timestamp() F 自 epoch 开始的,当前块的时间戳,以秒为单位
number() F 当前区块号
difficulty() F 当前区块的难度
gaslimit() F 当前区块的区块 gas 限制