Solidity 部件
Solidity定义了一种也可以在没有Solidity的情况下使用的汇编语言。 此汇编语言也可以用作Solidity源代码中的“内联汇编”。 我们从描述如何使用内联汇编以及它与独立程序集的区别开始,然后指定程序集本身。
TODO:编写内联汇编的范围规则有点不一样,例如使用库的内部函数时出现的复杂情况。 此外,请编写由编译器定义的符号。
内联汇编
为了更精细的控制,特别是为了通过编写库来增强语言,可以使用接近虚拟机的语言将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) { 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) }
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的几个重要安全功能。
例子
以下示例提供了用于访问另一个合约的代码并将其加载到字节变量中的库代码。 这是不可能的,在所有”plain Solidity”和想法是,组件库将用于提高这种方式的语言。
pragma solidity ^0.4.0;
library GetCode {
function at(address _addr) returns (bytes o_code) {
assembly {
// 检索代码的大小,这需要汇编
let size := extcodesize(_addr)
// 分配输出字节数组 - 这也可以在没有汇编的情况下完成
// by using o_code = new bytes(size)
o_code := mload(0x40)
// 新的“内存端”包括填充
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)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在优化器无法生成有效代码的情况下,内联汇编也可能是有益的。 请注意,汇编更难编写,因为编译器不执行检查,所以只有当你真的知道你在做什么时,才应该使用它来处理复杂的事情。
pragma solidity ^0.4.12;
library VectorSum {
// 此功能效率较低,因为优化程序当前无法删除阵列访问中的边界检查。
function sumSolidity(uint[] _data) returns (uint o_sum) {
for (uint i = 0; i < _data.length; ++i)
o_sum += _data[i];
}
// 我们知道我们只能在边界访问数组,所以我们可以避免检查。 0x20需要添加到数组,因为第一个插槽包含数组长度。
function sumAsm(uint[] _data) 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) returns (uint o_sum) {
assembly {
// 加载长度(前32个字节)
let len := mload(_data)
// 跳过长度字段。
//
// 保持临时变量,使其可以增加到位。
//
// 注意:增加_data将导致此程序集后不可用的_data变量
let data := add(_data, 0x20)
// 迭代,直到绑定不满足。
for
{ let end := add(data, len) }
lt(data, end)
{ data := add(data, 0x20) }
{
o_sum := add(o_sum, mload(data))
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
语法
汇编将注释,文字和标识符完全解析为Solidity,因此您可以使用通常的//和/ * * /注释。 内联装配体由装配{...}标记,并且在这些花括号内,可以使用以下内容(有关详细信息,请参阅后面的部分)
文字,即0x123的,42或 "ABC"(字符串最多32个字符)
操作码(在“指令式”),例如 mload sload dup1存储,列表见下文
函数式的操作码,例如 add(1,mlod(0))
标签,例如 name:
可变声明,例如 让x:= 7,让x:= add(y,3)或让x(empty (0)的初始值分配)
标识符(标签或汇编局部变量和外部作为内联汇编使用),比如jump(name), 3 x add
分配(在“指令式”),例如 3 =:x
函数式分配,例如 x:= add(y,3)
块,其中局部变量内作用域,例如 {let x:= 3 {let y:= add(x,1)}}
操作码
本文档不希望完整描述Ethereum虚拟机,但以下列表可用作其操作码的参考。
如果操作码接受参数(总是从堆栈的顶部),它们将以括号给出。 请注意,参数的顺序可以看作是在非功能性风格(如下所述)中颠倒。 标记的操作码 - 不要将一个项目推入堆栈,标有*的项目是特殊的,而其他所有其他项目都将一个项目推入堆栈。
在下文中,mem [a ... b]表示从位置a开始到(不包括)位置b的存储器的字节,存储器[p]表示位置p处的存储内容。
操作码pushi和jumpdest不能直接使用。
在语法中,操作码被表示为预定义的标识符。
stop - stop execution, identical to return(0,0)
add(x, y) x + y
sub(x, y) x - y
mul(x, y) x * y
div(x, y) x / y
sdiv(x, y) x / y, for signed numbers in two’s complement
mod(x, y) x % y
smod(x, y) x % y, for signed numbers in two’s complement
exp(x, y) x to the power of y
not(x) ~x, every bit of x is negated
lt(x, y) 1 if x < y, 0 otherwise
gt(x, y) 1 if x > y, 0 otherwise
slt(x, y) 1 if x < y, 0 otherwise, for signed numbers in two’s complement
sgt(x, y) 1 if x > y, 0 otherwise, for signed numbers in two’s complement
eq(x, y) 1 if x == y, 0 otherwise
iszero(x) 1 if x == 0, 0 otherwise
and(x, y) bitwise and of x and y
or(x, y) bitwise or of x and y
xor(x, y) bitwise xor of x and y
byte(n, x) nth byte of x, where the most significant byte is the 0th byte
addmod(x, y, m) (x + y) % m with arbitrary precision arithmetics
mulmod(x, y, m) (x * y) % m with arbitrary precision arithmetics
signextend(i, x) sign extend from (i*8+7)th bit counting from least significant
keccak256(p, n) keccak(mem[p…(p+n)))
sha3(p, n) keccak(mem[p…(p+n)))
jump(label) - jump to label / code position
jumpi(label, cond) - jump to label if cond is nonzero
pc current position in code
pop(x) - remove the element pushed by x
dup1 … dup16 copy ith stack slot to the top (counting from top)
swap1 … swap16 * swap topmost and ith stack slot below it
mload(p) mem[p..(p+32))
mstore(p, v) - mem[p..(p+32)) := v
mstore8(p, v) - mem[p] := v & 0xff - only modifies a single byte
sload(p) storage[p]
sstore(p, v) - storage[p] := v
msize size of memory, i.e. largest accessed memory index
gas gas still available to execution
address address of the current contract / execution context
balance(a) wei balance at address a
caller call sender (excluding delegatecall)
callvalue wei sent together with the current call
calldataload(p) call data starting from position p (32 bytes)
calldatasize size of call data in bytes
calldatacopy(t, f, s) - copy s bytes from calldata at position f to mem at position t
codesize size of the code of the current contract / execution context
codecopy(t, f, s) - copy s bytes from code at position f to mem at position t
extcodesize(a) size of the code at address a
extcodecopy(a, t, f, s) - like codecopy(t, f, s) but take code at address a
returndatasize size of the last returndata
returndatacopy(t, f, s) - copy s bytes from returndata at position f to mem at position t
create(v, p, s) create new contract with code mem[p..(p+s)) and send v wei and return the new address
create2(v, n, p, s) create new contract with code mem[p..(p+s)) at address keccak256(
. n . keccak256(mem[p..(p+s))) and send v wei and return the new address
call(g, a, v, in, insize, out, outsize) call contract at address a with input mem[in..(in+insize)) providing g gas and v wei and output area mem[out..(out+outsize)) returning 0 on error (eg. out of gas) and 1 on success
callcode(g, a, v, in, insize, out, outsize) identical to call but only use the code from a and stay in the context of the current contract otherwise
delegatecall(g, a, in, insize, out, outsize) identical to callcode but also keep caller and callvalue
staticcall(g, a, in, insize, out, outsize) identical to call(g, a, 0, in, insize, out, outsize) but do not allow state modifications
return(p, s) - end execution, return data mem[p..(p+s))
revert(p, s) - end execution, revert state changes, return data mem[p..(p+s))
selfdestruct(a) - end execution, destroy current contract and send funds to a
invalid - end execution with invalid instruction
log0(p, s) - log without topics and data mem[p..(p+s))
log1(p, s, t1) - log with topic t1 and data mem[p..(p+s))
log2(p, s, t1, t2) - log with topics t1, t2 and data mem[p..(p+s))
log3(p, s, t1, t2, t3) - log with topics t1, t2, t3 and data mem[p..(p+s))
log4(p, s, t1, t2, t3, t4) - log with topics t1, t2, t3, t4 and data mem[p..(p+s))
origin transaction sender
gasprice gas price of the transaction
blockhash(b) hash of block nr b - only for last 256 blocks excluding current
coinbase current mining beneficiary
timestamp timestamp of the current block in seconds since the epoch
number current block number
difficulty difficulty of the current block
gaslimit block gas limit of the current block
文本
您可以使用整数常数,以十进制或十六进制表示法键入,并且将自动生成适当的PUSHi指令。 以下创建代码添加2和3导致5,然后计算按位和字符串“abc”。 字符串左对齐存储,不能超过32个字节。
assembly { 2 3 add "abc" and }
1
函数式
您可以在操作码之后输入操作码,方式与字节码最终相同。 例如,在0x80的内存中添加3将是
3 0x80 mload add 0x80 mstore
1
2
由于通常很难看到某些操作码的实际参数是什么,Solidity inline assembly也提供了一个“函数式”符号,其中相同的代码将被写入如下
mstore(0x80, add(mload(0x80), 3))
1
函数式表达式不能在内部使用教学风格,即1 2 mstore(0x80,add)是无效的程序集,它必须写成mstore(0x80,add(2,1))。 对于不带参数的操作码,可以省略括号。
请注意,在函数式方面,参数的顺序与指令式的方式相反。 如果您使用功能样式,则第一个参数将结束于堆栈顶部。
访问外部变量和函数
可以通过简单地使用其名称来访问Solidity变量和其他标识符。 对于内存变量,这将推送地址,而不是将值推送到堆栈上。 存储变量不同:存储中的值可能不占用完整存储槽,因此它们的“地址”由该槽内的一个槽位和一个字节偏移量组成。 要检索由变量x指向的插槽,您使用x_slot并检索您使用x_offset的字节偏移量。
在分配(见下文)中,我们甚至可以使用本地Solidity变量来分配。
还可以访问内联汇编的外部函数:程序集将推入其入口标签(应用虚拟函数分辨率)。 在Solidity的调用语义是:
调用者推送返回标签,arg1,arg2,…,argn
该调用返回与ret1,ret2,…,retm
这个功能使用起来还是有点麻烦,因为堆栈偏移量在调用过程中基本上有变化,因此对局部变量的引用将是错误的。
pragma solidity ^0.4.11;
contract C {
uint b;
function f(uint x) returns (uint r) {
assembly {
r := mul(x, sload(b_slot)) // 忽略偏移量,我们知道它是零
}
}
}
1
2
3
4
5
6
7
8
9
10
标签
EVM组装中的另一个问题是jump和jumpi使用可以轻松更改的绝对地址。 Solidity inline assembly提供标签,使跳转更容易使用。 请注意,标签是低级功能,可以使用汇编功能,循环和开关指令(见下文)编写无标签的高效组装。 以下代码计算Fibonacci系列中的一个元素。
{
let n := calldataload(4)
let a := 1
let b := a
loop:
jumpi(loopend, eq(n, 0))
a add swap1
n := sub(n, 1)
jump(loop)
loopend:
mstore(0, a)
return(0, 0x20)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
请注意,自动访问堆栈变量只有在汇编器知道当前堆栈高度时才能工作。 如果跳转源和目标具有不同的堆栈高度,则无法正常工作。 使用这种跳转仍然很好,但是在这种情况下,您应该不会访问任何堆栈变量(甚至汇编变量)。
此外,堆栈高度分析器通过操作码(而不是根据控制流程)执行代码操作码,因此在以下情况下,汇编程序将对标签二的堆栈高度产生错误的印象:
{
let x := 8
jump(two)
one:
// 这里的堆栈高度是2(因为我们推x和7),但汇编器认为它是1,因为它读取从上到下。 这里访问堆栈变量x会导致错误。
x := 9
jump(three)
two:
7 // 把东西推到堆栈上
jump(one)
three:
}
1
2
3
4
5
6
7
8
9
10
11
12
可以通过手动调整汇编程序的堆栈高度来修复此问题 - 您可以提供在标签之前添加到堆栈高度的堆栈高度增量。 请注意,如果您只是使用循环和程序集级别的函数,则不需要关心这些事情。
作为一个例子,如何在极端情况下做到这一点,请参阅以下内容。
{
let x := 8
jump(two)
0 // 此代码无法访问,但会正确调整堆栈高度
one:
x := 9 // 现在可以正确访问x。
jump(three)
pop // 类似的负面矫正。
two:
7 // 把东西推到堆栈上
jump(one)
three:
pop // 我们要在这里再次弹出手动推值。
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
声明汇编局部变量
您可以使用let关键字声明仅在内联程序集中可见的变量,实际上仅在当前的{…} - 块中声明。 发生什么事情是,let指令将创建一个新的堆栈槽,为该变量保留,并在块的结尾达到时再次自动删除。 需要提供一种用于可以只是0的变量的初始值,但它也可以是一个复杂的函数式表达。
pragma solidity ^0.4.0;
contract C {
function f(uint x) 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在这里被“释放”了
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
分配
组件局部变量和函数局部变量都可以进行分配。 注意当您分配指向内存或存储的变量时,只会更改指针而不是数据。
有两种分配:函数式和命令式的。 对函数式的赋值(variable := value),您需要在一个函数式表达式中提供一个值,该表达式将产生一个堆栈值和指令样式(=:variable),该值仅从堆栈中获取 最佳。 对于这两种方式,冒号指向变量的名称。 通过将堆栈中的变量的值替换为新值来执行分配。
{
let v := 0 // 函数式赋值作为变量声明的一部分
let g := add(v, 2)
sload(10)
=: v // 指令样式分配,将`sload(10)`的结果放入v中
}
1
2
3
4
5
6
Switch
您可以使用switch语句作为“if / else”的非常基本的版本。 它需要一个表达式的值,并将其与几个常量进行比较。 对应于匹配常数的分支。 与某些编程语言的容易出错的行为相反,控制流程从一种情况到下一种情况都不会继续。 可能会有一个备用或默认情况称为默认。
{
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
1
2
3
4
5
6
7
8
9
10
11
案件清单不需要花括号,但案件确实需要它们。
循环
装配支持简单的for-style循环。 For-style循环有一个包含初始化部分,条件和后迭代部分的标题。 条件必须是功能性表达式,而另外两个是块。 如果初始化部分声明任何变量,这些变量的范围将扩展到正文(包括条件和后期部分)。
以下示例计算内存区域的总和。
{
let x := 0
for { let i := 0 } lt(i, 0x100) { i := add(i, 0x20) } {
x := add(x, mload(i))
}
}
1
2
3
4
5
6
For循环也可以写成它们的行为就像while循环:简单地将初始化和后处理部分留空。
{
let x := 0
let i := 0
for { } lt(i, 0x100) { } { // while(i < 0x100)
x := add(x, mload(i))
i := add(i, 0x20)
}
}
1
2
3
4
5
6
7
8
函数
装配允许定义低级功能。 这些从堆栈中获取参数(和返回PC),并将结果放在堆栈中。 调用函数看起来与执行功能式操作码相同。
函数可以在任何位置定义,并在它们声明的块中可见。在函数内部,不能访问在该函数之外定义的局部变量。 没有明确的return语句。
如果调用返回多个值的函数,则必须使用a,b:= f(x)或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) }
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
要避免的事情
内联汇编可能有一个相当高的水平看,但它实际上是非常低的水平。 函数调用,循环和开关由简单的重写规则转换,之后,唯一汇编为您做的是重新排列函数式操作码,跳的管理标签,用于组件 - 计数堆高度为变量访问和移除堆叠插槽 局部变量,当它们的块结束到达时。 特别是对于最后两种情况,重要的是要知道汇编程序只能从上到下计算堆栈高度,而不一定跟随控制流程。 此外,像swap这样的操作只会交换堆栈的内容,而不是变量的位置。
粘性合约
与EVM组装相反,Solidity知道比256位更窄的类型,例如uint24。为了使它们更有效率,大多数算术运算只将它们视为256位数,高阶位仅在需要的位置进行清除,即在写入存储器之前不久或执行比较之前。这意味着如果您从内联汇编中访问这样的变量,那么您可能必须首先手动清除较高位。
Solidity以非常简单的方式管理内存:内存中位置0x40有一个“可用内存指针”。如果要分配内存,只需从该位置使用内存并相应地更新指针。
Solidity中的内存数组中的元素总是占用32个字节的倍数(是的,这对于byte []甚至是正确的,但不是字节和字符串)。多维存储器阵列是指向存储器阵列的指针。动态数组的长度存储在数组的第一个时隙,然后只有数组元素。
静态大小的内存数组没有长度字段,但是它将很快添加,以便在静态和动态大小的数组之间实现更好的可转换性,所以请不要依赖这些数组。
独立装配
上面描述为内联汇编的汇编语言也可以单独使用,实际上该计划是将其用作Solidity编译器的中间语言。 在这种形式下,它试图实现几个目标:
编写的程序应该是可读的,即使代码是由Solidity的编译器生成的。
从汇编到字母代码的翻译应尽可能少的“惊喜”。
控制流程应易于检测,有助于形式验证和优化。
为了实现第一个和最后一个目标,程序集提供了高级结构,如循环,切换语句和函数调用。 应该可以编写不使用显式SWAP,DUP,JUMP和JUMPI语句的汇编程序,因为前两个模糊数据流和最后两个混淆控制流。 此外,mul(add(x,y),7)的函数语句优于纯操作码语句,如7 y x add mul,因为在第一种形式下,看起来哪个操作数用于哪个操作码要容易得多。
第二个目标是通过引入一个绝对的阶段来实现,该阶段只能以非常规则的方式去除较高级别的构造,并且仍允许检查生成的低级汇编代码。汇编器执行的唯一的非本地操作是用户定义的标识符(函数,变量…)的名称查找,它遵循非常简单和规则的范围规则,并从堆栈中清除局部变量。
范围设定:声明的标识符(标签,变量,函数,程序集)仅在声明的块中(包括当前块中的嵌套块)可见。访问函数边界的局部变量是不合法的,即使它们在范围内。不允许阴影。局部变量在声明之前无法访问,但标签,函数和程序集可以访问。组件是用于例如组件的特殊块。返回运行时代码或创建合同。子组件中没有外部组件的标识符可见。
如果控制流经过块的末尾,则插入与该块中声明的局部变量数相匹配的弹出指令。无论何时引用局部变量,代码生成器需要知道其当前在堆栈中的相对位置,因此需要跟踪当前所谓的堆栈高度。由于所有局部变量在块的末尾被移除,所以块之前和之后的堆栈高度应该相同。如果不是这种情况,则会发出警告。
为什么我们使用更高级的结构,如switch,for和函数:
使用switch,for和函数,应该可以手动编写复杂的代码而不使用jump或jumpi。 这使得分析控制流程变得更加容易,这允许改进形式验证和优化。
此外,如果允许手动跳转,则计算堆栈高度相当复杂。 堆栈上的所有局部变量的位置需要是已知的,否则,不会引用局部变量,也不会在块结束时自动从堆栈中删除局部变量,这将正常工作。 脱机机制正确地将操作插入到不可达到的块中,以在没有持续控制流的跳跃的情况下适当地调整堆栈高度。
例:
我们将按照Solidity的一个示例汇编到脱机装配。 我们考虑以下Solidity程序的运行时字节码:
pragma solidity ^0.4.0;
contract C {
function f(uint x) returns (uint y) {
y = 1;
for (uint i = 0; i < x; i++)
y = 2 * y;
}
}
1
2
3
4
5
6
7
8
9
将生成以下程序集:
{
mstore(0x40, 0x60) // 存储“可用内存指针”
// 函数调度程序
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) }
// 内存分配器
function $allocate(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)
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在desug阶段之后,它看起来如下:
{
mstore(0x40, 0x60)
{
let $0 := div(calldataload(0), exp(2, 226))
jumpi($case1, eq($0, 0xb3de648b))
jump($caseDefault)
$case1:
{
// the function call - we put return label and arguments on the stack
$ret1 calldataload(4) jump(f)
// This is unreachable code. Opcodes are added that mirror the
// effect of the function on the stack height: Arguments are
// removed and return values are introduced.
pop pop
let r := 0
$ret1: // the actual return point
$ret2 0x20 jump($allocate)
pop pop let ret := 0
$ret2:
mstore(ret, r)
return(ret, 0x20)
// although it is useless, the jump is automatically inserted,
// since the desugaring process is a purely syntactic operation that
// does not analyze control-flow
jump($endswitch)
}
$caseDefault:
{
revert(0, 0)
jump($endswitch)
}
$endswitch:
}
jump($afterFunction)
allocate:
{
// we jump over the unreachable code that introduces the function arguments
jump($start)
let $retpos := 0 let size := 0
$start:
// output variables live in the same scope as the arguments and is
// actually allocated.
let pos := 0
{
pos := mload(0x40)
mstore(0x40, add(pos, size))
}
// This code replaces the arguments by the return values and jumps back.
swap1 pop swap1 jump
// Again unreachable code that corrects stack height.
0 0
}
f:
{
jump($start)
let $retpos := 0 let x := 0
$start:
let y := 0
{
let i := 0
$for_begin:
jumpi($for_end, iszero(lt(i, x)))
{
y := mul(2, y)
}
$for_continue:
{ i := add(i, 1) }
jump($for_begin)
$for_end:
} // Here, a pop instruction will be inserted for i
swap1 pop swap1 jump
0 0
}
$afterFunction:
stop
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
大会发生在四个阶段:
解析
脱机(删除开关,用于和功能)
操作码流生成
字节代码生成
我们将以伪形式的方式指定步骤1到3。 更正式的规格将遵循。
解析/语法
解析器的任务如下:
将字节流转换成令牌流,丢弃C ++风格的注释(对源引用有一个特殊的注释,但是我们不会在这里解释)。
根据下面的语法将令牌流转换为AST
使用它们定义的块(AST节点的注释)来注册标识符,并注意可以访问变量的哪个点。
组合词典遵循由Solidity本身定义的词组。
空格用于分隔标记,它由空格,制表符和换行符组成。 注释是常规的JavaScript / C ++注释,并以与Whitespace相同的方式进行解释。
语法:
AssemblyBlock = '{' AssemblyItem* '}'
AssemblyItem =
Identifier |
AssemblyBlock |
FunctionalAssemblyExpression |
AssemblyLocalDefinition |
FunctionalAssemblyAssignment |
AssemblyAssignment |
LabelDefinition |
AssemblySwitch |
AssemblyFunctionDefinition |
AssemblyFor |
'break' | 'continue' |
SubAssembly | 'dataSize' '(' Identifier ')' |
LinkerSymbol |
'errorLabel' | 'bytecodeSize' |
NumberLiteral | StringLiteral | HexLiteral
Identifier = [a-zA-Z_$] [a-zA-Z_0-9]*
FunctionalAssemblyExpression = Identifier '(' ( AssemblyItem ( ',' AssemblyItem )* )? ')'
AssemblyLocalDefinition = 'let' IdentifierOrList ':=' FunctionalAssemblyExpression
FunctionalAssemblyAssignment = IdentifierOrList ':=' FunctionalAssemblyExpression
IdentifierOrList = Identifier | '(' IdentifierList ')'
IdentifierList = Identifier ( ',' Identifier)*
AssemblyAssignment = '=:' Identifier
LabelDefinition = Identifier ':'
AssemblySwitch = 'switch' FunctionalAssemblyExpression AssemblyCase*
( 'default' AssemblyBlock )?
AssemblyCase = 'case' FunctionalAssemblyExpression AssemblyBlock
AssemblyFunctionDefinition = 'function' Identifier '(' IdentifierList? ')'
( '->' '(' IdentifierList ')' )? AssemblyBlock
AssemblyFor = 'for' ( AssemblyBlock | FunctionalAssemblyExpression)
FunctionalAssemblyExpression ( AssemblyBlock | FunctionalAssemblyExpression) AssemblyBlock
SubAssembly = 'assembly' Identifier AssemblyBlock
LinkerSymbol = 'linkerSymbol' '(' StringLiteral ')'
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]+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
脱糖
AST转换消除了交换和功能结构。 结果仍然可以解析为同一个解析器,但它不会使用某些结构。 如果添加仅跳转到并且不继续的jumpdests,则添加有关堆栈内容的信息,除非没有访问外部范围的本地变量或堆栈高度与上一条指令相同。
伪代码:
desugar item: AST -> AST =
match item {
AssemblyFunctionDefinition('function' name '(' arg1, ..., argn ')' '->' ( '(' ret1, ..., retm ')' body) ->
<name>:
{
jump($<name>_start)
let $retPC := 0 let argn := 0 ... let arg1 := 0
$<name>_start:
let ret1 := 0 ... let retm := 0
{ desugar(body) }
swap and pop items so that only ret1, ... retm, $retPC are left on the stack
jump
0 (1 + n times) to compensate removal of arg1, ..., argn and $retPC
}
AssemblyFor('for' { init } condition post body) ->
{
init // cannot be its own block because we want variable scope to extend into the body
// find I such that there are no labels $forI_*
$forI_begin:
jumpi($forI_end, iszero(condition))
{ body }
$forI_continue:
{ post }
jump($forI_begin)
$forI_end:
}
'break' ->
{
// find nearest enclosing scope with label $forI_end
pop all local variables that are defined at the current point
but not at $forI_end
jump($forI_end)
0 (as many as variables were removed above)
}
'continue' ->
{
// find nearest enclosing scope with label $forI_continue
pop all local variables that are defined at the current point
but not at $forI_continue
jump($forI_continue)
0 (as many as variables were removed above)
}
AssemblySwitch(switch condition cases ( default: defaultBlock )? ) ->
{
// find I such that there is no $switchI* label or variable
let $switchI_value := condition
for each of cases match {
case val: -> jumpi($switchI_caseJ, eq($switchI_value, val))
}
if default block present: ->
{ defaultBlock jump($switchI_end) }
for each of cases match {
case val: { body } -> $switchI_caseJ: { body jump($switchI_end) }
}
$switchI_end:
}
FunctionalAssemblyExpression( identifier(arg1, arg2, ..., argn) ) ->
{
if identifier is function <name> with n args and m ret values ->
{
// find I such that $funcallI_* does not exist
$funcallI_return argn ... arg2 arg1 jump(<name>)
pop (n + 1 times)
if the current context is `let (id1, ..., idm) := f(...)` ->
let id1 := 0 ... let idm := 0
$funcallI_return:
else ->
0 (m times)
$funcallI_return:
turn the functional expression that leads to the function call
into a statement stream
}
else -> desugar(children of node)
}
default node ->
desugar(children of node)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
操作码流生成
在操作码流生成期间,我们跟踪计数器中的当前堆栈高度,以便可以通过名称访问堆栈变量。 使用修改堆栈的每个操作码和使用堆栈调整注释的每个标签修改堆栈高度。 每当引入新的局部变量时,它将与当前堆栈高度一起注册。 如果访问变量(用于复制其值或用于分配),则根据当前堆栈高度和引入变量点的堆栈高度之间的差异来选择适当的DUP或SWAP指令。
伪代码:
codegen item: AST -> opcode_stream =
match item {
AssemblyBlock({ items }) ->
join(codegen(item) for item in items)
if last generated opcode has continuing control flow:
POP for all local variables registered at the block (including variables
introduced by labels)
warn if the stack height at this point is not the same as at the start of the block
Identifier(id) ->
lookup id in the syntactic stack of blocks
match type of id
Local Variable ->
DUPi where i = 1 + stack_height - stack_height_of_identifier(id)
Label ->
// reference to be resolved during bytecode generation
PUSH<bytecode position of label>
SubAssembly ->
PUSH<bytecode position of subassembly data>
FunctionalAssemblyExpression(id ( arguments ) ) ->
join(codegen(arg) for arg in arguments.reversed())
id (which has to be an opcode, might be a function name later)
AssemblyLocalDefinition(let (id1, ..., idn) := expr) ->
register identifiers id1, ..., idn as locals in current block at current stack height
codegen(expr) - assert that expr returns n items to the stack
FunctionalAssemblyAssignment((id1, ..., idn) := expr) ->
lookup id1, ..., idn in the syntactic stack of blocks, assert that they are variables
codegen(expr)
for j = n, ..., i:
SWAPi where i = 1 + stack_height - stack_height_of_identifier(idj)
POP
AssemblyAssignment(=: id) ->
look up id in the syntactic stack of blocks, assert that it is a variable
SWAPi where i = 1 + stack_height - stack_height_of_identifier(id)
POP
LabelDefinition(name:) ->
JUMPDEST
NumberLiteral(num) ->
PUSH<num interpreted as decimal and right-aligned>
HexLiteral(lit) ->
PUSH32<lit interpreted as hex and left-aligned>
StringLiteral(lit) ->
PUSH32<lit utf-8 encoded and left-aligned>
SubAssembly(assembly <name> block) ->
append codegen(block) at the end of the code
dataSize(<name>) ->
assert that <name> is a subassembly ->
PUSH32<size of code generated from subassembly <name>>
linkerSymbol(<lit>) ->
PUSH32<zeros> and append position to linker table
} |
|