Solidity汇编开发简明教程
handii2006
发表于 2023-1-13 23:14:41
237
0
0
以太坊虚拟机和堆栈结构机器% S- Z. n% m$ U9 h4 G9 S# ?
以太坊虚拟机EVM有自己的指令集,该指令集中目前包含了 144个操作码,详情参考Geth源代码
这些指令是Solidity抽象出来的,可以在Solidity内联使用。例如:
contract Assembler { . o6 j: t. l4 }2 j6 h
function do_something_cpu() public {# l% \; P$ M. T
assembly {* m0 s1 f7 Q: a8 n4 m( a: b
// start writing evm assembler language
}/ h+ s4 x( S: s( R. q
}
}) R5 s9 z* c$ Q+ P
EVM是一个栈虚拟机,栈这种数据结构只允许两个操作:压入(PUSH)或弹出(POP)数据。 最后压入的数据位于栈顶,因此将被第一个弹出,这被称为后进先出 (LIFO:Last In, First Out):
9 J4 w* ^3 M8 R5 e
栈虚拟机将所有的操作数保存在栈上,关于栈虚拟机的详细信息 可以参考stack machine 基础 Y& l' P2 ^" K2 J
堆栈结构机器的操作码/ r" `3 V0 t- Q; y$ O
为了能够解决实际问题,栈结构机器需要实现一些额外的指令,例如 ADD、SUBSTRACT等等。指令执行时通常会先从堆栈弹出一个或多个值作为参数, 再将执行结果压回堆栈。这通常被称为逆波兰表示法(RPN:Reverse Polish Notation):
a + b // 标准表示法Infix
a b add // 逆波兰表示法RPN5 m" k, @7 Z0 Y( M
在Solidity合约中使用内联汇编
可以在Solidity中使用assembly{}来嵌入汇编代码段,这被称为内联汇编:0 ], x% c$ l4 }
assembly {
// some assembly code here; @. @* H1 n8 a% `
}/ H6 V" {: J& w- w) T: n
在assembly块内的代码开发语言被称为Yul,为了简化我们称其为 汇编或EVM汇编。/ m/ z) h9 w: ~% Z' C) L( E5 @! D; X( \
另一个需要注意的问题时,汇编代码块之间不能通信,也就是说在 一个汇编代码块里定义的变量,在另一个汇编代码块中不可以访问。 例如:! y: g& H2 x1 x. k7 j. s5 F
assembly { 7 b8 a9 B6 d G# w
let x := 2
} ) [3 Q) b$ P X& h" ^1 Y( j
assembly {) C0 V5 Z5 h( y
let y := x // Error
}
上面的代码编译时会报如下错误:
// DeclarationError: identifier not found4 i, c) R: X5 V- r }* e
// let y := x6 m+ y/ i% r* p) l( ^1 a" D
// ^
下面的代码使用内联汇编代码计算函数的两个参数的和并返回结果:3 n; y3 N' W3 b/ y% O
function addition(uint x, uint y) public pure returns (uint) {! K/ \2 d! T9 X. ~1 ~
assembly {$ z0 A7 B* L1 ^5 ~% Q; x$ S* I* h
let result := add(x, y) // x + y. y) a6 Y/ ?; z* Z: ]1 p9 Z
mstore(0x0, result) // 在内存中保存结果
return(0x0, 32) // 从内存中返回32字节; H) J) ^" h i- z8 s" h
}" x H3 g) S! Q- J) u0 R$ e h: B
}
让我们重写上面的代码,补充一些更详细的注释,以便说明每个指令 在EVM内部的运行原理。
function addition(uint x, uint y) public pure returns (uint) { ; D7 I0 r4 C3 M1 A* t
assembly { 7 k! E+ k- Q( K/ G' M' z& ^6 M
// 创建一个新的变量result- l- r8 P/ x& O( D
// -> 使用add操作码计算x+y2 b- e$ A+ ^: m. x; I- ]
// -> 将计算结果赋值给变量result 3 K* n0 q2 H0 q, U$ |, l
let result := add(x, y) // x + y * f* ~+ }* r2 }. U1 g8 L4 w, L
# A! ?2 U( h/ z4 n' q
// 使用mstore操作码
// -> 将result变量的值存入内存" S8 u! u& w4 ^, T. Y! r
// -> 指定内存地址 0x0 4 C4 D2 j9 H. M3 G+ I7 y
mstore(0x0, result) // 将结果存入内存
0 e& O" x0 @' S/ v9 P6 S4 P
// 从内存地址0x返回32字节
return(0x0, 32)
}
} g' c2 F" d( W
Solidity汇编中的变量定义与赋值8 \( g Y1 B% O
在Yul中,使用let关键字定义变量。使用:=操作符给变量赋值:
assembly {
let x := 2- L+ i2 I3 P: {9 P4 r2 D
}
如果没有使用:=操作符给变量赋值,那么该变量自动初始化为0值:
assembly {5 J7 O8 |8 [2 r7 u, T
let x // 自动初始化为 x = 01 h5 l3 x( j* {9 Q: p- k- V
x := 5 // x 现在的值是59 A$ P3 l" E7 B; S; b$ T) z7 b
}0 Y' ~7 B! |; `
你可以使用复杂的表达式为变量赋值,例如:, O' f3 D$ M& \2 h! K
assembly {
let x := 7 ( t, q; I! k% {3 n% l% H8 F/ Y5 Y
let y := add(x, 3)
let z := add(keccak256(0x0, 0x20), div(slength, 32))
let n
}
Solidity汇编中let指令的运行机制
在EVM的内部,let指令执行如下任务:! B; [9 _# c4 T& O
创建一个新的堆栈槽位为变量保留该槽位当到达代码块结束时自动销毁该槽位
( a, d. {% A: l: t
因此,使用let指令在汇编代码块中定义的变量,在该代码块 外部是无法访问的。
Solidity汇编中的注释( D; g' I# E( W' N/ E8 ~
在Yul汇编中注释的写法和Solidity一样,可以使用单行注释// 或多行注释/* */。例如:
assembly { . c- Z8 N# d1 k3 y
// single line comment
/*$ t8 Z& w6 p6 m6 b
Multi1 d- |4 G1 t# q( ?
line
comment
*/
}% z/ u& A7 v/ @& U
Solidity汇编中的字面量2 }/ T* ?! D% Q4 H
在Solidity汇编中字面量的写法与Solidity一致。不过,字符串字面量 最多可以包含32个字符。" e! o, @/ C% v4 i* b2 \# a2 }
assembly { 1 n3 x" Y) Y+ p
let a := 0x123 // 16进制" t3 w: X8 P0 I$ S3 c6 g
let b := 42 // 10进制
let c := "hello world" // 字符串
let d := "very long string more than 32 bytes" // 超长字符串,错误!$ a8 y$ E( q% O/ p
}
Solidity汇编中的块和作用范围
在Solidity汇编中,变量的作用范围遵循标准规则。一个块的范围使用 一对大括号标识。
在下面的示例中,y和z仅在定义所在块范围内有效。因此y变量的作用 范围是scope 1,z变量的作用范围是scope 2。
assembly { & F# y. ?; ]$ h' h8 I- u6 }9 L. t
let x := 3 // x在各处可见
" F# ?+ B- u* R; F$ N: ~
// Scope 1
{
let y := x // ok , V8 r# Q" w, }* X+ [1 p
} // 到此处会销毁y
// Scope 2
{ y0 r3 W* X; r0 K$ B, l: i
let z := y // Error
} // 到此处会销毁z
}
// DeclarationError: identifier not found* ^* K" k! U' E' [: |8 S
// let z := y
// ^
作用范围的唯一例外是函数和for循环,我们将在下面解释。
在Solidity汇编中使用函数的局部变量/ B# j9 h! c1 x1 M% ~
在Solidity汇编中,只需要使用变量名就可以访问局部变量, 无论该变量是定义在汇编块中,还是Solidity代码中,不过 变量必须是函数的局部变量:
function assembly_local_var_access() public pure { ' ~- ?% I' E f; A: ]0 ^ j
uint b = 5;
assembly { // defined inside an assembly block
let x := add(2, 3)
let y := 10
z := add(x, y)# @ p4 @) u" }/ B. n( O) z
}
assembly { // defined outside an assembly block, W: K! d7 B4 u% D9 z
let x := add(2, 3)+ W- I+ k% I! e" M- \9 @/ B
let y := mul(x, b)7 w# S& ]( }6 t' }+ C
}5 U7 Q1 U A( w' s( h
}
在Solidity汇编中使用for循环
先看一下Solidity中循环的使用。下面的Solidity函数代码中 计算变量的倍数n次,其中value和n是函数的参数:
function for_loop_solidity(uint n, uint value) public pure returns(uint) {
for ( uint i = 0; i . W8 v3 S2 d/ w# O1 b
等效的Solidity汇编代码如下:9 d `+ a7 [9 |" A$ k
function for_loop_assembly(uint n, uint value) public pure returns (uint) { : G r* _, d; n7 a
assembly { 1 W2 n; J1 y4 c) O0 p& V N, y% K. g
for { let i := 0 } lt(i, n) { i := add(i, 1) } { 3 j1 g+ Q6 \" d4 J0 _& O$ H
value := mul(2, value)
}
mstore(0x0, value)
return(0x0, 32), Z/ ?5 W) c4 P
}
}
类似于其他开发语言中的for循环,在Solidity汇编中,for循环也包含 3个元素:* h4 j0 z5 b6 a. l- T, D: C% B
初始化:let i := 0
执行条件:lt(i, n) ,必须是函数风格表达式9 s6 T. W3 Y6 c# y
迭代后续步骤:add(i, 1)" h# L1 e: j. }: R. F
注意:for循环中变量的作用范围略有不同。在初始化部分定义的变量 在循环的其他部分都有效。$ g) I; R9 \# R! b) s2 q
在Solidity汇编中使用while循环
在Solidity汇编中实际上是没有while循环关键字的,但是可以使用 for循环实现同样的功能:只要留空for循环的初始化部分和迭代后续步骤即可。& m: \- n1 p' `- P
assembly {
let x := 0
let i := 0% j! V K' U6 ^3 ?9 c
for { } lt(i, 0x100) { } { // 等价于:while(i ( }( `+ A8 y% a! U' x9 O
在Solidity汇编中使用if语句* J' l8 q. A% R
Solidity内联汇编支持使用if语句来设置代码执行的条件,但是 没有其他语言中的else部分。
assembly {
if slt(x, 0) { x := sub(0, x) } // Ok8 R# a% X3 z& r# V
if eq(value, 0) revert(0, 0) // Error, 需要大括号5 U; h$ q4 b' L4 f" K" n
}! s1 F- y' v, a$ T+ H8 o, X
if语句强制要求代码块使用大括号,即使需要保护的代码只有一行, 也需要使用大括号。这和solidity不同。
如果需要在Solidity内联汇编中检查多种条件,可以考虑使用 switch语句。
在Solidity汇编中使用switch语句0 s7 q/ M! y& J' Y0 ^) o9 R E
EVM汇编中也有switch语句,它将一个表达式的值于多个常量 进行对比,并选择相应的代码分支来执行。switch语句支持 一个默认分支default,当表达式的值不匹配任何其他分支条件时,将 执行默认分支的代码。
assembly { M9 t) X, {: X# H v( J
let x := 0/ \$ V! n% [1 _# n/ S# `& G: _. w
switch calldataload(4)& k6 m9 j2 m0 F5 p5 k1 }7 f7 M5 R: l
case 0 {% L* J/ y$ Q: P0 x
x := calldataload(0x24)( s% m' ?; L) C+ V5 l- E" d
}5 A- u7 C4 r: {" ~( T& x6 c
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))8 g7 ^5 @# |. p+ S3 S4 ?3 |
}
switch语句有一些限制:+ P: F* ?8 E7 W: M. {
分支列表不需要大括号,但是分支的代码块需要大括号
所有的分支条件值必须:1)具有相同的类型 2)具有不同的值, u+ ^3 x- w; w. e
如果分支条件已经涵盖所有可能的值,那么不允许再出现default条件
assembly {
let x := 34! N1 @* v4 x$ I Q* z
switch lt(x, 30)
case true {; o3 k) l) f6 Y% h1 ?
// do something
}: A, R3 o5 y) ~ ]5 ?9 [
case false {; I6 _" G/ {+ o# e+ b8 o2 m3 [8 Y
// do something els8 W! o* q% N7 X* J2 y
}6 @: ]$ Y2 S3 C# O3 s
default {
// 不允许
} / \0 |8 s& } N: d1 |/ A
}
在Solidity汇编中使用函数
也可以在Solidity内联汇编中定义底层函数。调用这些自定义的函数 和使用内置的操作码一样。3 k2 m! X2 }; _7 M/ Z' s" E: p* ?
下面的汇编函数用来分配指定长度的内存,并返回内存指针pos:$ D# A% a( J# d
assembly {
function allocate(length) -> pos {
pos := mload(0x40)
mstore(0x40, add(pos, length))2 y0 i4 z; P6 c, v
}
let free_memory_pointer := allocate(64)# X ~' [' x& Z
}
汇编函数的运行机制如下:9 _3 {& M2 t, ^! r4 E5 I! M
从堆栈提取参数将结果压入堆栈和Solidity函数不同,不需要指定汇编函数的可见性,例如public或private, 因为汇编函数仅在定义所在的汇编代码块内有效。, k- R, p7 H* b3 s" r1 }
Solidity汇编中的操作码
EVM操作码可以分为以下几类:' r2 a5 N1 J) A/ E5 \) ?6 l
算数和比较操作位操作密码学操作,目前仅包含keccak256环境操作,主要指与区块链相关的全局信息,例如blockhash或coinbase收款账号存储、内存和栈操作交易与合约调用操作停机操作日志操作/ g+ d' {2 v5 R
详细的操作码可以查看Solidity文档。
成为第一个吐槽的人