EVM原理及其功能扩展

EVM运行机制概述

EVM即以太坊虚拟机,用于执行智能合约。智能合约可用高级开发语言Solidity进行开发,合约源代码经过编译得到可在EVM中运行的字节码。在部署合约、与合约交互的时候,字节码都是以16进制字符串形式传递和展现。

EVM运行过程中,其本身并不是一个独立的协程、线程更不是进程,它只是交易处理的一部分,在交易处理过程中以函数方式被调用。

调用路径为:StateProcessor.Process --> core.ApplyTransaction(初始化evm对象) --> StateTransition.TransitionDb() --> 根据交易类型执行 evm.Create 或 evm.Call。

在evm.Create中会执行相关验证、转账、初始化Contract对象并调用 run 函数开始执行合约代码,执行成功后获得返回值也就是要存储到链上的合约代码,将返回值存储到链上。

而evm.Call既是调用入口,同时它本身也是一个可递归的函数,在合约字节码中指令 0xf1 即代表CALL操作,在CALL操作中会递归调用evm.Call。在evm.Call中会执行验证及转账、从数据库获取合约代码初始化Contract对象、并调用 run 函数开始执行合约代码这些处理。

重要:上述"从数据库获取合约代码初始化Contract对象"意味着,给合约账户进行转账时,合约账户所关联的合约代码将会被执行。
在合约代码中,会固定包含检查转账金额的逻辑,如果金额大于0,则会执行合约的fallback函数。
如果合约没有fallback函数,或者fallback函数没有payable修饰符,则代码执行会抛异常,从而交易失败。

EVM是基于栈的虚拟机,另外会有一个内存空间用于临时存储数据,而最终大部分的指令都是对栈中的数据进行操作。

EVM数据存储概述

合约状态值(状态变量、状态常量,即需要持久化的内容)在底层数据库中的存储方式,可用几条规则概括:

  1. 参照磁带存储原理,可以认为一个合约对应了一条无限长的磁带,磁带上以32字节为单位,拥有无数个存储槽;每个存储槽的位置就是它的key,也是用32字节表示。
  2. 对于简单的,大小在32字节以内的变量,以定义变量的顺序作为它的key来存储变量值。即第一个变量的key为0,第二个变量的key为1,……
  3. 结构体和定长数组也是顺序存储(只要每个值都是32字节以内的),比如结构体变量定义在位置1,结构体内部要两个成员,则这两个成员的key依序为 1和2。数组类似,只是在处理数组时编译器会多加一些边界检查的代码。
  4. 连续的若干个小的值,可能被优化为存储的同一个位置,比如:合约中前四个状态变量都是uint64类型的,则四个状态变量的值会被打包成一个32字节的值存储在0位置。
  5. map中内容的存储,如果map中的value在32字节以内,则会按以下公式得到数据库中的key:keccak256(bytes32(map中的key)+bytes32(map变量的位置)); 例如,一个map变量在合约中最先定义,map中一个key为"abc",则其在数据库中的存储位置为:keccak256(bytes32("abc")+bytes32(0))
  6. 如果map中的value是一个复杂类型,存储需求超过32字节,则会按上述公式得到第一个存储位置,然后依序加1得到后续数据的存储位置;
  7. 可变长度数组,与map类似,但更复杂点:以数组变量所在位置为key,存储数组的长度。然后从keccak256(bytes32(position))开始存储数组中的元素;
  8. 可变长度字节数组和字符串一样:如果长度小于等于31字节,则直接在变量位置处存储字符串值,并用值的最后一个字节存储字符串的编码长度。编码长度 = 字符数 * 2 。比如,"abc"的存储值为 "0x6162630000000000000000000000000000000000000000000000000000000006"
  9. 当可变长度字节数组或字符串长度大于31字节时,变量位置存储的是 编码长度,而此时编码长度公式变为 编码长度 = 字符数 * 2 + 1 。 然后,从 keccak256(bytes32(position))位置,使用连续的若干个存储槽存储字符串值。 从而,对于字符串,如果编码长度是奇数,则代表的是长字符串,如果是偶数则代表不超过31字节的字符串。

EVM代码结构

evm的代码都在core包里面,除了入口相关的一些代码,具体运行合约的代码都在core/vm包下

代码文件或结构体 说明
evm.go 定义了EVM运行环境结构体,并实现 转账处理 这些比较高级的,跟交易本身有关的功能
vm/evm.go 定义了EVM结构体,提供Create和Call方法,作为虚拟机的入口,分别对应创建合约和执行合约代码
vm/interpreter.go 虚拟机的调度器,开始真正的解析执行合约代码
vm/opcodes.go 定义了虚拟机指令码(操作码)
vm/instructions.go 绝大部分的操作码对应的实现都在这里
vm/gas_table.go 绝大部分操作码所需的gas都在这里计算
vm/jump_table.go 定义了operation,就是将opcode和gas计算函数、具体实现函数等关联起来
vm/stack.go evm所需要的栈
vm/memory.go evm的内存结构
vm/intpool.go *big.Int的池子,主要是性能上的考虑,跟业务逻辑无关

evm的opcode,大体上可以粗略地分为两类,一类是基础操作,如压栈、出栈、加减乘除等数学运算、逻辑比较、hash等等;另一类是跟交易业务密切相关的,可以称为业务指令,比如BALANCE、ADDRESS、CALLER、CALL等等,这些指令有些对应了Solidity中的全局函数或属性。

通过Solidity编写的合约,需要进行编译,编译后变成可供虚拟机执行的二进制码,这些二进制码实际上在所有地方都按16进制字节数组或字符串表示。

编译后二进制码的结构

以一个最简单的无实际功能无构造器的合约为例,看看编译后代码的结构。 合约代码:

pragma solidity ^0.4.11;
contract C {
}

编译后得到这样一串数据:60606040523415600e57600080fd5b5b603680601c6000396000f30060606040525b600080fd00a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029 这串数据需要作为16进制格式显示的字节数组对待,这就是evm需要执行的代码。

上面这串代码可分为三部分,即:部署代码、合约代码、Auxdata。

  1. 部署代码 在创建合约的时候,evm.Create会先创建合约账户,然后运行部署代码,运行完成后,它会将 合约代码+Auxdata 作为返回值返回,然后evm.Create函数中就会将返回值跟合约账户关联起来存储在区块链上,这样就完成了合约的部署。 上述代码中,部署代码为前面的 60606040523415600e57600080fd5b5b603680601c6000396000f300

  2. 合约代码 在这个合约中,合约代码只有11字节:60606040525b600080fd00 这部分的代码就是存储在链上,供后续调用的代码。

  3. Auxdata 每个合约最后面的43字节,就是Auxdata,会跟在合约代码后面被存储起来。即a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029 由于后续跟合约交互,是需要知道合约的ABI(应用二进制接口,也就是合约接口的描述信息)的,而在链上却没有存储ABI信息, 所以,要么就只能在部署合约时,用户自己保存好ABI以及合约地址(以太坊钱包就有这样的功能,帮我们保存了这些信息),但这样的话就只有自己能调用这个合约,别人不知道ABI就不能调用合约。 想要让别人也能调用我们部署的合约,在以太坊中提供了两种机制,一个就是用户直接将相关信息上传到etherscan.io这个网站跟合约关联起来,另一个就是以太坊的swarm网络。 而这里的Auxdata,就是给swarm网络使用的,可以认为就是swarm网络的地址,也就是以太坊希望后续自动将合约的相关信息包含ABI存储到swarm中,这样任何一个人从区块上查询到合约代码后,就可以通过auxdata到swarm网络中下载合约的信息。 Auxdata的固定格式为:0xa1 0x65 'b' 'z' 'z' 'r' '0' 0x58 0x20 <32 bytes swarm hash> 0x00 0x29

  4. 构造函数参数 如果合约有构造函数参数,则创建合约时还需要跟上编码之后的参数,代码就变成如下结构:部署代码+合约代码+Auxdata+构造参数 构造参数的编码方式,就是将参数值按顺序编码成32字节的数据,连接起来。不同的类型有不同的规则,具体见Solidity文档。

功能扩展

evm是以太坊合约的执行部分,想要从合约编程语言层面扩展功能,就需要同时在Solidity上和evm上实现扩展。Solidity上实现功能扩展,最重要的就是弄清楚它的编译过程。 Solidity是面向用户的高级编程语言,其中的一句代码,可能编译后就对应了一堆字节码。 真正要扩展功能,主要涉及以下几点:

  1. 增加opcode,从而与已有的opcode做功能上的区分,也就是扩展了指令
  2. 需要在Solidity语言中,提供供用户调用的接口,比如对Solidity中的address对象,增加address.balanceOf(bytes10 symbol)函数;
  3. 在Solidity编译器中,支持上述扩展的接口的识别,并编译成一段正确的指令组合。这段指令组合从逻辑上可以认为由三部分组成:新扩展指令所需参数的准备阶段+新扩展的指令+对结果的处理
  4. 在EVM中增加新扩展的指令的功能支持。

以下几点,有助于理解如何进行功能扩展:

  1. evm是基于栈的,用一个字节值表示指令。因此evm中指令的个数最多只有0x0~0xff共256个。因此任何一个指令,都有如下一些属性:(1)指令码;(2)该指令需要消耗栈顶多少个元素;(3)该指令执行完后,会往栈里压入几个元素;(4)其他一些属性,如针对PUSH指令,它所额外需要的memory中的元素数量,以及它是否除了对栈的操作外,还产生其他的影响等。
  2. 需要定义清楚指令所需栈顶的若干个元素的顺序,栈顶值代表什么,第二个值代表什么,在执行到这个指令之前,所有数据需要在栈中就绪;也就是,编译过程不能将高级语言的一条语句,对应成底层的一个指令,而应该是一堆指令。

在evm中扩展功能,相对比较简单,具体就是:

  1. 在opcode中定义指令码
  2. 在instructions中实现指令的处理,处理就是按照指令定义从栈顶取需要的数据,然后将结果压入栈顶。
  3. 在gas_table中提供gas函数
  4. 在jump_table中增加由上述内容组合成的operation

参考

深入了解以太坊虚拟机

EVM原理及其功能扩展相关推荐

  1. 腾讯基于 Flink SQL 的功能扩展与深度优化实践

    简介:本文由腾讯高级工程师杜立分享,主要介绍腾讯实时计算平台针对 Flink SQL 所做的优化. 整理:戴季国(Flink 社区志愿者) 校对:苗文婷(Flink 社区志愿者) 摘要:本文由腾讯高级 ...

  2. 【转】WPF自定义控件与样式(3)-TextBox RichTextBox PasswordBox样式、水印、Label标签、功能扩展...

    一.前言.预览 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等. 本文主要是对文本输入控件进行样式开发,及相关扩展功能开发,主要内容包括: 基本文 ...

  3. 基于开源蜜罐的实践与功能扩展

    0×00 前言 具有一定规模的公司都会有自己的机房,当网络规模和硬件系统到达一定程度,就需要跟进各种安全预警防护手段,而蜜罐系统就是一种常见的防护手段之一,蜜罐主要是通过在网络环境当中,用虚拟各种真实 ...

  4. TYPEC 转HDMI VGA+PD+ USB3.0 HUB+TF/CF/SD+RJ45等多功能扩展坞方案设计资料|TYPE-C转HDMI转VGA带PD USB3.0多功能拓展器方案介绍

    如何对TYPE-C转HDMI转VGA多功能扩展坞或者拓展器选择一款有效且低成本的方案?针对这一命题,我们选择一款芯片AG9321MCQ来实现设计,且这款方案是Algoltek安格科技 在2020年针对 ...

  5. 高并发其实挺容易的,当你明白了一万并发的原理,然后扩展到百万、千万、亿万级很easy

    来自知乎的一个大神的回答:https://zhuanlan.zhihu.com/p/38636111 高并发其实挺容易的,当你明白了一万并发的原理,然后扩展到百万.千万.亿万级很easy 要点有如下几 ...

  6. mini3d源码解析及功能扩展

    目录标题 简介 代码简析 功能扩展 完善三维变换功能 增加简单光照 小结 简介 mini3d是前网易员工@韦易笑开发的3d软渲染引擎,总代码量不到1000行,短小精悍,适合初学者学习. 本文结合源码给 ...

  7. usb扩展坞同时接键盘鼠标_这个多功能扩展坞,增加多个接口,笔记本秒变工作站...

    原标题:这个多功能扩展坞,增加多个接口,笔记本秒变工作站 为了给笔记本电脑"减负" 越做越薄,接口也是越来越少 很多接口功能都被省去 ... 可是想接入的设备却有很多:鼠标.U盘. ...

  8. C++类功能扩展预留五招

    第一招虚函数 通过派生类来进行功能扩展是基本的面向对象的方式,这种方式大如下: class base { public: virtual ~base(){} virtual void fun() { ...

  9. VSS自动发布站点功能扩展

      我们在做开发的时候,经常使用源代码管理器作为团队开发其中一种必备工具,在软件项目开发过程中,采用科学的管理思想,辅之以先进的管理工具,可以提升软件开发管理水平和保证软件的产品质量. 它使我们团队之 ...

最新文章

  1. python多线程用法及与单线程耗时比较
  2. 使用uWSGI部署django项目
  3. LintCode-7-二叉树的序列化和反序列化
  4. Qt 中static_cast 和 reinterpret_cast的区别
  5. 2021.01.04
  6. 让.net 2.0支持并行计算
  7. 依赖注入(DI)入门
  8. python输出unicode字符_如何在Python中打印Unicode字符?
  9. word流程图怎么使箭头对齐_word 流程图 怎么把箭头对整齐啊?
  10. 【踩坑笔记】java使用poi导出word文档换行
  11. SIRO Challenge 状态压缩 + DP 未解
  12. File.createTempFile创建临时文件
  13. JS正则——将字符串中的逗号替换成空格
  14. 收藏!人工智能学习路径总结
  15. 中小企业国际市场营销策略研究
  16. 支付宝-相互保,创新。
  17. POI XWPFParagraph.getRuns分段混乱问题解决
  18. 戴尔sc系列存储阵列柜服务器,Dell存储Compellent SC4020
  19. python判断汉字个数_python判断列表里数量python中文乱码问题大总结
  20. 阿桑奇,真正的网络英雄!

热门文章

  1. 2002年CRM沙场秋点兵
  2. 调试4G使用AT指令ping百度IP
  3. Linux常用搜索命令
  4. 350 皮秒脉冲发生器使用
  5. 自然语言处理:基于预训练模型的方法(一)
  6. 远程会议的正确打开方式
  7. 计算机基础-分类介绍
  8. (十六)Alian 的 Spring Cloud Eureka 集群配置(主机名方式)
  9. js中将一句英文中每个单词的首字母转成大写 (how are you and are you fine)
  10. 五大“创新”崛起软件城