EVM—calldata存储空间详解

  • KEN
  • 更新于 2024-04-19 09:42
  • 阅读 346

EVM—calldata存储空间详解文章旨在记录过去所学知识,若文章内容存在不当,欢迎指出。若对文章内容感兴趣,也欢迎评论区留言讨论!文章部分图片取自本人其他博客。一、Calldata的结构EVM中的主要数据存储结构包括Storage、Memeory、Stack、Calldata。本章主要介绍

EVM—calldata存储空间详解

文章旨在记录过去所学知识,若文章内容存在不当,欢迎指出。若对文章内容感兴趣,也欢迎评论区留言讨论!文章部分图片取自本人其他博客。

一、Calldata的结构

EVM中的主要数据存储结构包括Storage、Memeory、Stack、Calldata。本章主要介绍Calldata数据结构。在EVM中,Calldata是由字节组成的,是以和memory相同方式的连续布局,可以将每一单位看作是一个32字节的数据结构。 下面以常见的类型参数为例子说明calldata的布局:

1.纯静态数据

以下面函数为例: image.png 该函数参数kee数据在calldata中的布局: <!--StartFragment-->

<!--EndFragment-->

image.png 在分析calldata数据存储之前,明确一点:对于任意参数数据,EVM要想正确的处理该参数,比对要知道:1.参数的长度 2.参数的值。 对于静态数据,calldata只存放参数数据,因为静态数据的长度在编译的时候就已经确定了,其长度已经被写死字节码中,所以在calldata中只需要存放参数的值即可。而对于动态数据,其长度在编译的时候尚不得知,所以动态参数数据长度和参数值都会被存放在calldata中。

对于uint8[4],0x00-0x03这四个字节用来存放函数选择器,calldata中第一个存储空间的索引值是0x04,用于存放uint8[0],由于calldata中的存储空间以32字节为单位,所以calldata中的第二存储空间的索引值是0x24,用于存放uint8[1],以此类推。 由于结构体结构相对复杂,可以更好的帮助理解数据在calldata中的布局。对于一个结构体,如果其中的元素全部都为静态数据,那么这个结构体便是静态结构体。

image.png 该函数参数在calldata中的布局如下图所示: <!--StartFragment-->

<!--EndFragment-->

image.png 结构体中数据定义的先后顺序就反映了静态数据在calldata中存储的先后顺序。

2.纯动态数据

以下面的函数为例: image.png 该类型参数kee在calldata中的布局: <!--StartFragment--> <!--EndFragment--> image.png 对于动态数据kee,其元素个数在编译的时候我们尚不可知,只有当使用者输入参数具体运行函数test6之后,我们才能知道参数kee的具体个数,所以参数长度没有办法写在字节码中,只能在calldata中开辟空间进行存储。所以我们在0x64下面使用"..."来表示,意味着在真实的运行情况下,还可能有很多元素。 接下来详细解析以下kee在calldata中的布局

  1. 0x00-0x03:存储test6这个函数的函数选择器
  2. 存储槽0x04:offset,该值表示kee参数动态空间首元素的位置,在本函数中表示num的位置
  3. 存储槽0x24:num,该值表示kee参数数据的长度,对于本函数中的数组而言即为数组个数,对bytes类型表示字节数
  4. 存储槽0x44:参数kee的第一个元素
  5. 存储槽0x64:参数kee的第二个元素

通过该例子我希望大家能够了解对于一个动态类型的参数,他在calldata中至少包含:

  • offset:参数动态空间的首元素位置,在calldata中指向动态空间。
  • 动态空间:动态数据的长度num+动态参数的数据value。每个动态空间的首元素都是该动态参数的长度字段num,num后面跟着该参数的具体数据。

为了让大家更加熟悉动态参数在calldata中的存储,再来介绍一下复杂点的情况: <!--StartFragment-->

<!--EndFragment-->

image.png 以上述函数为例,介绍对于一个函数参数既有动态数据又有静态数据的情况。 将输入参数设置为[11,22],[44,55],33,在remix上得到整个calldata的情况: <!--StartFragment-->

<!--EndFragment-->

image.png 由于可读性较差,故将其转换为下图所示: <!--StartFragment--> <!--EndFragment--> image.png 参数kee的元素在calldata中包含:

  1. 0x04:offset1,该值表示动态空间1首元素在calldata中的位置即num1的位置0x60 注意: 这里可能会有疑问—num1的位置不是0x64吗,为啥offset1存储的是0x60?这其实是EVM为了方便我们知道这是num1在哪个存储槽而已,EVM在实际处理的时候会使用CALLDATALOAD取出offset1的值为0x60,然后EVM知道数据是存储在0x60这个存储槽,但是会先+4,使栈顶的元素为64,再使用CALLDATALOAD取出0x64的元素,即num1的值。
  2. 0x64:num1,参数kee动态空间的首元素,表示kee的数据长度
  3. 0x84:11
  4. 0xa4:22

参数lee的元素在calldata中包含:

  1. 0x24:offset2,该值表示动态空间2首元素在calldata中的位置即num2的位置0xc0
  2. 0xc4:num2,参数lee动态空间的首元素,表示lee的数据长度
  3. 0xe4:44
  4. 0x104:55

静态参数mee:

  1. 0x44:静态数据只存放数据本身mee,33

注意:有一个小问题,可以检测上面的内容你是否已经理解——为什么mee的数据存放在kee和lee的数据的前面,在参数中不是kee和lee在参数mee前面吗? 实际上是因为,静态数据的长度是在编译的时候确定,所以编译器将合约源代码编译成字节码时就已经会为其分配对应静态数据长度的内存空间,但是对于动态数据,其数据长度是在程序运行时才确定,所以编译器将合约源码编译成字节码时,不能确定动态数据的长度,所以只能按照参数在函数中的声明位置先使用一个offset字段为该动态数据占一个位置。该offset字段的值就直接指向动态数据在calldata中具体位置。

点赞 8
收藏 5
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

4 条评论

请先 登录 后评论
KEN
KEN
0x4e16...2573
江湖只有他的大名,没有他的介绍。