Solidity-18-Inline Assembly
Inline Assembly
您可以使用接近以太坊虚拟机的语言将 Solidity 语句与内联汇编交错。这为您提供了更细粒度的控制,这在您通过编写库来增强语言时特别有用。
在 Solidity 中用于内联汇编的语言称为 Yul,它记录在其自己的部分中。本节将仅介绍内联汇编代码如何与周围的 Solidity 代码交互。
警告
内联汇编是一种在低级别访问以太坊虚拟机的方法。这绕过了几个重要的安全特性和对 Solidity 的检查。你应该只将它用于需要它的任务,并且只有在你有信心使用它的情况下。
内联汇编块由汇编 { ... }
标记,其中大括号内的代码是 Yul 语言的代码。
内联汇编代码可以访问本地 Solidity 变量,如下所述。
不同的内联汇编块不共享命名空间,即不能调用 Yul 函数或访问在不同内联汇编块中定义的 Yul 变量。
例子
以下示例提供库代码来访问另一个合约的代码并将其加载到字节变量中。
通过使用 .code
,“plain Solidity”也可以做到这一点。
但这里的重点是,可重用的汇编库可以在不更改编译器的情况下增强 Solidity 语言。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 =0.4.16 =0.8.10 =0.7.0 , x) }`
从 Solidity 0.6.0 开始,内联汇编变量的名称可能不会影响内联汇编块范围内可见的任何声明(包括变量、合同和函数声明)。
从 Solidity 0.7.0 开始,在内联汇编块中声明的变量和函数可能不包含 .,而是使用 . 从内联汇编块外部访问 Solidity 变量是有效的。
# 避免的事情
内联汇编可能有一个相当高级的外观,但它实际上是非常低级的。
函数调用、循环、ifs 和开关通过简单的重写规则进行转换,之后,汇编器为您做的唯一一件事就是重新排列函数式操作码、计算变量访问的堆栈高度和删除汇编局部变量的堆栈槽 当到达他们的块的末尾时。
# Solidity 中的约定
## 类型变量的值
与 EVM 组装相比,Solidity 具有比 256 位更窄的类型,例如uint24。
为了提高效率,大多数算术运算忽略了类型可以短于 256 位这一事实,并且在必要时清除高阶位,即在将它们写入内存或执行比较之前不久。
这意味着如果您从内联汇编中访问这样的变量,您可能必须先手动清除高阶位。
## 内存管理
Solidity 通过以下方式管理内存。在内存的 0x40 位置有一个“空闲内存指针”。
如果要分配内存,请使用从该指针指向的位置开始的内存并对其进行更新。
不能保证之前没有使用过内存,因此你不能假设它的内容是零字节。没有释放或释放分配的内存的内置机制。
这是一个汇编片段,可用于按照上述过程分配内存
```js
function allocate(length) -> pos {
pos := mload(0x40)
mstore(0x40, add(pos, length))
}
内存的前 64 字节可用作“暂存空间”,用于短期分配。
空闲内存指针之后的 32 个字节(即,从 0x60 开始)意味着永久为零,并用作空动态内存数组的初始值。
这意味着可分配内存从 0x80 开始,这是空闲内存指针的初始值。
Solidity 中的内存数组中的元素总是占用 32 字节的倍数(对于 bytes1[] 甚至是这样,但对于字节和字符串则不然)。
多维内存数组是指向内存数组的指针。
动态数组的长度存储在数组的第一个槽中,然后是数组元素。
- 警告
静态大小的内存数组没有长度字段,但以后可能会添加它以允许在静态和动态大小的数组之间更好地转换,所以不要依赖这个。
内存安全
在不使用内联汇编的情况下,编译器可以依靠内存始终保持良好定义的状态。
这与通过 Yul IR 的新代码生成管道特别相关:此代码生成路径可以将局部变量从堆栈移动到内存以避免堆栈太深错误并执行额外的内存优化,如果它可以依赖于内存使用的某些假设.
虽然我们建议始终尊重 Solidity 的内存模型,但内联汇编允许您以不兼容的方式使用内存。
因此,默认情况下,将堆栈变量移动到内存和其他内存优化在存在任何包含内存操作或分配给内存中的实体变量的内联汇编块时禁用。
但是,您可以专门注释一个组装块,以表明它实际上尊重 Solidity 的内存模型,如下所示:
assembly ("memory-safe") {
...
}
特别是,内存安全的汇编块只能访问以下内存范围:
使用类似于上述分配函数的机制由您自己分配的内存。
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 memory-safe-assembly
assembly {
...
}
请注意,我们将在未来的中断版本中禁止通过注释进行注释,因此如果您不关心与旧编译器版本的向后兼容性,则更喜欢使用方言字符串。