您当前的位置: 首页 > 

MateZero

暂无认证

  • 4浏览

    0关注

    92博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Solidity内嵌汇编学习(一)

MateZero 发布时间:2022-04-23 22:00:09 ,浏览量:4

很多人在学习Solidity时会跳过内嵌汇编这一章,当然我也不例外。但随着我们相关开发的深入,有时会无法避免和内嵌汇编打交道。这时,攻克内嵌汇编也许是一种更好的选择。凡事俱怕认真二字,当我们认真研究后,一些乍一看比较难的问题就慢慢的不难了。

我们今天以Solidity 0.8.7官方文档为例,学习内嵌汇编的第一个简单示例:GetCode.sol

下面先看官方文档中的源码:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16  0xe0  (0x80 + 0x60) //自由内存指针
        *    0x60 => zero slot
             0x80 => 63 // 长度前缀
             0xa0 =>  // 前半部分 0x6080604052600080fdfea2646970667358221220ad1bbad09d41f2213b969ef0
             0xc0 =>  // 后半部分 0x728767ee6ac4b4ed5af6a01c4511fa370f5e8c6d64736f6c6343000804003300
             0xe0 => 新内存的的起点
        *
         */
        console.log("pointer :%s",pointer);
        console.log("length :%s",length);
        console.logBytes32(value1);
        console.logBytes32(value2);
    }
}

下面来具体学习一下内嵌汇编中的操作

  1. 读取外部地址的代码大小 ,注意是以字节为单位的,本例中为合约A的代码大小,为63.
  2. 读取自由内存指针指向的位置,此例为0x80,注意mload代表从某地址开始读取32字节。那么我们为什么不直接用0x80而还要读一次呢。因为有的时候会进行其它内存分配操作或者函数参数中包含了memory数据等,此其值就不是0x80了。本例是刚好没有相关操作或者数据,所以才为初始值0x80。因此这里一定要用mload(0x40)的方法获取。
  3. 因为bytes在内存保存时会有一个长度前缀(32字节),所以需要将获取的size加上32 再对 32取整(取能包含它的最小的32整数倍)。本例中 63 + 32 = 95。我们口算一下,需要32 * 3 = 96字节才能保存变量o_code。 这里,相应的计算公式为:trunc((code_size + 32 + 32 - 1) / 32) * 32,转化为对应的内嵌操作就为and(add(add(size, 0x20), 0x1f), not(0x1f))。 这时我们得到96。
  4. 接下为,将旧指针地址 与 新计算的大小(96) 相加,得到新的指针地址,并保存在0x40开始的一个字节中,这里使用的是mstore
  5. 原指针地址开始存放o_code,首先是长度前缀,所以先保存长度到一个字节中。
  6. 从原地址进行代码复制,因为长度前缀占用了一个字节,所以这里是从add(o_code, 0x20)进行保存。extcodecopy的函数说明请阅读官方文档中的介绍。

这里我们重点讲一下 and(add(add(size, 0x20), 0x1f), not(0x1f))操作。 这里分两步看:

  1. 该操作的功能。这里是为了得到包含指定大小的最小的能被32整除的数字。这个很好理解,假定我们大小为95字节(包含了长度前缀),那么我们需要多少个字节(solidity中,通常以一个word,32字节为操作单位,所以必须是32的整数倍)才能保存它呢。很显然,我们都知道是96。但是怎么计算呢?我们如果使用javascript去实现,应该为 Math.ceil(v/32) * 32。但是Solidity中除法为地板除,因此计算方式为 Math.floor((v + 31)/32) * 32 ,也就是公式: y = (x + 31 ) /32 * 32。这样,当x刚好能被32整除时,得到的结果便是x,如果有任何余数,那么得到的结果会是一个比x大的最小的32的倍数。 至于为什么这里 + 31,是为了有余数时地板除总能+1。如果你+30,那么余数为1的时候便有问题。
  2. 为什么会有andnot操作。接着上面的公式来 y = (x + 31 ) /32 * 32。这里我们假定z = x + 31,那么可以简化为 y = z /32 * 32。而我们知道,在Solidity中,unit 除以2就是右移一位,除于32就是右移5位,相应的乘于32就是左移5位。那么一个uint先右移5位再左移5位,会得到什么结果呢,会导致它的低五位全部清零。我们举一个简单的例子:z = 0xFF = 0b11111111。那么它先右移5位,得到z = 0b111,再左移5位,得到z = 0b11100000,相当于把它的低五位清除了。因此,我们只要把z的低五位清除就能得到 z /32 * 32 的效果。那么清除某一位最快捷的方式是将该位与0 相与(and),其它位与1相与(保留)。于是我们只要z and 111...11100000相与就可以了。可以看到not(0x1f)正是前面所有的位为1,后面5位为0的数。所以该公式进行了优化,得到了y = and(z ,not(0x1f)), 将先除后乘变成了直接清除后五位。

接下来我们增加的内嵌汇编操作是打印出相应的值进行验证。分别为:

  • 自由指针地址:这里为0x80 + 0x60 = 0xe0。(初始值0x80 + 96)
  • 代码长度:从长度前缀word(32字节)中获取,这里是63
  • value1,代码的第一部分,也就是前32字节。
  • value2,代码的第二部分,也就是后31字节 + 补0

最后我们打印出相应的值进行验证。

我们的单元测试文件为

const { ethers } = require("hardhat");

describe("GetCode", () => {
  it("GetCode Test", async () => {
    const A = await ethers.getContractFactory("A");
    const a = await A.deploy();
    await a.deployed();
    const GetCode = await ethers.getContractFactory("GetCode");
    const instance = await GetCode.deploy();
    await instance.deployed();
    const result = await instance.getCodeTest(a.address);
    console.log();
    console.log(result);
  });
});

运行单元测试,我们得到类似结果:

Compiled 1 Solidity file successfully


  GetCode
pointer :224
length :63
0x6080604052600080fdfea2646970667358221220e5457b554ed9901ed12a8d40
0xc71d05b4591105f6c4f1304ca9e68525d329e35664736f6c6343000804003300

0x6080604052600080fdfea2646970667358221220e5457b554ed9901ed12a8d40c71d05b4591105f6c4f1304ca9e68525d329e35664736f6c63430008040033
    ✔ GetCode Test (916ms)


  1 passing (918ms)

可以看到,我们的结果是和打印出的值相符的。

好了,今天的学习就到这里结了。重点是Solidity内存分配,bytes类型的变量在内存中的保存(保存的是一个起始地址,因为包含有长度前缀,真正内容是从起始地址加32字节开始的),重置自由指针地址(否则有可能读到污染数据)。

由于水平有限,难免有错误之处,恳请读者批评指正。

关注
打赏
1648304347
查看更多评论
立即登录/注册

微信扫码登录

0.0344s