Solidity内联汇编简明指南

Stella981
• 阅读 1018

在用Solidity开发以太坊智能合约时,使用汇编可以直接与EVM交互,降低gas开销成本,更精细的控制智能合约的行为,因此值得Solidity开发者学习并加以利用。本文是Solidity汇编开发的简明教程,旨在帮助你快速熟悉如何在Solidity智能合约代码中嵌入汇编代码。

以太坊教程链接:Dapp入门 | 电商Dapp实战 | Token实战 | Php对接 | Java对接 | Python对接 | C#对接 | Dart对接

2、以太坊虚拟机和堆栈结构机器

以太坊虚拟机EVM有自己的指令集,该指令集中目前包含了144个操作码,详情参考Geth源代码

这些指令是Solidity抽象出来的,可以在Solidity内联使用。例如:

contract Assembler {    
  function do_something_cpu() public {
    assembly {
      // start writing evm assembler language
    }
  }
}

EVM是一个栈虚拟机,栈这种数据结构只允许两个操作:压入(PUSH)或弹出(POP)数据。最后压入的数据位于栈顶,因此将被第一个弹出,这被称为后进先出(LIFO:Last In, First Out):

Solidity内联汇编简明指南

栈虚拟机将所有的操作数保存在栈上,关于栈虚拟机的详细信息可以参考stack machine 基础

3、堆栈结构机器的操作码

为了能够解决实际问题,栈结构机器需要实现一些额外的指令,例如ADD、SUBSTRACT等等。指令执行时通常会先从堆栈弹出一个或多个值作为参数,再将执行结果压回堆栈。这通常被称为逆波兰表示法(RPN:Reverse Polish Notation):

a + b      // 标准表示法Infix
a b add    // 逆波兰表示法RPN

4、在Solidity合约中使用内联汇编

可以在Solidity中使用assembly{}来嵌入汇编代码段,这被称为内联汇编:

assembly {
  // some assembly code here
}

assembly块内的代码开发语言被称为Yul,为了简化我们称其为汇编或EVM汇编。

另一个需要注意的问题时,汇编代码块之间不能通信,也就是说在一个汇编代码块里定义的变量,在另一个汇编代码块中不可以访问。例如:

assembly { 
    let x := 2
}        
assembly {
    let y := x          // Error
}

上面的代码编译时会报如下错误:

// DeclarationError: identifier not found
// let y := x
// ^

下面的代码使用内联汇编代码计算函数的两个参数的和并返回结果:

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内部的运行原理。

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关键字定义变量。使用:=操作符给变量赋值:

assembly {
  let x := 2
}

如果没有使用:=操作符给变量赋值,那么该变量自动初始化为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            
}

6、Solidity汇编中let指令的运行机制

在EVM的内部,let指令执行如下任务:

  • 创建一个新的堆栈槽位
  • 为变量保留该槽位
  • 当到达代码块结束时自动销毁该槽位

因此,使用let指令在汇编代码块中定义的变量,在该代码块外部是无法访问的。

7、Solidity汇编中的注释

在Yul汇编中注释的写法和Solidity一样,可以使用单行注释//或多行注释/* */。例如:

assembly {     
  // single line comment

  /*
    Multi
    line
    comment
  */
}

8、Solidity汇编中的字面量

在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" // 超长字符串,错误!
} 

9、Solidity汇编中的块和作用范围

在Solidity汇编中,变量的作用范围遵循标准规则。一个块的范围使用一对大括号标识。

在下面的示例中,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: identifier not found
// let z := y
// ^

作用范围的唯一例外是函数和for循环,我们将在下面解释。

10、在Solidity汇编中使用函数的局部变量

在Solidity汇编中,只需要使用变量名就可以访问局部变量,无论该变量是定义在汇编块中,还是Solidity代码中,不过变量必须是函数的局部变量:

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是函数的参数:

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汇编代码如下:

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循环的初始化部分和迭代后续步骤即可。

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部分。

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,当表达式的值不匹配任何其他分支条件时,将执行默认分支的代码。

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条件

    assembly {
    let x := 34

    switch lt(x, 30) case true { // do something } case false { // do something els } default { // 不允许 }
    }

15、在Solidity汇编中使用函数

也可以在Solidity内联汇编中定义底层函数。调用这些自定义的函数和使用内置的操作码一样。

下面的汇编函数用来分配指定长度的内存,并返回内存指针pos:

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
  • 环境操作,主要指与区块链相关的全局信息,例如blockhashcoinbase收款账号
  • 存储、内存和栈操作
  • 交易与合约调用操作
  • 停机操作
  • 日志操作

详细的操作码可以查看Solidity文档


原文链接:Solidity汇编开发简明教程 — 汇智网

点赞
收藏
评论区
推荐文章
待兔 待兔
3个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
可莉 可莉
3年前
10个开源的Python区块链项目
Python不是主流的区块链底层平台开发语言,但是在DApp开发、区块链仿真与数据分析、智能合约安全分析等领域,Python依然是不错的选择。本文介绍了10个最流行的Python区块链项并提供了相应的源代码下载地址。<!more区块链开发教程链接:以太坊(https://www.oschina.net/action/GoToLink
Stella981 Stella981
3年前
Defi数据引擎The Graph调用方法【含源码】
当你尝试访问以太坊智能合约以及DApp产生的区块链数据时,可能会发现很难将数据转换为一种可读的格式。TheGraph提供了一种用于查询以太坊和IPFS网络数据的索引协议,任何人都可以基于其提供的开放API创建并发布索引数据,即subgraph,这使得区块链数据更容易访问。在这个教程中,我们将学习如何使用TheGraph来查询Aave协议数据,使用的技术
Wesley13 Wesley13
3年前
ETH智能合约测试
_ETH的智能合约一般用Solidity语言编写,懂点基本solidity语法会更好地测试_测试中需要用到的工具:一份智能合约Remix(一个在线IDE,用来编译、编辑、部署智能合约,需要翻墙才能使用)点我跳转到Remix(https://www.oschina.net/action/GoToL
Stella981 Stella981
3年前
Solidity合约安全漏洞分析教程【Mythril】
在这个教程中,我们将学习Mythril的安装与使用方法,了解Mythril的工作原理,掌握如何利用EtherThief和Suicide模块分析合约的安全漏洞,以及如何配置Mythril安全分析的交易数量参数和执行超时参数。用自己熟悉的语言学习以太坊DApp开发:Java(https://www.oschina.net/action/GoToLi
Wesley13 Wesley13
3年前
37个常见的智能合约安全问题【以太坊】
SWCRegistry是以太坊安全人员和开发人员的必备知识库。它是SmartContractSecurity提供的以太坊智能合约安全漏洞分类及测试用例集,其中包含了37种以太坊Solidity智能合约开发中常见的安全问题的描述及后果,例如重入、溢出等,同时也给出了CWE漏洞分类、解决方案和作为示例的合约程序代码。用自己熟悉的语言学习以太坊DA
Stella981 Stella981
3年前
DeFi Compound开发REST API
在这个教程中,我们将学习如何为DeFi协议Compound的智能合约创建一个可以通过HTTP访问的API开发接口,并学习如何使用Infura作为以太坊网络和应用之间的桥梁。Compound是一个基于以太坊的数字资产借贷利率协议。Infura(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%
Stella981 Stella981
3年前
Haskell开发以太坊智能合约
ethereumH,这个程序包提供了写在Haskell上的工具,能使你连接到以太坊区块链。链接:Github:https://github.com/blockapps/ethereumH(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fgithub.com%2Fblockapp
Stella981 Stella981
3年前
Chainlink区块链Oracle网络使用指南
Chainlink是一个去中心化的预言机网络,它可以让区块链中的智能合约安全地访问外部世界的数据。在这个教程中,我们将探索chainlink预言机网络的搭建,并学习如何使用预置或自定义的适配器实现智能合约与外部世界数据的桥接。以太坊教程链接:Dapp入门(https://www.oschina.net/action/GoToLink?urlh