最近大致浏览了一下Aragon的DAO框架合约,Solidity编写的源代码里使用了很多内联汇编。虽然这种做法有待商榷,但它同时也表明了熟练使用Solidity内联汇编的必要性与紧迫性。CSDN上已经有很多人对Solidity汇编这一章进行了翻译并且翻译的很好,只是:纸上得来终觉浅,绝知此事要躬行, 并且版本有更新,还是需要自己动手才行。因此决定对照英文文档做一下简单的中文记录,目的是进行Solidity内联汇编的学习。这中间也参考了他人的翻译文档,结此表示感谢。
本文以solidity 0.6.0 官方文档英文版为准,并且省略了独立汇编部分。
在Solidity中,定义了一种既能在Solidity代码内部又能独立使用的汇编语言。本文主要介绍了怎么使用内联汇编及相关语法。 内联汇编你可以在Solidity语句中嵌入一种接近以太坊虚拟机(EVM)底层的汇编语言。它能给你更多控制,并且能增强你写的库的功能。 EVM作为一种堆栈机,它很难寻址正确的栈插槽并将参数传递给操作码。Solidity内联汇编会帮助你进行这样的操作,并且避免手写汇编可能会带来的一些额外问题。 对内联汇编来讲,堆栈是完全不可见的。但是你仔细观察,就会发现有一个直接从内联汇编到EVM操作码流转换的方法。
内联汇编有如下几个特性:
- 函数风格式的操作码:
mul(1, add(2,3))
- 汇编局部变量:
let x := add(2,3)
- 访问外部变量:
function f(uint x) public { assembly { x := sub(x, 1) } }
- 循环:
for { let i := 0 } lt(i, x) { i := add(i, 1) } { y := mul(2, y) }
- if 语句
- switch 语句
- 函数调用
内联汇编采用和Solidity相同的方式解析注释、字面值和标识符,你可以使用常用的//和/**/注释。这里有一个例外:内联汇编中的标识符可以含有.
。内联汇编使用 assembly {...}
标记,在大括号内部,你可以使用下列语法:
- 字面值,例如
0x123
,42
,"abc"
(字符串上限32个字符) - 函数风格式的操作码:
add(1,mload(0))
- 变量定义:
let x := 7
,let x
(默认初始化值为0) - 标识符(汇编局部变量或者外部变量):
add(3,x)
,sstore(x_slot,2)
- 赋值:
x := add(y,3)
- 定义代码块用来限定变量的作用域:
{ let x := 3 { let y := add(x, 1) } }
内联汇编管理着内部变量和控制流程。因此,有部分功能相冲突的操作码是无法使用的,它们包括dup
,swap
,jump
,同样不能使用的还有标签功能。
下面的例子演示了一个库访问另一个合约的代码并将它赋值给一个bytes
变量。这在普通的Solidity代码中是无法实现的。这也表明可复用的汇编库能在不改变编译器的情况下增强Solidity语言的功能。
pragma solidity >=0.4.0 =0.4.11 =0.4.16 a, b { }
let c, d := f()
}
If语句
If语句用来条件选择执行,它并没有"else"部分。当你需要"else"功能时,考虑使用switch作为替代。
{
if eq(value, 0) { revert(0, 0) }
}
执行体的{}
是必需的。
你可以使用switch语句作为一个基础版的"if/else"。它将表达式的值和多个常量作比较,符合的分支会被执行。和某些编程语言不同的是,控制流程并不会从一个分支自动转到另一个分支(也就是没有case穿透)。相似的,默认分支叫default
。
{
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
case列表不需要大括号,但是分支的执行体需要。
循环汇编支持一个简单的for循环。For循环有一个包含了初始条件、判定条件、末尾循环体的循环头。判定条件必须是函数风格式的表达式,其它两个是代码块。如果在初始条件中定义了任何变量,那么该变量在整个循环中有效(包括循环头和循环体)。 下面的例子计算了内存中某一区域的和。
{
let x := 0
for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
x := add(x, mload(i))
}
}
For循环也可以用作while循环,只需要将初始条件和末尾循环体为空代码块即可。
{
let x := 0
let i := 0
for { } lt(i, 0x100) { } { // while(i result {
switch exponent
case 0 { result := 1 }
case 1 { result := base }
default {
result := power(mul(base, base), div(exponent, 2))
switch mod(exponent, 2)
case 1 { result := mul(base, result) }
}
}
}
注意事项
内联汇编看上去像高等级语言,但是它实际上是相当低等级的。函数调用、循环、if和switch语句被转换为简单的重写规则,这在之后,汇编所做的事情只是重新安排函数风格的操作码,为变量统计堆栈高度并且在汇编局部变量到达作用域结束时进行释放。
Solidity惯例 和EVM汇编不同的是,Solidity中的数据类型可以小于256位。为了效率,绝大多数算术操作符会忽视这些小于256位的变量的实际类型,在必要时也会清除高位数据。这就意味着如果你要从内联汇编中访问这些变量,你需要首先手动清除高位数据。 Solidity使用下面的方式管理内存。在内存0x40
位置存有一个空闲内存指针。如果你想分配内存,从这个指针指向的位置开始分配并且更新它的指向。这里没有任何保证内存以前未被使用并且所有的数据都为字节0(为空)。并没有内建的机制用来释放和自由分配内存。下面的代码片断是一个遵循了上述流程进行分配内存的简单示例:
function allocate(length) -> pos {
pos := mload(0x40)
mstore(0x40, add(pos, length))
}
内存中开始64字节作为临时暂存空间。空闲内存指针后的32个字节(因为指针大小为32个字节,所以地址是从0x60
开始)将永远为0被用来初始化空的动态内存数组。这就意味着实际内存分配是从0x80
开始的,这也是空闲内存指针最初的值。 在Solidity中,内存数组中的元素经常占用32个字节(甚至是byte[]
,并不包括bytes
和string
)。多维内存数组是指向内存数组的指针。动态数组的长度被存储在数组的第一个槽位,接下来才是数组元素。
(略)
编程要严谨,欢迎大家指出文章中存在的错误或者表达不清晰、不准确的地方。
这里参考了这篇文章的部分内容:以太坊:深入理解Solidity-Solidity汇编 对文章的作者表示感谢。
英文原文: https://solidity.readthedocs.io/en/v0.6.0/assembly.html