EVM虚拟机合约的执行过程

野性酷女 2022-03-25 07:24 501阅读 0赞

文章目录

  • 简单合约实例
  • 汇编分析
  • 虚拟机的优化
    • 字节码优化
    • Gas 的使用
  • 总结

首先列出EVM虚拟机汇编指令集:https://gist.github.com/hayeah/bd37a123c02fecffbe629bf98a8391df

常用汇编指令: https://blog.csdn.net/qq_33733970/article/details/78572733 https://blog.csdn.net/baishuiniyaonulia/article/details/78504758

简单合约实例

编写一个简单合约

  1. pragma solidity ^0.4.11;
  2. contract C {
  3. uint256 a;
  4. function C( ) {
  5. a = 1;
  6. }
  7. }

使用remix查看汇编代码(点击detail查看):

在这里插入图片描述
可以查看BYTECODE、ABI、WEB3DEPLOY、RUNTIME BYTECODE 、ASSEMBLY等信息。
在这里插入图片描述

汇编分析

先看BYTECODE 。其中的object就是编译后的汇编指令。可以对照EVM虚拟机汇编指令集:https://gist.github.com/hayeah/bd37a123c02fecffbe629bf98a8391df 查看。下面的opcodes就是替换为对应指令后的结果。

再看WEB3DEPLOY,其中部署时data内容就是合约编译后的汇编指令。

接下来,我们重点看一下ASSEMBLY部分。从这部分,可以看到合约具体的执行过程。
在这里插入图片描述
我们先来回顾一下栈的操作过程(纯粹是按照过去的知识储备,与上面案例无关)。

假设有一个add操作,a=1+2。我们用[]符号来标识栈;用{}符号来标识合约存储器

往栈中压入值1 :[1]
王栈中压入值2:[2,1]
遇到add操作符
1和2出栈并且执行add计算,得到结果3
将3入栈 : [3]
将栈顶数据保存早0x0位置上, 清空栈:
栈:[]
存储:{0x0 → 3}
此时我们回到上面的案例,只看一下核心的 a=1 这个操作。

下图我将汇编和数字编码对应起来。5b6001600081905550 就是函数c的汇编代码:
在这里插入图片描述
具体执行过程:

  1. //tag1 就是a=1的具体执行代码。
  2. tag 1 function C() {\n a = 1;...
  3. //跳转到方法C [5b]
  4. JUMPDEST function C() {\n a = 1;...
  5. //将1压入栈中 [60 01]
  6. //执行结果:stack: [0x1]
  7. PUSH 1 1
  8. //将0压入栈中(这里是给a占个位置) [60 00]
  9. //执行结果:stack: [0x0 0x1]
  10. PUSH 0 a
  11. // 复制栈中的第二项 [81]
  12. //执行结果:stack: [0x1 0x0 0x1]
  13. DUP2 a = 1
  14. // 交换栈顶的两项数据 [90]
  15. //执行结果:stack: [0x0 0x1 0x1]
  16. SWAP1 a = 1
  17. // 55: 将数值0x01存储在0x0的位置上. 这个操作会消耗栈顶两项数据。 [55]
  18. //执行结果:stack: [0x1]
  19. // store: { 0x0 => 0x1 }
  20. SSTORE a = 1
  21. //丢弃栈顶数据 [50]
  22. //执行结果:stack: []
  23. // store: { 0x0 => 0x1 }
  24. POP a = 1

假设有两个变量:

  1. pragma solidity ^0.4.11;
  2. contract C {
  3. uint256 a;
  4. uint256 b;
  5. function C( ) {
  6. a = 1;
  7. b = 2;
  8. }
  9. }

汇编代码:
在这里插入图片描述
ASSEMBLY:
在这里插入图片描述
可以看到他的执行过程就是按照参数顺序,依次执行的。

虚拟机的优化

将多个小字节的数据,存储到一个存储位置(32字节)中。
我们写这么一个合约

  1. pragma solidity ^0.4.11;
  2. contract C {
  3. uint128 a;
  4. uint128 b;
  5. function C() {
  6. a = 1;
  7. b = 2;
  8. }
  9. }

编译之后的代码为:

  1. .code
  2. PUSH 60 contract C {\n uint128 a;...
  3. PUSH 40 contract C {\n uint128 a;...
  4. MSTORE contract C {\n uint128 a;...
  5. CALLVALUE function C() {\n a = 1;...
  6. ISZERO function C() {\n a = 1;...
  7. PUSH [tag] 1 function C() {\n a = 1;...
  8. JUMPI function C() {\n a = 1;...
  9. PUSH 0 function C() {\n a = 1;...
  10. DUP1 function C() {\n a = 1;...
  11. REVERT function C() {\n a = 1;...
  12. tag 1 function C() {\n a = 1;...
  13. JUMPDEST function C() {\n a = 1;...
  14. PUSH 1 1
  15. PUSH 0 a
  16. DUP1 a
  17. PUSH 100 a = 1
  18. EXP a = 1
  19. DUP2 a = 1
  20. SLOAD a = 1
  21. DUP2 a = 1
  22. PUSH FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF a = 1
  23. MUL a = 1
  24. NOT a = 1
  25. AND a = 1
  26. SWAP1 a = 1
  27. DUP4 a = 1
  28. PUSH FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF a = 1
  29. AND a = 1
  30. MUL a = 1
  31. OR a = 1
  32. SWAP1 a = 1
  33. SSTORE a = 1
  34. POP a = 1
  35. PUSH 2 2
  36. PUSH 0 b
  37. PUSH 10 b
  38. PUSH 100 b = 2
  39. EXP b = 2
  40. DUP2 b = 2
  41. SLOAD b = 2
  42. DUP2 b = 2
  43. PUSH FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF b = 2
  44. MUL b = 2
  45. NOT b = 2
  46. AND b = 2
  47. SWAP1 b = 2
  48. DUP4 b = 2
  49. PUSH FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF b = 2
  50. AND b = 2
  51. MUL b = 2
  52. OR b = 2
  53. SWAP1 b = 2
  54. SSTORE b = 2
  55. POP b = 2
  56. PUSH #[$] 0000000000000000000000000000000000000000000000000000000000000000 contract C {\n uint128 a;...
  57. DUP1 contract C {\n uint128 a;...
  58. PUSH [$] 0000000000000000000000000000000000000000000000000000000000000000 contract C {\n uint128 a;...
  59. PUSH 0 contract C {\n uint128 a;...
  60. CODECOPY contract C {\n uint128 a;...
  61. PUSH 0 contract C {\n uint128 a;...
  62. RETURN contract C {\n uint128 a;...
  63. .data
  64. 0:
  65. .code
  66. PUSH 60 contract C {\n uint128 a;...
  67. PUSH 40 contract C {\n uint128 a;...
  68. MSTORE contract C {\n uint128 a;...
  69. PUSH 0 contract C {\n uint128 a;...
  70. DUP1 contract C {\n uint128 a;...
  71. REVERT contract C {\n uint128 a;...
  72. .data

上述代码执行结果就是讲a和b两个变量存储在一个存储位置上(32字节):
在这里插入图片描述
进行打包的原因是因为目前最昂贵的操作就是存储的使用:

sstore指令第一次写入一个新位置需要花费20000 gas
sstore指令后续写入一个已存在的位置需要花费5000 gas
sload指令的成本是500 gas
大多数的指令成本是3~10 gas
通过使用相同的存储位置,Solidity为存储第二个变量支付5000 gas,而不是20000 gas,节约了15000 gas。

字节码优化

上面已经将存储优化了。还可以将编译后的字节码优化一下。

在remix中启用优化:
在这里插入图片描述
优化后的字节码为(只列出tag1的部分):

  1. tag 1 function C() {\n a = 1;...
  2. JUMPDEST function C() {\n a = 1;...
  3. PUSH 0 a
  4. DUP1 a = 1
  5. SLOAD a = 1
  6. PUSH 200000000000000000000000000000000 b = 2
  7. PUSH 1
  8. PUSH 80
  9. PUSH 2
  10. EXP
  11. SUB
  12. NOT
  13. SWAP1 a = 1
  14. SWAP2 a = 1
  15. AND a = 1
  16. PUSH 1 1
  17. OR a = 1
  18. PUSH 1
  19. PUSH 80
  20. PUSH 2
  21. EXP
  22. SUB
  23. AND b = 2
  24. OR b = 2
  25. SWAP1 b = 2
  26. SSTORE b = 2

可见,只有一次sstore命令。

不要小看这一次sstore指令的执行。在以太坊EVM执行中是需要消耗gas的。这样少一个sstore命令的执行,就节省了5000gas。

Gas 的使用

为什么ABI将方法选择器截断到4个字节?如果我们不使用sha256的整个32字节,会不会不幸的碰到不同方法发生冲突的情况? 如果这个截断是为了节省成本,那么为什么在用更多的0来进行填充时,而仅仅只为了节省方法选择器中的28字节而截断呢?

这种设计看起来互相矛盾…直到我们考虑到一个交易的gas成本。

每笔交易需要支付 21000 gas
每笔交易的0字节或代码需要支付 4 gas
每笔交易的非0字节或代码需要支付 68 gas
0要便宜17倍,0填充现在看起来没有那么不合理了。

方法选择器是一个加密哈希值,是个伪随机。一个随机的字符串倾向于拥有很多的非0字节,因为每个字节只有0.3%(1/255)的概率是0。

0x1填充到32字节成本是192 gas
431 (0字节) + 68 (1个非0字节)
sha256可能有32个非0字节,成本大概2176 gas
32 \
68
sha256截断到4字节,成本大概272 gas
32*4
ABI展示了另外一个底层设计的奇特例子,通过gas成本结构进行激励。

总结

EVM的编译器实际上不会为字节码的大小、速度或内存高效性进行优化。相反,它会为gas的使用进行优化,这间接鼓励了计算的排序,让以太坊区块链可以更高效一点。

我们也看到了EVM一些奇特的地方:

EVM是一个256位的机器。以32字节来处理数据是最自然的
持久存储是相当昂贵的
Solidity编译器会为了减少gas的使用而做出相应的优化选择
Gas成本的设置有一点武断,也许未来会改变。当成本改变的时候,编译器也会做出不同的优化选择。

发表评论

表情:
评论列表 (有 0 条评论,501人围观)

还没有评论,来说两句吧...

相关阅读