Solidity提供了许多高级语言抽象,但是这些特性使我很难理解当我的程序运行时发生了什么。 阅读Solidity文档仍然让我对基本的东西感到困惑。

字符串,字节32,字节[],字节之间有什么区别?

  • 什么时候用?
  • 当我将字符串转换为字节时发生了什么? 我可以投到byte []吗?
  • 他们花多少钱?

EVM如何存储映射?

  • 为什么我不能删除映射?
  • 我可以有映射的映射吗? (是的,但这是如何工作的?)
  • 为什么有存储映射,但没有内存映射?

编译的合同如何看待EVM?

  • 合同是如何创建的?
  • 什么是构造函数,真的吗?
  • 什么是后备功能?

我认为了解像Solidity这样的高级语言如何在以太坊VM(EVM)上运行是一项很好的投资。 由于几个原因。

  1. 坚定不是硬道理。 更好的EVM语言即将到来。 (可以吗?)
  2. EVM是一个数据库引擎。 要理解智能合约如何以任何EVM语言工作,您必须了解数据的组织,存储和操作方式。
  3. 知道如何成为贡献者。 以太坊工具链还很早。 很好地了解EVM将帮助您为自己和其他人制作出令人敬畏的工具。
  4. 智力挑战。 EVM为您在加密,数据结构和编程语言设计的交叉点上提供了一个很好的借口。

在一系列文章中,我想解构简单的Solidity合约,以便了解它如何用作EVM字节码。

我希望学习和写作的概述:

  • EVM字节码的基础知识。
  • 如何表示不同的类型(映射,数组)。
  • 新合同创建时发生了什么。
  • 当一个方法被调用时发生了什么。
  • ABI如何桥接不同的EVM语言。

我的最终目标是能够理解整个编译的Solidity合同。 首先阅读一些基本的EVM字节码!

此EVM指令集表格将是一个有用的参考。

简单的合同

我们的第一份合同有一个构造函数和一个状态变量:

  // c1.sol 杂注扎实0.4.11; 
 合同C {  uint256 a; 
 函数C(){  a = 1;  }  } 

solc编译这个合同:

  $ solc --bin --asm c1.sol 
  ======= c1.sol:C =======  EVM组件:  / *“c1.sol”:26:94合约C {... * /  mstore(0x40,0x60)  / *“c1.sol”:59:92 function C(){... * /  jumpi(tag_1,iszero(callvalue)) 为0x0  DUP1 还原  TAG_1:  TAG_2:  / *“c1.sol”:84:85 1 * / 为0x1  / *“c1.sol”:80:81 a * / 为0x0  / *“c1.sol”:80:85 a = 1 * /  DUP2  swap1  sstore 流行的  / *“c1.sol”:59:92 function C(){... * /  tag_3:  / *“c1.sol”:26:94合约C {... * /  tag_4: 数据尺寸(sub_0)  DUP1  dataOffset(sub_0) 为0x0  codecopy 为0x0 返回 停止 
  sub_0:程序集{  / *“c1.sol”:26:94合约C {... * /  mstore(0x40,0x60)  TAG_1: 为0x0  DUP1 还原 
  auxdata:0xa165627a7a72305820af3193f6fd31031a0e0d2de1ad2c27352b1ce081b4f3c92b5650ca4dd542bb770029  } 
 二进制:  60606040523415600e57600080fd5b5b60016000819055505b5b60368060266000396000f30060606040525b600080fd00a165627a7a72305820af3193f6fd31031a0e0d2de1ad2c27352b1ce081b4f3c92b5650ca4dd542bb770029 

数字6060604052...是EVM实际运行的字节码。

在婴儿步骤

一半的编译程序集样板文件在大多数Solidity程序中都是相似的。 我们稍后再看看。 现在,让我们来看看我们合约的独特部分,简单的存储变量赋值:

  a = 1 

这个任务由字节码6001600081905550表示。 让我们把它分解成每行一条指令:

  60 01  60 00  81  90  55  50 

EVM基本上是一个从上到下执行每条指令的循环。 让我们使用相应的字节码来标注汇编代码(在标签tag_2下缩进),以更好地了解它们的关联方式:

  TAG_2:  // 60 01 为0x1  // 60 00 为0x0  // 81  DUP2  // 90  swap1  // 55  sstore  // 50 流行的 

请注意,汇编代码中的0x1实际上是push(0x1)的简写push(0x1) 。 该指令将数字1推入堆栈。

盯着它看看发生了什么仍然很难。 不要担心,很容易一行一行地模拟EVM。

模拟EVM

EVM是一个堆栈机器。 指令可能使用堆栈中的值作为参数,并将值作为结果推送到堆栈。 让我们考虑操作add

假设堆栈中有两个值:

  [1 2] 

当EVM看到add ,它将前两项添加到一起,并将答案推回到堆栈上,导致:

  [3] 

在下面的内容中,我们将用[]注释堆栈:

  //空的堆栈 堆栈:[]  //堆叠三个项目。 最上面的项目是3.最下面的项目是1。 堆叠:[3 2 1] 

并用{}标记合约存储空间:

  //存储空间不存在 商店:{}  //值0x1存储在位置0x0。 存储:{0x0 => 0x1} 

现在我们来看看一些真正的字节码。 我们将按照EVM模拟字节码序列6001600081905550 ,并在每条指令后打印机器状态:

  // 60 01:将1推入堆栈 为0x1 堆栈:[0x1] 
  // 60 00:将0推入堆栈 为0x0 堆栈:[0x0 0x1] 
  // 81:复制堆栈中的第二个项目  DUP2 堆栈:[0x1 0x0 0x1] 
  // 90:交换前两个项目  swap1 堆栈:[0x0 0x1 0x1] 
  // 55:将值0x1存储在位置0x0  //这条指令消耗前两项  sstore 堆栈:[0x1] 存储:{0x0 => 0x1} 
  // 50:流行(丢掉顶级商品) 流行的 堆栈:[] 存储:{0x0 => 0x1} 

结束。 堆栈是空的,并且存储中有一个项目。

值得注意的是, uint256 a决定将状态变量uint256 a存储在位置0x0 。 其他语言可以选择在别处存储状态变量。

在伪代码中,EVM为6001600081905550的实质上是:

  // a = 1  sstore(0x0,0x1) 

仔细看,你会发现dup2,swap1,pop是多余的。 汇编代码可能更简单:

 为0x1 为0x0  sstore 

您可以尝试模拟上述3条指令,并确保它们确实导致相同的机器状态:

 堆栈:[] 存储:{0x0 => 0x1} 

两个存储变量

我们添加一个相同类型的额外存储变量:

  // c2.sol 杂注扎实0.4.11; 
 合同C {  uint256 a;  uint256 b; 
 函数C(){  a = 1;  b = 2;  }  } 

编译,重点关注tag_2

  $ solc --bin --asm c2.sol 
  // ...更多的东西被省略 
  TAG_2:  / *“c2.sol”:99:100 1 * / 为0x1  / *“c2.sol”:95:96 a * / 为0x0  / *“c2.sol”:95:100 a = 1 * /  DUP2  swap1  sstore 流行的  / *“c2.sol”:112:113 2 * /  0X2  / *“c2.sol”:108:109 b * / 为0x1  / *“c2.sol”:108:113 b = 2 * /  DUP2  swap1  sstore 流行的 

伪代码中的程序集:

  // a = 1  sstore(0x0,0x1)  // b = 2  sstore(0x1,0x2) 

我们在这里学到的是两个存储变量0x0定位,位置为0x0 ,位置为0x1

存储包装

每个插槽存储可以存储32个字节。 如果一个变量只需要16个字节,那么使用全部32个字节是浪费的。 如果可能,通过将两个较小的数据类型打包到一个存储插槽中,Solidity可优化存储效率。

让我们改变ab使它们每个只有16个字节:

 杂注扎实0.4.11; 
 合同C {  uint128 a;  uint128 b; 
 函数C(){  a = 1;  b = 2;  }  } 

编制合同:

  $ solc --bin --asm c3.sol 

生成的程序集现在更复杂:

  TAG_2:  // a = 1 为0x1 为0x0  DUP1 为0x100  EXP  DUP2  SLOAD  DUP2  0xffffffffffffffffffffffffffffffff  MUL 不 和  swap1  dup4  0xffffffffffffffffffffffffffffffff 和  MUL 要么  swap1  sstore 流行的 
  // b = 2  0X2 为0x0 为0x10 为0x100  EXP  DUP2  SLOAD  DUP2  0xffffffffffffffffffffffffffffffff  MUL 不 和  swap1  dup4  0xffffffffffffffffffffffffffffffff 和  MUL 要么  swap1  sstore 流行的 

上述汇编代码将这两个变量一起打包在一个存储位置( 0x0 )中,如下所示:

  [b] [a]  [16字节/ 128位] [16字节/ 128位] 

打包的理由是因为目前最昂贵的操作是存储使用情况:

  • sstore花费20000瓦斯首先写入新的位置。
  • sstore花费5000瓦斯用于后续写入现有位置。
  • sload 500个天然气。
  • 大多数指令需要3〜10个气体。

通过使用相同的存储位置,Solidity为第二个存储变量支付5000而不是20000,为我们节省了15000个气体。

更多优化

应该可以将两个128位的数字一起打包在内存中,然后使用一个sstore存储它们,从而节省额外的5000个气体,而不是使用两个单独的sstore指令来存储ab

您可以通过打开optimize标志来让Solidity进行优化:

  $ solc --bin --asm --optimize c3.sol 

它生成仅使用一个sload和一个sstore的汇编代码:

  TAG_2:  / *“c3.sol”:95:96 a * / 为0x0  / *“c3.sol”:95:100 a = 1 * /  DUP1  SLOAD  / *“c3.sol”:108:113 b = 2 * /  0x200000000000000000000000000000000  not(sub(exp(0x2,0x80),0x1))  / *“c3.sol”:95:100 a = 1 * /  swap1  swap2 和  / *“c3.sol”:99:100 1 * / 为0x1  / *“c3.sol”:95:100 a = 1 * / 要么  sub(exp(0x2,0x80),0x1)  / *“c3.sol”:108:113 b = 2 * / 和 要么  swap1  sstore 

字节码是:

  600080547002000000000000000000000000000000006001608060020a03199091166001176001608060020a0316179055 

并将字节码格式化为每行一条指令:

  //按下0x0  60 00  // dup1  80  // sload  54  // push17将下一个17字节作为32字节的数字  70 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
  / * not(sub(exp(0x2,0x80),0x1))* /  //推送0x1  60 01  //按0x80(32)  60 80  //按0x80(2)  60 02  // exp  0A  //子  03  //不是  19 
  // swap1  90  // swap2  91  //和  16  //推送0x1  60 01  // 要么  17 
  / * sub(exp(0x2,0x80),0x1)* /  //推送0x1  60 01  //按下0x80  60 80  //推送0x02  60 02  // exp  0A  //子  03 
  //和  16  // 要么  17  // swap1  90  // sstore  55 

汇编代码中使用了四个魔术值:

  • 0x1(16字节),使用较低的16字节
  //在字节码中表示为0x01 
  16:32 0x00000000000000000000000000000000  00:16 0x00000000000000000000000000000001 
  • 0x2(16字节),使用更高的16字节
  //在字节码中表示为0x200000000000000000000000000000000 
  16:32 0x00000000000000000000000000000002  00:16 0x00000000000000000000000000000000 
  • not(sub(exp(0x2, 0x80), 0x1))
  //高16位字节的位掩码 
  16:32 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF  00:16 0x00000000000000000000000000000000 
  • sub(exp(0x2, 0x80), 0x1)
  //低16位字节的位掩码 
  16:32 0x00000000000000000000000000000000  00:16 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 

该代码执行一些比特 - 使用这些值进行混洗以达到期望的结果:

  16:32 0x00000000000000000000000000000002  00:16 0x00000000000000000000000000000001 

最后,这个32字节的值被存储在位置0x0

燃气使用

60008054700 200000000000000000000000000000000 6001608060020a03199091166001176001608060020a0316179055

请注意, 0x200000000000000000000000000000000嵌入在字节码中。 但是编译器也可以选择用指令exp(0x2, 0x81)来计算值,这会导致较短的字节码序列。

但事实证明, 0x200000000000000000000000000000000exp(0x2, 0x81)便宜。 我们来看看所涉及的天然气费用:

  • 对于交易的每个零字节的数据或代码支付4种气体。
  • 对于交易的每个非零字节的数据或代码,有68个气体。

让我们来比较一下在天然气中的代表费用。

  • 字节码为0x200000000000000000000000000000000 。 它有很多零,价格便宜。

(1 * 68)+(16 * 4)= 196。

  • 字节码608160020a 。 更短,但没有零。

5 * 68 = 340。

更多零的更长序列实际上更便宜!

概要

EVM编译器不会针对字节码大小,速度或内存效率进行精确优化。 相反,它优化了天然气使用量,这是一个间接的层面,可以激励以太坊区块链可以有效地进行计算。

我们已经看到了EVM的一些古怪的方面:

  • EVM是一款256位机器。 以32字节的块操作数据是最自然的。
  • 持久存储非常昂贵。
  • Solidity编译器做出了有趣的选择,以最大限度地减少燃气使用。

天然气成本有些任意设定,未来可能会发生改变。 随着成本的变化,编译器会做出不同的选择。


在这篇关于EVM的文章系列中,我写到:

  • EVM汇编代码简介。
  • 如何表示固定长度的数据类型。
  • 如何表示动态数据类型。
  • ABI如何编码外部方法调用。
  • 新合同创建时发生了什么。

https://blog.qtum.org/diving-into-the-ethereum-vm-6e8d5d2f3c30

[译】Diving Into The Ethereum VM相关推荐

  1. 【译】 Diving Into The Ethereum VM Part 6 - How Solidity Events Are Implemented

    在如何解读智能合约方法调用中,我们了解到"方法"是如何构建在简单EVM基元之上的抽象,如"跳转"和"比较"指令. 在本文中,我们将深入探讨S ...

  2. 【译】Diving Into The Ethereum VM Part 4 - How To Decipher A Smart Contract Method Call

    在本系列的前几篇文章中,我们已经看到了Solidity如何在EVM存储中表示复杂的数据结构. 但是如果没有办法与数据交互,数据就毫无用处. 智能合约是数据与外部世界的中介. 在本文中,我们将看到Sol ...

  3. 【译】Diving Into The Ethereum VM Part 3 — The Hidden Costs of Arrays

    Solidity提供了其他编程语言中常见的数据结构. 除了诸如数字和结构之类的简单值之外,还有数据类型可以随着更多数据的添加而动态扩展. 这些动态类型的三个主要类别是: 映射: mapping(byt ...

  4. 【译】Diving Into The Ethereum VM Part 5 — The Smart Contract Creation Process

    在本系列的前几篇文章中,我们学习了EVM汇编的基础知识,以及ABI编码如何允许外部世界与合同进行通信. 在这篇文章中,我们将看到合同是如何从无到有的. 本系列的前几篇文章(按顺序). EVM汇编代码简 ...

  5. 【译】Diving Into The Ethereum VM Part 2 — How I Learned To Start Worrying And Count The Storage Cost

    在本系列的第一篇文章中,我们窥见了一个简单的Solidity合约的汇编代码: 合同C { uint256 a; 函数C(){ a = 1; } } 该合约归结为sstore指令的调用: // a = ...

  6. 从比特币脚本引擎到以太坊虚拟机

    这个系列是目标受众是区块链开发者和有其他开发经验的CS专业学生 面对媒体对区块链相关技术的解读和吹捧,许多人一时不知所措.投资人.大公司都在FOMO(fear of missing out)的心理驱动 ...

  7. 深入了解以太坊虚拟机第4部分——ABI编码外部方法调用的方式

    本文由币乎社区(bihu.com)内容支持计划赞助. 在本系列的上一篇文章中我们看到了Solidity是如何在EVM存储器中表示复杂数据结构的.但是如果无法交互,数据就是没有意义的.智能合约就是数据和 ...

  8. 深入了解以太坊虚拟机第3部分——动态数据类型的表示方法

    本文由币乎社区(bihu.com)内容支持计划赞助. Solidity提供了在其他编程语言常见的数据类型.除了简单的值类型比如数字和结构体,还有一些其他数据类型,随着数据的增加可以进行动态扩展的动态类 ...

  9. 深入了解以太坊虚拟机第2部分——固定长度数据类型的表示方法

    本文由币乎社区(bihu.com)内容支持计划赞助 在本系列的第一篇文章中,我们已经看到了一个简单的Solidity合约的汇编代码: contract C {uint256 a;function C( ...

最新文章

  1. springmvc DispatchServlet初始化九大加载策略(二)
  2. POJ 1716 区间最小点个数
  3. 大一计算机课程ppt作业,大学生计算机基础作业PPT.ppt
  4. C语言 指针数组-字符指针数组整型指针数组 char*s[3] int*a[5] 数组指针int(*p)[4]
  5. JavaScript的调用栈、回调队列和事件循环
  6. 杠上谷歌,微软利用人工智能加码必应搜索的市场竞争力
  7. MySQL 代码开发注意事项----开发高性能的sql
  8. ftl生成word文档
  9. 生物信息学:根据PDB名称、爬虫PDB数据库的信息、保存到Excel里
  10. H5抽奖小游戏万圣节抓南瓜
  11. 安装win10 我们无法格式化所选分区的解决办法
  12. Python学习模块 Pygame写游戏二(太空大战)
  13. word2vec模型训练保存加载及简单使用
  14. VMware虚拟机安装windows8 时报错“Oxc000035a”解决办法
  15. print out Fibonacci reversely
  16. Excel服务器数据库修改,用excel做服务器数据库
  17. 白苹果了怎么办_ios13更新遭遇白苹果了怎么办?
  18. android手机照片恢复,安卓手机照片怎么恢复?简单恢复方法分享
  19. 西安交通大学计算机在线作业,西交《计算机组成原理》在线作业.docx
  20. 永中科技破产拍卖为何无效?

热门文章

  1. hdu 1087 最大递增和
  2. ibatis mybatis传入List参数
  3. zpf 获取表单等数据的用法
  4. Django admin的一些有用定制
  5. SQL Server 数据库安全
  6. xcat 安装(liunx高性能刀片集群管理软件)
  7. 二十万字C/C++、嵌入式软开面试题全集宝典五
  8. [云炬创业基础笔记]第二章创业者测试7
  9. [云炬创业基础笔记]第一章创业环境测试11
  10. [云炬创业基础笔记]第六章商业模式测试11