# 深入理解 EVM(一) **Published by:** [xyyme.eth](https://paragraph.com/@xyyme/) **Published on:** 2022-07-20 **URL:** https://paragraph.com/@xyyme/evm ## Content 今天我们来聊聊 EVM,那么什么是 EVM?EVM 其实就是执行 bytecode(字节码)的机器,它的全称是 Ethereum Virtual Machine(以太坊虚拟机),和 Java 的 JVM 很类似。我们平时写合约都是用 Solidity (或者 Vyper)编写的,但是这种语言机器是没有办法理解的,我们需要先使用编译器进行编译,编译后的结果是一串二进制码,EVM 可以理解这些二进制的东西,因此它就可以执行这些代码,从而完成一笔交易。合约编译我们用一个简单的图示来解释这个过程:我们从一个很简单的例子开始(文件命名为 Demo.sol):// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.15; contract Demo { uint256 a; constructor() { a = 1; } } 我们平时开发项目用的都是 hardhat,forge 这种框架,他们的底层都是通过 solc 来进行编译。这次我们就直接使用编译工具 solc 来进行编译,可以参考这里进行安装。这里列出 Mac 的安装方法:brew update brew upgrade brew tap ethereum/ethereum brew install solidity 安装完成后,我们来编译试试,使用下面的命令:solc Demo.sol --bin输出以下内容:======= Demo.sol:Demo ======= Binary: 6080604052348015600f57600080fd5b506001600081905550603f8060256000396000f3fe6080604052600080fdfea26469706673582212204ca38d4a605f03f1487b9cb337c0853cca3c62a6c42f942ecb021fb7357002b564736f6c634300080f0033我们看到在结果中输出了一串 16 进制字符,这就是上面的合约经过编译过后的字节码。我们前面说过,字节码是一串二进制的字符,这里显示为 16 进制方便阅读。注意到我们在命令中指定了 --bin 参数,因此输出的是 16 进制。 第一眼看见这串字符是不是已经懵了,别慌,我们慢慢来研究。首先,我们需要知道,EVM 的核心实际上是一个 stack machine(栈机器),它会接受操作符和操作数,学过数据结构的朋友应该都了解栈的原理。其次,上面这些字符都是由操作符和操作数组成的。例如开头的 60 代表的是 PUSH1,也就是将后面的一个字节(这里是 80)压入栈中。后面又是一个 60,接着是 40,即代表将 40 压入栈中。后面是 52,代表 MSTORE,它需要消耗两个操作数,需要从栈中获取。也就是说,上面的 6080604052,就代表着:MSTORE 的两个操作数分别为 0x40、0x80,即 MSTORE(0x40, 0x80),也就是在内存中地址 0x40 处存储了数据 0x80。(这一句不明白没关系,我们先往下看) 现在我们已经明白了字节码的基本逻辑,它就是由操作符和操作数组成的。其中两个字符代表一个字节,操作符都是一个字节,但是操作数可能有多个字节。像我们前面看到的 PUSH1,是将后面的一个字节压入栈中,如果是 PUSH4,就是将后面的四个字节压入栈中。一般将操作符称为 Opcodes,这里可以看到所有的 Opcodes。需要注意的是操作符和操作数有可能重复,例如判断 60 是操作符还是操作数,取决于它在字节码中的位置,并不是绝对的。例如 6060,前面的 60 是操作符,后面的就是操作数,代表 PUSH1 60。字节码构造接下来我们看看字节码的构造,我们在上面的字节码中搜索 fe,可以看到其中有两个 fe,同时查询 Opcodes 对照表,可知 fe 是无效操作符(INVALID)。它的作用其实是分隔符,它将字节码分成了三部分:init bytecode(初始化字节码)runtime bytecode (运行时字节码)metadata hash(合约的一些 meta 信息哈希)那么我们前面编译的字节码就被分成了三部分:6080604052348015600f57600080fd5b506001600081905550603f8060256000396000f3(init bytecode) 6080604052600080fd(runtime bytecode) a26469706673582212204ca38d4a605f03f1487b9cb337c0853cca3c62a6c42f942ecb021fb7357002b564736f6c634300080f0033(metadata hash) 我们先来看看最后这里的 metadata hash,它默认是合约 metadata 文件的 IPFS 哈希值,我们可以使用:solc Demo.sol --metadata来获取到其 metadata:{ "compiler": { "version": "0.8.15+commit.e14f2714" }, "language": "Solidity", "output": { "abi": [ { "inputs": [], "stateMutability": "nonpayable", "type": "constructor" } ], "devdoc": { "kind": "dev", "methods": {}, "version": 1 }, "userdoc": { "kind": "user", "methods": {}, "version": 1 } }, "settings": { "compilationTarget": { "Demo.sol": "Demo" }, "evmVersion": "london", "libraries": {}, "metadata": { "bytecodeHash": "ipfs" }, "optimizer": { "enabled": false, "runs": 200 }, "remappings": [] }, "sources": { "Demo.sol": { "keccak256": "0xf6e99f20fac61b16466088a9996227f35c4ca82119a846ba19a83698e8e126b1", "license": "UNLICENSED", "urls": [ "bzz-raw://3fda90691cfa365f68a59d5b6bb76f8a0189153cebf93143879521ea309751b8", "dweb:/ipfs/QmP2dVkfPWy3tAriX17tar9phGzC8gqYzutmhEur6dwdiw" ] } }, "version": 1 } 可以看到其中主要包含了编译器版本,ABI,IPFS 等信息。这部分了解即可,我们平时也用不到这些,详细内容可以查看文档。 我们前面提到了一些操作数,例如 0x40、0x80,这些数字是什么意思呢。要了解这些,我们就得先明白 EVM 中的内存布局。前面的文章中,我们讲解过内存布局,但是当时讲的实际上是 Storage Layout,也就是状态变量的布局结构。而我们现在要讲的是 Memory Layout。区别在于 Storage 的数据是永久存在于区块链上的,类似于计算机的硬盘数据。而 Memory 的数据只有在发起交易的时候才有,交易完毕,数据全部消失,类似于计算机的内存数据。合约内存Memory 的数据结构就是一个简单的字节数组,数据可以以 1 字节(8 位)或者 32 字节(256 位)为单位进行存储,读取时只能以 32 字节为单位读取,但是读取时可以从任意字节处开始读取,不限定于 32 的倍数字节。图示:用于操作内存的一共有 3 个操作符:MSTORE (x, y) - 在内存 x 处开始存储 32 字节的数据 yMLOAD (x) - 将内存 x 处开始的 32 字节数据加载到栈中MSTORE8 (x, y) - 在内存 x 处存储 1 字节数据 y(32字节栈值中的最低有效字节)Solidity 中预留了 4 个 32 字节的插槽(slot),分别是:0x00 - 0x3f (64 字节): 哈希方法的暂存空间0x40 - 0x5f (32 字节): 当前已分配内存大小 (也称为空闲内存指针)0x60 - 0x7f (32 字节): 零槽,用作动态内存数组的初始值,永远不能写入值这里面最重要的就是中间这一项,也就是空闲指针。它会指向空闲空间的开始位置,也就是说,要将一个新变量写入内存,给它分配的位置就是空闲指针所指向的位置。需要注意的是,Solidity 中的内存是不会被释放(free)的。 对于空闲指针,它的更新遵守了很简单的原则:新的空闲指针位置 = 旧的空闲指针位置 + 分配的数据大小上图中我们看到,Solidity 的预留空间已经占据了 128 个字节,因此空闲指针的起始位置就只能从 0x80(128字节) 开始。空闲指针本身是存在于 0x40 位置的。由于我们在函数中的操作均需要在内存中进行,因此首要任务就是要通过空闲指针分配内存,所以我们前面才需要使用 6080604052,也就是 MSTORE(0x40, 0x80),来加载空闲指针。此时是不是已经有些明白为什么所有的合约都是以 6080604052 开头了(有些老版本合约以 6060604052 开头)。小结这篇文章我们就先介绍到这里,我们学习了合约的编译过程,字节码的构造,以及合约的内存分布。可以多看几遍消化消化。下篇文章我们将介绍合约的部署,也就是 init bytecode 部分,了解 EVM 在合约部署时的运行逻辑。关于我欢迎和我交流参考A deep-dive into Solidity - function selectors, encoding and state variablesIn the last post, we have seen how the Solidity compiler creates code - the init bytecode - to prepare and deploy the actual bytecode executed at runtime. Today, we will look at a few standard patterns that we find when looking at this runtime bytecode.http://leftasexercise.comEVM Deep Dives: The Path to Shadowy Super Coder 🥷 💻 - Part 2Let's take a trip down memory lanehttps://noxx.substack.com ## Publication Information - [xyyme.eth](https://paragraph.com/@xyyme/): Publication homepage - [All Posts](https://paragraph.com/@xyyme/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@xyyme): Subscribe to updates - [Twitter](https://twitter.com/xyymeeth): Follow on Twitter