内联程序集

您可以使用类似于以太坊虚拟机的语言将Solidity语句与内联程序集交错。这为您提供了更细粒度的控制,这在您通过编写库来增强语言时尤其有用。

Solidity中用于内联程序集的语言称为 Yul 它被记录在它自己的章节里。本节只讨论内联汇编代码如何与周围的Solidity代码接口。

警告

内联程序集是一种低级别访问以太坊虚拟机的方法。这将绕过几个重要的安全特性和可靠性检查。您应该只在需要它的任务中使用它,并且只有在您有信心使用它的情况下才使用它。

由内联程序集标记 assembly {{ ... }} ,其中大括号内的代码是 Yul 语言。

内联汇编代码可以访问局部的Solidity变量,如下所述。

不同的内联程序集块不共享命名空间,即不可能调用Yul函数或访问在不同内联程序集块中定义的Yul变量。

例子

下面的示例提供用于访问另一个协定的代码并将其加载到 bytes 变量。这对于“普通坚固”也是可能的,方法是使用 <address>.code 。但这里的要点是,可重用汇编库可以在不更改编译器的情况下增强语言的健壮性。

// 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 o_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 o_code = new bytes(size)
            o_code := mload(0x40)
            // new "memory end" including padding
            mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
            // store length in memory
            mstore(o_code, size)
            // actually retrieve the code, this needs assembly
            extcodecopy(_addr, add(o_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 data := add(_data, 0x20)

            // Iterate until the bound is not met.
            for
                { let end := add(data, mul(len, 0x20)) }
                lt(data, end)
                { data := add(data, 0x20) }
            {
                sum := add(sum, mload(data))
            }
        }
    }
}

访问外部变量、函数和库

可以使用Solidity变量和其他标识符的名称来访问它们。

值类型的局部变量可在内联程序集中直接使用。它们都可以被读取和分配给。

引用内存的局部变量计算的是变量在内存中的地址,而不是值本身。这类变量也可以赋值给,但请注意,赋值只会更改指针,而不会更改数据,并且您有责任尊重Solidity的内存管理。看见 Conventions in Solidity

同样,引用静态大小的calldata数组或calldata结构的局部变量的计算结果为calldata中变量的地址,而不是值本身。也可以为该变量分配一个新的偏移量,但请注意,没有验证来确保该变量不会指向更远的位置 calldatasize() 被执行。

对于外部函数指针,可以使用以下命令访问地址和函数选择器 x.addressx.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数组,可以使用以下命令访问它们的calldata偏移量(以字节为单位)和长度(元素数量 x.offsetx.length 。这两个表达式也都可以赋值,但是对于静电的情况,不会执行验证以确保结果数据区域在 calldatasize()

对于本地存储变量或状态变量,单个Yul标识符是不够的,因为它们不一定占用一个完整的存储插槽。因此,插槽是由一个字节组成的。检索变量指向的插槽 x ,您使用 x.slot ,并检索您使用的字节偏移量 x.offset . 使用 x 它本身会导致错误。

您还可以将 .slot 本地存储变量指针的一部分。对于这些(结构、数组或映射), .offset 零件始终为零。无法将其分配给 .slot.offset 不过,是状态变量的一部分。

局部实性变量可用于赋值,例如:

// 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位的类型的变量(例如 uint64address ,或 bytes16 ),则不能对不属于该类型编码的位做出任何假设。特别是,不要假设它们为零。为安全起见,在重要的上下文中使用数据之前,请始终正确清除数据: uint32 x = f(); assembly {{ x := and(x, 0xffffffff) /* now use x */ }} 若要清除签名类型,可以使用 signextend 操作码: assembly {{ signextend(<num_bytes_of_x_minus_one>, x) }}

由于Solidity 0.6.0,内联程序集变量的名称不能隐藏内联程序集块范围内可见的任何声明(包括变量、协定和函数声明)。

由于Solidity 0.7.0,在内联程序集块中声明的变量和函数不能包含 ., but using . 从内联程序集块外部访问实体变量是有效的。

要避免的事情

内联装配可能具有相当高的层次外观,但实际上是非常低的层次。函数调用、循环、ifs和开关通过简单的重写规则进行转换,然后,汇编程序为您所做的唯一一件事就是重新安排函数式操作码,计算变量访问的堆栈高度,并在它们的b结束时删除汇编局部变量的堆栈槽。已达到锁定状态。

一致性约定

与EVM程序集相比,solidity的类型窄于256位,例如 uint24 .为了提高效率,大多数算术运算都忽略了这样一个事实:类型可以短于256位,并且在必要时(即在写入内存或执行比较之前)清除高阶位。这意味着,如果您从内联程序集中访问这样一个变量,那么您可能必须首先手动清除高阶位。

Solidity通过以下方式管理内存。位置有一个“空闲内存指针” 0x40 在记忆中。如果要分配内存,请从此指针指向的位置开始使用内存并更新它。不能保证内存以前没有使用过,因此您不能假设它的内容是零字节。没有释放或释放已分配内存的内置机制。下面是一个程序集片段,您可以使用它来按照上面概述的过程分配内存

function allocate(length) -> pos {
  pos := mload(0x40)
  mstore(0x40, add(pos, length))
}

前64个字节的内存可以用作短期分配的“临时空间”。空闲内存指针后的32个字节(即从 0x60 )是指永久为零,并用作空动态内存数组的初始值。这意味着可分配内存开始于 0x80 ,这是可用内存指针的初始值。

坚固的存储器阵列中的元件总是占用32字节的倍数(甚至对于 bytes1[] ,但不是为了 bytesstring )。多维存储器阵列是指向存储器阵列的指针。动态数组的长度存储在数组的第一个槽中,后跟数组元素。

警告

静态大小的内存数组没有长度字段,但稍后可能会添加该字段,以便在静态大小的数组和动态大小的数组之间实现更好的可转换性,因此不要依赖于此。