Skip to content

Latest commit

 

History

History
140 lines (92 loc) · 6.18 KB

File metadata and controls

140 lines (92 loc) · 6.18 KB
title tags
04. 内存布局
solidity
memory

WTF Solidity内部标准: 04. 内存布局

《WTF Solidity内部标准》教程将介绍Solidity智能合约中的存储布局,内存布局,以及ABI编码规则,帮助大家理解Solidity的内部规则。

推特:@0xAA_Science

社区:Discord微信群官网 wtf.academy

所有代码和教程开源在github: github.com/AmazingAng/WTF-Solidity-Internals


这一讲,我们将介绍Solidity中的变量是如何在内存中保存的。

EVM的内存

EVM使用内存来支持交易执行期间的数据存储和读取。EVM的内存是一个线性寻址存储器,你可以把它理解为一个动态字节数组,可以根据需要动态扩展。它支持以8或256 bit写入(MSTORE8/MSTORE),但只支持以256 bit读取(MLOAD)。

需要注意的是,EVM的内存是“易失性”的:交易开始时,所有内存位置的值均为0;交易执行期间,值被更新;交易结束时,内存中的所有数据都会被清除,不会被持久化。如果需要永久保存数据,就需要使用EVM的存储。

内存布局

相比于存储,在内存上写入和读取数据要便宜的多,因此我们不需要像使用存储那样节省,可以豪横一点。这主要体现在我们不会将多个变量压缩保存在同一个内存槽中。

在内存中读写数据便宜的原因有两个:一是数据分别放在各自的内存槽中,而不是尽量压缩在一个槽中。如果是后者,那EVM每次使用数据时就会多一步操作,从槽中截取出真正自己要使用的数据,也就有了更多的gas消耗。二是内存空间是临时的,使用完就会释放,并不像Storage存储那样永久占用链上空间。因此,数据各自占用一个槽位,不仅带来了计算时的便利,又不会真正占用存储空间,gas费自然就低。

预留插槽

Solidity保留了前4个内存插槽,每个插槽32字节,用于特殊目的:

  1. 0x00 - 0x3f (64字节):用于哈希方法的临时空间,比如读取mapping里的数据时,要用到key的hash值,key的hash结果就暂存在这里。
  2. 0x40 - 0x5f (32字节):当前分配的内存大小,又称空闲内存指针(Free Memory Pointer),指向当前空闲的内存位置。Solidity 总会把新对象保存在空闲内存指针的位置,并更新它的值到下一个空闲位置。
  3. 0x60 - 0x7f (32字节): 32字节的0值插槽,用于需要零值的地方,比如动态长度数据的初始长度值。

值变量

每个值变量会占用一个内存插槽。

function testUint() public pure returns (uint){
    uint a = 3;
    return a;
}

可以看到,上面testUint()函数中的a变量的值被存在内存槽0x80中:

字符串/字节数组

对于内存布局,字符串/字节数组不论长短规则都是一样的。字符串/字节数组长度保存在单独的一个内存槽中,接着是内容,一个内存槽不够的话会顺序保存到后面的内存槽中。

function testShortString() public pure returns (string memory){
    string memory x = "WTF";
    return x;
}

上面的字符串变量x的长度为3,保存在内存槽0x80;内容为WTF,保存在内存槽0xa0

function testLongBytes() public pure returns (bytes memory){
    bytes memory x = hex"365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3";
    return x;
}

上面的字节数组变量x的长度为440x2c),保存在内存槽0x80;内容保存在内存槽0xa0-0xc0中。

静态数组

静态数组的每一个元素会占用一个单独的内存槽。

function testStaticArray() public pure returns (uint8[3] memory){
    uint8[3] memory b = [1,2,3];
    return b;
}

可以看到,上面testStaticArray()函数中的b变量的元素被顺序的存在内存槽0xe0-0x120中,虽然每个元素为uint8类型,但仍然占用一个单独的内存槽:

动态数组

动态数组的长度以及每一个元素会占用一个单独的内存槽。

function testDynamicArray() public pure returns (uint[] memory){
    uint[] memory x = new uint[](3);
    x[0] = 1;
    x[2] = 4;
    return x;
}

可以看到,上面testDynamicArray()函数中的x变量的长度和元素被顺序的存在内存槽0x80-0xe0中:

多维数组

当数组里存的是可变长度的数据或其他数组时,对应元素的内存槽存的是变长数据在内存中的指针也就是起始地址。

function testMultiDimensionalArray(string memory info, uint16 length) public pure returns (string[] memory){
    string[] memory x = new string[](length);
    x[0] = info;
    x[1] = "HELLO";
    x[length - 1] = "WTF";
    return x;
}

可以看到, testMultiDimensionalArray("123456789", 5)函数中,参数info的长度和数据存在内存槽0x80 - 0xa0中,参数length因为被编译器优化没有被存在接下来的内存槽中,内存槽0xc0存的是x变量的长度,内存槽0xe0 - 0x160存的是对应元素的string数据在内存中的起始地址, x[0]中存的是0x80x[1]中存的是0x180也就是string("HELLO")在内存中的起始位置,x[2]x[3]都存的是0x60,内存槽0x60中是恒定的32字节0,在这里就表示长度为0的string,x[4]中存的是0x1c0也就是string("WTF")在内存中的起始位置,0x200后的数据都是abi.encode过的返回数据。

总结

这一讲,我们介绍了Solidity合约的内存布局。内存布局与存储布局大致类似,但是由于内存操作消耗的gas很低,我们不需要像存储布局那样将多个变量保存在同一个内存槽中。