介绍了如 Solidity 智能合约中使用内联汇编语言(Inline Assembly)实现keccak256哈希函数的优化方法.
- 原文链接:dacian.me/solidity-ass...
- 译者:AI翻译官,校对:翻译小组
- 本文链接:learnblockchain.cn/article…
Solidity 智能合约通常使用 keccak256
对多个输入参数进行哈希;调用此函数的标准方式如下所示:
function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result) {
result = keccak256(abi.encode(a,b,c));
}
然而,通过以下汇编代码,可以将计算哈希的 gas 成本降低约 42%:
function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result) {
assembly {
let mPtr := mload(0x40)
mstore(mPtr, a)
mstore(add(mPtr, 0x20), b)
mstore(add(mPtr, 0x40), c)
result := keccak256(mPtr, 0x60)
}
}
让我们来看看这种节省 gas 成本的原因和原理!
为了理解下一部分内容,你需要完成 Updraft 的 Assembly & Formal Verification course 的第一部分,或者具有以下等效知识:
为了检查两个函数的执行,我们将使用以下独立的 Foundry 测试合约:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.25;
import "forge-std/Test.sol";
// 为每个实现创建单独的合约,通过测试合约中的接口访问实现
// 防止优化器过于“智能”,帮助更好地近似真实世界的执行
interface IGasImpl {
function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result);
}
contract GasImplNormal is IGasImpl {
function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result) {
result = keccak256(abi.encode(a,b,c));
}
}
contract GasImplAssembly is IGasImpl {
function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result) {
assembly {
let mPtr := mload(0x40)
mstore(mPtr, a)
mstore(add(mPtr, 0x20), b)
mstore(add(mPtr, 0x40), c)
result := keccak256(mPtr, 0x60)
}
}
}
// 实际测试合约
contract GasDebugTest is Test {
IGasImpl gasImplNormal = new GasImplNormal();
IGasImpl gasImplAssembly = new GasImplAssembly();
uint256 a = 1;
uint256 b = 2;
uint256 c = 3;
// forge test --match-contract GasDebugTest --debug test_GasImplNormal
function test_GasImplNormal() external {
bytes32 result = gasImplNormal.getKeccak256(a,b,c);
assertEq(result, 0x6e0c627900b24bd432fe7b1f713f1b0744091a646a9fe4a65a18dfed21f2949c);
}
// forge test --match-contract GasDebugTest --debug test_GasImplAssembly
function test_GasImplAssembly() external {
bytes32 result = gasImplAssembly.getKeccak256(a,b,c);
assertEq(result, 0x6e0c627900b24bd432fe7b1f713f1b0744091a646a9fe4a65a18dfed21f2949c);
}
}
接下来,我们将使用 Foundry 的调试器逐步检查汇编版本,执行以下命令:forge test --match-contract GasDebugTest --debug test_GasImplAssembly
我们关注的是 GasImplAssembly
合约内部的执行:
PUSH1(0x40)
开始,直到调用 keccak256
并将计算得到的哈希放到堆栈上上述执行从 PC 0x39 (57) 开始,到 0x4f (79) 结束,消耗了 341-224 = 117 gas。一些有用的缩写包括:
自由内存指针地址 (FMPA)
下一个自由内存地址的起始值 (SNFMA)
下一个自由内存地址 (NFMA)
让我们逐步检查执行,了解汇编版本的工作原理:
// 将自由内存指针地址 (FMPA) 推入堆栈
PUSH1(0x40) [Stack : 0x40, 0x03, 0x02, 0x01, 0x52, 0x05536b19]
[Memory: 0x40 = 0x80 ]
// 复制 FMPA
DUP1 [Stack : 0x40, 0x40, 0x03, 0x02, 0x01, 0x52, 0x05536b19]
[Memory: 0x40 = 0x80 ]
// 通过读取 FMPA 加载下一个自由内存地址 (SNFMA)
MLOAD [Stack : 0x80, 0x40, 0x03, 0x02, 0x01, 0x52, 0x05536b19]
[Memory: 0x40 = 0x80 ]
// 交换堆栈中第 5 和第 1 项
// 注意:这是一种常见模式,用于在内存中存储输入参数
SWAP4 [Stack : 0x01, 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]
[Memory: 0x40 = 0x80 ]
// 复制 SNFMA
DUP5 [Stack : 0x80, 0x01, 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]
[Memory: 0x40 = 0x80 ]
// 将值 `a` 存储到 SNFMA 指向的内存中
MSTORE [Stack : 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]
[Memory: 0x40 = 0x80, 0x80 = 0x01 ]
// 将 0x20 推入堆栈 (第一个 `add` 调用的第二个参数)
// 此值是计算下一个自由内存地址 (NFMA) 的偏移量
PUSH1(0x20) [Stack : 0x20, 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]
[Memory: 0x40 = 0x80, 0x80 = 0x01 ]
// 复制 SNFMA
DUP5 [Stack : 0x80, 0x20, 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]
[Memory: 0x40 = 0x80, 0x80 = 0x01 ]
// 通过 SNFMA + 偏移量 计算 NFMA (0x80 + 0x20)
ADD [Stack : 0xa0, 0x40, 0x03, 0x02, 0x80, 0x52, 0x05536b19]
[Memory: 0x40 = 0x80, 0x80 = 0x01 ]
// 交换堆栈中第 4 和第 1 项
SWAP3 [Stack : 0x02, 0x40, 0x03, 0xa0, 0x80, 0x52, 0x05536b19]
[Memory: 0x40 = 0x80, 0x80 = 0x01 ]
// 交换堆栈中第 2 和第 1 项
SWAP1 [Stack : 0x40, 0x02, 0x03, 0xa0, 0x80, 0x52, 0x05536b19]
[Memory: 0x40 = 0x80, 0x80 = 0x01 ]
// 交换堆栈中第 4 和第 1 项
SWAP3 [Stack : 0xa0, 0x02, 0x03, 0x40, 0x80, 0x52, 0x05536b19]
[Memory: 0x40 = 0x80, 0x80 = 0x01 ]
// 将值 `b` 存储到 NFMA 指向的内存中
MSTORE [Stack : 0x03, 0x40, 0x80, 0x52, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02]
// 通过 SNFMA + 偏移量 (0x80 + 0x40) 计算 NFMA
ADD [Stack : 0xc0, 0x03, 0x80, 0x52, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02]
// 将 `c` 的值存储到 NFMA 地址的内存中
MSTORE [Stack : 0x80, 0x52, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]
// 注意:所有输入参数现在都存储在内存中
// 将 `size` 参数推送到栈中以供调用 keccak
PUSH1(0x60) [Stack : 0x60, 0x80, 0x52, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]
// 交换栈中第二和第一元素
SWAP1 [Stack : 0x80, 0x60, 0x52, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]
// 调用 keccak256(offset, size)
KECCAK256 [Stack : result, 0x52, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]
汇编版本:
将第一个输入参数 a
存储在下一个可用内存地址 (SNFMA)
计算了 2 个额外的下一个可用内存地址 (NFMA),用于存储输入参数 b, c
一旦完成,仅需要再执行 3 个操作码;2 个用于准备 偏移量, 大小
输入参数,最后一个用于调用 keccak256
共执行了 20 个操作码,包括 1 个 MLOAD
和 3 个 MSTORE
在检查了汇编版本后,我们现在将转向使用 Foundry 的调试器的 Solidity 版本,通过执行以下命令:forge test --match-contract GasDebugTest --debug test_GasImplNormal
我们关心的是 GasImplNormal
合约中的执行:
从第一个 PUSH1(0x40)
开始在将 calldata 加载到栈中并调用 JUMPDEST
后
到 keccak256
被调用并计算出的哈希放置在栈上为止
上述执行从 PC 0x39 (57) 开始,直到 0x6c (108),使用了 428-224 = 204 gas,比汇编版本多 74%!
// 将自由内存指针地址 (FMPA) 推送到栈上
PUSH1(0x40) [Stack : 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80 ]
// 复制 FMPA
DUP1 [Stack : 0x40, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80 ]
// 通过读取 FMPA 加载起始下一个可用内存地址 (SNFMA)
MLOAD [Stack : 0x80, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80 ]
// 将 0x20 推送到栈上
// 这是计算下一个可用内存地址 (NFMA) 的偏移量
PUSH1(0x20) [Stack : 0x20, 0x80, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80 ]
// 复制偏移量
DUP1 [Stack : 0x20, 0x20, 0x80, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80 ]
// 复制 SNFMA
DUP3 [Stack : 0x80, 0x20, 0x20, 0x80, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80 ]
// 通过 SNFMA + 偏移量 (0x80 + 0x20) 计算 NFMA
ADD [Stack : 0xa0, 0x20, 0x80, 0x40, 0x03, 0x02, 0x01, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80 ]
// 交换栈中第七和第一个元素
// 注意:遵循一个常见的模式以在内存中存储输入参数
SWAP6 [Stack : 0x01, 0x20, 0x80, 0x40, 0x03, 0x02, 0xa0, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80 ]
// 交换栈中第二和第一个元素
SWAP1 [Stack : 0x20, 0x01, 0x80, 0x40, 0x03, 0x02, 0xa0, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80 ]
// 交换栈中第七和第一个元素
SWAP6 [Stack : 0xa0, 0x01, 0x80, 0x40, 0x03, 0x02, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80 ]
// 将 `a` 的值存储到 NFMA 地址的内存中
MSTORE [Stack : 0x80, 0x40, 0x03, 0x02, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01 ]
// 注意:正常版本未将第一个输入存储在 SNFMA,因此需要多计算一个 NFMA
// 以将所有输入存储到内存中,相比于组合版本
// 复制 SNFMA
DUP1 [Stack : 0x80, 0x80, 0x40, 0x03, 0x02, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01 ]
// 复制 FMPA
DUP3 [Stack : 0x40, 0x80, 0x80, 0x40, 0x03, 0x02, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01 ]
// 通过 FMPA + 偏移量 (0x40 + 0x80) 计算 NFMA
ADD [Stack : 0xc0, 0x80, 0x40, 0x03, 0x02, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01 ]
// 交换栈中第五和第一个元素
SWAP4 [Stack : 0x02, 0x80, 0x40, 0x03, 0xc0, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01 ]
// 交换栈中第二和第一个元素
SWAP1 [Stack : 0x80, 0x02, 0x40, 0x03, 0xc0, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01 ]
// 交换栈中第五和第一个元素
SWAP4 [Stack : 0xc0, 0x02, 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01 ]
// 将 `b` 的值存储到 NFMA 地址的内存中
MSTORE [Stack : 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02 ]
// 另一个偏移量用于计算 NFMA
PUSH1(0x60) [Stack : 0x60, 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02 ]
// 复制偏移量
DUP1 [Stack : 0x60, 0x60, 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02 ]
// 复制 SNFMA
DUP5 [Stack : 0x80, 0x60, 0x60, 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02 ]
// 通过 SNFMA + 偏移量 (0x80 + 0x60) 计算 NFMA
ADD [Stack : 0xe0, 0x60, 0x40, 0x03, 0x80, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02 ]
// 交换栈中第四和第一个元素
SWAP3 [Stack : 0x03, 0x60, 0x40, 0xe0, 0x80, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02 ]
// 交换栈中第二和第一个元素
SWAP1 [Stack : 0x60, 0x03, 0x40, 0xe0, 0x80, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02 ]
// 交换第 4 个和第 1 个栈元素
SWAP3 [Stack : 0xe0, 0x03, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02 ]
// 将 `c` 的值存储到 NFMA 的内存中
MSTORE [Stack : 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 注意:所有输入参数现在都存储在内存中
// 复制 FMPA
DUP1 [Stack : 0x40, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 通过读取 FMPA 来加载当前下一个可用内存地址 (SNFMA)
MLOAD [Stack : 0x80, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 复制 SNFMA
// 注意:后续操作码与 `abi.encode` 相关
DUP1 [Stack : 0x80, 0x80, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03 ]
// 复制 SNFMA
DUP5 [Stack : 0x80, 0x80, 0x80, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03 ]
// 将第 2 个元素从第 1 个中减去
SUB [Stack : 0x00, 0x80, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03 ]
// 交换第 2 个和第 1 个栈元素
SWAP1 [Stack : 0x80, 0x00, 0x40, 0x60, 0x80, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03 ]
// 交换第 4 个和第 1 个栈元素
SWAP3 [Stack : 0x60, 0x00, 0x40, 0x80, 0x80, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03 ]
// 计算 `size` 参数,用于后续 keccak256 调用
ADD [Stack : 0x60, 0x40, 0x80, 0x80, 0x20, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 复制 SNFMA
DUP3 [Stack : 0x80, 0x60, 0x40, 0x80, 0x80, 0x20, 0x6f, 0x05536b19]
[Memory: 0x40 = 0x80, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03 ]
// 将 `size` 参数 (0x60) 存储在 SNFMA 中
// 后续将用于 keccak256 调用
MSTORE [Stack : 0x40, 0x80, 0x80, 0x20, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 将 SNFMA 压入栈中
PUSH1(0x80) [Stack : 0x80, 0x40, 0x80, 0x80, 0x20, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 交换第 2 个和第 1 个栈元素
SWAP1 [Stack : 0x40, 0x80, 0x80, 0x80, 0x20, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 交换第 4 个和第 1 个栈元素
SWAP3 [Stack : 0x80, 0x80, 0x80, 0x40, 0x20, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 计算 NFMA
ADD [Stack : 0x100, 0x80, 0x40, 0x20, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 交换第 2 个和第 1 个栈元素
SWAP1 [Stack : 0x80, 0x100, 0x40, 0x20, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 交换第 3 个和第 1 个栈元素
SWAP2 [Stack : 0x40, 0x100, 0x80, 0x20, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x80, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 用下一个 NFMA 覆盖 FMPA 的值
MSTORE [Stack : 0x80, 0x20, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 注意:普通版本必须计算第二个额外的 NFMA
// 并在 FMPA 更新内存;优化版本没有这样做
// 复制 SNFMA
DUP1 [Stack : 0x80, 0x80, 0x20, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 将存储在 SNFMA 中的值放入栈中
// 这将是用于 keccak256 的 `size` 参数
// 调用之前计算的
MLOAD [Stack : 0x60, 0x80, 0x20, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 交换第 3 个和第 1 个栈元素
SWAP2 [Stack : 0x20, 0x80, 0x60, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 计算第一个参数的内存地址;用作
// keccak256 的 `offset`
ADD [Stack : 0xa0, 0x60, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 调用 keccak256(offset, size)
KECCAK256 [result, 0x6f, 0x05536b19 ]
[Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
与汇编版本相比,Solidity 版本:
未在起始下一个可用内存地址 (SNFMA) 中存储第一个输入参数 a
相反,它计算了 3 个额外的下一个可用内存地址 (NFMA),并用于存储输入参数 a, b, c
一旦完成,与汇编版本的 3 个操作码相比,增加了 22 个操作码
计算了用于 keccak256
的 offset
参数,而汇编版本中是硬编码的
更新了自由内存指针地址 (FMPA),而汇编版本不需要这样做,即使它没有使用更新的地址 (0x100
)
总共执行了 48 个操作码,而汇编版本为 20 个操作码,包括 3 个 MLOAD
和 5 个 MSTORE
使用了 204 gas,而汇编版本为 117,导致 gas 使用增加了 74% (204-117=87, (87/117)*100 = 74)
因此,汇编版本相比于 Solidity 版本节省了 42% 的 gas (204-117=87, (87/204)*100 = 42)
使用 --via-ir 编译为相关代码提供了适度的 gas 改进:
汇编版本使用 108 gas,从 117 gas 降低
Solidity 版本使用 195 gas,从 204 gas 降低
相关执行跟踪如下。
执行此命令:forge test --match-contract GasDebugTest --debug test_GasImplAssembly --via-ir
我们关心 GasImplAssembly
合约内部的执行:
从第一个输入参数 a
的第一个 CALLDATALOAD
开始
直到调用 keccak256
并将计算出的哈希放入堆栈
上述执行从 PC 0x32 (50) 开始到 0x46 (70),使用了 213-105 = 108 gas:
// 从 calldata 将 `a` 的值推送到堆栈
CALLDATALOAD [Stack : 0x01]
[Memory: ]
// 将下一个自由内存地址(SNFMA)推送到堆栈
PUSH1(0x80) [Stack : 0x80, 0x01]
[Memory: ]
// 将 `a` 的值存储到 SNFMA 的内存中
MSTORE [Stack : ]
[Memory: 0x80 = 0x01]
// 将偏移量推送到堆栈以读取下一个输入变量
PUSH1(0x24) [Stack : 0x24 ]
[Memory: 0x80 = 0x01]
// 从 calldata 将 `b` 的值推送到堆栈
CALLDATALOAD [Stack : 0x02 ]
[Memory: 0x80 = 0x01]
// 将下一个自由内存地址(NFMA)推送到堆栈
PUSH1(0xa0) [Stack : 0xa0, 0x02 ]
[Memory: 0x80 = 0x01]
// 注意:--via-ir 能够预计算自由内存地址
// 这样它不必在运行时计算它们,而可以直接将它们推送到堆栈
// 将 `b` 的值存储到 NFMA 的内存中
MSTORE [Stack : ]
[Memory: 0x80 = 0x01, 0xa0 = 0x02]
// 将偏移量推送到堆栈以读取下一个输入变量
PUSH1(0x44) [Stack : 0x44 ]
[Memory: 0x80 = 0x01, 0xa0 = 0x02]
// 从 calldata 将 `c` 的值推送到堆栈
CALLDATALOAD [Stack : 0x03 ]
[Memory: 0x80 = 0x01, 0xa0 = 0x02]
// 将下一个自由内存地址(NFMA)推送到堆栈
PUSH1(0xc0) [Stack : 0xc0, 0x03 ]
[Memory: 0x80 = 0x01, 0xa0 = 0x02]
// 将 `c` 的值存储到 NFMA 的内存中
MSTORE [Stack : ]
[Memory: 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]
// 注意:所有输入参数现在都存储在内存中
// 推送 `size` 参数以便调用 keccak 到堆栈
PUSH1(0x60) [Stack : 0x60 ]
[Memory: 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]
// 推送 `offset` 参数以便调用 keccak 到堆栈
PUSH1(0x80) [Stack : 0x80, 0x60 ]
[Memory: 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]
// 调用 keccak256(offset, size)
KECCAK256 [Stack : result ]
[Memory: 0x80 = 0x01, 0xa0 = 0x02, 0xc0 = 0x03]
执行此命令: forge test --match-contract GasDebugTest --debug test_GasImplNormal --via-ir
我们关注的是 GasImplNormal
合约内的执行:
从第一个输入参数 a
的第一个 CALLDATALOAD
开始
直到调用 keccak256
并将计算出的哈希放入堆栈
上述执行从 PC 0x3a (58) 开始到 0x72 (114),使用了 318-123 = 195 gas:
// 从 calldata 将 `a` 的值推送到堆栈
CALLDATALOAD [Stack : 0x01, 0xa0, 0x80, 0x00]
[Memory: ]
// 将下一个自由内存地址(NFMA)重复推送到堆栈
DUP2 [Stack : 0x0a, 0x01, 0xa0, 0x80, 0x00]
[Memory: ]
// 将 `a` 的值存储到 NFMA 的内存中
MSTORE [Stack : 0xa0, 0x80, 0x00]
[Memory: 0xa0 = 0x01 ]
// 将偏移量推送到堆栈以读取下一个输入变量
PUSH1(0x24) [Stack : 0x24, 0xa0, 0x80, 0x00]
[Memory: 0xa0 = 0x01 ]
// 从 calldata 将 `b` 的值推送到堆栈
CALLDATALOAD [Stack : 0x02, 0xa0, 0x80, 0x00]
[Memory: 0xa0 = 0x01 ]
// 将自由内存指针地址(FMPA)推送到堆栈
PUSH1(0x40) [Stack : 0x40, 0x02, 0xa0, 0x80, 0x00]
[Memory: 0xa0 = 0x01 ]
// 重复 0x80
DUP4 [Stack : 0x80, 0x40, 0x02, 0xa0, 0x80, 0x00]
[Memory: 0xa0 = 0x01 ]
// 计算下一个自由内存地址(NFMA)
ADD [Stack : 0xc0, 0x02, 0xa0, 0x80, 0x00]
[Memory: 0xa0 = 0x01 ]
// 将 `b` 的值存储到 NFMA 的内存中
MSTORE [Stack : 0xa0, 0x80, 0x00 ]
[Memory: 0xa0 = 0x01, 0xc0 = 0x02]
// 将偏移量推送到堆栈以读取下一个输入变量
PUSH1(0x44) [Stack : 0x44, 0xa0, 0x80, 0x00 ]
[Memory: 0xa0 = 0x01, 0xc0 = 0x02]
// 从 calldata 将 `c` 的值推送到堆栈
CALLDATALOAD [Stack : 0x03, 0xa0, 0x80, 0x00 ]
[Memory: 0xa0 = 0x01, 0xc0 = 0x02]
// 推送偏移量以计算 NFMA
PUSH1(0x60) [Stack : 0x60, 0x03, 0xa0, 0x80, 0x00]
[Memory: 0xa0 = 0x01, 0xc0 = 0x02 ]
// 重复 0x80
DUP4 [Stack : 0x80, 0x60, 0x03, 0xa0, 0x80, 0x00]
[Memory: 0xa0 = 0x01, 0xc0 = 0x02 ]
// 计算 NFMA
ADD [Stack : 0xe0, 0x03, 0xa0, 0x80, 0x00]
[Memory: 0xa0 = 0x01, 0xc0 = 0x02 ]
// 将 `c` 的值存储到 NFMA 的内存中
MSTORE [Stack : 0xa0, 0x80, 0x00 ]
[Memory: 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 注意:所有输入参数现在都存储在内存中
// 推送 0x60,将保存到内存,并将在后面
// 用作 keccak256 调用的 `size` 参数
PUSH1(0x60) [Stack : 0x60, 0xa0, 0x80, 0x00 ]
[Memory: 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 注意:后续与 `abi.encode` 相关的操作码
// 复制内存地址以保存先前推送的 `size` 参数
DUP3 [Stack : 0x80, 0x60, 0xa0, 0x80, 0x00 ]
[Memory: 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 将 `size` 参数保存到内存中
MSTORE [Stack : 0xa0, 0x80, 0x00 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 用于计算 NFMA
PUSH1(0x80) [Stack : 0x80, 0xa0, 0x80, 0x00 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 用于计算 NFMA
DUP3 [Stack : 0x80, 0x80, 0xa0, 0x80, 0x00 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 计算 NFMA;该值用于 LT 和 GT 比较
// 然后稍后保存到 FMPA
ADD [Stack : 0x100, 0xa0, 0x80, 0x00 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 交换第 3 和第 1 个堆栈元素
SWAP2 [Stack : 0x80, 0xa0, 0x100, 0x00 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 用于 LT 比较
DUP1 [Stack : 0x80, 0x80, 0xa0, 0x100, 0x00 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 用于 LT 比较
DUP4 [Stack : 0x100, 0x80, 0x80, 0xa0, 0x100, 0x00 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 0x100 < 0x80 ? false
LT [Stack : 0x00, 0x80, 0xa0, 0x100, 0x00 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 用于 GT 比较
PUSH8(0xffffffffffffffff)
[Stack : 0xffffffffffffffff, 0x00, 0x80, 0xa0, 0x100, 0x00 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 复制
DUP5 [Stack : 0x100, 0xffffffffffffffff, 0x00, 0x80, 0xa0, 0x100, 0x00]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03 ]
// 0x100 > 0xffffffffffffffff ? false
GT [Stack : 0x00, 0x00, 0x80, 0xa0, 0x100, 0x00 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 0x00 或 0x00 = 0x00
OR [Stack : 0x00, 0x80, 0xa0, 0x100, 0x00 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 不确定为什么这里会被推送
PUSH1(0x76) [Stack : 0x76, 0x00, 0x80, 0xa0, 0x100, 0x00 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 跳转? false
JUMPI [Stack : 0x80, 0xa0, 0x100, 0x00 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 不确定为什么这里会被推送
PUSH1(0x20) [Stack : 0x20, 0x80, 0xa0, 0x100, 0x00 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 交换第 5 个和第 1 个栈元素
SWAP4 [Stack : 0x00, 0x80, 0xa0, 0x100, 0x20 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 移除顶部元素 0x00
POP [Stack : 0x80, 0xa0, 0x100, 0x20 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 复制
DUP3 [Stack : 0x100, 0x80, 0xa0, 0x100, 0x20 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 准备用先前计算的 NFMA 覆盖 FMPA
PUSH1(0x40) [Stack : 0x40, 0x100, 0x80, 0xa0, 0x100, 0x20 ]
[Memory: 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 用下一个 NFMA 覆盖 FMPA 的值
// 但 FMPA 并没有被使用?
MSTORE [Stack : 0x80, 0xa0, 0x100, 0x20 ]
[Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 加载 keccak256 调用的 `size` 参数
MLOAD [Stack : 0x60, 0xa0, 0x100, 0x20 ]
[Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 交换第 2 个和第 1 个栈元素
SWAP1 [Stack : 0xa0, 0x60, 0x100, 0x20 ]
[Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
// 调用 keccak256(offset, size)
KECCAK256 [Stack : result, 0x100, 0x20 ]
[Memory: 0x40 = 0x100, 0x80 = 0x60, 0xa0 = 0x01, 0xc0 = 0x02, 0xe0 = 0x03]
可以将以下函数添加到现有的 GasDebugTest
合约中,以便正式验证使用 Halmos 证明汇编和 Solidity 版本生成相同的输出:
// halmos --match-contract GasDebugTest
function check_GasImplEquivalent(uint256 a1, uint256 b1, uint256 c1) external {
bytes32 resultNormal = gasImplNormal.getKeccak256(a1,b1,c1);
bytes32 resultAssembly = gasImplAssembly.getKeccak256(a1,b1,c1);
assertEq(resultNormal, resultAssembly);
}
执行形式验证使用:halmos --match-contract GasDebugTest
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!