CKB/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, 计算后得到的大小是 4G, 也就是说可支持的物理内存最大是 4G. 这正是近年来为什么不断淘汰 32 位操作系统/CPU 的原因.
EVM 由于采用 256 位比特长度机器码, 其寻址宽度达到了惊人的 0 ~ 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, 理论上可以寻址近乎无限的内存. EVM 临时内存的基本单位是 word
: 一个 word 的大小是 256 位(或者 32 byte, 因为大多数 EVM 实现均使用 byte 数组来实现临时内存). 临时内存的大小总是 word 的整数倍.
临时内存的三个基本操作是 Get
, Set
与 Expand(扩容)
. 我们使用如下一段长度为 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 |%[email protected]|
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 格式来形象化的表现永久内存: 一个部署在 0xbd770416a3345f91e4b34576cb804a576fa48eb1
地址的合约在其位置为0x0000000000000000000000000000000000000000000000000000000000000000
的永久内存中存入了 0x000000000000000000000000000000000000000000000000000000000000002a
这个值.
{
"root": "df8a18a629f2fefa816a785c49431fa6f8c146045b0e33cf7782c0aa83d48e4a",
"accounts": {
"bd770416a3345f91e4b34576cb804a576fa48eb1": {
"balance": "0",
"nonce": 1,
"root": "0x..",
"codeHash": "0x..",
"code": "0x..",
"storage": {
"0x0000000000000000000000000000000000000000000000000000000000000000": "0x000000000000000000000000000000000000000000000000000000000000002a"
}
}
}
}
操作码
操作码将 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 将按照特定规则被逐渐消耗. 每个操作码都有其 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.