# 深入理解 EVM（一）

By [xyyme.eth](https://paragraph.com/@xyyme) · 2022-07-20

---

今天我们来聊聊 EVM，那么什么是 EVM？EVM 其实就是执行 bytecode（字节码）的机器，它的全称是 Ethereum Virtual Machine（以太坊虚拟机），和 Java 的 JVM 很类似。我们平时写合约都是用 Solidity （或者 Vyper）编写的，但是这种语言机器是没有办法理解的，我们需要先使用编译器进行编译，编译后的结果是一串二进制码，EVM 可以理解这些二进制的东西，因此它就可以执行这些代码，从而完成一笔交易。

合约编译
----

我们用一个简单的图示来解释这个过程：

![](https://storage.googleapis.com/papyrus_images/c559b03091f56aabbecc5b8338a139b4bc9f9cc129beb8d868c88c2b0cb4548d.png)

我们从一个很简单的例子开始（文件命名为 Demo.sol）：

    // SPDX-License-Identifier: UNLICENSED
    pragma solidity 0.8.15;
    
    contract Demo {
        uint256 a;
        constructor() {
          a = 1;
        }
    }
    

我们平时开发项目用的都是 `hardhat`，`forge` 这种框架，他们的底层都是通过 `solc` 来进行编译。这次我们就直接使用编译工具 `solc` 来进行编译，可以参考[这里](https://docs.soliditylang.org/en/v0.8.15/installing-solidity.html)进行安装。这里列出 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，[这里](https://www.evm.codes/)可以看到所有的 Opcodes。需要注意的是操作符和操作数有可能重复，例如判断 `60` 是操作符还是操作数，取决于它在字节码中的位置，并不是绝对的。例如 `6060`，前面的 `60` 是操作符，后面的就是操作数，代表 `PUSH1 60`。

字节码构造
-----

接下来我们看看字节码的构造，我们在上面的字节码中搜索 `fe`，可以看到其中有两个 `fe`，同时查询 Opcodes 对照表，可知 `fe` 是无效操作符（INVALID）。它的作用其实是分隔符，它将字节码分成了三部分：

1.  init bytecode（初始化字节码）
    
2.  runtime bytecode （运行时字节码）
    
3.  metadata hash（合约的一些 meta 信息哈希）
    

![](https://storage.googleapis.com/papyrus_images/0452c9d45e8f4e895f0f8af0cb54d3fc03b9551e115fdc62214a61a5b5e7e8a1.png)

那么我们前面编译的字节码就被分成了三部分：

    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 等信息。这部分了解即可，我们平时也用不到这些，详细内容可以查看[文档](https://docs.soliditylang.org/en/latest/metadata.html)。

我们前面提到了一些操作数，例如 0x40、0x80，这些数字是什么意思呢。要了解这些，我们就得先明白 EVM 中的内存布局。前面的[文章](https://mirror.xyz/xyyme.eth/5eu3_7f7275rqY-fNMUP5BKS8izV9Tshmv8Z5H9bsec)中，我们讲解过内存布局，但是当时讲的实际上是 Storage Layout，也就是状态变量的布局结构。而我们现在要讲的是 Memory Layout。区别在于 Storage 的数据是永久存在于区块链上的，类似于计算机的硬盘数据。而 Memory 的数据只有在发起交易的时候才有，交易完毕，数据全部消失，类似于计算机的内存数据。

合约内存
----

Memory 的数据结构就是一个简单的字节数组，数据可以以 1 字节（8 位）或者 32 字节（256 位）为单位进行存储，读取时只能以 32 字节为单位读取，但是读取时可以从任意字节处开始读取，不限定于 32 的倍数字节。图示：

![](https://storage.googleapis.com/papyrus_images/f184f12ba28424cacbd1492c35ea1d7d610e943310074e64b3fef236173b8534.png)

用于操作内存的一共有 3 个操作符：

*   MSTORE (x, y) - 在内存 x 处开始存储 32 字节的数据 y
    
*   MLOAD (x) - 将内存 x 处开始的 32 字节数据加载到栈中
    
*   MSTORE8 (x, y) - 在内存 x 处存储 1 字节数据 y（32字节栈值中的最低有效字节）
    

Solidity 中预留了 4 个 32 字节的插槽（slot），分别是：

*   `0x00` - `0x3f` (64 字节): 哈希方法的暂存空间
    
*   `0x40` - `0x5f` (32 字节): 当前已分配内存大小 (也称为空闲内存指针)
    
*   `0x60` - `0x7f` (32 字节): 零槽，用作动态内存数组的初始值，永远不能写入值
    

![](https://storage.googleapis.com/papyrus_images/49a14371575dd4f319e9eae52d81863d4813d5b58f1f445a5c019a5a7b1d0f64.png)

这里面最重要的就是中间这一项，也就是空闲指针。它会指向空闲空间的开始位置，也就是说，要将一个新变量写入内存，给它分配的位置就是空闲指针所指向的位置。需要注意的是，Solidity 中的内存是不会被释放（free）的。

对于空闲指针，它的更新遵守了很简单的原则：

> 新的空闲指针位置 = 旧的空闲指针位置 + 分配的数据大小

上图中我们看到，Solidity 的预留空间已经占据了 128 个字节，因此空闲指针的起始位置就只能从 0x80（128字节） 开始。空闲指针本身是存在于 0x40 位置的。由于我们在函数中的操作均需要在内存中进行，因此首要任务就是要通过空闲指针分配内存，所以我们前面才需要使用 `6080604052`，也就是 `MSTORE(0x40, 0x80)`，来加载空闲指针。此时是不是已经有些明白为什么所有的合约都是以 `6080604052` 开头了（有些老版本合约以 `6060604052` 开头）。

小结
--

这篇文章我们就先介绍到这里，我们学习了合约的编译过程，字节码的构造，以及合约的内存分布。可以多看几遍消化消化。下篇文章我们将介绍合约的部署，也就是 init bytecode 部分，了解 EVM 在合约部署时的运行逻辑。

关于我
---

欢迎[和我交流](https://linktr.ee/xyymeeth)

参考
--

[

A deep-dive into Solidity - function selectors, encoding and state variables
----------------------------------------------------------------------------

In 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.com

![](https://storage.googleapis.com/papyrus_images/10d90edfc0c38d43ffdf6c2314576bd9096bee4ff0da1bfd79139eacd83dd55b.webp)

](https://leftasexercise.com/2021/09/08/a-deep-dive-into-solidity-function-selectors-encoding-and-state-variables/)

[

EVM Deep Dives: The Path to Shadowy Super Coder 🥷 💻 - Part 2
--------------------------------------------------------------

Let's take a trip down memory lane

https://noxx.substack.com

![](https://storage.googleapis.com/papyrus_images/da564bc9eda253d84b99e5eb96bcf57af98a64055a9ad7017f064a7b6291cc24.jpg)

](https://noxx.substack.com/p/evm-deep-dives-the-path-to-shadowy-d6b)

---

*Originally published on [xyyme.eth](https://paragraph.com/@xyyme/evm)*
