跳到主要内容

【译】 Solidity 内联汇编 - Part 2

· 阅读需 11 分钟

原文链接:https://mirror.xyz/0xB38709B8198d147cc9Ff9C133838a044d78B064B/Hh69VJWM5eiFYFINxYWrIYWcRRtPm8tw3VFjpdpx6T8

在上一部分中,我们了解了一些关于 solidity编译器的事实,并使用内联汇编来编写 Box 合约的函数。这一部分将用纯汇编编写 Box 合约。下面是上一部分中使用内联汇编的 Box合约。

pragma solidity ^0.8.7;

contract Box {
uint256 public value;

function retrieve() public view returns(uint256) {
assembly {
// load value at slot 0 of storage
let v := sload(0)

// store into memory at 0
mstore(0x80, v)

// return value at 0 memory address of size 32 bytes
return(0x80, 32)
}
}

function store(uint256 newValue) public {
assembly {
// store value at slot 0 of storage
sstore(0, newValue)

// emit event
mstore(0x80, newValue)
log1(0x80, 0x20, 0xac3e966f295f2d5312f973dc6d42f30a6dc1c1f76ab8ee91cc8ca5dad1fa60fd)
}
}
}

前置知识

你已经通过内联汇编体验了汇编的魅力。接下来将使用之前函数主体类似的操作码来执行操作。注意,当 Box 合约用 Yul 编写时,它将不再是一个 Solidity 合约。solidity编译器只识别带有assembly { }块的内联汇编。所以,如果你在 Remix IDE 上编写,一定要选择 Yul 编译器。

在进入汇编之前,先解释一下合约到底是如何被 EVM 初始化的。我们的 Box 合约没有构造函数,但这并不意味着不会有任何初始设置的执行。合约编译后字节码有两部分:

初始化字节码

这一部分字节码包含设置任何初始状态(合约构造器逻辑)的指令。同时初始化字节码还要在合约部署期间将运行时字节码的复制到EVM。在 EVM 收到运行时字节码后,它将其保存到链上并与一个地址相关联。因此,最终部署在链上的字节码只有运行时字节码。初始化字节码在合约的部署过程中只执行一次,并不存储在链上。

例如,下面的字节码

600a600c600039600a6000f3602a60505260206050f3

前面的 600a600c600039600a6000f3 是初始化字节码,其余是运行时字节码

由于我们将用纯汇编编写,任何初始化操作现在都需要手动执行。Box 合约没有任何构造函数,所以没有初始状态变量将被设置。唯一要做的是复制运行时代码并将其返回给 EVM。剩下的部分将由 EVM 本身自动处理。

运行时字节码

运行时字节码包括合约中的任何东西(方法、事件、常量等),但除了构造函数代码。而且其由合约被交互时的所包含执行逻辑组成。

当合约被调用方法交互时,一个经过编码的 calldata被发送到该合约中。合约提取这个 calldata 的前 4 个字节,也就是函数selector,然后运行一系列的 if/else 语句来决定哪个函数与之匹配并执行

例如,下面的 calldata

0x6057361d000000000000000000000000000000000000000000000000000000000000000a

其中 6057361d 是函数选择器--它将匹配 Boxstore() 函数,并在匹配后执行。

Yul 基本结构

Yul 代码由 "块 "和 "子块 "组成,由 object关键字定义。使用 codedata 节点进行分组。data 节点是一些静态数据。而code节点是对象的可执行代码。在code节点内,你可以用循环、if-else语句、操作码等编写逻辑。

object "ExampleObject" {
code {
// some logic code
}

data "ExampleData" hex"123"

object "ExampleSubObject" {
code {
// some other logic code
}
}
}

查看示例

纯汇编实现 Box 合约

合约由一个带有子对象的单一对象组成,子对象代表要部署的(运行时)代码。code结点是一个对象的可执行代码。例如,我们定义/构造 Box 合同如下。

object "Box" {
code {
// initialization code
}

object "runtime" {
code {
// runtime code within sub-object
}
}
}

先关注初始化部分,然后再转到运行时部分。

初始化代码

如前所述,初始化代码必须返回运行时代码。为此,我们需要确定运行时代码在合约字节码中的偏移量以及它的大小。然后把这个字节码复制到内存中,这样就可以返回了。

Yul语言实际上提供了三个具体的函数来做这件事:

  • datasize(x) 返回运行时部分的大小
  • dataoffset(x) 返回运行时部分的偏移量
  • datacopy(t, f, l)EVMcodecopy相似。从代码的偏移量拷贝一定长度的数据。

最后用用 return 操作码返回

object "Box" {
code {
let runtime_size := datasize("runtime")
let runtime_offset := dataoffset("runtime")
datacopy(0, runtime_offset, runtime_size)
return(0, runtime_size)
}

object "runtime" {
code {
// runtime code within sub-object
}
}
}

运行时代码

使用 Yul,我们可以像以前使用内联汇编那样编写函数。困难的是确定在收到 calldata后执行哪个函数。记得之前讨论过,这是通过提取函数 selector, 也就是 calldata 的前四个字节,并与合约中定义的函数相匹配来完成。我们如何做到这一点呢?让我们来看看!

在解决如何进行函数 selector 的事情之前,首先需要定义 retrievestore 函数。

object "Box" {
code {
// ..
}

object "runtime" {
code {
function retrieve() -> memloc {
let val := sload(0)
memloc := 0x80
mstore(memloc, val)
}

function store(val) {
sstore(0, val)
mstore(0x80, val)
log1(0x80, 0x20,0xac3e966f295f2d5312f973dc6d42f30a6dc1c1f76ab8ee91cc8ca5dad1fa60fd)
}
}
}
}

store()函数与之前的内联汇编基本相同。然而,retrieve()不是预先返回值,而是将值存储在内存中,并返回值所存储的内存位置。原因在下文说明。

接下来处理从 calldata 中提取函数 selector以匹配要执行的函数。

首先从 calldata 中计算出 selector, 除了函数 selector 之外,calldata 还可能包括编码后的调用参数。calldata 使用 calldataload 操作码来提取,其需要一个输入:读取 calldata 的 偏移值,并返回从偏移值开始后固定的 32 个字节长度的数据。

例如,调用 store(10) (10 = 0xa),calldata看起来像下面这样

0x6057361d000000000000000000000000000000000000000000000000000000000000000a

如果调用 calldataload(0) 会返回从位置 0 往后的 32 个字节长度的数据

6057361d00000000000000000000000000000000000000000000000000000000

那如何只提取前 4 个字节呢,可以用 div 操作码进行除法

div(
calldataload(0),
0x100000000000000000000000000000000000000000000000000000000
)

// gives first four bytes of calldataload(0) i.e. 6057361d

为这么这种方法会有效呢。用 10 进制数字来举例,如果 9876543calldata, 用除法运算取前 4 位数字,可以除以什么数呢,答案是 1000

9876543 / 1000 = 9876  // 只考虑整数部分

同样的,为了得到 32 个字节长度(64 个十六进制字符)的前 4 个字节的十六进制,我们用 0x1000000000000000000000000000000000000000000(56 个 0)除以它。

在此基础上,可以写一个辅助函数,从 calldata 中提取函数选择器。

object "Box" {
code { .. }

object "runtime" {
code {
function retrieve() -> memloc { .. }

function store(val) { .. }

function selector() -> s {
s := div(calldataload(0),
0x100000000000000000000000000000000000000000000000000000000)
}
}
}
}

现在剩下唯一的事情就是根据提取到的 selector ,去调用匹配的函数。

当合约被编译成字节码的时候,每个函数的 selector 也都被预先计算并附加到字节码中的。因此我们也需要预先计算 store()retrieve() 的函数 selector

// retrieve function
bytes4(keccak256("retrieve()")); // 0x2e64cec1

// store function
bytes4(keccak256("store(uint256)")); // 0x6057361d

接下来就可以编写 switch 语句了

object "Box" {
code { .. }

object "runtime" {
code {
switch selector()

// 匹配的是 retrieve 函数
case 0x2e64cec1 {
let memloc := retrieve()
return(memloc, 32)
}

// 匹配的是 store 函数
case 0x6057361d {
store(calldataload(4))
}

// revert if no match
default {
revert(0, 0)
}

function retrieve() -> memloc { .. }

function store(val) { .. }

function selector() -> s {
s := div(calldataload(0),
0x100000000000000000000000000000000000000000000000000000000)
}
}
}
}

还记得我们从 retrieve() 函数中返回内存位置而不是值吗?是因为我们可以用 return 操作码来使用它

另一个需要注意的是传递给 store() 的参数,通过 calldataload(4) 读取,也就是跳过前 4 个字节,读取其后的 32 个字节数据。从前面调用 store(10)calldata 例子来看:

0x6057361d000000000000000000000000000000000000000000000000000000000000000a

calldataload(4) 会返回

000000000000000000000000000000000000000000000000000000000000000a

这 16 进制中表示 10

最终,纯汇编编写的 Box 合约就是下面这样的

object "Box" {
code {
let runtime_size := datasize("runtime")
let runtime_offset := dataoffset("runtime")
datacopy(0, runtime_offset, runtime_size)
return(0, runtime_size)
}

object "runtime" {
code {
switch selector()

// retrieve function match
case 0x2e64cec1 {
let memloc := retrieve()
return(memloc, 32)
}

// store function match
case 0x6057361d {
store(calldataload(4))
}

// revert if no match
default {
revert(0, 0)
}

function retrieve() -> memloc {
let val := sload(0)
memloc := 0
mstore(memloc, val)
}

function store(val) {
sstore(0, val)
mstore(0, val)
log1(0x80, 0x20,0xac3e966f295f2d5312f973dc6d42f30a6dc1c1f76ab8ee91cc8ca5dad1fa60fd)
}

function selector() -> s {
s := div(calldataload(0),
0x100000000000000000000000000000000000000000000000000000000)
}
}
}
}

参考资料