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操作栈的指令,在后面分析合约的时候会用到;
- Pop指令(操作码0x50)用来从栈顶弹出一个元素;
- PushX指令用来把紧跟在后面的1-32字节元素推入栈顶,Push指令一共32条,从Push1(0x60)到Push32(0x7A),因为栈的一个
字
是256bit,一个字节8bit,所以Push指令最多可以把其后32字节的元素放入栈中而不溢出。 - DupX指令用来复制从栈顶开始的第1-16个元素,复制后把元素在推入栈顶,Dup指令一共16条,从Dup1(0x80)到Dup16(0x8A)。
- SwapX指令把栈顶元素和从栈顶开始数的第1-16个元素进行交换,Swap指令一共16条,从Swap1(0x01)一直到Swap16(0x9A)。
Args
args
也叫calldata
,是一段只读的可寻址的保存函数调用参数的空间,与栈不同的地方的是,如果要使用calldata里面的数据,必须手动指定偏移量和读取的字节数。
EVM提供的用于操作calldata的指令有三个:
calldatasize
返回calldata的大小。calldataload
从calldata中加载32bytes到stack中。calldatacopy
拷贝一些字节到内存中。
通过一个合约来看一下如何使用calldata
,假如我们要写一个合约,合约有一个add的方法,用来把传入的两个参数相加,通常会这样写。
当然我们也可以用内联汇编的形式这样写。
首先我们我们加载了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则采用了其他方法,下篇文章在讲 (╹▽╹)