以太坊EVM智能合约中的数据存储

article/2025/10/30 23:38:27

目录

EVM基本信息

数据管理

Stack

Args

Memory

Storage

固定长度的值

动态长度数组

Mappings

复杂类型的组合

 总结


EVM基本信息

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

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

数据管理

       可以看到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的方法,用来把传入的两个参数相加,通常会这样写。

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则采用了其他方法。

固定长度的值

对于已知拥有固定长度的值,通常的方法是在存储空间给他们分配一个预留的位置存储值。

contract StorageTest {uint256 a;uint256[2] b;struct Entry {uint256 id;uint256 value;}Entry c;
}

对于上面的代码:

  • a存储在slot 0(在solidity属于中,存储空间中的每一个位置称为“slot”)
  • b存储在slot 1和2中(因为b是一个数组,且数组长度为2)
  • c存储位置从slot 3开始,消耗两个slot,因为Entry结构体存储了两个32字节长度的值

这些slot位置在合约编译的时候就确定了,并且严格按照变量在合约中的定义顺序确定的。

动态长度数组

动态长度的数组需要一个位置来存储数组的长度和数组中的所有元素。

contract StorageTest {uint256 a;     // slot 0uint256[2] b;  // slots 1-2struct Entry {uint256 id;uint256 value;}Entry c;       // slots 3-4Entry[] d;
}

对于上面的代码:

  • 动态数组d存储在slot 5位置中,但slot 5中存储的值是数组d的长度,数组中的元素连续存储在以hash(5)开始的位置中。即通过对动态数组dslot进行hash运算,求出数组中的元素存储位置。

下面一段solidity代码用来计算动态数组中元素的位置:

function arrLocation(uint256 slot, uint256 index, uint256 elementSize)publicpurereturns (uint256)
{return uint256(keccak256(slot)) + (index * elementSize);
}

Mappings

映射需要找到与给定键对应的位置的有效方法。散列键是一个好的开始,但是必须注意确保不同的映射生成不同的位置。

mapping需要一个有效的方法通过给定的key找到相应的存储位置,通过对mapping的key进行hash运算是一个不错的方法,但需要确保针对不同的mapping对象的相同的key生成不同的存储位置。

contract StorageTest {uint256 a;     // slot 0uint256[2] b;  // slots 1-2struct Entry {uint256 id;uint256 value;}Entry c;       // slots 3-4Entry[] d;     // slot 5 for length, keccak256(5)+ for datamapping(uint256 => uint256) e;mapping(uint256 => uint256) f;
}

对于以上代码:

  • eslot是6,fslot是7,但在这两个位置中并没有存储任何值,因为mapping没有长度值需要存储

要寻找mapping中的值的位置,需要将keymappingslot一起进行hash运算。

下面的solidity函数用于计算mapping中值的存储位置:

function mapLocation(uint256 slot, uint256 key) public pure returns (uint256) {return uint256(keccak256(key, slot));
}

 注意:当传入多个参数到keccak256方法时,首先会将这些参数进行连接,然后在进行hash运算。因为是将mapping的slot值与key值同时进行的hash运算,所以不同mapping之间是不会存在冲突的。

复杂类型的组合

动态大小的数组与mapping可以相互嵌套在一起,当这种情况发生时,可以通过递归的方式找到值的存储位置。

contract StorageTest {uint256 a;     // slot 0uint256[2] b;  // slots 1-2struct Entry {uint256 id;uint256 value;}Entry c;       // slots 3-4Entry[] d;     // slot 5 for length, keccak256(5)+ for datamapping(uint256 => uint256) e;    // slot 6, data at h(k . 6)mapping(uint256 => uint256) f;    // slot 7, data at h(k . 7)mapping(uint256 => uint256[]) g;  // slot 8mapping(uint256 => uint256)[] h;  // slot 9
}

要寻找这些复杂类型中的值的存储位置可以使用上面定义的函数:

  • arrLocation
  • mapLocation

例:寻找g[123][0]的存储位置:

// 首先找到g[123]的位置,g是mapping,g的slot是8,key是123,用mapLocation计算存储位置
arrLoc = mapLocation(8, 123);  // g is at slot 8// 然后 查找arr[0]
itemLoc = arrLocation(arrLoc, 0, 1);

例:寻找 h[2][456]的存储位置:

// 首先查找h[2]位置,h是动态数组,h的slot是9
mapLoc = arrLocation(9, 2, 1);  // h is at slot 9// 然后查找 map[456]位置
itemLoc = mapLocation(mapLoc, 456);

 总结

  • 每个智能合约中的storage都是以2^256长度的数组形式存在的,并且数组中的所有元素的初始值为0
  • 0值是不会被显示存储的,所以当给一个对象赋值0时,就相当于生命回收相应的存储空间
  • 对与固定长度的值,solidity是通过预分配的方式分配存储位置的
  • 对于动态长度类型的值,Solidity通过hash运算的方法动态确定存储位置

下表展示了如何计算不同类型的存储位置。slot表示在合约中定义的变量的位置。

KindDeclarationValueLocation
一般类型T vvv's slot
定长数组T[10] vv[n](v's slot) + n * (size of T)
不定长数组T[] vv[n]keccak256(v's slot) + n * (size of T)
v.lengthv's slot
Mappingmapping(T1 => T2) vv[key]keccak256(key . (v's slot))
  • 查看合约的存储可通过编译合约查看
solc --bin --asm --optimize test.sol
  • 参考链接:
  • 理解以太坊中智能合约中的存储
  • EVM深度分析之数据存储

http://chatgpt.dhexx.cn/article/mOwImykx.shtml

相关文章

以太坊虚拟机EVM究竟是个啥

基本概念 EVM(ETHereum Virtual Machine)是「以太坊虚拟机」的缩写。如果你有一些软件开发的背景,一定听过java虚拟机。通俗的解释java虚拟机的就是: 我们写的java代码(不只是java,groovy,scala等也可以&a…

介绍 EVM 等效性

介绍 EVM 等效性 上个月,我们宣布了Optimistic 以太坊历史上最重要的升级。最近,我们将Optimistic Kovan迁移到真正的一键式部署,并增加了稳定性,主网在不到三周的时间内就会跟上。 这篇文章是关于我们相信EVM 等效性——完全符合…

EVM的深入研究和分析

最终目标是能够完整地理解已编译的Solidity合同 1、执行 evm-tools 安装 https://github.com/CoinCulture/evm-tools/blob/master/INSTALL.md /home/xue/go/bin/evm --debug --code 366020036101000a600035045b6001900380600c57 --input 05 不同字节码编译成不同EVM指令 1、基…

Ethereum EVM简介

1. 引言 首先需了解以下基本概念: 1)区块链2)世界状态3)账号4)交易5)消息6)去中心化账本7)原子性和顺序性 1.1 何为以太坊区块链? 以太坊可看成是基于交易的状态机&a…

误差向量幅度(EVM)

转自:http://blog.sina.com.cn/s/blog_6c46cb860100otm3.html 误差向量幅度(EVM):误差向量(包括幅度和相位的矢量)是在一个给定时刻理想无误差基准信号与实际发射信号的向量差。Error Vector Magnitude E…

WiFi基础知识

术语和定义 1. 发射功率RF Power 在给定频段范围内发射机通过天线对空间辐射的能量。 2. 矢量误差(EVM) EVM是发射信号理想状态下的IQ分量与实际发送信号的IQ分量之间的矢量差,如图1 所示,其数值等于误差矢量幅度与最大符号幅度之比(取百…

三分钟读懂什么是EVM

虚拟机指的是,通过软件模拟的、具备完整硬件系统功能并运行在隔离环境下的完整计算机系统,比如虚拟化物理机VMware、Java虚拟机等。实际上在PC上常见的各种手机模拟器、主机模拟器,也都是一种虚拟机。这样的虚拟机往往需要固件运行&#xff0…

射频指标之EVM

说到EVM首先先介绍下EVM是什么,其是指目标功率与实际功率的一个矢量差,用下图可以比较详细的表示: 下面就以几个问题来讨论EVM的问题: 1.问题背景: TC芯片输出的EVM正常,单独测试PA输出的EVM也正常&#…

.lib 静态链接库 的破解方法(局限)(1)

因为之前程序调用了一个试用版的.lib库 , 而这个库有时间限制 , 导致程序在试用期过后不能使用 . 然后编译之后要破解一下编译出来的程序 , 很难受 闲来无事,突然想破解一下.lib , 毕竟是个代码库 , 看看是不是破解完了在编译 , 能正常使用 结果当然是能正常用了 不然就没这…

InfluxDB源码编译、安装、配置及主从同步实现

先扯点蛋 公司有个项目要求使用InfluxDB时序数据库储存点东西。第一次听说还有这种数据库,哈哈哈,孤陋寡闻了,先从各位大佬的博客看起,慢慢学习,逐渐了解了之后在服务器上进行安装。直接使用官方包进行安装很简单&…

一种破解静态链接库(.lib)的简单方法

一种破解静态链接库(.lib)的简单方法 作者:游蓝海 博客:http://blog.csdn.net/you_lan_hai 最近,在研究某代码时,遇到这样一个问题:整个解决方案中,有一个工程没有cpp源码,只有头文件跟一个静…

Linux下静态库生成和使用

一.静态库概念 1.库是预编译的目标文件(object files)的集合,它们可以被链接进程序。静态库以后缀为”.a”的特殊的存档(archive file)存储。 2.标准系统库可在目录/usr/lib与/lib中找到。比如,在类Unix系统中C语言的数序库一般…

libuvc介绍及简单使用

libuvc是一个用于USB视频设备的跨平台库,构建在libusb之上,编译libuvc时需要依赖libusb。libuvc的License为BSD,最新发布版本为0.0.6,源码地址: https://github.com/libuvc/libuvc libuvc支持在非windows系统上直接编译&#xff0…

linux下封装函数库——动态库.so和静态库.a(代码实现及链接方式)

在linux环境下的链接库分为静态链接库(.a库)和动态链接库(.so库),其作用是把C程序编译好做成一种可执行链接文件,主程序文件调用这些程序的函数接口是可以使用a库或so库,在主程序中只需要includ…

Hyperledger fabric应用的多机部署(自动化一键部署)

前面关于fabric部署的介绍都是基于单机环境下的,实际生产环境中一般会根据应用场景将节点分开部署在多台物理机上,面临的难题主要是不同主机间的节点如何通过网络进行通信。文章最后会分享一键完成多机增加组织的自动化部署脚本。 前言 这里仍然以balan…

Linux系统编程makefile制作动态库和静态库

目录 制作动态库 制作静态库 首先准备简单的add.c&#xff0c;sub.c,main.c,head.h.具体代码如下 #head.h文件 int Add(int a, int b); int Sub(int a, int b);#add.c文件 #include <stdio.h> int Add(int a, int b) {return a b; }#sub.c文件 #include <stdio.h&…

Linux动态库的下载与配置(以libevent库为例)

** Linux动态库的下载与配置 ** 本章以下载、安装、配置libevent库为例(安装libevent的前提是已经成功安装openssl库) 一、libevent的下载 https://libevent.org/ //官网下载源码包如果学习下载1.0版本为好&#xff0c;使用可以下载2.0版本 1.如果Linux已经进行桥接联网&am…

Linux下编译安装libusb动态库(.so) - libus1-0 vs libusb-0.1

最近在调试代码的过程中&#xff0c;发现libusb库中的一些方法没办法debug到&#xff0c;所以试着下载了一版源码&#xff0c;编译安装到指定的目录。这样&#xff0c;在工程的pro文件中&#xff0c;直接指定库和头文件的目录就可以引用自己编译的libusb库了。 在网上查了相关的…

linux编译生成动态库、静态库,以及使用

一、介绍 在实际开发过程中&#xff0c;当代码的文件较多&#xff0c;可以将一部分代码编译成动态库或者静态库然后再加载到程序中使用 编译过程 1、预编译 2、编译 3、汇编 4、链接 静态库和动态库的差异 1、链接静态库简单理解就是复制目标代码嵌入可执行文件中 2、动态库是…

Linux·libusb源码编译

libusb系列--Linux下libusb源码编译​​ ​ ​源码下载及解压​​ ​​下载源码​​ ​​解压下载的源码压缩包​​ ​​打开终端​​ ​​准备编译环境​​ ​​安装make dh-autoreconf​​ ​​安装 libudev-dev​​ sudo ​./autogen.sh​ ​​​​sudo ./configure --prefi…