Solidity 定义了一种汇编语言,在没有 Solidity 的情况下也可以使用。这种汇编语言也可以嵌入到 Solidity 源代码中当作“内联汇编”使用。 我们从如何使用内联汇编开始,介绍它如何区别于独立汇编语言,然后详细讲述这种汇编语言。
内联汇编
为了实现更细粒度的控制,尤其是为了通过编写库来增强语言,可以利用接近虚拟机的语言将内联汇编与 Solidity 语句结合在一起使用。 由于 EVM 是基于栈的虚拟机,因此通常很难准确地定位栈内插槽(存储位置)的地址,并为操作码提供正确的栈内位置来获取参数。 Solidity 的内联汇编试图通过提供以下特性来解决这个问题以及手工编写汇编代码时可能出现的问题:
-
函数风格操作码:
mul(1, add(2, 3))
而不是push1 3 push1 2 add push1 1 mul
-
汇编局部变量:
let x := add(2, 3) let y := mload(0x40) x := add(x, y)
-
可以访问外部变量:
function f(uint x) public { assembly { x := sub(x, 1) } }
-
标签:
let x := 10 repeat: x := sub(x, 1) jumpi(repeat, eq(x, 0))
-
循环:
for { let i := 0 } lt(i, x) { i := add(i, 1) } { y := mul(2, y) }
-
if 语句:
if slt(x, 0) { x := sub(0, x) }
-
switch 语句:
switch x case 0 { y := mul(x, 2) } default { y := 0 }
-
函数调用:
function f(x) -> y { switch x case 0 { y := 1 } default { y := mul(x, f(sub(x, 1))) } }
现在我们详细讲解内联汇编语言。
警告
内联汇编是一种在底层访问以太坊虚拟机的语言。这抛弃了很多 Solidity 提供的重要安全特性。
注解
TODO:写出在内联汇编中作用域规则的细微差别,以及在使用库合约的内部函数时产生的复杂性。此外,还要编写有关编译器定义的符号。
eg
下面例子展示了一个库合约的代码,它可以取得另一个合约的代码,并将其加载到一个 bytes
变量中。 这对于“常规 Solidity”来说是根本不可能的,汇编库合约则可以通过这种方式来增强语言特性。
pragma solidity ^0.4.0;
library GetCode {
function at(address _addr) public view returns (bytes o_code) {
assembly {
// 获取代码大小,这需要汇编语言
let size := extcodesize(_addr)
// 分配输出字节数组 – 这也可以不用汇编语言来实现
// 通过使用 o_code = new bytes(size)
o_code := mload(0x40)
// 包括补位在内新的“memory end”
mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f))))
// 把长度保存到内存中
mstore(o_code, size)
// 实际获取代码,这需要汇编语言
extcodecopy(_addr, add(o_code, 0x20), 0, size)
}
}
}
在优化器无法生成高效代码的情况下,内联汇编也可能更有好处。请注意,由于编译器无法对汇编语句进行相关的检查,所以编写汇编代码肯定更加困难; 因此只有在处理一些相对复杂的问题时才需要使用它,并且你需要明确知道自己要做什么。
pragma solidity ^0.4.16;
library VectorSum {
// 因为目前的优化器在访问数组时无法移除边界检查,
// 所以这个函数的执行效率比较低。
function sumSolidity(uint[] _data) public view returns (uint o_sum) {
for (uint i = 0; i < _data.length; ++i)
o_sum += _data[i];
}
// 我们知道我们只能在数组范围内访问数组元素,所以我们可以在内联汇编中不做边界检查。
// 由于 ABI 编码中数组数据的第一个字(32 字节)的位置保存的是数组长度,
// 所以我们在访问数组元素时需要加入 0x20 作为偏移量。
function sumAsm(uint[] _data) public view returns (uint o_sum) {
for (uint i = 0; i < _data.length; ++i) {
assembly {
o_sum := add(o_sum, mload(add(add(_data, 0x20), mul(i, 0x20))))
}
}
}
// 和上面一样,但在内联汇编内完成整个代码。
function sumPureAsm(uint[] _data) public view returns (uint o_sum) {
assembly {
// 取得数组长度(前 32 字节)
let len := mload(_data)
// 略过长度字段。
//
// 保持临时变量以便它可以在原地增加。
//
// 注意:对 _data 数值的增加将导致 _data 在这个汇编语句块之后不再可用。
// 因为无法再基于 _data 来解析后续的数组数据。
let data := add(_data, 0x20)
// 迭代到数组数据结束
for
{ let end := add(data, mul(len, 0x20)) }
lt(data, end)
{ data := add(data, 0x20) }
{
o_sum := add(o_sum, mload(data))
}
}
}
}
语法
和 Solidity 一样,Assembly 也会解析注释、文字和标识符,所以你可以使用通常的 //
和 /* */
来进行注释。 内联汇编程序由 assembly { ... }
来标记,在这些大括号内可以使用以下内容(更多详细信息请参阅后面部分)。
字面常数,也就是
0x123
、42
或"abc"
(不超过 32 个字符的字符串)操作码(在“instruction style”内),比如
mload sload dup1 sstore
,操作码列表请看后面函数风格操作码,比如
add(1,mlod(0))
标签,比如
name:
变量声明,比如
let x := 7
、let x := add(y, 3)
或者let x
(初始值将被置为 empty(0))标识符(标签或者汇编局部变量以及用作内联汇编时的外部变量),比如
jump(name)
、3 x add
赋值(在“instruction style”内),比如
3 =: x
函数风格赋值,比如
x := add(y,3)
一些控制局部变量作用域的语句块,比如
{let x := 3 { let y := add(x,1) }}
操作码
本文档不是以太坊虚拟机的详细描述,但下边的列表可以作为操作码参考。
如果一个操作码需要参数(总是来自堆栈顶部),它们会在括号中给出。请注意:参数顺序可以看作是在非函数风格中逆序(下面会解释)。 标有 -
的操作码不会向栈中压入(push)数据,标有 *
的操作码有特殊操作,而所有其他操作码都只会将一个数据压入(push)栈中。 用 F
、H
、B
或 C
标记的操作码代表它们从 Frontier、Homestead、Byzantium 或 Constantinople 开始被引入。 Constantinople 目前仍在计划中,所以标记为 C
的指令目前都会导致一个非法指令异常。
在下表中,mem[a...b)
表示从位置 a
开始至(不包括)位置 b
的内存字节数,storage[p]
表示位置 p
处的存储内容。
pushi
和 jumpdest
这两个操作码不能直接用。
在语法表中,操作码是作为预定义标识符提供的。
Instruction | Explanation | ||
---|---|---|---|
stop | - | F | 停止执行,与 return(0,0) 等价 |
add(x, y) | F | x + y | |
sub(x, y) | F | x - y | |
mul(x, y) | F | x * y | |
div(x, y) | F | x / y | |
sdiv(x, y) | F | x / y,以二进制补码作为符号 | |
mod(x, y) | F | x % y | |
smod(x, y) | F | x % y,以二进制补码作为符号 | |
exp(x, y) | F | x 的 y 次幂 | |
not(x) | F | ~x,对 x 按位取反 | |
lt(x, y) | F | 如果 x < y 为 1,否则为 0 | |
gt(x, y) | F | 如果 x > y 为 1,否则为 0 | |
slt(x, y) | F | 如果 x < y 为 1,否则为 0,以二进制补码作为符号 | |
sgt(x, y) | F | 如果 x > y 为 1,否则为 0,以二进制补码作为符号 | |
eq(x, y) | F | 如果 x == y 为 1,否则为 0 | |
iszero(x) | F | 如果 x == 0 为 1,否则为 0 | |
and(x, y) | F | x 和 y 的按位与 | |
or(x, y) | F | x 和 y 的按位或 | |
xor(x, y) | F | x 和 y 的按位异或 | |
byte(n, x) | F | x 的第 n 个字节,这个索引是从 0 开始的 | |
shl(x, y) | C | 将 y 逻辑左移 x 位 | |
shr(x, y) | C | 将 y 逻辑右移 x 位 | |
sar(x, y) | C | 将 y 算术右移 x 位 | |
addmod(x, y, m) | F | 任意精度的 (x + y) % m | |
mulmod(x, y, m) | F | 任意精度的 (x * y) % m | |
signextend(i, x) | F | 对 x 的最低位到第 (i * 8 + 7) 进行符号扩展 | |
keccak256(p, n) | F | keccak(mem[p…(p + n))) | |
jump(label) | - | F | 跳转到标签 / 代码位置 |
jumpi(label, cond) | - | F | 如果条件为非零,跳转到标签 |
pc | F | 当前代码位置 | |
pop(x) | - | F | 删除(弹出)栈顶的 x 个元素 |
dup1 … dup16 | F | 将栈内第 i 个元素(从栈顶算起)复制到栈顶 | |
swap1 … swap16 | * | F | 将栈顶元素和其下第 i 个元素互换 |
mload(p) | F | mem[p…(p + 32)) | |
mstore(p, v) | - | F | mem[p…(p + 32)) := v |
mstore8(p, v) | - | F | mem[p] := v & 0xff (仅修改一个字节) |
sload(p) | F | storage[p] | |
sstore(p, v) | - | F | storage[p] := v |
msize | F | 内存大小,即最大可访问内存索引 | |
gas | F | 执行可用的 gas | |
address | F | 当前合约 / 执行上下文的地址 | |
balance(a) | F | 地址 a 的余额,以 wei 为单位 | |
caller | F | 调用发起者(不包括 | |
callvalue | F | 随调用发送的 Wei 的数量 | |
calldataload(p) | F | 位置 p 的调用数据(32 字节) | |
calldatasize | F | 调用数据的字节数大小 | |
calldatacopy(t, f, s) | - | F | 从调用数据的位置 f 的拷贝 s 个字节到内存的位置 t |
codesize | F | 当前合约 / 执行上下文地址的代码大小 | |
codecopy(t, f, s) | - | F | 从代码的位置 f 开始拷贝 s 个字节到内存的位置 t |
extcodesize(a) | F | 地址 a 的代码大小 | |
extcodecopy(a, t, f, s) | - | F | 和 codecopy(t, f, s) 类似,但从地址 a 获取代码 |
returndatasize | B | 最后一个 returndata 的大小 | |
returndatacopy(t, f, s) | - | B | 从 returndata 的位置 f 拷贝 s 个字节到内存的位置 t |
create(v, p, s) | F | 用 mem[p…(p + s)) 中的代码创建一个新合约、发送 v wei 并返回 新地址 | |
create2(v, n, p, s) | C | 用 mem[p…(p + s)) 中的代码,在地址 keccak256(<address> . n . keccak256(mem[p…(p + s))) 上 创建新合约、发送 v wei 并返回新地址 | |
call(g, a, v, in, insize, out, outsize) | F | 使用 mem[in…(in + insize)) 作为输入数据, 提供 g gas 和 v wei 对地址 a 发起消息调用, 输出结果数据保存在 mem[out…(out + outsize)), 发生错误(比如 gas 不足)时返回 0,正确结束返回 1 | |
callcode(g, a, v, in, insize, out, outsize) | F | 与 | |
delegatecall(g, a, in, insize, out, outsize) | F | 与 | |
staticcall(g, a, in, insize, out, outsize) | F | 与 | |
return(p, s) | - | F | 终止运行,返回 mem[p…(p + s)) 的数据 |
revert(p, s) | - | B | 终止运行,撤销状态变化,返回 mem[p…(p + s)) 的数据 |
selfdestruct(a) | - | F | 终止运行,销毁当前合约并且把资金发送到地址 a |
invalid | - | F | 以无效指令终止运行 |
log0(p, s) | - | F | 以 mem[p…(p + s)) 的数据产生不带 topic 的日志 |
log1(p, s, t1) | - | F | 以 mem[p…(p + s)) 的数据和 topic t1 产生日志 |
log2(p, s, t1, t2) | - | F | 以 mem[p…(p + s)) 的数据和 topic t1、t2 产生日志 |
log3(p, s, t1, t2, t3) | - | F | 以 mem[p…(p + s)) 的数据和 topic t1、t2、t3 产生日志 |
log4(p, s, t1, t2, t3, t4) | - | F | 以 mem[p…(p + s)) 的数据和 topic t1、t2、t3 和 t4 产生日志 |
origin | F | 交易发起者地址 | |
gasprice | F | 交易所指定的 gas 价格 | |
blockhash(b) | F | 区块号 b 的哈希 - 目前仅适用于不包括当前区块的最后 256 个区块 | |
coinbase | F | 当前的挖矿收益者地址 | |
timestamp | F | 从当前 epoch 开始的当前区块时间戳(以秒为单位) | |
number | F | 当前区块号 | |
difficulty | F | 当前区块难度 | |
gaslimit | F | 当前区块的 gas 上限 |
字面常量
你可以直接键入十进制或十六进制符号来作为整型常量使用,这会自动生成相应的 PUSHi
指令。 下面的代码将计算 2 加 3(等于 5),然后计算其与字符串 “abc” 的按位与。字符串在存储时为左对齐,且长度不能超过 32 字节。
assembly { 2 3 add "abc" and }
函数风格
你可以像使用字节码那样在操作码之后键入操作码。例如,把 3
与内存位置 0x80
处的数据相加就是
3 0x80 mload add 0x80 mstore
由于通常很难看到某些操作码的实际参数是什么,所以 Solidity 内联汇编还提供了一种“函数风格”表示法,同样功能的代码可以写做
mstore(0x80, add(mload(0x80), 3))
函数风格表达式内不能使用指令风格的写法,即 1 2 mstore(0x80, add)
是无效汇编语句, 它必须写成 mstore(0x80, add(2, 1))
这种形式。对于不带参数的操作码,括号可以省略。
注意,在函数风格写法中参数的顺序与指令风格相反。如果使用函数风格写法,第一个参数将会位于栈顶。
访问外部变量和函数
通过简单使用它们名称就可以访问 Solidity 变量和其他标识符。对于内存变量,这会将地址而不是值压入栈中。 存储变量是不同的,因为存储变量的值可能不占用完整的存储槽,因此其“地址”由存储槽和槽内的字节偏移量组成。 为了获取变量 x
所使用的存储槽,你可以使用 x_slot
,并用的 x_offset
获取其字节偏移量。
在赋值语句中(见下文),我们甚至可以使用 Solidity 局部变量来赋值。
对于内联汇编而言的外部函数也可以被访问:汇编会将它们的入口标签(带有虚拟函数解析)压入栈中。Solidity 中的调用语义为:
调用者压入
return label
、arg1
、arg2
、…、argn
被调用方返回
ret1
、ret2
、…、retm
这个特性使用起来还是有点麻烦,因为在调用过程中堆栈偏移量发生了根本变化,因此对局部变量的引用将会出错。
pragma solidity ^0.4.11;
contract C {
uint b;
function f(uint x) public returns (uint r) {
assembly {
r := mul(x, sload(b_slot)) // 因为偏移量为 0,所以可以忽略
}
}
}
汇编局部变量声明
你可以使用 let
关键字来声明只在内联汇编中可见的变量,实际上只在当前的 {...}
块中可见。 下面发生的事情应该是:let
指令将创建一个为变量保留的新数据槽,并在到达块末尾时自动删除。 你需要为变量提供一个初始值,它可以只是 0
,但它也可以是一个复杂的函数风格表达式。
pragma solidity ^0.4.16;
contract C {
function f(uint x) public view returns (uint b) {
assembly {
let v := add(x, 1)
mstore(0x80, v)
{
let y := add(sload(v), 1)
b := y
} // y 会在这里被“清除”
b := add(b, v)
} // v 会在这里被“清除”
}
}
赋值
可以给汇编局部变量和函数局部变量赋值。请注意:当给指向内存或存储的变量赋值时,你只是更改指针而不是数据。
有两种赋值方式:函数风格和指令风格。对于函数风格赋值(变量 := 值
),你需要在函数风格表达式中提供一个值,它恰好可以产生一个栈里的值; 对于指令风格赋值(=: 变量
),则仅从栈顶部获取数据。对于这两种方式,冒号均指向变量名称。赋值则是通过用新值替换栈中的变量值来实现的。
{
let v := 0 // 作为变量声明的函数风格赋值
let g := add(v, 2)
sload(10)
=: v // 指令风格的赋值,将 sload(10) 的结果赋给 v
}
If
if 语句可以用于有条件地执行代码,且没有“else”部分;如果需要多种选择,你可以考虑使用“switch”(见下文)。
{
if eq(value, 0) { revert(0, 0) }
}
代码主体的花括号是必需的。
Switch
作为“if/else”的非常初级的版本,你可以使用 switch 语句。它计算表达式的值并与几个常量进行比较。选出与匹配常数对应的分支。 与某些编程语言容易出错的情况不同,控制流不会从一种情形继续执行到下一种情形。我们可以设定一个 fallback 或称为 default
的默认情况。
{
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
Case 列表里面不需要大括号,但 case 主体需要。
循环
汇编语言支持一个简单的 for-style 循环。For-style 循环有一个头,它包含初始化部分、条件和迭代后处理部分。 条件必须是函数风格表达式,而另外两个部分都是语句块。如果起始部分声明了某个变量,这些变量的作用域将扩展到循环体中(包括条件和迭代后处理部分)。
下面例子是计算某个内存区域中的数值总和。
{
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 < 0x100)
x := add(x, mload(i))
i := add(i, 0x20)
}
}
函数
汇编语言允许定义底层函数。底层函数需要从栈中取得它们的参数(和返回 PC),并将结果放入栈中。调用函数的方式与执行函数风格操作码相同。
函数可以在任何地方定义,并且在声明它们的语句块中可见。函数内部不能访问在函数之外定义的局部变量。这里没有严格的 return
语句。
如果调用会返回多个值的函数,则必须使用 a,b:= f(x)
或 let a,b:= f(x)
的方式把它们赋值到一个元组。
下面例子通过平方和乘法实现了幂运算函数。
{
function power(base, exponent) -> 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) }
}
}
}
汇编语法
解析器任务如下:
-
将字节流转换为符号流,丢弃 C ++ 风格的注释(对源代码引用存在特殊注释,我们这里不解释它)。
-
根据下面的语法,将符号流转换为 AST。
-
注册语句块中定义的标识符(注释到 AST 节点),并注明变量从哪个地方开始可以访问。
汇编词法分析器遵循由 Solidity 自己定义的规则。
空格用于分隔所有符号,它由空格字符、制表符和换行符组成。注释格式是常规的 JavaScript/C++ 风格,并被解释为空格。
Grammar:
AssemblyBlock = '{' AssemblyItem* '}'
AssemblyItem =
Identifier |
AssemblyBlock |
AssemblyExpression |
AssemblyLocalDefinition |
AssemblyAssignment |
AssemblyStackAssignment |
LabelDefinition |
AssemblyIf |
AssemblySwitch |
AssemblyFunctionDefinition |
AssemblyFor |
'break' |
'continue' |
SubAssembly
AssemblyExpression = AssemblyCall | Identifier | AssemblyLiteral
AssemblyLiteral = NumberLiteral | StringLiteral | HexLiteral
Identifier = [a-zA-Z_$] [a-zA-Z_0-9]*
AssemblyCall = Identifier '(' ( AssemblyExpression ( ',' AssemblyExpression )* )? ')'
AssemblyLocalDefinition = 'let' IdentifierOrList ( ':=' AssemblyExpression )?
AssemblyAssignment = IdentifierOrList ':=' AssemblyExpression
IdentifierOrList = Identifier | '(' IdentifierList ')'
IdentifierList = Identifier ( ',' Identifier)*
AssemblyStackAssignment = '=:' Identifier
LabelDefinition = Identifier ':'
AssemblyIf = 'if' AssemblyExpression AssemblyBlock
AssemblySwitch = 'switch' AssemblyExpression AssemblyCase*
( 'default' AssemblyBlock )?
AssemblyCase = 'case' AssemblyExpression AssemblyBlock
AssemblyFunctionDefinition = 'function' Identifier '(' IdentifierList? ')'
( '->' '(' IdentifierList ')' )? AssemblyBlock
AssemblyFor = 'for' ( AssemblyBlock | AssemblyExpression )
AssemblyExpression ( AssemblyBlock | AssemblyExpression ) AssemblyBlock
SubAssembly = 'assembly' Identifier AssemblyBlock
NumberLiteral = HexNumber | DecimalNumber
HexLiteral = 'hex' ('"' ([0-9a-fA-F]{2})* '"' | '\'' ([0-9a-fA-F]{2})* '\'')
StringLiteral = '"' ([^"\r\n\\] | '\\' .)* '"'
HexNumber = '0x' [0-9a-fA-F]+
DecimalNumber = [0-9]+