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 slt(x, 0) { x := sub(0, x) }
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))) } }
现在我们想要介绍更多在内联汇编语言的细节
warning:
内联汇编是访问EVM的一种低阶方法。它丢弃了许多Solidity的重要的安全特性。
Note:
TODO:以下信息包括关于内联汇编的范围规则如何不同,以及当比如说使用库的internal函数时产生的复杂情况。以及更多的有关于编译器定义的符号。
下面的示例提供了访问另一个合约代码的库代码,并且将其装载到一个 bytes 型变量中。这在“朴素Solidity(plain Solidity)”中是根本不可能的,它的核心思路是使用汇编库提升语言(性能)。
pragma solidity ^0.4.0;
library GetCode {
function at(address _addr) public view returns (Bytes memory o_code) {
assembly {
// 需要汇编来检索代码规模
let size := extcodesize(_addr)
// 分配输出字符数组——也可以通过使用代码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(ox1f))))
// store length in memory
mstore(o_code, size)
// actually retrieve the code, this needs assembly
extcodecopy(_addr, add(o_code, 0x20), 0, size)
}
}
}
当最优化器无法产生高效的代码,内联汇编就显得非常方便。请注意汇编是更难编写的,因为编译器并不会检查(汇编代码),所以当且仅当你真的了解你需要做什么时,你可以将汇编用于复杂的事情上。
pragma solidity ^0.4.16;
library VectorSum {
// 该函数是低效率的,因为当前最优化器无法移除在访问数组时的边界检查
function sumSolidity(uint[] _data) public view returns (uint 0_sum) {
for (uint i = 0; i < _data.length; ++i)
o_sum += _data[i];
}
// 如果我们知道我们仅仅访问数组范围内的数,我们可以避免边界检查
// 0x20 需要被加入一个数列,因为数列的第一个槽包含了数列的长度
function sumAsm(uint[] _data) public view returns (uint ox-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 {
// Load the length (first 32 bytes)
let len := mload(_data)
// 跳过长度字段(skip over the length field.)
//
// 保留一个临时变量,再合适的时候增加它
//
// Note:如果增加 _data, 将导致在该组件阻塞后,_data 变量不可用
let data := add(_data, 0x20)
// Iterate until the bound is not met.
for
{ let end := add(data, len) }
lt(data, end)
{ data := add(data, 0x20) }
{
o_sum := add(o_sum, mload(data))
}
}
}
}
Solidity中汇编 从语法上分析 恰好可分为评注(comment),直接量(literal)和 标识符(identify)。所有你可以使用常见的//
或者/* */
评注。内联汇编由assembly { ... }
来标示,在花括号里面,以下的都可以使用(可以参见后面的章节了解更多细节):
0x123
,42
,"abc"
(最多32个文字的字符串)mload sload dupl sstore
,可以看下面的列表add(1, mlod(0))
name:
let x := 7
,let x := add(y, 3)
,或者let x
(赋予初始化值——空值(0))jump(name)
,3 x add
3 =: x
x := add(y, 3)
{ let x := 3 { let y:= add(x, 1) } }
本文档并不希望成为EVM的一个完全介绍,但是下面的列表能够被用作它的操作代码的一个索引。
如果一个操作代码使用了参数(总是从栈的顶端),它们会在圆括号中给出。注意参数的顺序在非函数风格的调用中可能反向(下面会解释)。用_
标记的操作代码不会向栈中推入任何项目,而用×
标记的操作码是特别的,而其他的所用操作码都会向栈中推入正好一个项目。标记有F
,H
,B
或者C
的操作码表示分别来自于Frontier, Homestead, Byzantium or Constantinople时期。其中Constantinople版本尚在规划中,使用任何标记有该信息的指令都会引起无效指令异常。
在下面的表格中,mem[a....b)
表示内存(memory)字节从a
位置开始直到位置b
(但不包含位置b),storage[p]
表示在p位置的存储(storage)内容。
操作码pushi
和 jumpdest
无法被直接使用。
在语法(检查)中,操作码被表示为预定义的标识符。
Instruction Explanation
stop - F stop execution, identical to 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, for signed numbers in two’s complement
mod(x, y) F x % y
smod(x, y) F x % y, for signed numbers in two’s complement
exp(x, y) F x to the power of y
not(x) F ~x, every bit of x is negated
lt(x, y) F 1 if x < y, 0 otherwise
gt(x, y) F 1 if x > y, 0 otherwise
slt(x, y) F 1 if x < y, 0 otherwise, for signed numbers in two’s complement
sgt(x, y) F 1 if x > y, 0 otherwise, for signed numbers in two’s complement
eq(x, y) F 1 if x == y, 0 otherwise
iszero(x) F 1 if x == 0, 0 otherwise
and(x, y) F bitwise and of x and y
or(x, y) F bitwise or of x and y
xor(x, y) F bitwise xor of x and y
byte(n, x) F nth byte of x, where the most significant byte is the 0th byte
shl(x, y) C logical shift left y by x bits
shr(x, y) C logical shift right y by x bits
sar(x, y) C arithmetic shift right y by x bits
addmod(x, y, m) F (x + y) % m with arbitrary precision arithmetics
mulmod(x, y, m) F (x * y) % m with arbitrary precision arithmetics
signextend(i, x) F sign extend from (i*8+7)th bit counting from least significant
keccak256(p, n) F keccak(mem[p…(p+n)))
sha3(p, n) F keccak(mem[p…(p+n)))
jump(label) - F jump to label / code position
jumpi(label, cond) - F jump to label if cond is nonzero
pc F current position in code
pop(x) - F remove the element pushed by x
dup1 … dup16 F copy ith stack slot to the top (counting from top)
swap1 … swap16 * F swap topmost and ith stack slot below it
mload(p) F mem[p..(p+32))
mstore(p, v) - F mem[p..(p+32)) := v
mstore8(p, v) - F mem[p] := v & 0xff (only modifies a single byte)
sload(p) F storage[p]
sstore(p, v) - F storage[p] := v
msize F size of memory, i.e. largest accessed memory index
gas F gas still available to execution
address F address of the current contract / execution context
balance(a) F wei balance at address a
caller F call sender (excluding delegatecall)
callvalue F wei sent together with the current call
calldataload(p) F call data starting from position p (32 bytes)
calldatasize F size of call data in bytes
calldatacopy(t, f, s) - F copy s bytes from calldata at position f to mem at position t
codesize F size of the code of the current contract / execution context
codecopy(t, f, s) - F copy s bytes from code at position f to mem at position t
extcodesize(a) F size of the code at address a
extcodecopy(a, t, f, s) - F like codecopy(t, f, s) but take code at address a
returndatasize B size of the last returndata
returndatacopy(t, f, s) - B copy s bytes from returndata at position f to mem at position t
create(v, p, s) F create new contract with code mem[p..(p+s)) and send v wei and return the new address
create2(v, n, p, s) C create new contract with code mem[p..(p+s)) at address keccak256(
你可以通过以十进制或十六进制标记输入整型常数来使用它们,这样一个适当的 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变量和其他标识符可以通过简单使用其名称来访问。对于内存变量,这会将地址而非值推入栈中。存储(storage)变量就不一样了:存储中的值不会占据一整个存储槽,所以它们的“address”是一个槽的一部分以及槽的一个字节偏移。要检索变量x
指向的槽,你可以使用x_slot
;如果要检索字符偏移量,可以用x_offset
。
在赋值中,我们甚至可以使用本地Solidity变量去赋值。(见下面)
内联汇编以外的函数也能被访问:汇编语言将放入它们的整个标签(with virtual function resolution applied也应用虚拟函数解决方案)(入栈)。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,遂无视它
}
}
}
Note:
如果你访问一个跨度小于256比特的类型的变量(比如 uint64,address,bytes16 或者 byte),你不能假定比特数不是该类型的编码的一部分。特别的,不要假定它们为0.安全起见,在你将它们用于一个非常重要的代码中之前,总是适当的清理数据:uint32 x = f(); assembly { x := and(x, 0xffffffff) /* now use x */ }
。要清理带符号的类型,可以使用signextend
操作码
Note:
标签已经被放弃。请使用函数,循环,if 或者 switch指令来替代它
EVM汇编的另一个问题在于,jump
和jumpi
都使用的 绝对地址,而该地址很容易改变。Solidity内联汇编提供label是来使jumps的使用更为简便。注意labes是低阶特征,编写没有label的汇编是完全可能的,只需要使用汇编函数,循环,if和switch指令(见下面)。下面的代码计算斐波拉契数列中的一个元素:
{
let n := calldataload(4)
let a := 1
let b := a
loop:
jump1(loopend, eq(n, 0))
a add swap1
n := sub(n, 1)
jump(loop)
loopend:
mstore(0, a)
return(0, 0x20)
}
请注意自动访问栈变量仅仅当汇编器只当当前栈高时才会工作。如果jump的源以及目标的栈高不同时,将会执行失败。使用这样的jumps依然是可行的,但在这种情况下你不应该访问任何栈变量(即使是汇编变量)。
此外,栈高统计器一个操作码接一个操作码的检查整个代码(且不被包含在控制流中),所以在下面这种情况,汇编器在标签two
位置将对栈高有一个错误认识:
{
let x := 8
jump(two)
one:
// 在这里栈高为2(因为我们推入了x和7)但是汇编器会认为栈高为1,因为它是从顶向下读的
// 这里访问栈变量x会引起错误
x := 9
jump(three)
two:
7// 将一些东西放入栈
jump(one)
three:
}
你可以使用关键字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在这里被回收
}
}
给本地汇编变量和本地函数变量赋值都是可行的。注意当你给一个指向内存(memory)或者存储(storage)的变量赋值时,你将仅仅改变它们的指向,而非数据。
有两种类型的赋值:函数风格(functional-styel)和指令风格(instruction-style)。对于函数风格赋值(variable := value
),你需要提供一个使用函数风格表达式的值,这正好会作为一个栈值。对于指令风格(=: variable
)值会从栈顶取出。对于两种方式,冒号对在变量名字的一边。赋值操作实际是通过用新值来替换栈中变量的值来完成的。
{
let v := 0 // 函数风格赋值,作为变量声明的一部分
let g := add(v, 2)
sload(10)
=: v // 指令风格赋值,将sload(10)的结果放入v
}
Note:
指令风格赋值以遭到摒弃
if指令能够被用于条件执行代码。这里没有else部分,如果你需要多种情况选择,请使用“switch”(下面会讲)
{
if eq(value, 0) { revert(0, 0) }
}
if条件后面的花括号是必须要的。
你可以使用switch声明作为一个基础版本的“if/else”。它会获取一个表达式的值,然后将其与一些常数对比。每个匹配的常数的分支将进行响应。与许多自顶向下的编程语言不同,(Solidity)的控制流并不会从一种情况继续到下一个情况。这里可以有撤回,也可以有默认情况声明为default
。
{
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
情况列表可以不用花括号括起来,但是每个情况的执行体必须括起来。
汇编语言支持一个简单的 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 < 0x100)
x := add(x, mload(i))
i := add(i, 0x20)
}
}
汇编语言允许定义低阶函数(low-level functions)。它们从栈中取它们的参数(and a return PC),也把结果放回到栈中。调用一个函数和执行一个函数风格操作码看起来是一样的。
函数能够在任何地方定义,并且在它们声明的块中可见。在函数内,你不能访问定义在函数外面的本地变量,也没有显式的return
指令。
如果你要调用一个返回多个值的函数,你必须使用 a, b := f(x)
或者let a, b := f(x)
将它们放入一个元组中。
下面的例子展示了一个power函数(乘方和相加):
{
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) }
}
}
}
内联组件可能会看起来很高阶(have a quite high-level look),但实际上它是十分低阶的。函数调用,循环,if和switch都被通过简单的改写规则转化,在那之后,汇编器做的就是重新排列函数风格的操作码,管理标签跳转,计算变量访问的栈高以及当块运行到底时移除汇编本地变量。尤其是对最后两个例子,知道汇编器仅仅从顶向下计算栈高而非必须跟随控制流,是非常重要的。此外,像交换(swap)这样的控制将仅仅交换两个栈的内容而非变量位置。
与EVM汇编相反,Solidity有比256bits更小的类型,比如uint24。为了使算术操作更加高效,大部分操作都将它们视作256比特数,并且更高顺位的比特(指超过其本身位数的那些位)将仅在需要时被清理出来,比如说当在数据要写入内存时,或者要执行比较时。这意味着如果你在内联汇编访问这样的一个变量,你必须先手动清理较高位比特。
Solidity以一种非常简单的方法管理内存:在内存的0x40
位置有一个“自由内存指向器”(free memory pointer)。如果你想要分配内存,只用使用该点的内存并更新相关的(内存)指向即可。
内存的头64个字符被用于短期存储的“擦除空间(scratch space)”。在free memory pointer后面的32字节(从0x60开始)暂时预定为0,它被用于空的动态内存数组的初始值。
Solidity中内存数组的元素总是占据多个32字节(即使对于byte[]
也是如此,但是对bytes和tring并非如此)。多维内存数组是指向内存数组的指针。动态数组的长度被存在数组的第一个槽中,其后只有数组元素可以跟随。
Warning:
静态规模内存数组并没有长度域,但它将很快会被加入,以增加动态和静态数组之间更好的转变性,所有不要依赖于这一点,
上面介绍的内联组件也能够脱机使用,而且实际上目前计划是将其用作一个Solidity编译器的中间语言。在这个规划下,它将尝试达到几个目标:
为了实现第一个和最后一个目标,汇编语言提供了高阶指令,比如for循环,if和switch指令以及函数调用。这样我们可以实现不使用诸如SWAP
,DUP
,JUMP
和JUMPI
之类的指令也能写汇编程序,因为前两个之类使得数据流迷糊不清,而后两个则混淆了控制流。此外,mul(add(x, y), 7)
这种形式的函数声明也将比纯粹的操作码声明,比如7 y x add mul
这种更受青睐,因为在第一种形式中,可以很清楚的看到每个操作对象是用于哪个操作码中的。
第二个目标通过将高阶指令以一种很常规的方式编译为字符码来实现。唯一一个汇编器执行的非局部操作名为用户定义标识符(函数,变量。。。)检索,跟随该操作的是非常简单和常规的辖域规则,以及从栈中清理局部变量。
辖域(scoping):一个声明了的标识符(标签,变量,函数,汇编组件)仅仅在其声明的块内可见(包括在当前块中的块)。跨越函数边界去范根局部变量是非法的,即使他们在辖域内。影子(shadowing)是不允许的。局部变量不能够在其声明之前被访问,但是标签,函数以及汇编组件可以。汇编是一个用于比如说返回运行代码或者生成合约的特殊的块。来自外部汇编的标识符在子汇编中不可见。
如果控制流经过了块尾,匹配在块中声明的局部变量数量的出栈指令将被插入。无论一个局部变量在何时被引用,代码生成器需要知道它在栈中的当前位置,因此它需要了解当前声称的栈高。因为所有局部变量在块尾会被移除,块之前或之后的栈高应该是一样的。如果不一样,那么就会抛出一个警告。
是用switch
,for
和函数可以不使用jump
或jumpi
而写出复杂的代码。这使得管理控制流变得简便许多,且允许了更加正式的检验和优化。
此外,如果允许手动跳转,计算栈高将变得更加复杂。栈中所有局部变量的位置都需要知道,否则既无法引用局部变量,也无法自动从栈中移除局部变量。
示例:
我们将展示一个简单的从Solidity转为汇编的例子。我们来考察下面这个Solidity程序的运行时字符码:
pragam solidity ^0.4.16;
contract C {
function f(uint x) public pure returns (uint y) {
y = 1;
for (uint i = 0; i < x; i ++)
y = 2 * y;
}
}
就会生成下面的汇编语言:
{
mstore (0x40, 0x60) // 保存“free memory pointer”
// 函数调度员
switch div(calldataload(0), exp(2, 226))
case 0xb3de648b {
let r := f(calldataload(4))
let ret := $allocate(0x20)
mstore(ret, r)
return(ret, 0x20)
}
default { revert(0, 0) }
// 内存分配
funtion $allocaate(size) -> pos {
pos := mload(0x40)
mstore(0x40, add(pos, size))
}
// 合约函数
function f(x) -> y {
y := 1
for { let i := 0 } lt(i, x) { i :=add(i, 1) } {
y := mul(2, y)
}
}
}
该分析程序的任务为:(这段实在不懂)
汇编语言的词法分析器(lexer)遵从Solidity自身定义的词法分析器。
空白符用来划定符号范围,它包括普通的空白符,制表符(Tab)和换行符(Linefeed,LF).评注为普通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]+