1. 北竹林首页
  2. 资讯
  3. 技术指南

解构智能合约:什么是功能选择器?

HI,欢迎我一起继续解构智能合约。本文是系列文章的第三部分,如果您还没有阅读之前的文章,请先看一下:

(1)前言:基础代码与操作方式;

(2)创造与运行时间代码解析;

我们正在解构一个简单的S…

5

HI,欢迎我一起继续解构智能合约。本文是系列文章的第三部分,如果您还没有阅读之前的文章,请先看一下:

(1)前言:基础代码与操作方式

(2)创造与运行时间代码解析

我们正在解构一个简单的Solidity智能合约的EVM字节码。

在上一篇文章中,我们确定了将智能合约的字节码分为创建和运行两部分,并且知道了为什么这么做,深入了解了创建部分后,现在是时候开始我们对运行时部分的探索了。如果你看一下解构图,我们可以首先看一下名为BasicToken.evm(运行时)的第二个大拆分块。

这可能看起来有点可怕,因为运行的代码长度至少是创建代码大小的四倍!但不要担心,我们在之前的文章中为了解EVM代码而开发的技能,加上我们使用绝对可靠的“分而治之”的策略,将使这一挑战更加系统化,甚至可能更容易。这只是开始,我们将继续识别独立结构,继续拆分直到分解为一个个可解决的问题为止。

首先,让我们回到Remix在线编辑器,并使用运行时字节码启动调试会话。我们怎么做?上次,我们部署了智能合约并调试了部署事务。这一次,我们将使用其中一个函数与已部署的智能合约接口进行交互,并调试该事务。

首先,一起回想一下我们的智能合约:

我们启用了优化的编译器的Javascript VM,v0.4.24版本,并将10000作为初始供应。部署智能合约后,你应该看到它在Remix上的“run”面板“Deployed Contracts”的部分中列出。单击它可以展开看到智能合约的界面。

这个界面是什么?它是智能合约中所有公共或外部方法的列表 – 即任何以太坊帐户或智能合约都可以与之交互。私人和内部方法不会在这里显示,如何与智能合约运行时代码的特定部分进行交互,将是本文解构的重点。

我们可以尝试一下,单击Remix的“run”面板中的totalSupply按钮。您应该立即在按钮下方看到响应,这是我们所期望的,因为我们将智能合约作为初始token供应部署。现在,在Console面板中,单击Debug按钮以使用此特定事务启动调试会话。请注意,Console面板中将有多个Debug按钮; 确保你使用的是最新版本。

在这种情况下,我们没有调试到该0x0地址的事务,正如我们在上一篇文章中看到的那样,创建了一个智能合约。相反,我们正在调试对智能合约本身的事务 – 即它的运行时代码。

如果弹出“ 指令”面板,则应该能够验证Remix列出的指令与解构图中BasicToken.evm(运行时)部分中的指令相同。如果它们不匹配,则出现问题。尝试重新开始并确保您使用上述正确的设置。

您可能会注意到的第一件事是调试器将您置于指令246并且事务滑块位于字节码的大约60%处。为什么?因为Remix是一个非常慷慨的程序,它直接带你到EVM即将执行totalSupply函数体的部分。然而,在此之前发生了很多事情,这些都是我们在这里要注意的。实际上,我们甚至不会在本文中研究函数体的执行情况。我们唯一关注的是Solidity生成的EVM代码如何路由传入的事务,这是我们作为合约的“功能选择器”将要理解的工作。

因此,抓住滑块并将其一直拖到左边,这样我们就从指令零开始。正如我们之前看到的,EVM 总是从指令0执行代码,没有异常,然后流经其余代码。让我们通过操作码来完成这个执行操作码。

出现的第一个结构是我们以前见过的(实际上我们会看到很多):

1
图1.空闲内存指针

这是Solidity生成的EVM代码将始终在调用之前执行的任何操作:将一个点保存在内存中以便稍后使用。

让我们看看接下来会发生什么:

2
图2. Calldata长度检查。

如果在Debug选项卡中打开Remix的Stack面板并跳过指令5到7,您将看到堆栈现在包含两次数字。如果您在阅读这些超长数字时遇到问题,请注意您调整Remix调试面板的宽度,以便数字很好地适合单行。第一个来自常规推送,但第二个是执行操作码的结果,正如黄皮书所述,没有参数并返回“当前环境中的输入数据”的大小,或者我们经常称为calldata:4CALLDATASIZE

什么是calldata?正如 Solidity 的文档ABI规范中所解释的,calldata是一个十六进制数字的编码块,其中包含有关我们要调用的智能合约函数的信息,以及它的参数或数据。简单地说,它由一个“函数id”组成,它是通过散列函数的签名(截断到前四个字节)然后是压缩参数数据生成的。如果需要,您可以详细研究文档链接,但不要担心这个包装如何工作到最精细的细节。它在文档中有解释,但有点难以一次掌握。用实际例子来理解它会容易得多。

让我们看看这个calldata是什么。在Remix的调试器中打开“ 调用数据”面板,查看:0x18160ddd。这是通过将keccak256函数签名上的算法应用为字符串来 精确生成的四个字节”totalSupply()” 并执行所述截断。由于此特定函数不带参数,因此它只是:一个四字节的函数id。当CALLDATASIZE被调用时,它只是将第二个推4入堆栈。

然后,指令8 LT用于验证calldata大小是否小于4。如果是,则以下两条指令执行JUMPI指令86(0x0056)。这不到四个字节,因此在这种情况下不会有跳转,执行流将继续执行指令13.但在我们这样做之前,让我们假设我们用空的calldata调用我们的智能合约 – 也就是说,0x0而不是0x18160ddd。你不能用Remix btw做到这一点,但你可以手动构建交易。

在这种情况下,我们最终会进入指令86,它基本上将几个零推送到堆栈并将它们提供给REVERT操作码。为什么?好吧,因为这个智能合约没有后备功能。如果字节码没有识别传入数据,它会将流转移到回退函数,如果该结构没有“捕获”调用,则此恢复结构将终止执行,绝对没有回滚。如果没有什么可以回归,那么就没有任何事可做,而且呼叫完全恢复了。

现在,让我们做一些更有趣的事情。返回Remix的Run选项卡,复制Account地址,并将其用作参数来调用balanceOf而不是totalSupply调试该事务。这是一个全新的调试会话; 让我们暂时忘记吧totalSupply。导航到指令8,CALLDATASIZE现在将36(0x24)推入堆栈。如果你看看calldata,那就是现在0x70a08231000000000000000000000000ca35b7d915458ef540ade6068dfe2f44e8fa733c。

这个新的calldata实际上非常容易分解:前四个字节70a08231是签名的散列,后面”balanceOf(address)”的32个字节包含我们作为参数传递的地址。为什么32个字节,如果以太坊地址只有20个字节长,好奇的读者可能会问?ABI总是使用32字节“字”或“槽”来保存函数调用中使用的参数。

继续在我们的balanceOf调用环境中,让我们在指令13处离开我们离开的地方,此时堆栈中没有任何内容。指令13然后推0xffffffff送到堆栈,并且下一条指令将29字节长的0x000000001000…000数字推送到堆栈。我们马上就会明白为什么。现在,只需要注意一个包含四个字节,另一个包含四个字节的0’s’。

接下来CALLDATALOAD取一个参数(在指令48处推送到堆栈的那个参数)并从该位置的calldata读取一个32字节的块,在这种情况下,在Yul中将是:

calldataload(0)

基本上将我的整个calldata推到堆栈。现在来了有趣的部分。DIV从堆栈中消耗两个参数,获取calldata并将其除以奇怪的0x000000001000…000数字,有效地过滤除了 calldata中的函数签名之外的所有内容,并将其留在堆栈中:0x000…000070a08231。下一条指令使用AND,它也消耗堆栈中的两个元素:我们的函数id和带有四个字节的数字f。这是为了确保签名哈希正好是八个字节长,如果存在任何其他内容,则屏蔽其他任何内容。我认为,Solidity使用的安全措施。

简而言之,我们只是检查calldata是否太短,如果是这样,还原,然后稍微改进,以便我们在堆栈中有我们的函数,

另外,我们差不多完成了。下一部分会很容易理解:

3
图3.函数选择器

在指令53处,代码将18160ddd(函数id totalSuppy)推送到堆栈,然后使用a DUP2来复制70a08231当前存在于堆栈中第二位置的传入calldata 值。为什么要复制?因为EQ指令59处的操作码将消耗堆栈中的两个值,并且我们希望保持70a08231值,因为我们经历了从calldata中提取它的麻烦。

代码现在将尝试将calldata中的函数id与已知函数id之一进行匹配。由于70a08231进入,它将不匹配18160ddd,跳过JUMPIat指令63.但它将在下一次检查中匹配并在指令74跳转到JUMPI。

让我们花点时间观察一下这些平等检查对智能合约的每个公共或外部功能。这是函数选择器的核心:充当某种switch语句,只是简单地将执行路由到代码的正确部分。这是我们的“中心”。

因此,由于最后一个案例是匹配,执行流程将我们带到JUMPDESTat位置130,正如我们将在本系列的下一部分中看到的那样,该balanceOf函数的ABI“包装器” 。正如我们将看到的,这个包装器将负责解包事务的数据以供函数体消耗。

继续尝试transfer这次调试功能。功能选择器真的没有神秘感。这是一个简单而有效的结构,位于每个合约的门口(至少所有那些从Solidity编译)并将执行重定向到代码中的适当位置。这就是Solidity为智能合约的字节码提供模拟多个入口点的能力的方式,因此也就是界面。

看一下解构图,这就是我们刚刚解构的:

4
图4.函数选择器和智能合约的运行时代码主入口点。

总而言之,伙计们,不知不觉中你对solidity的底层代码熟悉程度超过了大部分人,坚持下去,你就能彻底把它打开。

*本文由Alejandro Santander首发于medium,由猎豹区块链安全翻译并整理*

声明:登载此文出于传递更多信息之目的,观点仅代表作者本人,绝不代表北竹林赞同其观点或证实其描述。

提示:投资有风险,入市须谨慎。本资讯不作为投资理财建议。

联系我们

QQ:

1739447883

邮箱:

1739447883@qq.com