内联汇编
你可以使用接近以太坊虚拟机语言的内联汇编将 Solidity 语句混合在一起。这使你可以更精细地控制,尤其是在编写库以增强语言时非常有用。
Solidity 中内联汇编使用的语言称为 Yul,它在单独的部分中进行了说明。本节只介绍内联汇编代码如何与周围的 Solidity 代码交互。
警告
内联汇编是访问以太坊虚拟机的低级方式。它绕过了 Solidity 的一些重要的安全特性和检查。你应该只在需要时使用它,而且只有在你对使用它有信心时才使用。
内联汇编块以 assembly { ... }
标记,其中大括号内的代码是 Yul 语言的代码。
内联汇编代码可以访问本地 Solidity 变量,如下所述。
不同的内联汇编块没有共享的命名空间,也就是说,无法调用 Yul 函数或访问在另一个内联汇编块中定义的 Yul 变量。
示例
以下示例提供了库代码来访问另一个合约的代码并将其加载到 bytes
变量中。这也可以通过使用 <address>.code
使用“纯 Solidity”来实现。但重点是,可重用的汇编库可以在不更改编译器的情况下增强 Solidity 语言。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
library GetCode {
function at(address addr) public view returns (bytes memory code) {
assembly {
// retrieve the size of the code, this needs assembly
let size := extcodesize(addr)
// allocate output byte array - this could also be done without assembly
// by using code = new bytes(size)
code := mload(0x40)
// new "memory end" including padding
mstore(0x40, add(code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
// store length in memory
mstore(code, size)
// actually retrieve the code, this needs assembly
extcodecopy(addr, add(code, 0x20), 0, size)
}
}
}
内联汇编在优化器无法生成有效代码的情况下也很有用,例如
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
library VectorSum {
// This function is less efficient because the optimizer currently fails to
// remove the bounds checks in array access.
function sumSolidity(uint[] memory data) public pure returns (uint sum) {
for (uint i = 0; i < data.length; ++i)
sum += data[i];
}
// We know that we only access the array in bounds, so we can avoid the check.
// 0x20 needs to be added to an array because the first slot contains the
// array length.
function sumAsm(uint[] memory data) public pure returns (uint sum) {
for (uint i = 0; i < data.length; ++i) {
assembly {
sum := add(sum, mload(add(add(data, 0x20), mul(i, 0x20))))
}
}
}
// Same as above, but accomplish the entire code within inline assembly.
function sumPureAsm(uint[] memory data) public pure returns (uint sum) {
assembly {
// Load the length (first 32 bytes)
let len := mload(data)
// Skip over the length field.
//
// Keep temporary variable so it can be incremented in place.
//
// NOTE: incrementing data would result in an unusable
// data variable after this assembly block
let dataElementLocation := add(data, 0x20)
// Iterate until the bound is not met.
for
{ let end := add(dataElementLocation, mul(len, 0x20)) }
lt(dataElementLocation, end)
{ dataElementLocation := add(dataElementLocation, 0x20) }
{
sum := add(sum, mload(dataElementLocation))
}
}
}
}
访问外部变量、函数和库
你可以使用标识符名称来访问 Solidity 变量和其他标识符。
值类型的本地变量可以直接在内联汇编中使用。它们既可以读取也可以赋值。
引用内存的本地变量会评估为内存中变量的地址,而不是其值本身。这些变量也可以赋值,但请注意,赋值只会改变指针,不会改变数据,并且你需要负责遵守 Solidity 的内存管理。请参阅 Solidity 中的约定。
类似地,引用静态大小的 calldata 数组或 calldata 结构的本地变量会评估为 calldata 中变量的地址,而不是其值本身。变量也可以被分配一个新的偏移量,但请注意,不会执行任何验证来确保变量不会指向超出 calldatasize()
的范围。
对于外部函数指针,可以使用 x.address
和 x.selector
访问地址和函数选择器。选择器包含四个右对齐的字节。这两个值都可以赋值。例如
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.10 <0.9.0;
contract C {
// Assigns a new selector and address to the return variable @fun
function combineToFunctionPointer(address newAddress, uint newSelector) public pure returns (function() external fun) {
assembly {
fun.selector := newSelector
fun.address := newAddress
}
}
}
对于动态 calldata 数组,可以使用 x.offset
和 x.length
访问它们的 calldata 偏移量(以字节为单位)和长度(元素数量)。这两个表达式也可以赋值,但与静态情况一样,不会执行任何验证来确保结果数据区域在 calldatasize()
的范围内。
对于本地存储变量或状态变量,单个 Yul 标识符是不够的,因为它们不一定占用一个完整的存储槽。因此,它们的“地址”由一个槽和该槽内的字节偏移量组成。要检索变量 x
指向的槽,可以使用 x.slot
,要检索字节偏移量,可以使用 x.offset
。使用 x
本身会导致错误。
你还可以为本地存储变量指针的 .slot
部分赋值。对于这些(结构体、数组或映射),.offset
部分始终为零。虽然无法为状态变量的 .slot
或 .offset
部分赋值。
本地 Solidity 变量可用于赋值,例如
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract C {
uint b;
function f(uint x) public view returns (uint r) {
assembly {
// We ignore the storage slot offset, we know it is zero
// in this special case.
r := mul(x, sload(b.slot))
}
}
}
警告
如果你访问跨越少于 256 位的类型的变量(例如 uint64
、address
或 bytes16
),则无法对类型编码中未包含的位进行任何假设。特别地,不要假设它们为零。为了安全起见,在使用它们之前始终清除数据,在这种情况下,清除数据很重要:uint32 x = f(); assembly { x := and(x, 0xffffffff) /* 现在 使用 x */ }
要清除有符号类型,可以使用 signextend
操作码:assembly { signextend(<num_bytes_of_x_minus_one>, x) }
从 Solidity 0.6.0 开始,内联汇编变量的名称不能屏蔽内联汇编块范围内的任何可见声明(包括变量、合约和函数声明)。
从 Solidity 0.7.0 开始,在内联汇编块内声明的变量和函数不能包含 .
,但使用 .
有效地从内联汇编块外部访问 Solidity 变量。
需要注意的事项
内联汇编可能看起来相当高级,但实际上它非常低级。函数调用、循环、if 语句和 switch 语句通过简单的重写规则进行转换,之后,汇编器为你做的唯一事情就是重新排列函数式操作码,为变量访问计算堆栈高度,并在汇编本地变量的块结束时删除堆栈槽。
Solidity 中的约定
类型化变量的值
与 EVM 汇编不同,Solidity 有比 256 位更窄的类型,例如 uint24
。为了效率,大多数算术运算会忽略类型可能小于 256 位的事实,并且在需要时会清除高位,即在它们被写入内存或进行比较之前不久。这意味着,如果从内联汇编中访问这样的变量,你可能需要先手动清除高位。
内存管理
Solidity 以以下方式管理内存。内存中位置 0x40
处有一个“空闲内存指针”。如果你要分配内存,请使用从该指针指向的位置开始的内存,并更新该指针。不能保证内存之前没有被使用过,因此你不能假设它的内容是零字节。没有内置的机制来释放或释放分配的内存。以下是一个你可以用来分配内存的汇编代码片段,它遵循上面概述的过程
function allocate(length) -> pos {
pos := mload(0x40)
mstore(0x40, add(pos, length))
}
内存的前 64 个字节可以用作短时间分配的“暂存空间”。空闲内存指针后的 32 个字节(即从 0x60
开始)应该永远为零,并用作空动态内存数组的初始值。这意味着可分配的内存从 0x80
开始,这是空闲内存指针的初始值。
Solidity 中内存数组中的元素始终占用 32 字节的倍数(即使对于 bytes1[]
也是如此,但对于 bytes
和 string
则不适用)。多维内存数组是指向内存数组的指针。动态数组的长度存储在数组的第一个槽中,紧随其后的是数组元素。
警告
静态大小的内存数组没有长度字段,但以后可能会添加它,以允许在静态大小和动态大小的数组之间更好地转换;因此,不要依赖于这一点。
内存安全
在不使用内联汇编的情况下,编译器可以依赖内存始终保持在定义良好的状态。这对于 通过 Yul IR 的新的代码生成管道 尤其重要:如果可以依赖内存使用的某些假设,这个代码生成路径可以将本地变量从堆栈移动到内存,以避免堆栈过深错误并执行其他内存优化。
虽然我们建议始终遵守 Solidity 的内存模型,但内联汇编允许你以不兼容的方式使用内存。因此,默认情况下,在包含内存操作或为内存中的 Solidity 变量赋值的任何内联汇编块中,将本地变量移动到内存和其他内存优化都将被全局禁用。
但是,你可以专门注释一个汇编块,以指示它实际上遵守 Solidity 的内存模型,如下所示
assembly ("memory-safe") {
...
}
特别是,一个内存安全的汇编块可能只访问以下内存范围
使用像上面描述的
allocate
函数这样的机制自行分配的内存。由 Solidity 分配的内存,例如您引用的内存数组边界内的内存。
上面提到的内存偏移量 0 到 64 之间的临时空间。
位于汇编块开始时的空闲内存指针值之后的临时内存,即在不更新空闲内存指针的情况下,在空闲内存指针处“分配”的内存。
此外,如果汇编块为内存中的 Solidity 变量赋值,您需要确保对 Solidity 变量的访问仅访问这些内存范围。
由于这主要与优化器相关,即使汇编块回滚或终止,也需要遵循这些限制。例如,以下汇编片段不是内存安全的,因为returndatasize()
的值可能会超过 64 字节的临时空间
assembly {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
另一方面,以下代码是内存安全的,因为空闲内存指针指向的位置之外的内存可以安全地用作临时空间
assembly ("memory-safe") {
let p := mload(0x40)
returndatacopy(p, 0, returndatasize())
revert(p, returndatasize())
}
请注意,如果后面没有分配,则无需更新空闲内存指针,但您只能使用从空闲内存指针给出的当前偏移量开始的内存。
如果内存操作使用长度为零,则也可以使用任何偏移量(不仅仅是在临时空间内)
assembly ("memory-safe") {
revert(0, 0)
}
请注意,不仅内联汇编本身的内存操作可能会导致内存不安全,在内存中对引用类型的 Solidity 变量赋值也可能导致内存不安全。例如,以下操作不是内存安全的
bytes memory x;
assembly {
x := 0x40
}
x[0x20] = 0x42;
既不涉及访问内存的任何操作也不为内存中的任何 Solidity 变量赋值的内联汇编会自动视为内存安全,无需注释。
警告
您有责任确保汇编实际上满足内存模型。如果您将汇编块注释为内存安全,但违反了内存假设之一,这将导致无法通过测试轻松发现的错误和未定义行为。
如果您正在开发旨在与多个 Solidity 版本兼容的库,可以使用特殊注释将汇编块注释为内存安全
/// @solidity memory-safe-assembly
assembly {
...
}
请注意,我们将在未来破坏性版本中禁止通过注释进行注释;因此,如果您不关心与旧编译器版本的向后兼容性,建议使用方言字符串。