[译】Diving Into The Ethereum VM

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的文章系列中,我写到:



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

在使用Python来安装geopandas包时,由于geopandas依赖于几个其他的Python库(如GDAL, Fiona, Pyproj, Shapely等),因此安装过程可能需要一些额外的步骤。以下是一个基本的安装指南,适用于大多数用户: 使用pip安装 确保Python和pip已安装: 首先,确保你的计算机上已安装了Python和pip。pip是Python的包管理工具,用于安装和管理Python包。 安装依赖库: 由于geopandas依赖于GDAL, Fiona, Pyproj, Shapely等库,你可能需要先安装这些库。通常,你可以通过pip直接安装这些库,但有时候可能需要从其他源下载预编的二进制包(wheel文件),特别是GDAL和Fiona,因为它们可能包含一些系统级的依赖。 bash pip install GDAL Fiona Pyproj Shapely 注意:在某些系统上,直接使用pip安装GDAL和Fiona可能会遇到问题,因为它们需要编一些C/C++代码。如果遇到问题,你可以考虑使用conda(一个Python包、依赖和环境管理器)来安装这些库,或者从Unofficial Windows Binaries for Python Extension Packages这样的网站下载预编的wheel文件。 安装geopandas: 在安装了所有依赖库之后,你可以使用pip来安装geopandas。 bash pip install geopandas 使用conda安装 如果你正在使用conda作为你的Python包管理器,那么安装geopandas和它的依赖可能会更简单一些。 创建一个新的conda环境(可选,但推荐): bash conda create -n geoenv python=3.x anaconda conda activate geoenv 其中3.x是你希望使用的Python版本。 安装geopandas: 使用conda-forge频道来安装geopandas,因为它提供了许多地理空间相关的包。 bash conda install -c conda-forge geopandas 这条命令会自动安装geopandas及其所有依赖。 注意事项 如果你在安装过程中遇到任何问题,比如编错误或依赖问题,请检查你的Python版本和pip/conda的版本是否是最新的,或者尝试在不同的环境中安装。 某些库(如GDAL)可能需要额外的系统级依赖,如地理空间库(如PROJ和GEOS)。这些依赖可能需要单独安装,具体取决于你的操作系统。 如果你在Windows上遇到问题,并且pip安装失败,尝试从Unofficial Windows Binaries for Python Extension Packages网站下载相应的wheel文件,并使用pip进行安装。 脚本示例 虽然你的问题主要是关于如何安装geopandas,但如果你想要一个Python脚本来重命名文件夹下的文件,在原始名字前面加上字符串"geopandas",以下是一个简单的示例: python import os # 指定文件夹路径 folder_path = 'path/to/your/folder' # 遍历文件夹中的文件 for filename in os.listdir(folder_path): # 构造原始文件路径 old_file_path = os.path.join(folder_path, filename) # 构造新文件名 new_filename = 'geopandas_' + filename # 构造新文件路径 new_file_path = os.path.join(folder_path, new_filename) # 重命名文件 os.rename(old_file_path, new_file_path) print(f'Renamed "{filename}" to "{new_filename}"') 请确保将'path/to/your/folder'替换为你想要重命名文件的实际文件夹路径。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值