Solidity汇编开发简明教程
Posted 跨链技术践行者
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Solidity汇编开发简明教程相关的知识,希望对你有一定的参考价值。
在用Solidity开发以太坊智能合约时,使用汇编可以直接与EVM交互,降低 gas开销成本,更精细的控制智能合约的行为,因此值得Solidity开发者学习 并加以利用。本文是Solidity汇编开发的简明教程,旨在帮助你快速熟悉 如何在Solidity智能合约代码中嵌入汇编代码。
2、以太坊虚拟机和堆栈结构机器
以太坊虚拟机EVM有自己的指令集,该指令集中目前包含了 144个操作码,详情参考Geth源代码
这些指令是Solidity抽象出来的,可以在Solidity内联使用。例如:
1 2 3 4 5 6 7 | contract Assembler { function do_something_cpu() public { assembly { // start writing evm assembler language } } } |
EVM是一个栈虚拟机,栈这种数据结构只允许两个操作:压入(PUSH)或弹出(POP)数据。 最后压入的数据位于栈顶,因此将被第一个弹出,这被称为后进先出 (LIFO:Last In, First Out):
栈虚拟机将所有的操作数保存在栈上,关于栈虚拟机的详细信息 可以参考stack machine 基础
3、堆栈结构机器的操作码
为了能够解决实际问题,栈结构机器需要实现一些额外的指令,例如 ADD、SUBSTRACT等等。指令执行时通常会先从堆栈弹出一个或多个值作为参数, 再将执行结果压回堆栈。这通常被称为逆波兰表示法(RPN:Reverse Polish Notation):
1 2 | a + b // 标准表示法Infix a b add // 逆波兰表示法RPN |
4、在Solidity合约中使用内联汇编
可以在Solidity中使用assembly{}
来嵌入汇编代码段,这被称为内联汇编:
1 2 3 | assembly { // some assembly code here } |
在assembly
块内的代码开发语言被称为Yul,为了简化我们称其为 汇编或EVM汇编。
另一个需要注意的问题时,汇编代码块之间不能通信,也就是说在 一个汇编代码块里定义的变量,在另一个汇编代码块中不可以访问。 例如:
1 2 3 4 5 6 | assembly { let x := 2 } assembly { let y := x // Error } |
上面的代码编译时会报如下错误:
1 2 3 | // DeclarationError: identifier not found // let y := x // ^ |
下面的代码使用内联汇编代码计算函数的两个参数的和并返回结果:
1 2 3 4 5 6 7 | function addition(uint x, uint y) public pure returns (uint) { assembly { let result := add(x, y) // x + y mstore(0x0, result) // 在内存中保存结果 return(0x0, 32) // 从内存中返回32字节 } } |
让我们重写上面的代码,补充一些更详细的注释,以便说明每个指令 在EVM内部的运行原理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function addition(uint x, uint y) public pure returns (uint) { assembly { // 创建一个新的变量result // -> 使用add操作码计算x+y // -> 将计算结果赋值给变量result let result := add(x, y) // x + y // 使用mstore操作码 // -> 将result变量的值存入内存 // -> 指定内存地址 0x0 mstore(0x0, result) // 将结果存入内存 // 从内存地址0x返回32字节 return(0x0, 32) } } |
5、Solidity汇编中的变量定义与赋值
在Yul中,使用let
关键字定义变量。使用:=
操作符给变量赋值:
1 2 3 | assembly { let x := 2 } |
如果没有使用:=
操作符给变量赋值,那么该变量自动初始化为0值:
1 2 3 4 | assembly { let x // 自动初始化为 x = 0 x := 5 // x 现在的值是5 } |
你可以使用复杂的表达式为变量赋值,例如:
1 2 3 4 5 6 | assembly { let x := 7 let y := add(x, 3) let z := add(keccak256(0x0, 0x20), div(slength, 32)) let n } |
6、Solidity汇编中let指令的运行机制
在EVM的内部,let
指令执行如下任务:
- 创建一个新的堆栈槽位
- 为变量保留该槽位
- 当到达代码块结束时自动销毁该槽位
因此,使用let指令在汇编代码块中定义的变量,在该代码块 外部是无法访问的。
7、Solidity汇编中的注释
在Yul汇编中注释的写法和Solidity一样,可以使用单行注释//
或多行注释/* */
。例如:
1 2 3 4 5 6 7 8 9 | assembly { // single line comment /* Multi line comment */ } |
8、Solidity汇编中的字面量
在Solidity汇编中字面量的写法与Solidity一致。不过,字符串字面量 最多可以包含32个字符。
1 2 3 4 5 6 7 | assembly { let a := 0x123 // 16进制 let b := 42 // 10进制 let c := "hello world" // 字符串 let d := "very long string more than 32 bytes" // 超长字符串,错误! } |
9、Solidity汇编中的块和作用范围
在Solidity汇编中,变量的作用范围遵循标准规则。一个块的范围使用 一对大括号标识。
在下面的示例中,y和z仅在定义所在块范围内有效。因此y变量的作用 范围是scope 1,z变量的作用范围是scope 2。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | assembly { let x := 3 // x在各处可见 // Scope 1 { let y := x // ok } // 到此处会销毁y // Scope 2 { let z := y // Error } // 到此处会销毁z } // DeclarationError: identifier not found // let z := y // ^ |
作用范围的唯一例外是函数和for循环,我们将在下面解释。
10、在Solidity汇编中使用函数的局部变量
在Solidity汇编中,只需要使用变量名就可以访问局部变量, 无论该变量是定义在汇编块中,还是Solidity代码中,不过 变量必须是函数的局部变量:
1 2 3 4 5 6 7 8 9 10 11 12 | function assembly_local_var_access() public pure { uint b = 5; assembly { // defined inside an assembly block let x := add(2, 3) let y := 10 z := add(x, y) } assembly { // defined outside an assembly block let x := add(2, 3) let y := mul(x, b) } } |
11、在Solidity汇编中使用for循环
先看一下Solidity中循环的使用。下面的Solidity函数代码中 计算变量的倍数n次,其中value和n是函数的参数:
1 2 3 4 5 6 | function for_loop_solidity(uint n, uint value) public pure returns(uint) { for ( uint i = 0; i < n; i++ ) { value = 2 * value; } return value; } |
等效的Solidity汇编代码如下:
1 2 3 4 5 6 7 8 9 | function for_loop_assembly(uint n, uint value) public pure returns (uint) { assembly { for { let i := 0 } lt(i, n) { i := add(i, 1) } { value := mul(2, value) } mstore(0x0, value) return(0x0, 32) } } |
类似于其他开发语言中的for循环,在Solidity汇编中,for循环也包含 3个元素:
- 初始化:
let i := 0
- 执行条件:
lt(i, n)
,必须是函数风格表达式 - 迭代后续步骤:
add(i, 1)
注意:for循环中变量的作用范围略有不同。在初始化部分定义的变量 在循环的其他部分都有效。
12、在Solidity汇编中使用while循环
在Solidity汇编中实际上是没有while循环关键字的,但是可以使用 for循环实现同样的功能:只要留空for循环的初始化部分和迭代后续步骤即可。
1 2 3 4 5 6 7 8 | assembly { let x := 0 let i := 0 for { } lt(i, 0x100) { } { // 等价于:while(i < 0x100) x := add(x, mload(i)) i := add(i, 0x20) } } |
13、在Solidity汇编中使用if语句
Solidity内联汇编支持使用if
语句来设置代码执行的条件,但是 没有其他语言中的else
部分。
1 2 3 4 | assembly { if slt(x, 0) { x := sub(0, x) } // Ok if eq(value, 0) revert(0, 0) // Error, 需要大括号 } |
if语句强制要求代码块使用大括号,即使需要保护的代码只有一行, 也需要使用大括号。这和solidity不同。
如果需要在Solidity内联汇编中检查多种条件,可以考虑使用 switch
语句。
14、在Solidity汇编中使用switch语句
EVM汇编中也有switch
语句,它将一个表达式的值于多个常量 进行对比,并选择相应的代码分支来执行。switch
语句支持 一个默认分支default
,当表达式的值不匹配任何其他分支条件时,将 执行默认分支的代码。
1 2 3 4 5 6 7 8 9 10 11 | assembly { let x := 0 switch calldataload(4) case 0 { x := calldataload(0x24) } default { x := calldataload(0x44) } sstore(0, div(x, 2)) } |
switch
语句有一些限制:
- 分支列表不需要大括号,但是分支的代码块需要大括号
- 所有的分支条件值必须:1)具有相同的类型 2)具有不同的值
- 如果分支条件已经涵盖所有可能的值,那么不允许再出现default条件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | assembly { let x := 34 switch lt(x, 30) case true { // do something } case false { // do something els } default { // 不允许 } } |
15、在Solidity汇编中使用函数
也可以在Solidity内联汇编中定义底层函数。调用这些自定义的函数 和使用内置的操作码一样。
下面的汇编函数用来分配指定长度的内存,并返回内存指针pos:
1 2 3 4 5 6 7 | assembly { function allocate(length) -> pos { pos := mload(0x40) mstore(0x40, add(pos, length)) } let free_memory_pointer := allocate(64) } |
汇编函数的运行机制如下:
- 从堆栈提取参数
- 将结果压入堆栈
和Solidity函数不同,不需要指定汇编函数的可见性,例如public或private, 因为汇编函数仅在定义所在的汇编代码块内有效。
16、Solidity汇编中的操作码
EVM操作码可以分为以下几类:
- 算数和比较操作
- 位操作
- 密码学操作,目前仅包含
keccak256
- 环境操作,主要指与区块链相关的全局信息,例如
blockhash
或coinbase
收款账号 - 存储、内存和栈操作
- 交易与合约调用操作
- 停机操作
- 日志操作
详细的操作码可以查看Solidity文档。
原文链接:Solidity Tutorial : all about Assembly
以上是关于Solidity汇编开发简明教程的主要内容,如果未能解决你的问题,请参考以下文章