以太坊虚拟机EVM的工作原理是怎样的
如果你打算尝试在以太坊区块链上开发智能合约,或者已经在该领域工作了一段时间,可能会遇到EVM
一词,EMV是太坊虚拟机的缩写。 虚拟机本质上是在执行代码和执行的机器之间创建一个抽象级别。需要这一层抽象来提高软件的可移植性,以及确保应用程序彼此分离,并与运行它们的主机分离。
智能合约通常用一种名为Solidity
的编程语言编写,这种语言类似于 JavaScript 和 C++。 其他编写智能合约的语言包括 Vyper 和 Bamboo。 在 Solidity 发布之前,使用了其他语言,如 Serpent(已弃用)和 Mutan(已弃用)。
像 Solidity 这样的智能合约语言不能由 EVM 直接执行。 相反,它们需要被编译为低级别的指令(称为操作码)。
在底层,EVM 使用一组指令(称为操作码)来执行特定任务。 在撰写本文时,有 140 个唯一操作码。 这些操作码一起使 EVM 成为图灵完备的环境。 这意味着在有足够资源的情况下,EVM 能够(几乎)计算任何东西。 因为操作码是 1 个字节,所以最多只能有 256 (16²) 个操作码。 简单起见,我们可以将所有操作码分为以下几类:
为了有效地存储操作码,它们被编码为字节码。 每个操作码都分配有一个字节(例如;STOP 是 0x00)。 我们来看看下面的字节码:0x6001600101
在执行过程中,字节码被分成它的字节(1 个字节等于 2 个十六进制字符)。 0x60–0x7f (PUSH1-PUSH32) 范围内的字节处理方式不同,因为它们包含推送数据(需要附加到操作码,而不是被视为单独的操作码)。
第一条指令是 0x60,它转换为 PUSH1。 因此,我们知道推送数据是 1 个字节长,我们将下一个字节添加到堆栈中。 堆栈现在包含 1 个项,我们可以移动到下一条指令。 由于我们知道 0x01 是 PUSH 指令的一部分,因此我们需要执行的下一条指令是另一个 0x60 (PUSH1) 以及相同的数据。 堆栈现在包含 2 个相同的项。 最后一条指令是 0x01,翻译为 ADD。 该指令将从堆栈中取出 2 个项,并将这些项目的总和压入堆栈。 堆栈现在包含一项:0x02
虽然许多流行的顶级编程语言允许用户直接将参数传递给函数(function(argument1,argument2)),但低级编程语言通常使用堆栈将值传递给函数。 EVM 使用 256 位寄存器堆栈,可以一次访问或操作最近的 16 项。总共,堆栈只能容纳 1024 个项。
由于这些限制,复杂的操作码改为使用合约内存来检索或传递数据。然而,记忆不是持久的。合约执行完成后,内存内容不会被保存。可以将堆栈类比为函数参数,将内存类比为声明变量。
为了无限期地存储数据并使其可用于未来的合约执行,可以使用存储。合约存储本质上充当公共数据库,可以从外部读取值,而无需向合约发送交易(不收费!)。但是,与写入内存相比,写入存储非常昂贵(高达 6000 倍成本)。
由于所有合约的执行都是由运行以太坊节点的个人运行的,攻击者可以尝试创建包含大量计算成本高的操作的合约来减慢网络速度。 为了防止此类攻击的发生,每个操作码都有自己的基本 gas 成本。 此外,一些复杂的操作码也会收取动态 gas 成本。 例如,操作码 KECCAK256(以前称为 SHA3)的基本成本为 30 gas,每个字的动态成本为 6 gas(字是 256 位项目)。 计算成本高的指令比简单、直接的指令收取更高的 gas 费用。 最重要的是,每笔交易都以 21000 gas 开始。
当执行减少状态大小的指令时,gas 也可以退还。 将存储值从非零设置为零会退还 15000 gas,而完全删除合约(使用 SELFDESTRUCT 操作码)会退还 24000 gas。 退款仅在合同执行完成后才会发生,因此合同无法自行支付。 此外,退款不能超过当前合同调用所用 gas 的一半。
如果有兴趣阅读更多有关gas的信息,请随时查看这篇出色的文章:“什么是gas?”
https://support.mycrypto.com/general-knowledge/ethereum-blockchain/what-is-gas/
部署智能合约时,会创建一个常规交易,没有地址。 此外,添加了一些字节码作为输入数据。 此字节码充当构造函数,需要在将运行时字节码复制到合约代码之前将初始变量写入存储。 在部署期间,创建字节码只会运行一次,而运行时字节码将在每次合约调用时运行。
我们可以把上面的字节码分为三个部分
构造器
60806040526001600055348015601457600080fd5b5060358060226000396000f3fe
运行时
6080604052600080fdfe
元数据
a165627a7a723058204e048d6cab20eb0d9f95671510277b55a61a582250e04db7f6587a1bebc134d20029
在这个字节码的末尾,附加了一个由 Solidity 创建的元数据文件的 Swarm 散列。 Swarm 是一个分布式存储平台和内容分发服务,或者更简单地说:一个去中心化的文件存储。 尽管 Swarm 哈希也将包含在运行时字节码中,但它永远不会被 EVM 解释为操作码,因为它的位置永远无法到达。 目前,Solidity 使用以下格式:
0xa1 0x65 'b' 'z' 'z' 'r' '0' 0x58 0x20 [32 bytes swarm hash] 0x00 0x29
因此,在这种情况下,我们可以提取以下 Swarm 哈希:
4e048d6cab20eb0d9f95671510277b55a61a582250e04db7f6587a1bebc134d2
元数据文件包含有关合约的各种信息,例如编译器版本或合约的功能。 不幸的是,这是一个实验性功能,没有多少合约公开将其元数据上传到 Swarm 网络。
有几个项目已经创建了工具来尝试使字节码更具可读性。 例如你可以使用 eveem.org 或 ethervm.io 在主网上反编译合约。 不过,由于编译器进行了优化,原始合约源的某些部分(例如函数名称或事件名称)总是丢失。 尽管如此,大多数函数名称仍然可以通过将函数签名与包含流行函数和事件名称的大型数据集进行比较来强制执行(参见 4byte.directory)。
合约调用通常需要“ABI”(应用程序二进制接口),它是记录所有功能和事件的数据,包括它们所需的输入和输出。 在合约上调用函数时,函数签名是通过对函数名称(包括其输入)进行哈希处理(使用 keccak256)并截断除前 4 个字节之外的所有内容来确定的。
如上图所示,我们的函数 HelloWorld() 解析为签名哈希 0x7fffb7bd。 如果我们想调用这个函数,我们的交易数据需要以 0x7fffb7bd 开头。 需要传递给函数的参数(在这种情况下没有)可以在交易输入数据中的签名哈希之后添加到称为单词的 32 字节片段中。
如果一个参数包含超过 32 个字节(256 位)的数据,如数组或字符串,则该参数将拆分为多个字,这些字会在包含所有其他参数后添加到输入数据中。 此外,所有单词的总大小作为另一个单词包含在所有数组单词之前。 在包含参数的位置,将添加数组单词(包括大小单词)的起始位置。
以太坊为开发人员提供了一个去中心化的生态系统,可以使用 Solidity 和 EVM 构建出色的产品。 尽管通过智能合约与 EVM 交互可能比在传统服务器上运行程序要昂贵得多,但在许多用例中,去中心化更受大家的关注。
如果阅读本文使您有兴趣了解有关开发智能合约的更多信息,请通过查看“智能合约简介”来深入了解 Solidity 的工作原理。 谢谢阅读!
本篇翻译自下面这篇文章
https://medium.com/mycrypto/the-ethereum-virtual-machine-how-does-it-work-9abac2b7c9e