深入学习以太坊虚拟机(EVM)#2:部署空合约学习字节码

  • jpmorais
  • 发布于 2023-04-26 22:39
  • 阅读 56

本文深入介绍了以太坊虚拟机(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

80PUSH1 操作码,表示下一个字节应该被推入栈中。因此,60 80 的意思是:将字节 0x80 放入栈中。

同样,60 40 将字节 0x40 放入栈中。我们可以在下面的插图中看到栈的配置。

之后,字节 52 被执行。它是 MSTORE 操作码,用于将数据放入内存。我们将在接下来的课程中更多地讨论内存;在此之前,可以将内存理解为我们可以存储数据的地方。此操作码不需要操作数,而是使用栈上的 2 个值。栈上的第一个值指示应将数据存储在哪里,第二个值指示存储什么。

栈上的第一个值是 0x40,第二个值是 0x80,因此该指令将是:在内存位置 0x40 存储 0x80。之后,这两个信息将从栈中移除,栈再次变为空。

我们在这里做的是注册自由内存指针。它指示哪个内存位置可以被使用。

检查交易中是否发送了 Ether

下一个操作码是编号 34CALLVALUE。它将交易发送的值放入栈中。这个值将是动态的,因为每个交易都有自己的调用值。假设交易没有发送任何值,因此调用值将为零。

下一个操作码是编号 80DUP1,它会复制栈的第一个值。栈上只有一个值,即 0x00(调用值),现在它将有两个相同的值。我们可以在下面的插图中看到。

下一个操作码编号为 15ISZERO。它将栈上的第一个值与 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 推入栈中。然后字节 80DUP1,它翻倍栈顶。之后,60 1d 将值 1d 放入栈中。最后,60 00 将值 0 放入栈中。我们的栈现在包含 4 个项目:00 1d 3f 3f。栈流可以在下面的插图中看到。

下一个指令是字节 39,它是 CODECOPY 操作码。它将把部分字节码复制到内存中。该操作码需要 3 个值,这将是栈上的前三个值。第一个值是在内存中要复制的字节码位置。第二个值是要复制的代码起始行。最后一个值是要复制的字节数。使用后,这 3 个项目将从栈中移除。

然后,指令将是将字节码的一部分从行 1d 开始复制到内存地址 0,其大小为 3f 字节。程序正在准备将部署的字节码写入以太坊。它首先需要将已部署的字节码放入内存中以完成此操作。

如果你想知道自由内存指针发生了什么,它被忽略了。编译器不再需要它,因为它已处于程序执行的末尾。

将部署的字节码写入区块链

栈现在只剩下 1 个项目,即 3f,这是内存中复制代码的大小。下一个指令是 60 00,即将 00 放入栈中。无论如何,最后一个指令 f3RETURN 操作码,它结束了这个故事。让我们理解它的作用。

RETURN 操作码成功返回交易,返回一定数量的数据。要返回的数据在内存中,并由栈上的 2 个值指示。第一个值指示内存中返回的内容的位置,第二个值指示返回的字节大小。我们的栈是 00 3f。因此,将返回的是位于内存地址 00 且大小为 3f 字节的内容。正是我们在内存中写入的已部署字节码!

这就是为什么它被称为已部署字节码:这就是将存储在以太坊上的字节码的一部分。这是合约账户创建交易中发生的事情:程序所返回的内容将被记录为合约代码。

总结

我们已经走过了一条漫长的道路。我们分析了字节码的整个执行过程,这是在创建新合约账户时执行的。我们看到每条指令位于特定行,并且可以使用 JUMPI 操作码根据条件跳转到某一行。我们还看到,在合约创建交易中,交易返回的内容将被记录为合约代码。

感谢阅读!

欢迎对本文发表评论和建议。

任何贡献都欢迎。 www.buymeacoffee.com/jpmorais

  • 原文链接: medium.com/coinmonks/lea...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
jpmorais
jpmorais
江湖只有他的大名,没有他的介绍。