Solidity 智能合约的内存布局问题详解

  • Dapplink
  • 发布于 2025-04-20 12:34
  • 阅读 23

Solidity的存储结构简介以太坊智能合约有三种数据存储位置

一.Solidity 的存储结构简介

以太坊智能合约有三种数据存储位置:

  • storage(链上永久存储,Gas 消耗高)
  • memory(临时内存,Gas 较低)
  • calldata(仅用于函数输入参数,不可修改) Solidity 的状态变量全部存储于 storage 中,存储布局以 32 字节(256位)为单位,这个单位称作 slot
  • uint256: 32 字节
  • uint8: 1 个字节
  • bool: 1 个字节
  • address: 20 个字节
  • string: 占据空间长度取决于字符串 每个合约的状态变量都是按照槽位(slot)的顺序排列存储的

二.基础类型变量的存储布局

基础类型变量包括:uint256、address、bool 等。 contract SimpleStorage { uint256 a; // slot 0 address b; // slot 1 bool c; // slot 2 }

  • 32 字节的每个基础类型单独占用一个slot
  • 变量不足 32 字节,所有变量加起来小于于 32 字节,单独占一个slot
  • 大于 32 字节的往下移一个 slot

三.变量紧密打包(Packing)

当多个较小的变量(例如:uint8, uint16, bool 等)连续声明时,编译器会将多个变量紧密打包到同一个槽位中,以节约空间: contract PackedStorage { uint128 a; // slot 0 (前16字节) uint64 b; // slot 0 (中间8字节) uint64 c; // slot 0 (最后8字节) uint256 d; // slot 1 (新槽位) } 紧密打包规则:

  • Solidity 会尽量把连续声明的小变量塞进同一个32字节槽位中
  • 如果超出32字节,则另开新槽位

四.动态类型变量的存储布局

动态类型变量主要包括:

  • 动态长度数组(如:uint256[])

  • mapping 映射(如:mapping(address => uint256))

  • bytes 和 string 类型

  1. 动态数组存储布局 动态数组定义如下: contract DynamicArray { uint256[] arr; // slot 0 } 存储布局为:
  • 槽位本身(slot 0):存储数组长度;

  • 数组元素:元素从槽位 keccak256(slot) 开始存储,连续排列:元素 i 的槽位位置 = keccak256(slot) + i 例如:

  • arr.length 存储于 slot 0;

  • arr[0] 存储于 slot: keccak256(0);

  • arr[1] 存储于 slot: keccak256(0) + 1;

  • arr[2] 存储于 slot: keccak256(0) + 2;以此类推。

    2.mapping 存储布局 mapping 是动态哈希表: contract MappingStorage { mapping(uint256 => uint256) map; // slot 0 }

  • 槽位本身(slot 0): 不存储任何实际数据,仅用于哈希计算起点;

  • 每个键值对存储位置计算方式: slot(key) = keccak256(abi.encode(key, slot)) 例如:

  • map[5] 存储在 keccak256(abi.encode(5, 0));

  • map[100] 存储在 keccak256(abi.encode(100, 0))。

    3.bytes 和 string 存储布局 bytes 和 string 类型本质上也是动态数组,其存储机制为:

  • 如果长度小于等于31字节:

    • 存储在单个槽位内(紧密打包);
    • 最低位的字节存储长度 length * 2,高位存储数据。
  • 如果长度大于31字节:

    • 槽位本身存储:长度*2 + 1;
    • 数据存储位置从 keccak256(slot) 开始,连续存储。

五.结构体(struct)类型的存储布局

contract StructStorage {
    struct Person {
        uint256 id;  // slot 0
        address addr; // slot 1
        uint128 age;  // slot 2 (前半)
         uint128 score;// slot 2 (后半)
    }
Person p; // 从 slot 0 开始排列(结构体内变量依次排列)
}

结构体:

  • 每个成员依次占用连续的槽位;
  • 小成员按紧密打包原则节省空间。

六.嵌套结构存储布局

复杂数据结构(如结构体数组、mapping 嵌套数组)存储布局,简单举例: contract NestedStorage { mapping(uint => uint[]) nested; // slot 0 } 计算位置步骤:mapping 元素位置计算方式: nested[key] => keccak256(abi.encode(key, 0))

  • 数组长度存储于上述位置;
  • 数组元素依次存储于: keccak256(keccak256(abi.encode(key, 0))) + index

七.插槽分配总结表

变量类型 存储位置计算方式
基础类型(uint256) 直接分配槽位
动态数组 长度在槽位本身,元素存储从 keccak256(slot) 开始连续存储
mapping 数据存储位置 = keccak256(abi.encode(key, slot))
bytes/string (≤31B) 槽位内紧密存储
bytes/string (>31B) 槽位存长度标记,数据从 keccak256(slot) 开始存储
结构体 成员依次存储,遵循紧密打包原则

八.slot 存储解析的实际应用

在智能合约安全审计、调试、链上数据读取过程中,经常用到明确的 slot 定位和计算,比如:

  • 通过链上存储位置,精确读取合约状态;
  • 分析合约升级、代理合约、ERC-1967 存储槽位。

九.示例:手动计算 mapping 存储位置(实操)

例如定义 mapping(uint256 => uint256) public balances; // slot 3 计算 balances[999] 的具体位置

bytes32 location = keccak256(abi.encode(uint256(999), uint256(3))); 通过 EVM 读取此位置数据 assembly { let value := sload(location) }

十.总结与要点

  • Solidity 存储以 32 字节为单位,称为 slot;
  • 基本类型逐个 slot 存储;
  • 动态数组、mapping、bytes/string 等动态类型利用 keccak256 哈希确定数据存储位置;
  • Solidity 编译器优化紧密打包,小变量会尽可能共享槽位以节省空间;
  • 存储槽位的正确理解对安全开发、审计、底层优化至关重要。
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Dapplink
Dapplink
0xBdcb...f214
首个模块化、可组合的Layer3协议。