Solidity 内联汇编允许开发者直接编写 EVM 汇编代码,用于优化 Gas 成本、执行底层操作或访问 Solidity 无法直接提供的功能。
1. 基础语法
Solidity 支持两种汇编方言:assembly { ... }(推荐)和 assembly { ... }。
soliditycontract AssemblyBasic { function add(uint256 a, uint256 b) public pure returns (uint256) { uint256 result; assembly { // 使用 mload 和 mstore 操作内存 result := add(a, b) // := 是赋值操作符 } return result; } function getCaller() public view returns (address) { address caller; assembly { caller := caller() // 获取调用者地址 } return caller; } }
2. 访问变量
汇编代码可以读写 Solidity 变量,但需要注意变量类型和存储位置。
soliditycontract VariableAccess { uint256 public storedValue; // storage 变量 function manipulateVariables(uint256 input) public returns (uint256) { uint256 localVar = 10; // memory 变量 assembly { // 读取 storage 变量 let stored := sload(storedValue.slot) // 写入 storage 变量 sstore(storedValue.slot, add(stored, input)) // 修改 memory 变量(注意:需要知道内存位置) // localVar 在内存中的位置需要计算 } return storedValue; } // 更安全的变量访问方式 function safeAccess(uint256 newValue) public { assembly { // 使用 .slot 访问 storage 变量位置 sstore(storedValue.slot, newValue) // 使用 .offset 访问结构体成员偏移 } } }
3. 内存操作
EVM 内存是一个线性字节数组,需要手动管理。
soliditycontract MemoryOperations { function memoryDemo() public pure returns (bytes32) { bytes32 result; assembly { // 在内存位置 0x00 存储 32 字节数据 mstore(0x00, 0x1234567890abcdef) // 从内存位置 0x00 读取 32 字节 result := mload(0x00) // 只存储 1 字节(使用 mstore8) mstore8(0x20, 0xff) } return result; } function copyMemory(bytes memory data) public pure returns (bytes memory) { bytes memory result = new bytes(data.length); assembly { // 获取 data 的内存位置(跳过 32 字节长度前缀) let dataPtr := add(data, 0x20) // 获取 result 的内存位置 let resultPtr := add(result, 0x20) // 数据长度 let len := mload(data) // 复制内存 for { let i := 0 } lt(i, len) { i := add(i, 0x20) } { mstore(add(resultPtr, i), mload(add(dataPtr, i))) } } return result; } }
4. 存储操作
Storage 是永久存储,操作成本很高。
soliditycontract StorageOperations { uint256 public value; mapping(address => uint256) public balances; function storageDemo(address user) public { assembly { // 简单 storage 变量 let slot := value.slot let current := sload(slot) sstore(slot, add(current, 1)) // mapping 的 storage 位置计算 // mapping[key] 的位置 = keccak256(key . slot) mstore(0x00, user) mstore(0x20, balances.slot) let mappingSlot := keccak256(0x00, 0x40) // 读取 mapping 值 let balance := sload(mappingSlot) // 写入 mapping 值 sstore(mappingSlot, add(balance, 100)) } } }
5. 函数调用和返回
soliditycontract FunctionCalls { function externalCall(address target, bytes memory data) public returns (bytes memory) { bytes memory result; assembly { // 获取 data 的长度和指针 let dataLen := mload(data) let dataPtr := add(data, 0x20) // 分配返回数据内存 result := mload(0x40) // 获取空闲内存指针 // 调用外部合约 let success := call( gas(), // 传递所有可用 gas target, // 目标地址 0, // 发送的 ETH 数量 dataPtr, // 输入数据指针 dataLen, // 输入数据长度 result, // 返回数据指针 0x40 // 返回数据最大长度 ) // 检查调用是否成功 if iszero(success) { revert(0, 0) } // 更新空闲内存指针 mstore(0x40, add(result, 0x60)) } return result; } // 使用 delegatecall function delegateCall(address target, bytes memory data) public returns (bytes memory) { bytes memory result; assembly { let dataLen := mload(data) let dataPtr := add(data, 0x20) result := mload(0x40) let success := delegatecall( gas(), target, dataPtr, dataLen, result, 0x40 ) if iszero(success) { revert(0, 0) } mstore(0x40, add(result, 0x60)) } return result; } }
6. 条件判断和循环
soliditycontract ControlFlow { function findMax(uint256[] memory arr) public pure returns (uint256) { require(arr.length > 0, "Empty array"); uint256 max; assembly { // 数组长度 let len := mload(arr) // 数组数据开始位置 let dataPtr := add(arr, 0x20) // 初始最大值设为第一个元素 max := mload(dataPtr) // 循环遍历 for { let i := 1 } lt(i, len) { i := add(i, 1) } { // 当前元素位置 let elemPtr := add(dataPtr, mul(i, 0x20)) let elem := mload(elemPtr) // 如果当前元素更大,更新最大值 if gt(elem, max) { max := elem } } } return max; } function conditional(uint256 x) public pure returns (uint256) { uint256 result; assembly { // if-else 逻辑 switch gt(x, 10) case 1 { result := mul(x, 2) } default { result := add(x, 5) } } return result; } }
7. 实用优化示例
Gas 优化的字符串拼接
soliditycontract StringOptimization { // Solidity 方式(Gas 较高) function concatSolidity(string memory a, string memory b) public pure returns (string memory) { return string(abi.encodePacked(a, b)); } // 汇编方式(Gas 更低) function concatAssembly(string memory a, string memory b) public pure returns (string memory result) { assembly { let aLen := mload(a) let bLen := mload(b) let totalLen := add(aLen, bLen) // 分配内存 result := mload(0x40) mstore(result, totalLen) // 复制 a let aPtr := add(a, 0x20) let resultPtr := add(result, 0x20) // 使用 identity 预编译合约进行内存复制(更高效) // 或者手动复制 for { let i := 0 } lt(i, aLen) { i := add(i, 0x20) } { mstore(add(resultPtr, i), mload(add(aPtr, i))) } // 复制 b let bPtr := add(b, 0x20) let bResultPtr := add(resultPtr, aLen) for { let i := 0 } lt(i, bLen) { i := add(i, 0x20) } { mstore(add(bResultPtr, i), mload(add(bPtr, i))) } // 更新空闲内存指针 mstore(0x40, add(add(resultPtr, totalLen), 0x20)) } } }
高效的数组操作
soliditycontract ArrayOptimization { // 高效的数组元素删除(不保持顺序) function removeElement(uint256[] storage arr, uint256 index) internal { require(index < arr.length, "Invalid index"); assembly { // 获取数组长度位置 let lenSlot := arr.slot let len := sload(lenSlot) // 如果不是最后一个元素,用最后一个元素替换 if lt(add(index, 1), len) { // 计算存储位置 let lastIndex := sub(len, 1) let indexSlot := add(keccak256(lenSlot, 0x20), index) let lastSlot := add(keccak256(lenSlot, 0x20), lastIndex) // 替换 sstore(indexSlot, sload(lastSlot)) } // 减少长度 sstore(lenSlot, sub(len, 1)) } } }
8. 安全注意事项
危险操作示例
soliditycontract AssemblyDangers { // 危险:直接操作 storage 可能导致数据损坏 function dangerousStorageWrite(uint256 slot, uint256 value) public { assembly { sstore(slot, value) // 可以写入任意 slot! } } // 危险:内存溢出 function dangerousMemory() public pure { assembly { // 写入超出分配范围的内存 mstore(0x1000000, 1) // 可能覆盖其他数据 } } // 危险:未检查的外部调用 function dangerousCall(address target) public { assembly { let success := call(gas(), target, 0, 0, 0, 0, 0) // 没有检查 success! } } }
安全使用指南
soliditycontract SafeAssembly { // 安全:验证输入 function safeStorageWrite(bytes32 slot, uint256 value) public { // 只允许写入特定 slot require(slot == keccak256("allowed_slot"), "Invalid slot"); assembly { sstore(slot, value) } } // 安全:检查内存边界 function safeMemoryOperation(bytes memory data) public pure returns (bytes32) { require(data.length >= 32, "Data too short"); bytes32 result; assembly { result := mload(add(data, 0x20)) } return result; } // 安全:检查外部调用结果 function safeExternalCall(address target, bytes memory data) public returns (bool) { bool success; assembly { let dataLen := mload(data) let dataPtr := add(data, 0x20) success := call( gas(), target, 0, dataPtr, dataLen, 0, 0 ) } return success; } }
9. 常见使用场景
| 场景 | 汇编优势 | 示例 |
|---|---|---|
| Gas 优化 | 减少操作码 | 自定义存储布局 |
| 底层操作 | 访问 EVM 特性 | 调用预编译合约 |
| 复杂计算 | 更高效的算法 | 加密操作 |
| 代理合约 | delegatecall | 可升级合约 |
| 数据编码 | 自定义序列化 | 紧凑存储 |
10. 最佳实践
- 仅在必要时使用汇编:Solidity 编译器已经很优化
- 充分测试:汇编代码难以调试,需要更多测试
- 添加详细注释:汇编代码可读性差,需要解释
- 使用常量:避免魔法数字
- 验证所有输入:防止意外行为
- 考虑使用库:将汇编封装在库中复用
- 审计必不可少:汇编代码必须经过专业审计
soliditylibrary SafeAssembly { // 将汇编操作封装在库中 function safeDelegateCall(address target, bytes memory data) internal returns (bool success, bytes memory result) { // 实现... } }