本文深入介绍了以太坊虚拟机(EVM)如何逐步执行合约的字节码,尤其是简单合约的部署过程。文章通过分步骤解析字节码和相关操作码,帮助读者理解合约执行的逻辑和流程。
在本课中,我们将生成合约的字节码,并分析以太坊虚拟机执行它的过程。通过这个过程,我们将理解程序的执行流以及一些操作码的使用。在随后的课程中,我们将更详细地查看 EVM 定义的操作码,以及操作码、Solidity 和 Yul 之间的关系。
让我们开始编写尽可能简单的合约:一个空的合约。
pragma solidity ^0.8.18;
contract Empty {}
尽管合约体是空的,但仍然会生成字节码。这个合约可能看起来什么都不做,但它至少做到了一点:它不允许任何人向它发送以太。如果有人试图通过发送以太来执行对该合约的交易,这笔交易将会回滚。交易将回滚是因为将执行 REVERT 操作码。
该合约是用 0.8.18
版本编译的,并将优化启用到 200
。结果如下所示。
6080604052348015600f57600080fd5b50603f80601d6000396000f3fe6080604052600080fdfea26469706673582212203e36aa9cdce1afe76ba43f8db331ebffbf999f00a4b256986e35ed233425b4fc64736f6c63430008120033
要在以太坊上部署合约,我们必须发送一个不指定接收地址的交易。这会触发以太坊虚拟机执行有效载荷,正是该字节码。在本课中,我们将确切研究这一字节码的执行。
EVM 拥有一个寄存器,即程序计数器。它持有应执行的操作码的信息。字节码是一个编号结构,每个字节都有一个编号。字节码的第一个字节编号为 0x00,第二个字节编号为 0x01,依此类推,直到最后一个字节。我们的示例字节码长度为 92 字节,因此它有 92 “行”。因此,其第一个字节在 “行” 0x00,而最后一个字节在行 0x5b,也就是十六进制的 91。
查看此内容的一个很好的地方是 https://www.evm.codes/playground。你可以粘贴字节码,它将显示所有操作码及其对应的行,带有操作符和操作数。
下图显示了字节码及其相应操作码和行号的结果。
每个操作码位于特定的行上。
让我们分析字节码的前几个字节: 60
80
60 40 52
。
80
是 PUSH1 操作码,表示下一个字节应该被推入栈中。因此,60 80
的意思是:将字节 0x80 放入栈中。
同样,60
40
将字节 0x40 放入栈中。我们可以在下面的插图中看到栈的配置。
之后,字节 52
被执行。它是 MSTORE 操作码,用于将数据放入内存。我们将在接下来的课程中更多地讨论内存;在此之前,可以将内存理解为我们可以存储数据的地方。此操作码不需要操作数,而是使用栈上的 2 个值。栈上的第一个值指示应将数据存储在哪里,第二个值指示存储什么。
栈上的第一个值是 0x40,第二个值是 0x80,因此该指令将是:在内存位置 0x40 存储 0x80。之后,这两个信息将从栈中移除,栈再次变为空。
我们在这里做的是注册自由内存指针。它指示哪个内存位置可以被使用。
下一个操作码是编号 34
,CALLVALUE。它将交易发送的值放入栈中。这个值将是动态的,因为每个交易都有自己的调用值。假设交易没有发送任何值,因此调用值将为零。
下一个操作码是编号 80
,DUP1,它会复制栈的第一个值。栈上只有一个值,即 0x00(调用值),现在它将有两个相同的值。我们可以在下面的插图中看到。
下一个操作码编号为 15
,ISZERO。它将栈上的第一个值与 0 进行比较。如果为零,则移除该值并放入 1。如果不为零,则移除该值并放入 0。由于栈上的第一个值为 0,因此将被替换为 1。
在这一轮中,我们检查了是否向合约发送了任何值。进行此检查是必要的,因为该合约没有可支付的构造函数,因此在部署时无法接收 Ether。现在我们必须进入一个条件。如果发送了 Ether(0x00 将在栈顶),则交易应回滚。如果没有发送 Ether(栈顶为 0x01),则应继续部署。
下一个指令是 60
0f
,它将值 0f
推入栈中。现在栈中有 3 个项目,从顶部到底部:0f
01
00
。是时候运行条件了。汇编语言不使用 if/else 条件,而是通过跳转。下一个字节是 57
,表示 JUMPI 操作码。
JUMPI 需要 2 个栈值。第二个值指示程序计数器是否应跳转到特定值,或继续到下一行。如果第二个值为 0,则转到下一行。如果为 1,则跳转到栈顶所指示的值。
由于我们的第二个栈值为 01
,程序计数器将更改为 0f
。还记得每个字节都在一行吗?现在要执行的行将是编号 0f
。
由于 0f
在十六进制中是 15,因此下一个要执行的操作码将是字节码的第十六个字节。在这种情况下,它是字节 5b
,代表 JUMPDEST 操作码。这个操作码没有任何操作;但是,每当有 JUMPI (或 JUMP)时,目标必须是 JUMPDEST。如果程序跳转到一个 JUMPDEST 的操作码,交易将被回滚。
下一个字节是 50
,代表 POP 操作码。它从栈顶移除一个项目。我们的栈现在为空。下一个指令 60 3f
将值 3f
推入栈中。然后字节 80
为 DUP1,它翻倍栈顶。之后,60
1d
将值 1d
放入栈中。最后,60 00
将值 0
放入栈中。我们的栈现在包含 4 个项目:00 1d 3f 3f
。栈流可以在下面的插图中看到。
下一个指令是字节 39
,它是 CODECOPY 操作码。它将把部分字节码复制到内存中。该操作码需要 3 个值,这将是栈上的前三个值。第一个值是在内存中要复制的字节码位置。第二个值是要复制的代码起始行。最后一个值是要复制的字节数。使用后,这 3 个项目将从栈中移除。
然后,指令将是将字节码的一部分从行 1d
开始复制到内存地址 0
,其大小为 3f
字节。程序正在准备将部署的字节码写入以太坊。它首先需要将已部署的字节码放入内存中以完成此操作。
如果你想知道自由内存指针发生了什么,它被忽略了。编译器不再需要它,因为它已处于程序执行的末尾。
栈现在只剩下 1 个项目,即 3f
,这是内存中复制代码的大小。下一个指令是 60 00
,即将 00
放入栈中。无论如何,最后一个指令 f3
是 RETURN 操作码,它结束了这个故事。让我们理解它的作用。
RETURN 操作码成功返回交易,返回一定数量的数据。要返回的数据在内存中,并由栈上的 2 个值指示。第一个值指示内存中返回的内容的位置,第二个值指示返回的字节大小。我们的栈是 00
3f
。因此,将返回的是位于内存地址 00
且大小为 3f
字节的内容。正是我们在内存中写入的已部署字节码!
这就是为什么它被称为已部署字节码:这就是将存储在以太坊上的字节码的一部分。这是合约账户创建交易中发生的事情:程序所返回的内容将被记录为合约代码。
我们已经走过了一条漫长的道路。我们分析了字节码的整个执行过程,这是在创建新合约账户时执行的。我们看到每条指令位于特定行,并且可以使用 JUMPI 操作码根据条件跳转到某一行。我们还看到,在合约创建交易中,交易返回的内容将被记录为合约代码。
感谢阅读!
欢迎对本文发表评论和建议。
任何贡献都欢迎。 www.buymeacoffee.com/jpmorais。
- 原文链接: medium.com/coinmonks/lea...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!