|
15 | 15 | ## Notes |
16 | 16 |
|
17 | 17 | <!-- Content_START --> |
| 18 | +# 2025-08-18 |
| 19 | + |
| 20 | +# Solidity Opcode |
| 21 | + |
| 22 | +## MSTORE |
| 23 | + |
| 24 | +MSTORE 是内存存储操作码,用于将 32 字节数据写入内存。 |
| 25 | + |
| 26 | + MSTORE 功能 |
| 27 | + |
| 28 | + - 操作码: 0x52 |
| 29 | + - Gas消耗: 3 + 内存扩展成本 |
| 30 | + - 栈输入: [offset, value] |
| 31 | + - 栈输出: 无 |
| 32 | + - 功能: 将 32 字节的 value 存储到内存 offset 位置 |
| 33 | + |
| 34 | +MSTORE 详细解释 |
| 35 | + |
| 36 | + 1. 核心特性 |
| 37 | + |
| 38 | + - 总是写入 32 字节:即使 value 小于 32 字节,也会用 0 填充左侧 |
| 39 | + - 可以覆盖:新值会完全覆盖指定位置的 32 字节 |
| 40 | + - 内存扩展:访问新内存区域会触发内存扩展,增加 gas 成本 |
| 41 | + |
| 42 | + 2. 内存布局 |
| 43 | + |
| 44 | + 0x00-0x3F: Scratch space (临时空间) |
| 45 | + 0x40-0x5F: Free memory pointer (自由内存指针) |
| 46 | + 0x60-0x7F: Zero slot (零槽) |
| 47 | + 0x80+: 可用内存 |
| 48 | + |
| 49 | + 3. Gas 成本计算 |
| 50 | + |
| 51 | + - 基础成本:3 gas |
| 52 | + - 内存扩展:memory_cost = (memory_size^2) / 512 + (3 * memory_size) |
| 53 | + - 首次访问高地址会很贵 |
| 54 | + |
| 55 | + 4. 常见用途 |
| 56 | + |
| 57 | + - 返回数据:配合 RETURN 使用 |
| 58 | + - 构建数组/字符串:动态数据结构 |
| 59 | + - 函数参数传递:ABI 编码 |
| 60 | + - 临时计算:使用 scratch space |
| 61 | + |
| 62 | + 5. 与 MSTORE8 的区别 |
| 63 | + |
| 64 | + - MSTORE: 存储 32 字节 |
| 65 | + - MSTORE8: 只存储 1 字节(最右边的字节) |
| 66 | + |
| 67 | + 6. 注意事项 |
| 68 | + |
| 69 | + - 跨 32 字节边界存储会覆盖两个槽 |
| 70 | + - 内存不会自动清零,可能包含脏数据 |
| 71 | + - 总是要更新自由内存指针(0x40) |
| 72 | + |
| 73 | +Example: |
| 74 | + |
| 75 | +``` |
| 76 | +mstore(0x40, 0x4444) |
| 77 | +``` |
| 78 | + |
| 79 | +-> |
| 80 | + |
| 81 | +``` |
| 82 | +PUSH2(0x4444) |
| 83 | +PUSH1(0x40) |
| 84 | +MSTORE |
| 85 | +
|
| 86 | +memory: |
| 87 | +040| 00...00 44 44 |
| 88 | +``` |
| 89 | + |
| 90 | +## EVM 内存布局详解 |
| 91 | + |
| 92 | + 0x00-0x3F (0-63字节): Scratch Space 临时空间 |
| 93 | + |
| 94 | + - 用途: 编译器的临时工作区 |
| 95 | + - 特点: 可以被随时覆盖,不保证持久 |
| 96 | + - 常见操作: |
| 97 | + - keccak256 哈希计算的输入准备 |
| 98 | + - 函数返回值的临时存放 |
| 99 | + - ABI 编码的中间步骤 |
| 100 | + |
| 101 | + 0x40-0x5F (64-95字节): Free Memory Pointer 自由内存指针 |
| 102 | + |
| 103 | + - 用途: 追踪下一个可用内存位置 |
| 104 | + - 初始值: 0x80(跳过所有保留区域) |
| 105 | + - 重要性: 防止内存覆盖,确保内存分配安全 |
| 106 | + - 使用方式: |
| 107 | + let ptr := mload(0x40) // 获取当前可用位置 |
| 108 | + // ... 使用内存 ... |
| 109 | + mstore(0x40, add(ptr, size)) // 更新指针 |
| 110 | + |
| 111 | + 0x60-0x7F (96-127字节): Zero Slot 零槽 |
| 112 | + |
| 113 | + - 用途: 提供一个永远为 0 的位置 |
| 114 | + - 优化: 避免使用 PUSH1 0x00 指令,节省 gas |
| 115 | + - 保证: EVM 确保这个位置始终为 0 |
| 116 | + |
| 117 | + 0x80+ (128字节起): 可用内存 |
| 118 | + |
| 119 | + - 用途: 实际数据存储区域 |
| 120 | + - 分配: 通过自由内存指针动态分配 |
| 121 | + - 内容: 数组、字符串、结构体、返回数据等 |
| 122 | + |
| 123 | + 内存管理原则 |
| 124 | + |
| 125 | + 1. 永远不要硬编码内存地址(0x80 以下) |
| 126 | + 2. 总是使用自由内存指针分配新内存 |
| 127 | + 3. 分配后更新指针防止覆盖 |
| 128 | + 4. 内存只会增长,不会缩小或释放 |
| 129 | + |
| 130 | +## 创建变量和 MLOAD 的流程 |
| 131 | + |
| 132 | +``` |
| 133 | +uint256 val0; uint256 val32; uint256 val64; uint256 val96; uint256 val128; |
| 134 | + assembly { |
| 135 | + val0 := mload(0x00) // Read from byte 0 |
| 136 | + val32 := mload(0x20) // Read from byte 32 |
| 137 | + val64 := mload(0x40) // Read from byte 64 |
| 138 | + val96 := mload(0x60) // Read from byte 96 |
| 139 | + val128 := mload(0x80) // Read from byte 128 |
| 140 | + } |
| 141 | +``` |
| 142 | + |
| 143 | +uint256 val0 -> PUSH0 to Stack |
| 144 | +uint256 val32 -> DUP1 ??TODO 为什么不使用 PUSH0? |
| 145 | + |
| 146 | +val64 := mload(0x40) -> PUSH1(0x40) set address -> MLOAD get value -> SWAP3 set to the stack position -> POP remove the tempory value |
| 147 | + |
| 148 | +## 几个之前的问题 |
| 149 | + |
| 150 | +### opcode 和源代码的关系 |
| 151 | + |
| 152 | +源代码实际上会翻译成 opcode 进行实际的执行,所以多个 opcode 对应一行具体的代码。通过 source map 进行映射 |
| 153 | + |
18 | 154 | # 2025-08-15 |
19 | 155 |
|
20 | 156 | 这个 forge debug 无法找到 source map 这个问题还是比较严重的,只能在 test case 里面 debug 有什么用呢? |
|
0 commit comments