EVM 设计与实现 - Mohanson

本页内容已在线发布, 地址: http://accu.cc/content/share/evm












关于作者












EVM 的基本概念

  • Ethereum Vitual Machine(EVM) 是以太坊执行模块的重要组成部分, 其职责是执行 EVM Code. EVM 使用了 256 比特长度的机器码, 是一种基于堆栈的虚拟机.
  • EVM Code 是一段字节码, 通常使用 16 字节字符串表示. 比如: 0x6005600401. 它是 EVM 实际执行的代码.
  • EVM Assembly 是 EVM Code 的人类可读表现形式. 例如 EVM Code 0x6005600401 可以表示为 EVM Assembly:
PUSH1 0x05
PUSH1 0x04
ADD












EVM 介绍

  • Ethereum Vitual Machine(EVM) 是以太坊执行模块的重要组成部分, 其职责是执行 EVM Code. EVM 使用了 256 比特长度的机器码, 是一种基于堆栈的虚拟机.
  • EVM Code 是一段字节码, 通常使用 16 字节字符串表示. 比如: 0x6005600401. 它是 EVM 实际执行的代码.
  • EVM Assembly 是 EVM Code 的人类可读表现形式. 例如 EVM Code 0x6005600401 可以表示为 EVM Assembly:
PUSH1 0x05
PUSH1 0x04
ADD












EVM 堆栈的栈顶大小是 256 位, 最大深度是 1024. 由于堆栈数据结构只允许在一端进行操作, 因而按照后进先出(LIFO, Last In First Out) 的原理运作. 栈的两个基本操作是 PUSH 和 POP:

|---------|                    |------------|              |---------|
| 0000:   |                    | 0000: 0x42 |              | 0000:   |
| 0001:   |                    | 0001:      |              | 0001:   |
| .       | --> PUSH 0x42 -->  | .          | --> POP -->  | .       | && Return 0x42
| .       |                    | .          |              | .       |
| 1023:   |                    | 1023       |              | 1023:   |
|---------|                    |------------|              |---------|












临时内存

CPU 是通过寻址来访问内存的. 32 位 CPU 的寻址宽度是 0 ~ 0xFFFFFFFF, 计算后得到的大小是 4 G, 也就是说可支持的物理内存最大是 4 G. 这正是近年来为什么不断淘汰 32 位操作系统/CPU 的原因.

EVM 由于采用 256 位比特长度机器码, 其寻址宽度是 0 ~ 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 理论上可以寻址近乎无限的内存. EVM 临时内存的基本单位是 word: 一个 word 的大小是 256 位比特(或者 32 byte, 因为大多数 EVM 实现均使用 byte 数组来实现临时内存). 临时内存的大小总是 word 的整数倍.

临时内存的三个基本操作是 Get, SetExpand(扩容). 我们使用如下一段长度为 0xdf 的随机初始化的临时内存的可读形式来演示这几个操作.

00000000  60 80 60 40 52 60 04 36  10 60 49 57 60 00 35 7c  |`.`@R`.6.`IW`.5||
00000010  01 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 90 04 63  |...............c|
00000030  ff ff ff ff 16 80 63 60  fe 47 b1 14 60 4e 57 80  |......c`.G..`NW.|
00000040  63 6d 4c e6 3c 14 60 78  57 5b 60 00 80 fd 5b 34  |cmL.<.`xW[`...[4|
00000050  80 15 60 59 57 60 00 80  fd 5b 50 60 76 60 04 80  |..`YW`...[P`v`..|
00000060  36 03 81 01 90 80 80 35  90 60 20 01 90 92 91 90  |6......5.` .....|
00000070  50 50 50 60 a0 56 5b 00  5b 34 80 15 60 83 57 60  |PPP`.V[.[4..`.W`|
00000080  00 80 fd 5b 50 60 8a 60  aa 56 5b 60 40 51 80 82  |...[P`.`.V[`@Q..|
00000090  81 52 60 20 01 91 50 50  60 40 51 80 91 03 90 f3  |.R` ..PP`@Q.....|
000000a0  5b 80 60 00 81 90 55 50  50 56 5b 60 00 80 54 90  |[.`...UPPV[`..T.|
000000b0  50 90 56 00 a1 65 62 7a  7a 72 30 58 20 99 c6 6a  |P.V..ebzzr0X ..j|
000000c0  25 d5 9f 0a a7 8f 7e bc  40 74 8f a1 d1 fb c3 35  |%.....~.@t.....5|
000000d0  d8 d7 80 f2 84 84 1b 30  e0 36 5a cd 96 00 29 00  |.......0.6Z...).|
                               ⬇
                      Set(0x00, 0x00..0042)
                               ⬇
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 42  |..............."|
                               ⬇
                      Get(0x0f, 0x01) ---------> Return 42
                               ⬇
                      Set(0xe0, 0x00..0042) // No avaliable memory!
                               ⬇
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 42  |..............."|
........
........
000000e0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 42  |..............."|












永久内存

永久内存是持久化存储智能合约状态的地方. 永久内存十分类似临时内存, 唯一区别是它是永久的. 大多数 EVM 实现使用 KV 数据库来实现永久内存, 其 Key 与 Value 大小均为 256 比特. 但我们可以以 JSON 格式来形象化的表现永久内存: 一个部署在 bd770416a3345f91e4b34576cb804a576fa48eb1 地址的合约在其位置为0000000000000000000000000000000000000000000000000000000000000000 的永久内存中存入了 0x000000000000000000000000000000000000000000000000000000000000002a 这个值.

{
    "root": "df8a18a629f2fefa816a785c49431fa6f8c146045b0e33cf7782c0aa83d48e4a",
    "accounts": {
        "bd770416a3345f91e4b34576cb804a576fa48eb1": {
            "balance": "0",
            "nonce": 1,
            "root": "0x..",
            "codeHash": "0x..",
            "code": "0x..",
            "storage": {
                "0000000000000000000000000000000000000000000000000000000000000000": "0x000000000000000000000000000000000000000000000000000000000000002a"
            }
        }
    }
}












操作码

操作码[1] 将 EVM Code 映射为具体执行逻辑. 一部分操作码与栈交互, 一部分操作码与临时内存交互, 一部分操作码与永久内存交互, 一部分操作码与 EVM 执行上下文交互. 简要演示几种操作码的执行过程:

ADD

ADD 操作码的值是 0x01. 它表示从栈中 POP 两个元素, 相加后重新 PUSH 入栈.

|------------|                        |------------|
| 0000: 0x01 |                        | 0000: 0x02 |
| 0001: 0x01 |                        | 0001:      |
| .          | --> EVM Code: 0x01 --> | .          |
| .          |                        | .          |
| 1023:      |                        | 1023       |
|------------|                        |------------|

BALANCE

BALANCE 操作码的值是 0x31. 它表示从栈中 POP 一个元素作为地址, 之后取得该地址的余额, 并将余额 PUSH 入栈. 假设地址 0xbd770416a3345f91e4b34576cb804a576fa48eb1 的余额是 2 Wei, 则:

|--------------------------------------------------|                        |------------|
| 0000: 0xbd770416a3345f91e4b34576cb804a576fa48eb1 |                        | 0000: 0x02 |
| 0001:                                            |                        | 0001:      |
| .                                                | --> EVM Code: 0x31 --> | .          |
| .                                                |                        | .          |
| 1023:                                            |                        | 1023       |
|--------------------------------------------------|                        |------------|

MSTORE

MSTORE 操作码的值是 0x52. 它表示从栈中 POP 两个元素, 第一个作为临时内存偏移值, 第二个作为存储的值. 将数据写入临时内存.

|----------------|                        |-------|
| 0000: 0x00..00 |                        | 0000: |
| 0001: 0x00..42 |                        | 0001: |
| .              | --> EVM Code: 0x52 --> | .     |
| .              |                        | .     |
| 1023:          |                        | 1023  |
|----------------|                        |-------|

临时内存变化

00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 42  |..............."|

SSTORE

SSTORE 操作码的值是 0x55. 它表示从栈中 POP 两个元素, 第一个作为永久内存偏移值, 第二个作为存储的值, 将数据写入永久内存.

全部操作码的意义请参考以太坊黄皮书.












Gas

EVM 是图灵等价的, 为了限制执行交易所需的工作量, 当 EVM 执行交易时, Gas[2] 将按照特定规则被逐渐消耗. 每个操作码都有其 Gas 消耗量, 无论执行到什么位置, 一旦 Gas 被耗尽, 将会触发一个 out-of-gas 异常. 当前调用帧所做的所有状态修改都将被回滚. Gas 依据操作码执行的预期资源消耗定义. 比如 ADD(加法) 操作码消耗 2 Gas, 而 DIV(除法) 操作码消耗 3 Gas. 同时, 内存扩容, 数据拷贝等操作同样会消耗 Gas, 并且数据量越大, 消耗 Gas 越多.

在 EVM 执行过程中, Gas 的扣费是先于操作码执行的.

详细 Gas 消耗规则请参考以太坊黄皮书.












一段示例代码的执行过程

我们使用一段真实 EVM Code, 人工模拟 EVM 执行过程.

EVM Code:

0x603760005360005160005560016000f3

等价的 EVM Assembly:

PUSH1 0x37
PUSH1 0x00
MSTORE8
PUSH1 0x00
MLOAD
PUSH1 0x00
SSTORE
PUSH1 0x01
PUSH1 0x00
RETURN

假设初始 Gas 为 100000, 模拟执行过程如下:

PUSH1           pc=00000000 gas=100000 cost=3

PUSH1           pc=00000002 gas=99997 cost=3
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000037

MSTORE8         pc=00000004 gas=99994 cost=6
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000000
00000001  0000000000000000000000000000000000000000000000000000000000000037
Memory:
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

PUSH1           pc=00000005 gas=99988 cost=3
Memory:
00000000  37 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |7...............|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

MLOAD           pc=00000007 gas=99985 cost=3
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000000
Memory:
00000000  37 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |7...............|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

PUSH1           pc=00000008 gas=99982 cost=3
Stack:
00000000  3700000000000000000000000000000000000000000000000000000000000000
Memory:
00000000  37 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |7...............|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

SSTORE          pc=00000010 gas=99979 cost=20000
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000000
00000001  3700000000000000000000000000000000000000000000000000000000000000
Memory:
00000000  37 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |7...............|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
Storage:
0000000000000000000000000000000000000000000000000000000000000000: 3700000000000000000000000000000000000000000000000000000000000000

PUSH1           pc=00000011 gas=79979 cost=3
Memory:
00000000  37 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |7...............|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
Storage:
0000000000000000000000000000000000000000000000000000000000000000: 3700000000000000000000000000000000000000000000000000000000000000

PUSH1           pc=00000013 gas=79976 cost=3
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000001
Memory:
00000000  37 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |7...............|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
Storage:
0000000000000000000000000000000000000000000000000000000000000000: 3700000000000000000000000000000000000000000000000000000000000000

RETURN          pc=00000015 gas=79973 cost=0
Stack:
00000000  0000000000000000000000000000000000000000000000000000000000000000
00000001  0000000000000000000000000000000000000000000000000000000000000001
Memory:
00000000  37 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |7...............|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
Storage:
0000000000000000000000000000000000000000000000000000000000000000: 3700000000000000000000000000000000000000000000000000000000000000


Return  = 37












EVM 的缺陷与不足

  • 仅支持 256 位整数, 现行 32/64 位 CPU 无法直接处理 EVM code. 256 位整数同时造成大量的数据冗余, 这体现在内存寻址, Gas 计算, EVM Code 跳转等地方(你几乎不可能用到 2^256 的内存寻址).

  • Gas 模型上的经济问题. EVM 鼓励低 Gas 消耗的代码, 而不鼓励高效率的代码, 这导致以太坊区块链上存在大量性能低效, 冗余但 Gas 高效的代码. 比如为了对抗溢出而编写的 SafeMath 代码, 这一小段代码被开发者复制了无数次并部署到以太坊上.

  • JUMP/JUMPI 指令的设计使 EVM 字节码几乎无法被静态分析. 参看 EIP-615.












参考文献

  • [1] Ethereum Yellow Paper: APPENDIX H. VIRTUAL MACHINE SPECIFICATION. P28.
  • [2] Ethereum Yellow Paper: APPENDIX G. FEE SCHEDULE. P25.