初探EVM虚拟机

以太坊虚拟机 EVM 的作用是将智能合约代码翻译成可以在以太坊上执行的机器码,并且提供一个沙盒运行环境,在运行期间不能访问宿主机的网络,文件,系统,即使不同的合约之间也有有限的访问权限。

EVM的特点

官方给出的EVM主要的设计目标如下:

  • 简单性,操作码尽可能少且低级,数据类型尽可能少,虚拟机的结构尽可能简单。
  • 确定性,EVM的语句没有产生歧义的空间,结果在不同机器上的执行结果是确定一致的。
  • 节约空间,EVM的组件尽可能紧凑。
  • 区块链定制化的,必须可以处理20bytes的账户地址,自定义32bytes密码学算法的等。
  • 安全模型简单安全,Gas的计价模型应该是简单易行准确的。
  • 便于优化,以便即时编译(JIT)和VM的性能优化。

EVM基本信息

以太坊是一种基于栈的虚拟机,基于栈的虚拟机数据的存取为先进先出,在后面介绍EVM指令的时候会看到这个特性。同时基于栈的虚拟机实现简单,移植性也不错,这也是以太坊选择基于栈的虚拟机的原因。

EVM采用了32字节(256bit)的 字长 ,最多可以容纳2014个 为最小的操作单位。

数据管理

接下来看一下EVM的数据是如何管理的。

可以看到code和storage里存储的数据是非易失的,而stack,args,memory里存储的数据是易失的,其中code的数据是智能合约的二进制源码,是非易失的很好理解,部署合约的时候data字段也就是合约内容会存储在EVM的code中。

如果要操作这些存储结构里的数据,就需要用到EVM指令,由于EVM的操作码被限制在一个字以内,所以EVM最多容纳256条指令,目前EVM已经定义了约142条指令,还有100多条用于以后的扩展。这142条指令包括了算法运算,密码学计算,栈操作,memory,storage操作等。

接下来看一下各个存储位置的含义;

Stack

stack 可以免费使用,没有gas消耗,用来保存函数的局部变量,数量被限制在了16个,当在合约里中声明的局部变量超过16个时,再编译合约就会遇到 Stack too deep, try removing local variables 错误。

介绍几个EVM操作栈的指令,在后面分析合约的时候会用到;

Args

args 也叫 calldata ,是一段只读的可寻址的保存函数调用参数的空间,与栈不同的地方的是,如果要使用calldata里面的数据,必须手动指定偏移量和读取的字节数。

EVM提供的用于操作calldata的指令有三个:

calldatasize
calldataload
calldatacopy

通过一个合约来看一下如何使用 calldata ,假如我们要写一个合约,合约有一个add的方法,用来把传入的两个参数相加,通常会这样写。

pragma solidity ^0.5.1;

contract Calldata {
  function add(uint256 a, uint256 b) public view returns (uint256) {
      return a + b;
  }
}

当然我们也可以用内联汇编的形式这样写。

contract Calldata {
  function add(uint256 a, uint256 b) public view returns (uint256) {
    assembly {
      let a := mload(0x40)
      let b := add(a, 32)
      calldatacopy(a, 4, 32)
      calldatacopy(b, add(4, 32), 32)
      result := add(mload(a), mload(b))
    }
  }
}

首先我们我们加载了0x40这个地址,这个地址EVM存储空闲memory的指针,然后我们用a重命名了这个地址,接着我们用b重命名了a偏移32字节以后的空余地址,到目前为止这个地址所指向的内容还是空的。

calldatacopy(a, 4, 32) 这行代码把calldata的从第4字节到第36字节的数据拷贝到了a中,同样 calldatacopy(b, add(4, 32), 32) 把36到68字节的数据拷贝到了b中,接着 add(mload(a), mload(b)) 把栈中的a,b加载到内存中相加。最后的结果等价于第一个合约。

而为什么 calldatacopy(a, 4, 32) 的偏移要从4开始呢?在EVM中,前四位是存储函数指纹的,计算公式是bytes4(keccak256(“add(uint256, uint256)”)),从第四位开始才是args。

Memory

Memory是一个易失性的可以读写修改的空间,主要是在运行期间存储数据,将参数传递给内部函数。内存可以在字节级别寻址,一次可以读取32字节。

EVM提供的用于操作memory的指令有三个:

  • Mload加载一个字从stack到内存;
  • Mstore存储一个值到指定的内存地址,格式mstore(p,v),存储v到地址p;
  • Mstore8存储一个byte到指定地址 ;

当我们操作内存的时候,总是需要加载0x40,因为这个地址保存了空闲内存的指针,避免了覆盖已有的数据。

Storage

Storage是一个可以读写修改的持久存储的空间,也是每个合约持久化存储数据的地方。Storage是一个巨大的map,一共2^256个插槽,一个插糟有32byte。

EVM提供的用于操作storage的指令有两个:

  • Sload用于加载一个字到stack中;
  • Sstore用于存储一个字到storage中;

solidity将定义的状态变量,映射到插糟内,对于静态大小的变量从0开始连续布局,对于动态数组和map则采用了其他方法,下篇文章在讲 ( ╹▽╹ )

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章