主页 > 苹果版imtoken > 智能合约学习123:深入理解以太坊虚拟机

智能合约学习123:深入理解以太坊虚拟机

苹果版imtoken 2023-08-11 05:09:54

单位时间.media

以太坊可以运行智能合约_siteqq.com 以太坊智能合约_以太坊智能合约的应用

全球视野,独到见解

《Solidity提供了很多高级语言抽象,但是这些特性让人很难理解程序运行时会发生什么。我看了Solidity的文档,但是还有几个基本的问题我不明白,str​​ing,bytes32, byte[], bytes 之间有什么区别?”

siteqq.com 以太坊智能合约_以太坊智能合约的应用_以太坊可以运行智能合约

Solidity 提供了很多高级语言的抽象概念,但是这些特性让人很难理解程序运行时会发生什么。 我阅读了 Solidity 文档,但仍然有一些我不理解的基本问题。

我觉得学习像在以太坊虚拟机 (EVM) 上运行的 Solidity 这样的高级语言是一项很好的投资,原因如下:

1. Solidity 不是最后的语言。 更好的 EVM 语言即将到来。 (请?)

2. EVM是一个数据库引擎。 要了解智能合约如何在任何 EVM 语言中工作,有必要了解数据是如何组织、存储和操作的。

3.知道如何成为贡献者。 以太坊工具链仍处于早期阶段,了解 EVM 可以帮助您实现一个很棒的工具,供您自己和他人使用。

4.智力挑战。 EVM 使您有充分的理由在密码学、数据结构和编程语言设计的交叉点之间飞来飞去。

在本系列文章中,我将拆解一个简单的 Solidity 合约,让您了解它是如何在 EVM 字节码(bytecode)中运行的。

希望学习和撰写的文章大纲:

我的最终目标是从整体上理解已编译的 Solidity 合约。 让我们从阅读一些基本的 EVM 字节码开始。

EVM 指令集将是一个有用的参考。

一个简单的合同

我们的第一个合约有一个构造函数和一个状态变量:

以太坊智能合约的应用_以太坊可以运行智能合约_siteqq.com 以太坊智能合约

使用 solc 编译这个合约:

以太坊可以运行智能合约_以太坊智能合约的应用_siteqq.com 以太坊智能合约

以太坊可以运行智能合约_以太坊智能合约的应用_siteqq.com 以太坊智能合约

6060604052……这串数字就是EVM实际运行的字节码。

一步一步来

上面一半的编译和组装是大多数 Solidity 程序中都会存在的样板语句。 我们稍后会回来讨论这些。 现在,让我们看看合约的独特部分,即简单的存储变量分配:

以太坊智能合约的应用_siteqq.com 以太坊智能合约_以太坊可以运行智能合约

表示此分配的字节码是 6001600081905550。让我们将其分解为每行一个命令:

以太坊智能合约的应用_siteqq.com 以太坊智能合约_以太坊可以运行智能合约

EVM 本质上是一个循环,从上到下执行每个命令。 让我们用相应的字节码注释汇编代码(在 tag_2 下缩进)以更好地了解它们之间的关系:

siteqq.com 以太坊智能合约_以太坊智能合约的应用_以太坊可以运行智能合约

请注意,0x1 实际上是汇编代码中 push(0x1) 的简写。 该指令将值 1 压入堆栈。

仅仅盯着它看仍然很难理解发生了什么,但别担心,逐行模拟 EVM 相对简单。

模拟 EVM

EVM 是一个堆栈机器。 指令可以将栈上的值作为参数,并将值作为结果压入栈中。 让我们考虑一下添加操作。

假设栈上有两个值

以太坊智能合约的应用_siteqq.com 以太坊智能合约_以太坊可以运行智能合约

当 EVM 看到 add 时,它会将 2 个项目添加到堆栈顶部,然后将答案压入堆栈,结果是:

以太坊智能合约的应用_siteqq.com 以太坊智能合约_以太坊可以运行智能合约

接下来,我们使用 [] 符号来标识堆栈:

以太坊智能合约的应用_以太坊可以运行智能合约_siteqq.com 以太坊智能合约

以太坊可以运行智能合约_以太坊智能合约的应用_siteqq.com 以太坊智能合约

使用 {} 符号标识合约存储:

以太坊智能合约的应用_以太坊可以运行智能合约_siteqq.com 以太坊智能合约

现在让我们看看实际的字节码。 我们将像 EVM 一样模拟 6001600081905550 字节序列,并打印出每条指令的机器状态:

以太坊智能合约的应用_以太坊可以运行智能合约_siteqq.com 以太坊智能合约

最后栈为空,内存中只有一项数据。

值得注意的是,Solidity 已经决定将状态变量 uint256a 存储在 0x0 处。 其他语言可能会选择在其他任何地方存储状态变量。

6001600081905550字节序列本质上用EVM的操作伪代码表示:

以太坊可以运行智能合约_以太坊智能合约的应用_siteqq.com 以太坊智能合约

仔细观察会发现dup2、swap1、pop是多余的,汇编代码可以更简单

以太坊智能合约的应用_siteqq.com 以太坊智能合约_以太坊可以运行智能合约

你可以模拟上面3条指令,你会发现它们的机器状态结果是一样的:

以太坊可以运行智能合约_以太坊智能合约的应用_siteqq.com 以太坊智能合约

两个存储变量

让我们添加一个相同类型的附加存储变量:

siteqq.com 以太坊智能合约_以太坊智能合约的应用_以太坊可以运行智能合约

编译后主要看tag_2:

siteqq.com 以太坊智能合约_以太坊智能合约的应用_以太坊可以运行智能合约

汇编伪代码:

siteqq.com 以太坊智能合约_以太坊可以运行智能合约_以太坊智能合约的应用

以太坊可以运行智能合约_siteqq.com 以太坊智能合约_以太坊智能合约的应用

我们可以看到两个存储变量的存储位置是按顺序排列的,a在0x0,b在0x1。

存储包

每个内存插槽可存储 32 个字节。 如果一个变量只需要 16 个字节,那么使用完整的 32 个字节是一种浪费。 Solidity 提供了一种高效存储的优化方案:如果可能的话,将两个较小的数据类型打包并存储在一个存储槽中。

我们将 a 和 b 修改为 16 字节的变量:

以太坊智能合约的应用_siteqq.com 以太坊智能合约_以太坊可以运行智能合约

编译这个合约:

以太坊可以运行智能合约_以太坊智能合约的应用_siteqq.com 以太坊智能合约

生成的汇编代码现在有点复杂:

以太坊智能合约的应用_以太坊可以运行智能合约_siteqq.com 以太坊智能合约

上面的汇编代码将这两个变量打包到一个存储位置(0x0),如下所示:

siteqq.com 以太坊智能合约_以太坊可以运行智能合约_以太坊智能合约的应用

打包的原因是因为到目前为止最昂贵的操作是存储使用:

sstore 命令第一次写入新位置需要 20000 gas

随后通过 sstore 命令写入现有位置会花费 5000 gas

sload 指令的成本是 500 gas

大多数指令花费 3~10 gas

通过使用相同的存储位置,Solidity 为存储第二个变量支付 5000 gas 而不是 20000 gas,节省了 15000 gas。

更多优化

应该可以把两个128位的数打包成一个数放到内存中,然后用一条‘sstore’指令进行存储操作,而不是用两条单独的sstore指令来存储变量a和b,需要额外保存5000 气。

以太坊智能合约的应用_siteqq.com 以太坊智能合约_以太坊可以运行智能合约

您可以通过添加优化选项使 Solidity 实现上述优化:

以太坊智能合约的应用_siteqq.com 以太坊智能合约_以太坊可以运行智能合约

生成的汇编代码只有一条 sload 指令和一条 sstore 指令:

siteqq.com 以太坊智能合约_以太坊可以运行智能合约_以太坊智能合约的应用

字节码是:

以太坊智能合约的应用_siteqq.com 以太坊智能合约_以太坊可以运行智能合约

将字节码解析成一行一行的指令:

以太坊智能合约的应用_以太坊可以运行智能合约_siteqq.com 以太坊智能合约

上面的汇编代码中用到了4个魔法值:

0x1(16字节),使用低16字节

siteqq.com 以太坊智能合约_以太坊可以运行智能合约_以太坊智能合约的应用

0x2(16字节),使用高16字节

以太坊可以运行智能合约_以太坊智能合约的应用_siteqq.com 以太坊智能合约

不是(子(exp(0x2,0x80),0x1))

以太坊智能合约的应用_siteqq.com 以太坊智能合约_以太坊可以运行智能合约

sub(exp(0x2, 0x80), 0x1)

siteqq.com 以太坊智能合约_以太坊可以运行智能合约_以太坊智能合约的应用

代码对这些值进行了一些位转换,以达到想要的结果:

以太坊智能合约的应用_以太坊可以运行智能合约_siteqq.com 以太坊智能合约

siteqq.com 以太坊智能合约_以太坊可以运行智能合约_以太坊智能合约的应用

最后,32 字节的值存储在 0x0。

煤气用量

60008054700200000000000000000000000000000006001608060020a03199091166001176001608060020a0316179055

请注意,字节码中嵌入了 0x200000000000000000000000000000000。 但是编译器也可以选择使用 exp(0x2, 0x81) 指令来计算值,这会导致字节码序列更短。

但事实证明 0x200000000000000000000000000000000 比 exp(0x2, 0x81) 便宜。 让我们看看与gas成本相关的信息:

交易的每个零字节数据或代码花费 4 gas

交易中每个非零字节的数据或代码花费 68 gas

计算接下来两个表示的 gas 成本:

0x200000000000000000000000000000000字节码包含很多0,比较便宜。

(1 * 68) + (32 * 4) = 196

608160020a 字节码更短,但没有 0。

5 * 68 = 340

更长的字节码序列有很多 0,所以实际上更便宜!

总结

EVM 的编译器实际上并没有针对字节码大小、速度或内存效率进行优化。 相反,它优化了 gas 的使用,间接鼓励了计算的排序以太坊可以运行智能合约,使以太坊区块链更加高效。

我们还看到了 EVM 的一些特性:

EVM是256位的机器,处理32字节的数据是最自然的

持久存储非常昂贵

Solidity编译器会做出相应的优化选择以太坊可以运行智能合约,以减少gas的使用

Gas 成本的设置有点随意,将来可能会改变。 当代价发生变化时,编译器也会做出不同的优化选择。