内联汇编的作用:
1)节约gas
2)实现solidity不能或者不好实现的功能。
3)对EVM有更细粒度的控制,在编写库函数时很有用。
基本汇编语法
内联汇编就是对数据的入栈和出栈进行处理。
内联汇编中使用的所有变量都是值类型
内联汇编中,所有的类型都是uint256
solidity中,内存数据的元素总是占用32bytes倍数的大小空间
先是存储数组的元素个数,然后再是实际元素。
1、引入汇编
在Solidity中使用assembly{}
来引入汇编代码段,这被称为内联汇编:
assembly {
// some assembly code here
}
在assembly块内的代码开发语言被称为Yul,为了简化我们称其为 汇编、汇编代码或EVM汇编。
另一个需要注意的问题是,汇编代码块之间不能通信,也就是说在 一个汇编代码块里定义的变量,在另一个汇编代码块中不可以访问。 例如:
Solidity 文档有这样的解释:
不同的内联汇编块不共享任何名称空间,即不可能调用Yul函数或访问在其他内联汇编块中定义的Yul变量。
assembly {
let x := 2
}
assembly {
let y := x // Error
}
// DeclarationError: identifier not found
// let y := x
// ^
第二个汇编代码使用了第一个汇编代码中的变量x,就会报错。
2、简单汇编示例和运行原理
下面的代码使用内联汇编代码计算函数的两个参数的和并返回结果:
以下是一个简单的示例,函数接受两个参数,并将它们的和作为返回值,看看使用 Assembly
是怎么实现的?了解它们在 EVM
的工作方式。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract AssemblyExample {
function addition(uint x, uint y) public pure returns (uint) {
assembly {
//声明一个 result 变量,并将 x,y之和赋值给它
let result := add(x, y) // x + y
//使用 mstore 操作码将 result存在 memory 中,地址是 0x0
mstore(0x0, result) // store result in memory
//返回 32 字节的 memory 地址
return(0x0, 32)
}
}
}
让我们来看看一个简单的例子。我们将数据存放在storage(存储)
中,然后再去调用它。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
contract StorageDataExample {
function setData(uint256 newValue) public {
assembly {
sstore(0, newValue)
}
}
function getData() public view returns(uint256) {
assembly {
let v := sload(0)
mstore(0x80, v)
return(0x80, 32)
}
}
}
setData
函数使用了sstore
操作码将变量newValue
写入storage(存储)
中。
getData
函数先是用了sload
操作码来加载storage(存储)
中的数据,它并不能从storage
中直接返回。所以才需要mstore
操作码将其写入memory(内存)
中,最后我们返回引用memory(内存)
中存放数据的地址和 32 字节长度的数据。
3、汇编中的变量定义与赋值
在Yul中,使用let
关键字定义变量。使用:=
操作符给变量赋值:
assembly {
let x := 2
}
和Solidity 不同,Solidity只需要用
=
, 因此不要忘了:
.
如果没有使用:=
操作符给变量赋值,那么该变量自动初始化为0值:
assembly {
let x // 自动初始化为 x = 0
x := 5 // x 现在的值是5
}
你可以使用复杂的表达式为变量赋值,例如:
assembly {
let x := 7
let y := add(x, 3)
let z := add(keccak256(0x0, 0x20), div(slength, 32))
let n // 自动初始化为 n = 0
}
汇编中let指令的运行机制
在EVM的内部,let
指令执行如下任务:
- 创建一个新的堆栈槽位
- 为变量保留该槽位
- 当到达代码块结束时自动销毁该槽位
因此,使用let指令在汇编代码块中定义的变量,在该代码块 外部是无法访问的。
4、汇编的注释
在Yul汇编中注释的写法和Solidity一样,可以使用单行注释//
或多行注释/* */
。例如:
assembly {
// 单行注释
/*
多
行
注释
*/
}
5、汇编中的字面量
在Solidity汇编中字面量的写法与Solidity一致。不过,字符串字面量最多可以包含32个字符。
assembly {
let a := 0x123 // 16进制
let b := 42 // 10进制
let c := "hello world" // 字符串
let d := "very long string more than 32 bytes" // 超长字符串,出错!
}
// TypeError: String literal too long (35 < 32)
// let d := "really long string more than 32 bytes"
//
6、汇编中的块和作用域
在下面的示例中,y和z仅在定义所在块范围内有效。因此y变量的作用范围是scope 1
,z变量的作用范围是scope 2
assembly {
let x := 3 // x在各处可见
// Scope 1
{
let y := x // ok
} // 到此处会销毁y
// Scope 2
{
let z := y // Error
} // 到此处会销毁z
}
// DeclarationError: