作者:Gimer Cervera,以太坊智能合约开发者 翻译:善欧巴,金色财经
本文介绍了一系列文章,深入探讨以太坊虚拟机 (EVM)和Solidity Assembly,以实现智能合约优化和安全性。
以太坊虚拟机(EVM)是以太坊网络的核心组件。EVM 是一款软件,允许部署和执行用高级语言(例如 Solidity)编写的智能合约。编写合约后,将其编译为字节码并部署到EVM。EVM 运行在以太坊网络的每个节点上。
Solidity Assembly 是一种低级编程语言,允许开发人员在更接近 EVM 本身的级别编写代码。它提供了对智能合约执行的更精细的控制,允许仅通过更高级别的 Solidity 代码无法实现的优化和定制。
Solidity 中用于内联汇编的语言称为Yul。该编程语言充当编译为 EVM 字节码的中介。它被设计为一种低级语言,使开发人员能够更细粒度地控制智能合约的执行。它可以在独立模式下使用,也可以在 Solidity 中进行内联汇编。Yul 被设计为一种基于低级堆栈的语言,使开发人员能够编写更优化、更高效的代码。在解释 Solidity 组装之前,我们需要了解 EVM 的组件如何工作。
EVM 是一个准图灵完备的状态机。在这种情况下,术语“准”意味着流程的执行仅限于有限数量的计算步骤,具体取决于任何给定智能合约执行可用的 Gas 量。这就是以太坊处理停止问题和执行可能(恶意或意外)永远运行的情况的方式。这样就避免了以太坊平台全面瘫痪的情况。
Gas是一个衡量在以太坊中完成交易所需的计算量的概念。交易成本以以太币支付,并与 Gas 和 Gas 价格相关。我们在此过程中的目标是学习如何在不影响安全性的情况下最大限度地减少消耗的Gas总量。
内联汇编是一种在较低级别访问 EVM 的方法。它绕过了 Solidity 的几个重要的安全功能和检查。正确使用内联汇编可以显着降低执行成本。但是,您应该仅将其用于需要它的任务,并且仅当您知道自己在做什么时。使用内联汇编优化代码可能会给您的代码带来新的安全问题。要掌握内联汇编,我们需要了解 EVM 及其组件的工作原理。
在EVM中,每次第一次访问任何存储变量时都必须付费,这称为“冷”访问,需要花费2100个gas。第二次或连续一次被称为“热”访问,需要花费 100 Gas。
以下代码是我们如何使用 Yul 优化代码的示例。函数SetData1以传统方式使用 Solidity为全局变量值设置新值。我们第一次分配这个新值时需要花费 22514 个gas。第二个花费要少得多,即 5414 Gas。
函数setData2实现内联汇编。内联汇编块由 assembly { … } 标记,其中大括号内的代码是 Yul 语言的代码。此时无需了解源代码,只需记住该软件正在较低级别访问存储空间即可。因此,执行成本会更低。
在我们的示例中,第一次修改该值将花费 22484 个 Gas。连续几次,成本为 5384 Gas。差异可能看起来并不显着,但是我们应该考虑到这段代码可能会执行数千次。
为什么存储这么贵?请记住,我们处于一个去中心化的世界,数据不仅仅存储在一个地方,而是存储在数以万计的节点上。如果未来的交易需要访问或更改它,它还必须可供网络中的每个节点轻松使用。该数据的总体成本等于其消耗的存储空间和在整个网络上生成该数据的计算量的总和。
EVM 是一种基于堆栈的机器,它在称为堆栈的数据结构上运行,该结构保存值并执行操作。EVM 有自己的一组指令(称为操作码),用于执行读取和写入存储、调用其他合约以及执行数学运算等任务。堆栈按照后进先出 (LIFO)方式运行,请参见图 1,这意味着最近插入的项存储在堆栈的顶部,并且是第一个要删除的项。
当执行智能合约时,EVM 创建一个包含各种数据结构和状态变量的执行上下文。执行完成后,执行上下文将被丢弃,为下一个合约做好准备。在执行期间,EVM 会维护一个临时内存,该内存在事务之间不会持续存在。EVM 执行深度为 1024 项的堆栈机。每个项目都是一个 256 位字,选择此大小是为了便于使用 256 位哈希和椭圆曲线加密。
EVM 具有以下组件,见图 2:
堆栈:EVM 的堆栈是一种按后输入先输出 (LIFO) 方式运行的数据结构,用于在智能合约执行期间存储临时值。
存储:永久存储,是以太坊状态的一部分,仅在第一次初始化为零。
内存:易失性、动态大小的字节数组,用于存储合约执行期间的中间数据。每次创建新的执行上下文时,内存都会初始化为零。
Calldata:这也是一个易失性数据存储区域,类似于内存。然而它存储不可变的数据。它旨在保存作为智能合约交易的一部分发送的数据。
程序计数器:程序计数器 (PC) 指向 EVM 要执行的下一条指令。PC通常在一条指令执行后增加一个字节。
虚拟ROM:智能合约作为字节码存储在该区域中。虚拟 ROM 是只读的。
在该架构中,程序的指令和数据保存在内存中,程序的执行由指向堆栈顶部的堆栈指针控制。堆栈指针跟踪下一个值或指令将在堆栈上保存或检索的位置。当程序运行时,它将值添加到堆栈中并对已经存在的值执行操作。当代码想要将两个数字相加时,它将数字压入堆栈,然后对顶部的两个值执行加法操作。然后结果返回到堆栈。
基于堆栈的架构最重要的特征之一是它允许高度简单且高效的操作执行。由于堆栈是一种 LIFO 数据结构,因此可以轻松快速地处理数据和指令。
EVM 有自己的一组指令,称为操作码。操作码用于执行读取和写入存储、调用其他合约以及执行数学运算等任务。EVM 指令集提供您可能期望的大部分操作,包括:
堆栈操作:POP、PUSH、DUP、SWAP
算术/比较/按位:ADD、SUB、GT、LT、AND、OR
环境:CALLER、CALLVALUE、NUMBER
内存操作:MLOAD、MSTORE、MSTORE8、MSIZE
存储操作:SLOAD、SSTORE
程序计数器相关操作码:JUMP、JUMPI、PC、JUMPDEST
停止操作码:STOP、RETURN、REVERT、INVALID、SELFDESTRUCT
EVM 存储是非易失性空间,保存 256 位 –> 256 位的键值对。合约中的存储槽位总数为 2²⁵⁶,这是一个非常庞大的槽位数量。区块链上的每个智能合约都有自己的存储空间。
在函数调用期间,存储用于在函数调用之间需要记住的数据。它用于存储即使在智能合约执行结束后也需要可用的变量和数据结构。
访问存储的操作码是:SLOAD 和 SSTORE
该帐户的存储是永久数据存储,仅由智能合约使用。外部拥有的帐户 (EOA) 将始终没有代码且存储空间为空。
内存是架构中的易失性内存,其数据在区块链中不持久。内存是一种随机访问数据结构,在智能合约执行期间存储临时数据。
内存分为四部分:2 个槽用于暂存空间,1 个槽用于空闲内存指针,0 槽和 1 个槽指向可用的空闲内存。前 64 个字节的空间将由散列方法使用,散列方法在最终返回最终输出之前需要临时空间来存储中间输出。
空闲内存指针只是指向空闲内存开始位置的指针。它确保智能合约跟踪哪些内存位置已被写入以及哪些仍然可用。这可以防止合约覆盖已分配给另一个变量的某些内存。图 6 显示了内存是如何划分的:
内存用于存储不需要保存在存储器中的变量和数据结构。智能合约执行期间可以调整内存大小,但访问速度比堆栈更慢且成本更高。
考虑内存是零初始化的,用于访问内存的操作码是:MLOAD、MSTORE、MSTORE8
在本文中,我们回顾了与以太坊虚拟机(EVM)相关的一些基本概念。实现内联汇编代码需要深入了解 EVM。这是因为我们正在与 EVM 的一些组件进行交互。在以后的课程中,我们将更详细地分析其他 EVM 元素,例如:存储、内存和 Calldata。此外,我们还将回顾字节码、Gas 和应用程序二进制接口 (ABI) 等重要概念。最后,我们将讨论操作码的工作原理以及更多内联汇编示例,以安全地优化智能合约的执行。
来源:金色财经