5月28日 01:54

Solidity 中如何使用 Assembly 进行底层优化?有哪些注意事项?

Assembly 是 Solidity 中的底层编程方式,允许开发者直接编写 EVM 操作码,绕过编译器的高级抽象。在 Gas 敏感的关键路径上,Assembly 能带来显著的性能提升,但也伴随更高的安全风险和维护成本。

为什么需要 Assembly

Solidity 编译器在大多数场景下已经能生成足够高效的字节码,但在以下情况中,手写 Assembly 是合理的:

  • 跳过编译器的冗余抽象:Solidity 对数组、结构体的边界检查会产生额外 Gas 开销,Assembly 可以绕过这些检查
  • 访问 EVM 底层特性:如 returndatasize()extcodehash() 等操作在纯 Solidity 中无法直接调用
  • 实现 Solidity 不支持的操作:如自定义内存布局、精确控制 calldata 解析
  • 极致 Gas 优化:在 DeFi 协议等对 Gas 极度敏感的场景中,几万 Gas 的节省直接影响用户体验

但必须注意:Assembly 跳过了 Solidity 的安全检查(溢出保护、边界检查),任何错误都可能导致资金损失。原则是能不用就不用,非用不可时必须充分测试和审计

Assembly 基础语法

内联汇编

Solidity 中通过 assembly 关键字嵌入汇编代码块:

solidity
contract BasicAssembly { function add(uint256 a, uint256 b) external pure returns (uint256 result) { assembly { result := add(a, b) } } function calculate(uint256 x) external pure returns (uint256) { assembly { let y := add(x, 10) let z := mul(y, 2) mstore(0x00, z) return(0x00, 32) } } }

数据类型与运算

Assembly 中只有一种数据类型:256 位整数。所有值都在 256 位栈上操作:

solidity
contract AssemblyDataTypes { function operations() external pure { assembly { let a := 100 let b := 0xFF // 算术运算 let sum := add(a, b) let diff := sub(a, b) let prod := mul(a, b) let quot := div(a, b) let rem := mod(a, b) // 位运算 let andResult := and(a, b) let orResult := or(a, b) let xorResult := xor(a, b) let notResult := not(a) let shifted := shl(2, a) // 左移 let shiftedR := shr(2, a) // 逻辑右移 let shiftedA := sar(2, a) // 算术右移(保留符号位) } } }

注意:not(a) 执行的是按位取反(等同于 a ^ 0xFFFF...FF),不是逻辑非。逻辑非需要用 iszero(a)

内存操作

EVM 内存布局

理解内存布局是写好 Assembly 的前提:

偏移量大小用途
0x00-0x3f64 字节哈希函数临时空间
0x40-0x5f32 字节空闲内存指针
0x60-0x7f32 字节零值槽(solidity 用作空 bytes32)
0x80-...-实际数据存储区
solidity
contract MemoryOps { function memoryBasics() external pure returns (uint256) { assembly { // 读取空闲内存指针 let freePtr := mload(0x40) // 在空闲位置存储 32 字节数据 mstore(freePtr, 12345) // 更新空闲内存指针(前移 32 字节) mstore(0x40, add(freePtr, 0x20)) // 读取存储的值 let value := mload(freePtr) mstore(0x00, value) return(0x00, 32) } } // 存储多个值到连续内存 function storeMultiple() external pure returns (uint256, uint256) { assembly { let freePtr := mload(0x40) mstore(freePtr, 100) mstore(add(freePtr, 0x20), 200) mstore(add(freePtr, 0x40), 300) mstore(0x40, add(freePtr, 0x60)) // 更新指针 mstore(0x00, mload(freePtr)) mstore(0x20, mload(add(freePtr, 0x20))) return(0x00, 64) } } }

关键原则:每次写入内存后必须更新 0x40 处的空闲指针,否则后续操作可能覆盖你的数据。

存储操作

存储槽布局

EVM 的存储是键值对结构,每个槽 32 字节。Solidity 按声明顺序分配存储槽:

solidity
contract StorageLayout { uint256 public value1; // slot 0 uint256 public value2; // slot 1 mapping(address => uint256) public balances; // slot 2 address public owner; // slot 3 uint256[] public dynamicArray; // slot 4 }

直接读写存储

solidity
contract StorageOps { uint256 public value1; // slot 0 mapping(address => uint256) public balances; // slot 2 uint256[] public dynamicArray; // slot 4 function storageRead() external view returns (uint256) { assembly { let v := sload(0) // 读取 slot 0 mstore(0x00, v) return(0x00, 32) } } function storageWrite(uint256 _value) external { assembly { sstore(0, _value) // 写入 slot 0 } } }

Mapping 的存储计算

Mapping 不直接存储值,而是通过 keccak256(key ++ slot) 计算实际存储位置:

solidity
function readMapping(address _key) external view returns (uint256) { assembly { // 在内存中拼接 key 和 slot mstore(0x00, _key) mstore(0x20, 2) // balances 在 slot 2 let slot := keccak256(0x00, 0x40) let value := sload(slot) mstore(0x00, value) return(0x00, 32) } }

动态数组的存储计算

动态数组的长度存储在声明槽,元素从 keccak256(slot) 开始顺序排列:

solidity
function readArrayElement(uint256 _index) external view returns (uint256) { assembly { // 数组长度在 slot 4 let length := sload(4) // 元素起始位置 = keccak256(4) mstore(0x00, 4) let baseSlot := keccak256(0x00, 0x20) // 第 _index 个元素 let elementSlot := add(baseSlot, _index) let value := sload(elementSlot) mstore(0x00, value) return(0x00, 32) } }

函数调用

外部调用(call)

call 是最常用的外部调用方式,可以发送 ETH 并执行目标合约的函数:

solidity
function callExternal(address _target, bytes memory _data) external returns (bool success, bytes memory result) { assembly { let dataPtr := add(_data, 0x20) // 跳过长度字段 let dataSize := mload(_data) let resultPtr := mload(0x40) success := call( gas(), // 剩余 Gas _target, // 目标地址 0, // 发送的 ETH 数量 dataPtr, // 输入数据起始位置 dataSize, // 输入数据大小 resultPtr, // 返回数据起始位置 0x40 // 返回数据大小上限 ) let resultSize := returndatasize() returndatacopy(resultPtr, 0, resultSize) mstore(0x40, add(resultPtr, resultSize)) mstore(result, resultSize) mstore(add(result, 0x20), resultPtr) } }

静态调用(staticcall)

staticcall 保证不修改状态,适用于 view 函数调用:

solidity
function staticCall(address _target, bytes memory _data) external view returns (bool success, bytes memory result) { assembly { let dataPtr := add(_data, 0x20) let dataSize := mload(_data) let resultPtr := mload(0x40) success := staticcall( gas(), _target, dataPtr, dataSize, resultPtr, 0x40 ) let resultSize := returndatasize() returndatacopy(resultPtr, 0, resultSize) } }

委托调用(delegatecall)

delegatecall 在当前合约的上下文中执行目标合约代码,是代理模式的核心:

solidity
function delegateToImplementation(bytes memory _data) external returns (bytes memory) { assembly { let dataPtr := add(_data, 0x20) let dataSize := mload(_data) let resultPtr := mload(0x40) let success := delegatecall( gas(), sload(0), // implementation 地址 dataPtr, dataSize, resultPtr, 0x40 ) let resultSize := returndatasize() returndatacopy(resultPtr, 0, resultSize) if iszero(success) { revert(resultPtr, resultSize) } return(resultPtr, resultSize) } }

面试常问:callstaticcalldelegatecall 三者的区别是什么?——call 在目标上下文执行、可发 ETH、可修改状态;staticcall 在目标上下文执行、禁止修改状态;delegatecall 在当前上下文执行目标代码、使用当前合约的 storage 和 msg.sender。

Gas 优化实战

循环优化

这是最常见的 Assembly 优化场景。对比同一功能的 Solidity 和 Assembly 实现:

solidity
contract GasComparison { uint256[] public items; // Solidity 实现:约 2800 Gas/次访问 function sumSolidity() external view returns (uint256 total) { for (uint i = 0; i < items.length; i++) { total += items[i]; } } // Assembly 实现:约 2200 Gas/次访问 function sumAssembly() external view returns (uint256 total) { assembly { mstore(0x00, items.slot) let baseSlot := keccak256(0x00, 0x20) let length := sload(items.slot) for { let i := 0 } lt(i, length) { i := add(i, 1) } { total := add(total, sload(add(baseSlot, i))) } } } }

优化点:Assembly 跳过了边界检查、直接操作存储槽,每次循环迭代节省约 60-80 Gas。

批量转账优化

solidity
function batchTransfer(address[] memory _recipients, uint256[] memory _amounts) external payable { require(_recipients.length == _amounts.length, "Length mismatch"); assembly { let recipientsPtr := add(_recipients, 0x20) let amountsPtr := add(_amounts, 0x20) let length := mload(_recipients) for { let i := 0 } lt(i, length) { i := add(i, 1) } { let recipient := mload(add(recipientsPtr, mul(i, 0x20))) let amount := mload(add(amountsPtr, mul(i, 0x20))) let success := call(gas(), recipient, amount, 0, 0, 0, 0) if iszero(success) { revert(0, 0) } } } }

不安全指针优化(unchecked pointer)

solidity
function optimizedSum(uint256[] memory _values) external pure returns (uint256) { assembly { let ptr := add(_values, 0x20) let length := mload(_values) let total := 0 // Assembly 中的 add 不做溢出检查 // 等同于 Solidity 0.8+ 的 unchecked 块 for { let i := 0 } lt(i, length) { i := add(i, 1) } { total := add(total, mload(add(ptr, mul(i, 0x20)))) } mstore(0x00, total) return(0x00, 32) } }

合约创建

create 与 create2

solidity
contract ContractCreation { // create:地址不可预测 function deploy(bytes memory _bytecode) external returns (address addr) { assembly { let size := mload(_bytecode) let ptr := add(_bytecode, 0x20) addr := create(0, ptr, size) if iszero(extcodesize(addr)) { revert(0, 0) } } } // create2:确定性地址 = keccak256(0xff ++ sender ++ salt ++ keccak256(bytecode)) function deployWithSalt(bytes memory _bytecode, bytes32 _salt) external returns (address addr) { assembly { let size := mload(_bytecode) let ptr := add(_bytecode, 0x20) addr := create2(0, ptr, size, _salt) if iszero(extcodesize(addr)) { revert(0, 0) } } } }

面试追问:create2 的确定性地址有什么用途?——主要用于反事实部署(counterfactual deployment),即在不实际部署合约的情况下预先计算其地址,常见于 Layer 2 的账户抽象和工厂合约模式。

安全注意事项

Assembly 绕过了 Solidity 的所有内置安全机制,以下是必须关注的风险点:

1. 内存指针管理

忘记更新 0x40 处的空闲内存指针是 Assembly 中最常见的 bug:

solidity
// 错误:忘记更新空闲指针,后续内存操作可能覆盖数据 assembly { let ptr := mload(0x40) mstore(ptr, 123) // 缺少 mstore(0x40, add(ptr, 0x20)) } // 正确:每次写入后更新指针 assembly { let ptr := mload(0x40) mstore(ptr, 123) mstore(0x40, add(ptr, 0x20)) }

2. 整数溢出

Solidity 0.8+ 默认检查溢出,Assembly 不会:

solidity
function safeAdd(uint256 a, uint256 b) external pure returns (uint256) { assembly { let result := add(a, b) // 手动检查溢出 if lt(result, a) { revert(0, 0) } mstore(0x00, result) return(0x00, 32) } }

3. 重入攻击

Assembly 不会自动应用 Solidity 的重入锁,必须手动实现:

solidity
contract AssemblyReentrancyGuard { uint256 private constant NOT_ENTERED = 1; uint256 private constant ENTERED = 2; uint256 private status = NOT_ENTERED; function safeTransferETH(address _to, uint256 _amount) external { assembly { // 检查重入锁 if eq(sload(0), 2) { revert(0, 0) } // 加锁 sstore(0, 2) // 检查地址有效性 if iszero(_to) { revert(0, 0) } // 执行转账 let success := call(gas(), _to, _amount, 0, 0, 0, 0) // 解锁 sstore(0, 1) if iszero(success) { revert(0, 0) } } } }

4. 存储槽冲突

直接用 sstore 写入存储时,必须确保不会覆盖其他变量的存储槽:

solidity
// 危险:直接写入任意 slot assembly { sstore(0, 999) // 可能覆盖 value1! } // 安全:使用 Solidity 变量的 .slot 属性 assembly { sstore(value1.slot, 999) }

常见实用模式

高效哈希计算

solidity
function efficientHash(bytes memory _data) external pure returns (bytes32) { assembly { let ptr := add(_data, 0x20) let size := mload(_data) let hash := keccak256(ptr, size) mstore(0x00, hash) return(0x00, 32) } }

检测合约地址

solidity
function hasCode(address _addr) external view returns (bool) { assembly { let size := extcodesize(_addr) mstore(0x00, gt(size, 0)) return(0x00, 32) } }

注意:extcodesize 在合约构造函数执行期间返回 0,因此不能用来可靠区分 EOA 和合约。

switch 多条件判断

solidity
function optimizedCondition(uint256 x) external pure returns (uint256) { assembly { switch x case 0 { mstore(0x00, 100) } case 1 { mstore(0x00, 200) } default { mstore(0x00, 300) } return(0x00, 32) } }

数组查找并删除

solidity
function findAndRemove(uint256[] storage _arr, uint256 _value) external { assembly { let length := sload(_arr.slot) mstore(0x00, _arr.slot) let baseSlot := keccak256(0x00, 0x20) for { let i := 0 } lt(i, length) { i := add(i, 1) } { if eq(sload(add(baseSlot, i)), _value) { // 用最后一个元素替换被删除的元素 let lastSlot := add(baseSlot, sub(length, 1)) sstore(add(baseSlot, i), sload(lastSlot)) sstore(_arr.slot, sub(length, 1)) stop() } } } }

Yul:更安全的汇编选择

Solidity 0.8.x 推荐使用 Yul 作为 Assembly 的替代方案。Yul 是一种中间语言,比原始 Assembly 更结构化:

solidity
// Yul 独立模式示例 object "MyContract" { code { // 部署代码 datacopy(0, dataoffset("Runtime"), datasize("Runtime")) return(0, datasize("Runtime")) } object "Runtime" { code { // 运行时代码 switch selector() case 0x12345678 { // 函数逻辑 } } } }

Yul 的优势:

  • 编译器可以跨块优化
  • 支持类型检查(比 Assembly 更严格)
  • 可通过 --via-ir 编译管道获得更好的优化效果

总结

Assembly 在 Solidity 开发中是一把双刃剑:

适用场景:Gas 敏感的关键路径(DeFi 核心逻辑、批量操作)、实现 Solidity 不支持的 EVM 特性、极致的内存和存储控制。

使用原则:只在有明确收益时使用,每一段 Assembly 都需要详细注释、充分测试、专业审计。优先考虑 Solidity 0.8+ 的 unchecked 块和 --via-ir 编译选项作为轻量级替代。

安全底线:正确管理内存指针、手动检查溢出、实现重入防护、避免存储槽冲突、验证所有外部调用的返回值。

标签:Solidity