主页 > 苹果版imtoken > 智能合约学习123:深入理解以太坊虚拟机
智能合约学习123:深入理解以太坊虚拟机
单位时间.media
全球视野,独到见解
《Solidity提供了很多高级语言抽象,但是这些特性让人很难理解程序运行时会发生什么。我看了Solidity的文档,但是还有几个基本的问题我不明白,string,bytes32, byte[], bytes 之间有什么区别?”
Solidity 提供了很多高级语言的抽象概念,但是这些特性让人很难理解程序运行时会发生什么。 我阅读了 Solidity 文档,但仍然有一些我不理解的基本问题。
我觉得学习像在以太坊虚拟机 (EVM) 上运行的 Solidity 这样的高级语言是一项很好的投资,原因如下:
1. Solidity 不是最后的语言。 更好的 EVM 语言即将到来。 (请?)
2. EVM是一个数据库引擎。 要了解智能合约如何在任何 EVM 语言中工作,有必要了解数据是如何组织、存储和操作的。
3.知道如何成为贡献者。 以太坊工具链仍处于早期阶段,了解 EVM 可以帮助您实现一个很棒的工具,供您自己和他人使用。
4.智力挑战。 EVM 使您有充分的理由在密码学、数据结构和编程语言设计的交叉点之间飞来飞去。
在本系列文章中,我将拆解一个简单的 Solidity 合约,让您了解它是如何在 EVM 字节码(bytecode)中运行的。
希望学习和撰写的文章大纲:
我的最终目标是从整体上理解已编译的 Solidity 合约。 让我们从阅读一些基本的 EVM 字节码开始。
EVM 指令集将是一个有用的参考。
一个简单的合同
我们的第一个合约有一个构造函数和一个状态变量:
使用 solc 编译这个合约:
6060604052……这串数字就是EVM实际运行的字节码。
一步一步来
上面一半的编译和组装是大多数 Solidity 程序中都会存在的样板语句。 我们稍后会回来讨论这些。 现在,让我们看看合约的独特部分,即简单的存储变量分配:
表示此分配的字节码是 6001600081905550。让我们将其分解为每行一个命令:
EVM 本质上是一个循环,从上到下执行每个命令。 让我们用相应的字节码注释汇编代码(在 tag_2 下缩进)以更好地了解它们之间的关系:
请注意,0x1 实际上是汇编代码中 push(0x1) 的简写。 该指令将值 1 压入堆栈。
仅仅盯着它看仍然很难理解发生了什么,但别担心,逐行模拟 EVM 相对简单。
模拟 EVM
EVM 是一个堆栈机器。 指令可以将栈上的值作为参数,并将值作为结果压入栈中。 让我们考虑一下添加操作。
假设栈上有两个值
当 EVM 看到 add 时,它会将 2 个项目添加到堆栈顶部,然后将答案压入堆栈,结果是:
接下来,我们使用 [] 符号来标识堆栈:
使用 {} 符号标识合约存储:
现在让我们看看实际的字节码。 我们将像 EVM 一样模拟 6001600081905550 字节序列,并打印出每条指令的机器状态:
最后栈为空,内存中只有一项数据。
值得注意的是,Solidity 已经决定将状态变量 uint256a 存储在 0x0 处。 其他语言可能会选择在其他任何地方存储状态变量。
6001600081905550字节序列本质上用EVM的操作伪代码表示:
仔细观察会发现dup2、swap1、pop是多余的,汇编代码可以更简单
你可以模拟上面3条指令,你会发现它们的机器状态结果是一样的:
两个存储变量
让我们添加一个相同类型的附加存储变量:
编译后主要看tag_2:
汇编伪代码:
我们可以看到两个存储变量的存储位置是按顺序排列的,a在0x0,b在0x1。
存储包
每个内存插槽可存储 32 个字节。 如果一个变量只需要 16 个字节,那么使用完整的 32 个字节是一种浪费。 Solidity 提供了一种高效存储的优化方案:如果可能的话,将两个较小的数据类型打包并存储在一个存储槽中。
我们将 a 和 b 修改为 16 字节的变量:
编译这个合约:
生成的汇编代码现在有点复杂:
上面的汇编代码将这两个变量打包到一个存储位置(0x0),如下所示:
打包的原因是因为到目前为止最昂贵的操作是存储使用:
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 气。
您可以通过添加优化选项使 Solidity 实现上述优化:
生成的汇编代码只有一条 sload 指令和一条 sstore 指令:
字节码是:
将字节码解析成一行一行的指令:
上面的汇编代码中用到了4个魔法值:
0x1(16字节),使用低16字节
0x2(16字节),使用高16字节
不是(子(exp(0x2,0x80),0x1))
sub(exp(0x2, 0x80), 0x1)
代码对这些值进行了一些位转换,以达到想要的结果:
最后,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 成本的设置有点随意,将来可能会改变。 当代价发生变化时,编译器也会做出不同的优化选择。