Loading [MathJax]/jax/input/TeX/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >解构 Solidity 合约 #2 - 函数选择器

解构 Solidity 合约 #2 - 函数选择器

作者头像
Tiny熊
发布于 2023-01-09 09:50:33
发布于 2023-01-09 09:50:33
58100
代码可运行
举报
运行总次数:0
代码可运行

译文出自:登链翻译计划[1] 译者:翻译小组[2] 校对:Tiny 熊[3]

这是解构系列另一篇。如果你没有读过前面的文章[4],请先看一下。我们正在解构一个简单的Solidity 合约[5]的EVM 字节码[6]

在上一篇文章[7]中,我们发现有必要将合约的字节码分为创建时和运行时代码。在对创建部分进行了深入研究之后,现在是时候开始对运行时部分的探索了。

运行时代码

如果你看一下解构图[8],我们将从第二个大的部分开始,对应结构图标题为BasicToken.evm(runtime)的部分。

在一开始可能看起来有点吓人,因为运行时的代码看起来至少是创建代码的四倍!但不要担心,技能的学习是很重要的。在前面的文章中我们对 EVM 代码已经有所理解,结合无懈可击的分而治之的策略,使用系统化方式解决这个挑战,会把它变得容易。简单的开始查看代码,识别独立的结构,并继续分割,直到没有其他东西可以分割。

所以,为了开始,让我们回到Remix[9],用运行时字节码启动一个调试会话。我们怎么做呢?上一次,我们部署了合约并调试了部署交易。这一次,我们将与已部署的合约的接口交互,与它的一个函数交互,并对该交易进行调试。回顾一下我们的合约:

https://gist.github.com/ajsantander/dce951a95e7608bc29d7f5deeb6e2ecf#file-basictoken-sol

在 Remix 中使用 Javascript 虚拟机、启用了优化, 编译器 0.4.24 版以及 10000 作为初始发行量来部署它。一旦合约部署完毕,你应该看到它被列在 Remix 的DEPLOY & RUN面板的Deployed Contracts部分。点击它,展开合约的界面。就像专栏[10]的这篇文章[11]那样进入调试。

展开合约的界面列出了合约的所有方法,这些方法要么是公共的(public),要么是外部的(external),也就是说,任何以太坊账户或合约都可以与之交互。私有的和内部的方法不会显示在这里,事实上,从 "外部世界 "是无法到达的。如何与合约的运行时代码的特定部分交互将是本文的重点。

入口检查

我们要不要试一下?点击 Remix 的Run面板上的totalSupply按钮。你应该马上看到按钮下面有一个响应。0: uint256: 10000,这是我们所期望的,因为我们以10000作为初始代币供应来部署合约。现在,在控制台面板上,点击调试(Debug)按钮,开始对这个特定的交易进行调试。注意,在Console面板上会有多个Debug按钮,请确保你使用的是最新的一个。

在这个例子中,我们不是在调试0x0地址的交易,它创建了一个合约, 正如我们在前面的文章中看到的。现在,我们要调试的是对合约本身的交易--也就是对其运行时代码的交易。

如果你打开调试面板,你应该能够验证 Remix 列出的指令与解构图[12]中*BasicToken.evm(运行时)*部分的指令是一致的。如果它们不匹配,就说明出了问题。试着重新开始,并确保你使用了上述的正确设置。

一切顺利吗?你可能注意到的第一件事是,调试器把你放在指令 246 处,交易滑块被定位在字节码的大约 60%处。为什么呢?因为 Remix 是一个非常好的、慷慨的程序,它直接把你带到 EVM 刚要执行totalSupply函数的部分。然而,在这之前发生了很多事情,这些都是我们在这里要注意的。事实上,在这篇文章中,我们甚至不会去研究函数主体的执行。我们唯一关心的是 Solidity 生成的 EVM 代码如何引导进入的交易,我们将理解为合约的 "函数选择器 "的工作。

所以,抓住那个滑块,把它一直向左拖,这样我们就可以从指令 0 开始。正如我们之前所看到的,EVM 总是从指令 0 开始执行代码,没有例外。让我们逐个操作码走过这个执行过程。

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

图 1. 空闲内存指针。

这是 Solidity 生成的 EVM 代码, 在调用中总是在其他事情之前做的事情:在内存中保存一个位置以便以后使用。

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

图 2. Calldata 长度检查。

如果你在Debug标签中打开 Remix 的Stack面板,走过指令 5 到 7,你会看到堆栈现在包含数字4两次。如果你在阅读这些超长的数字时遇到困难,请注意调整 Remix 的 Debug 面板的宽度,使这些数字很好地融入单行。第一个数字来自普通的推送,但第二个数字是执行操作码CALLDATASIZE的结果,如黄皮书所述[13],它不需要参数,并返回 当前交易上下文环境中的输入数据的大小,或者我们通常所说的calldata

什么是 calldata? 正如 Solidity 的文档 ABI 规范中所解释的那样[14],calldata 是一个十六进制数字的编码块,它包含了关于我们想要调用合约的哪个函数的信息,以及它的参数或数据。简单地说,它由一个 "函数 ID "组成,它是由函数的签名哈希值产生(截断到前四个字节)和打包的参数数据。如果你想的话,你可以详细研究一下文档链接,文档里有最详细的解释,也许一下子难以掌握,但先不要担心没法理解这种打包的工作方式,用实际的例子来理解要容易得多。

让我们看看这个 calldata 是什么。在 Remix 的调试器中打开Call Data面板,可以看到:0x18160ddd。这是四个字节,正是通过对字符串 totalSupply()的函数签名应用 keccak256算法,并进行前四个字节截断而产生的。由于这个特殊的函数不需要参数,它只是:一个四字节的函数 ID。当CALLDATASIZE被调用时,它只是把第二个4推到堆栈上。

然后指令 8 使用LT来验证 calldata 的大小是否小于 4。如果是,下面的两条指令表示跳转(JUMPI)到指令 86(0x0056)。所以在此案例中,将不会有跳转,执行流程将继续到指令 13。但在这之前,让我们想象一下,我们用空的 calldata 调用我们的合约--也就是用0x0而不是0x18160ddd。在 Remix 中你不能这样做,但如果你手动构建交易,你可以这样做。

在此案例中,我们会在 86 号指令中结束,它基本上是把几个 0 推到堆栈中,并把它们送入REVERT操作码。为什么呢?嗯,因为这个合约没有回退函数(fallback)[15]。如果字节码不能识别传入的数据,它就会把数据流转到回退函数,如果没有回退函数 接住这个调用,那么就会无情地终止执行。如果没有什么可以回退的,那么就没有什么可以做的,调用就会被完全退回(revert)。

现在,让我们做一些更有趣的事情。回到 Remix 的Run标签,复制Account地址,用它作为参数调用balanceOf而不是totalSupply,并调试该交易。这是一个全新的调试环节;现在我们先忘记totalSupply。导航到指令 8,CALLDATASIZE现在将推送 36(0x24)到堆栈。如果你看一下 calldata,它现在是0x70a08231000000000000000000ca35b7d915458ef540ade6068dfe2f44e8fa733c

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

继续我们的balanceOf调用,让我们从第 13 条指令开始,这时堆栈中没有任何东西。第 13 条指令将0xffffffff推入堆栈,下一条指令将一个 29 字节长的0x000000001000...000数字推入堆栈。我们稍后会看到原因。现在,只需注意一个包含四个字节的f,另一个包含四个字节的0

接下来CALLDATALOAD接收一个参数(第 48 条指令中推到堆栈的参数)并从该位置的 Calldata 中读取 32 字节的大块数据,在本例中Yul[16]将是:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
calldataload(0)

基本上是把我们的整个 calldata 推到堆栈中。现在是有趣的部分。DIV从堆栈中消耗了两个参数,把 calldata 除以那个奇怪的0x000000001000...000数字,有效地过滤了 calldata 中除了函数签名以外的所有东西,并把它单独留在堆栈中:0x000...000070a08231。下一条指令使用AND,它也消耗了堆栈中的两个元素:我们的函数 ID 和带有f的四个字节的数字。这是为了确保签名哈希值正好是 8 个字节的长度,掩盖了其他的东西(如果有任何东西存在的话)。我想这是 Solidity 使用的安全措施。

长话短说,我们已经简单地检查了 calldata 是否太短,如果是的话,就退回,然后把东西洗了一下,这样我们的函数 ID 就在堆栈里了:70a08231

接下来的部分真的很容易理解:

图 3. 函数选择器。

在指令 53,代码将18160dddtotalSuppy的函数 ID)推入堆栈,然后使用DUP2来复制我们传入的 calldata 70a08231值,目前在堆栈的第二个位置。为什么是 DUP?因为指令 59 的EQ操作码将消耗堆栈中的两个值,我们想保留70a08231的值,因为我们已经费尽心思从 calldata 中提取它。

现在代码将尝试将 calldata 中的函数 ID 与一个已知的函数 ID 相匹配。由于堆中是 70a08231 ,它将不会与 18160ddd 匹配,跳过指令 63 的 JUMPI。但在接下来的检查中,它将与之匹配,并跳入指令 74 的 JUMPI。

让我们花点时间观察一下,合约的每个公共或外部函数都有一个这样的匹配检查(EQ)。这是函数选择器的核心:作为某种开关语句,简单地将执行路由到代码的正确部分,它是我们的 "hub(枢纽)"。

因此,由于上一个案例是匹配的,执行流将我们带到 130 位置(0x82)的JUMPDEST,我们将在本系列下一部分看到它,它是balanceOf函数的 ABI Wrapper(包装器)。这个包装器将负责对交易的数据进行解包,供函数主体使用。

继续,这次尝试调试transfer函数。函数选择器其实并不神秘。它是一个简单而有效的结构,位于每一个合约(至少是所有从 Solidity 编译的合约)的大门口,并将执行重定向到代码中的适当位置。它是 Solidity 赋予合约的字节码模拟多个入口点的能力的方式,因此也是一个接口。

看一下解构图[17],这就是我们刚刚解构的内容:

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

下一篇,我们继续解构 函数包装器。

原文链接:https://blog.openzeppelin.com/deconstructing-a-solidity-contract-part-iii-the-function-selector-6a9b6886ea49/

参考资料

[1]

登链翻译计划: https://github.com/lbc-team/Pioneer

[2]

翻译小组: https://learnblockchain.cn/people/412

[3]

Tiny 熊: https://learnblockchain.cn/people/15

[4]

前面的文章: https://learnblockchain.cn/article/5190

[5]

Solidity合约: https://gist.github.com/ajsantander/dce951a95e7608bc29d7f5deeb6e2ecf

[6]

EVM字节码: https://gist.github.com/ajsantander/03a4a183756980ef0865825bea96d6f5

[7]

上一篇文章: https://learnblockchain.cn/article/5190

[8]

解构图: https://img.learnblockchain.cn/pics/20221214153051.svg

[9]

Remix: https://remix.ethereum.org/

[10]

专栏: https://learnblockchain.cn/column/22

[11]

这篇文章: https://learnblockchain.cn/article/4913

[12]

解构图: https://img.learnblockchain.cn/pics/20221214153051.svg

[13]

如黄皮书所述: https://ethereum.github.io/yellowpaper/paper.pdf

[14]

正如Solidity的文档ABI规范中所解释的那样: https://learnblockchain.cn/docs/solidity/abi-spec.html

[15]

回退函数(fallback): https://learnblockchain.cn/docs/solidity/contracts.html#fallback

[16]

Yul: https://solidity.readthedocs.io/en/v0.4.24/julia.html

[17]

解构图: https://img.learnblockchain.cn/pics/20221214153051.svg

Twitter : https://twitter.com/NUpchain Discord : https://discord.gg/pZxy3CU8mh

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-12-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
解构 Solidity 合约 #3:函数包装器
号外,今天我们的登链社区网站做了一点小更新, 作者们可以关联自己的社交账号,关联后,在文章右侧的作者区域就可以看到点亮的小图标,让更多的小伙伴通过内容交朋友,也欢迎大家关注登链社区的账号。
Tiny熊
2023/01/09
7330
解构 Solidity 合约 #3:函数包装器
解构 Solidity 合约 #4: 函数体
这是解构系列另一篇。如果你没有读过前面的文章[4],请先看一下。我们正在解构一个简单的Solidity 合约[5]的EVM 字节码[6]。
Tiny熊
2023/01/09
8480
解构 Solidity 合约 #4: 函数体
最详细的解释EVM的函数选择原理
在我们开始前,这篇文章假定读者具备 solidity 的基础知识,以及了解它是如何部署在以太坊网络的。本文将简要地讨论这部分知识,如果你想对这些知识进行系统复习,请看这篇文章[2]众所周知,solidity 代码在部署到以太坊网络之前需要被编译成字节码。这个字节码对应的是 evm 所解析的一系列操作码指令。本系列文章主要分析编译后的字节码特定部分,并阐明它们的工作原理。在阅读完每篇文章后,你应该对每个组件的功能有一个更清晰的了解。在这一过程中,你会学到很多与 evm 相关的基础概念。我们先来看一个基本的 solidity 合约,以及它部分字节码/操作码,以展示 evm 是如何选择函数的。由 solidity 合约创建的运行态(runtime)字节码是整个合约的内容总结(reoresentation)。在合约中,你可能写有多个函数,一旦部署在链上,就可以被调用。学习 evm 和合约的一个常见问题是,EVM 是如何知道根据合同的哪个函数被调用来执行哪一块字节码?这个问题是我们用来帮助理解 evm 的底层机制以及如何处理这种特殊情况的第一个问题。
Tiny熊
2022/04/08
6890
通过逆向和调试深入EVM #6 - 完整的智能合约布局
在这个合约中,我们将逆向一个完整的智能合约。这一部分的目标是全面了解智能合约布局,全面了解智能合约的布局,并通过手动的方式对其进行反编译。
Tiny熊
2023/01/09
7110
如何调试EVM智能合约(第1篇): 理解汇编
你可能已经知道,当一个智能合约在区块链中没有被验证时,你无法读取它的实体代码,只有字节代码被显示。
Tiny熊
2022/11/07
1.2K0
如何调试EVM智能合约(第1篇): 理解汇编
Solidity 优化 - 隐藏的 Gas 成本
本文将研究以太坊虚拟机(EVM)的内部工作,以说明如何 "利用 "EVM 的特殊特性,为用户最小化 solidity 智能合约的执行成本。社区发布许多关于 solidity 开发者可以利用的知识来设计和开发更安全、更节省 Gas 的智能合约。
Tiny熊
2023/01/09
8350
Solidity 优化 - 隐藏的 Gas 成本
Solidity 0.8.5 发布
Solidity v0.8.5[4]允许从bytes转换为bytesNN值,增加了verbatim内置函数以在 Yul 中注入任意字节码,并修复了几个较小的错误。
Tiny熊
2021/07/14
4790
EVM 学习手册
一组博客文章,深入 EVM 的特定部分,让你从 solidity 代码到 EVM 的操作代码。
Tiny熊
2022/11/07
6740
深入EVM-合约分类这件小事背后的风险
本文从合约为什么要分类出发,结合每个场景可能面对怎样的恶意攻击,最终给出一套达成相对安全的合约分类分析算法。
十四君
2023/09/01
3080
深入EVM-合约分类这件小事背后的风险
深入理解EVM操作码,让你写出更好的智能合约
你的一些编程“好习惯”反而会让你写出低效的智能合约。对于普通编程语言而言,计算机做运算和改变程序的状态顶多只是费点电或者费点时间,但对于 EVM 兼容类的编程语言(例如 Solidity 和 Vyper),执行这些操作都是费钱 的!这些花费的形式是区块链的原生货币(如以太坊的 ETH,Avalanche 的 AVAX 等等...),想象成你是在用原生货币购买计算资源。
Tiny熊
2023/01/09
1.4K0
深入理解EVM操作码,让你写出更好的智能合约
专栏开篇:破解以太坊 EVM 谜题
前段时间翻译了 Ethernaut 题库闯关系列文章[2],发布为了专栏, 效果还不错,自己也有很大提高。
Tiny熊
2022/11/07
3610
专栏开篇:破解以太坊 EVM 谜题
以太坊合约 ABI 和 EVM 字节码
本文解释以太坊中的合约 ABI[2] 和 EVM[3] 字节码。由于以太坊使用 EVM(Ethereum Virtual Machine - 以太坊虚拟机)作为系统的核心,因此用高级语言编写的智能合约代码需要编译成 EVM 字节码和合约 ABI 才能运行。在与智能合约交互时,有必要先了解它们。
Tiny熊
2022/05/25
1.5K0
以太坊合约 ABI 和 EVM 字节码
搞定EVM中的内存数据区,学他!
在第一部分[2],我们分析了 remix 的第一个合约示例 1_Storage.sol。
Tiny熊
2022/04/08
9960
搞定EVM中的内存数据区,学他!
阐述DAPP智能合约流动性质押挖矿分红系统开发技术详细及代码分析
因为EVM是基于栈的虚拟机,它根据操作的内容来计算gas,所以如果牵涉到十分复杂的计算,把运算过程放在EVM中执行就可能十分地低效,同时消耗非常多的gas。
VX_I357O98O7I8
2022/12/15
5710
智能合约Gas 优化的几个技术
每次交易被发送到区块链上,必须支付 Gas 费用。消耗的 Gas 与交易所需的计算量有关,即:EVM 执行交易所需的计算量(如果交易不涉及 EVM,例如简单的以太币转账,Gas 的数量是固定的)。
Tiny熊
2022/11/07
1.4K0
智能合约Gas 优化的几个技术
通过调试理解EVM(#4):结束/中止执行的5种指令
在 EVM 中,总共有 5 种方式来结束智能合约的执行。我们将在这篇文章中详细研究它们。让我们现在就开始吧!
Tiny熊
2023/01/09
9940
通过调试理解EVM(#4):结束/中止执行的5种指令
通过逆向和调试深入EVM #5 - EVM如何处理 if/else/for/functions
在这篇文章中,我们将讨论执行流程。像 if/for 或嵌套函数这样的语句是如何被 EVM 在汇编中处理的?
Tiny熊
2023/01/09
5730
为将傅恒与魏璎珞的爱情上链,作为技术小白的我读了EVM上百行代码,终于搞定了
延禧攻略最近大火,傅恒和魏璎珞求而不得的爱情也令很多人觉得惋惜。那么傅恒到底为什么爱上魏璎珞呢?有网友真相了。
区块链大本营
2018/09/21
9420
为将傅恒与魏璎珞的爱情上链,作为技术小白的我读了EVM上百行代码,终于搞定了
如何调试EVM智能合约 #2 :部署智能合约
在第二部分(本文)中,我们将分析当你在区块链中部署一个智能合约时发生了什么,例如,在点击 remix 中的 "部署 "按钮时。
Tiny熊
2022/11/07
7750
如何调试EVM智能合约 #2 :部署智能合约
深入Solidity数据存储位置
文章较长,内容很详细、很深入。但是不要吓到,坐下来,喝杯咖啡或你最喜欢的饮料,慢慢体会。
Tiny熊
2022/11/07
1.2K0
深入Solidity数据存储位置
推荐阅读
相关推荐
解构 Solidity 合约 #3:函数包装器
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验