{"content":{"title":"调用，预编译和编译器到底是怎么工作的","body":"---\r\ntitle: A call, a precompile and a compiler walk into a bar（调用，预编译和编译器到底是怎么工作的）\r\nauthor URL: \"\"\r\noriginal URL: https://blog.theredguild.org/a-call-a-precompile-and-a-compiler-walk-into-a-bar/\r\ntranslator: \"张云帆\"\r\nreviewer: \"\"\r\n---\r\n\r\n<!-- more -->\r\n\r\n\r\n写于2024年3月15日  作者 tincho  — 阅读时间8分钟\r\n\r\n用Solidity写了5年程序，我以为我知道调用（calls）是如何工作的。但这一切在我遇到一段L2中的不可能的Solidity代码时发生了改变。\r\n\r\n我遇到了一段代码它应该是不能运行的。如果我对Solidity的了解都是正确的，那么我遇到的这个合约就不应该正常运行。但不知什么原因，事实并非如此。\r\n\r\n测试显示没有错误。 测试网已经运行了好几周。这个系统经过了多次安全审查。这样一个损坏的代码不应该已经被报告并修复吗？ 甚至另一个更流行的 L2 也使用类似的代码。\r\n\r\n我所看到的一切都与我对Solidity外部调用的了解相矛盾。我会错得这么离谱吗？\r\n\r\n我的debug技巧让我失败了。这里面有很多令人感动的事情。如果你曾经尝试调试一个交易，这个交易使用预部署，调用自定义预编译，这个预编译对L2的自定义版本的geth中的内容进行 ABI解码，而该版本派生了另一个L2的代码，你就会明白我的感受。\r\n\r\n怀疑演变成绝望。盲目信仰的诱惑愈演愈烈。但我不会屈服！幸运的是，我只用了几个小时就完成了突然的启发、理解到解脱的过程。\r\n\r\n有些人在宗教书籍中发现了揭示真相的真理。 有些人则在机场休息室浏览自助书籍。而我在 C++文件的第2718行找到了它。\r\n\r\n\r\n## 先检查 再调用\r\n\r\n外部调用（external call）的Solidity语法如下所示：\r\n\r\n```Solidity\r\npragma solidity ^0.8.0;\r\ninterface ISomeInterface {\r\n  function foo() external;\r\n}\r\ncontract Example {\r\n  function callAccount(address account) external {\r\n    ISomeInterface(account).foo();\r\n  }\r\n}\r\n```\r\n\r\n使用外部调用的示例合约\r\n\r\n如果你编译这个合约，会弹出没有包含许可证标识符（license identifier）的警告，你会看到这个字节码\r\n\r\n```\r\n...\r\nCALL\r\n...\r\n```\r\n\r\n用solc编译后的EVM字节码0.8.15\r\n\r\n不出所料，编译器将Solidity高级调用转换为`CALL`操作码。你觉得过于简单？好吧，让我们深入一点。\r\n\r\n那些处理Solidity超过一个去中心化金融夏天（defi summer）的人知道编译器包括安全检查。\r\n\r\n在`CALL`之前，编译器放置字节码来验证调用的目标是否有代码。它放置了一个`EXTCODESIZE`，包括在`CALL`之前到达`REVERT`的必要逻辑，以防目标的`EXTCODESIZE`为0。\r\n\r\n```\r\nEXTCODESIZE\r\n...\r\nREVERT\r\n...\r\nCALL\r\n```\r\n\r\n使用solc编译后更准确的字节码0.8.15\r\n\r\n但是，即使是一个在2021年夏天去中心化金融后半段开始并从那以后一直在编写Solidity，为下一个牛市做好准备的开发人员也知道这一点。他们可能已经在字节码中看到了它，或者，更准确地说，可能已经在Solidity文档[4]中发现了它：\r\n\r\n> 由于EVM认为对不存在的合约的调用总是成功的，Solidity在执行外部调用时使用`extcodesize`操作码进行额外检查。这确保了即将被调用的合约要么实际存在（它包含代码），要么引发异常。\r\n我对上述内容深信不疑。以至于当我第一次看到这样的代码时，我很难相信：  \r\n\r\n```Solidity\r\npragma solidity ^0.8.0;\r\ninterface IPrecompile {\r\n  function foo() external returns (uint256);\r\n  function bar() external;\r\n}\r\ncontract Example {\r\n  // Function to execute a custom precompile\r\n  function doSomething() external {\r\n    // [...]\r\n    IPrecompile(customPrecompileAddress).foo();\r\n  }\r\n}\r\n```\r\n在深入研究之前，我们先熟悉一下一些概念。\r\n\r\n## 预编译\r\n\r\n预编译是没有存储字节码但可以执行代码的EVM帐户。它们的执行代码存储在节点本身中。通常你会发现它们[在可能地址的最低范围内][5]。\r\n\r\n要执行预编译，您需要调用它所在的地址。例如，`ecRecovery`是地址为“0x00…01”的EVM的一个预编译。\r\n\r\n让我们看看它的代码：\r\n\r\n```bash\r\ncast code 0x0000000000000000000000000000000000000001\r\n0x\r\n```\r\n\r\n它没有EVM字节码。它的实际代码[在节点中][6]。\r\n\r\n虽然以太坊有自己的预编译，但没有什么可以阻止L2将新编译包含到其节点中。这可能是增强EVM功能的强大方式。\r\n\r\n## 从Solidity调用预编译\r\n\r\n预编译没有EVM字节码。我认为Solidity不允许对没有字节码的帐户进行高级调用。它会在调用之前恢复。\r\n\r\n因此，要调用预编译，我会使用Solidity低级调用（对地址而不是合约实例进行操作的调用）。正如[文档][7]所解释的那样，这种调用不包括`EXTCODESIZE`。\r\n\r\n例如，要在0x04调用预编译：\r\n\r\n```Solidity\r\n// Call precompile at address 0x04\r\n(, bytes memory returndata) = address(4).call(somedata)\r\n```\r\n\r\n标准的EVM预编译非常简单，因此用这种方式调用它们也很简单。你发送一些原始数据字节，它们执行一些计算，并返回一组带有结果的原始字节。\r\n\r\nSolc确实有内置函数来调用一些（但不是全部）预编译，例如`ecRecovery`。只是为了让你不用编写低级调用。但这在这里是无关紧要的。\r\n\r\nL2的预编译可能比EVM中的“标准”编译更复杂。它们可能在单个预编译中包含不同的`_functions_`。例如，可能有一个预编译实现了我们之前看到的接口：\r\n\r\n```Solidity\r\ninterface IPrecompile {\r\n  function foo() external returns (uint256);\r\n  function bar() external;\r\n}\r\n```\r\n\r\n因此，假设预编译可以以某种方式处理它（我们稍后会看到一个示例），你可以使用以下内容调用它的`foo`函数：\r\n\r\n```Solidity\r\n(, bytes memory returndata) = address(customPrecompileAddress).call(abi.encodeWithSelector(IPrecompile.foo.selector));\r\nuint256 result = abi.decode(returndata, (uint256));\r\n```\r\n\r\n但不是像这样的调用\r\n\r\n```Solidity\r\nuint256 result = IPrecompile(precompileAddress).foo();\r\n```\r\n\r\n那会失败的。我告诉你。我读到的文档是这么说的，我们之前看到了`EXTCODESIZE`检查。\r\n\r\n不要坚持了，这是行不通的。\r\n\r\n哈哈，我只是开个玩笑。高级调用也有效。为了理解背后的原因，首先我们需要创建一个自定义预编译，然后做一些测试，最后检查solc是如何在后台工作的。\r\n\r\n## 添加新的预编译\r\n\r\n让我们首先在[go-ethereum][8]的“core/vm/contracts.go”文件中创建一个自定义预编译。\r\n💡\r\n有更聪明的方法可以将一组复杂的自定义预编译添加到EVM。这是一个更实际的例子，研究[ArbOS是如何做到的][9]。\r\n\r\n我将创建的预编译根据`foo`和`bar`的函数选择器检查输入字节。当`foo`的选择器匹配时，它返回数字43。当`bar`的选择器匹配时，它不返回任何内容。\r\n\r\n```Go\r\ntype myPrecompile struct{}\r\nfunc (p *myPrecompile) RequiredGas(_ []byte) uint64 {\r\n\treturn 0\r\n}\r\nfunc (p *myPrecompile) Run(input []byte) ([]byte, error) {\r\n\tif len(input) < 4 {\r\n\t\treturn nil, errors.New(\"short input\")\r\n\t}\r\n\tif input[0] == 0xC2 && input[1] == 0x98 && input[2] == 0x55 && input[3] == 0x78 { // function selector of `foo()`\r\n\t\treturn common.LeftPadBytes([]byte{43}, 32), nil\r\n\t} else if input[0] == 0xFE && input[1] == 0xBB && input[2] == 0x0F && input[3] == 0x7E { // function selector of `bar()\r\n\t\treturn nil, nil\r\n\t} else {\r\n\t\treturn nil, errors.New(\"bad input\")\r\n\t}\r\n}\r\n```\r\n\r\n预编译会在'0x0b'地址：\r\n\r\n```Go\r\nvar PrecompiledContractsCancun = map[common.Address]PrecompiledContract{\r\n  // [...]\r\n  common.BytesToAddress([]byte{0x0b}): &myPrecompile{},\r\n}\r\n```\r\n\r\n然后构建go-ethereum（'make geth'）并在开发模式下运行它（'./build/bin/geth--dev--http'）。\r\n\r\n使用[cast][10]验证预编译是否有效：  \r\n\r\n\r\n```bash\r\ncast call 0x000000000000000000000000000000000000000b \"foo()\"\r\n0x000000000000000000000000000000000000000000000000000000000000002b\r\ncast call 0x000000000000000000000000000000000000000b \"bar()\"\r\n0x\r\ncast call 0x000000000000000000000000000000000000000b\r\nError: \r\n(code: -32000, message: short input, data: None)\r\ncast call 0x000000000000000000000000000000000000000b \"somefunction()\"\r\nError: \r\n(code: -32000, message: bad input, data: None)\r\n```\r\n\r\n快速测试从cast调用新的预编译\r\n\r\n都准备好了！现在让我们转向Solidity。\r\n\r\n## 调用自定义预编译\r\n\r\n是时候调用我在地址“0x0b”新创建的预编译`foo`函数了。\r\n\r\n我将使用一个高级调用。据我所知，这应该不起作用。它应该在触发调用之前恢复，因为编译器包含的`EXTCODESIZE`检查将为“0x0b”地址返回0，因此在字节码中到达`REVERT`。\r\n\r\n```Solidity\r\n// SPDX-License-Identifier: UNLICENSED\r\npragma solidity 0.8.15;\r\ninterface IPrecompile {\r\n    function foo() external returns (uint256);\r\n    function bar() external;\r\n}\r\ncontract PrecompileCaller {\r\n    function callFoo() external {\r\n        // This call to `foo` should revert\r\n        uint256 result = IPrecompile(address(0x0b)).foo();\r\n        \r\n        require(result == 43, \"Unexpected result\");\r\n    }\r\n}\r\n```\r\n\r\n测试对预编译的高级调用的示例合约\r\n\r\n这是一个简单的[Hardhat][11]测试来执行它：\r\n\r\n```Javascript\r\ndescribe(\"PrecompileCaller\", function () {\r\n  let precompileCaller;\r\n  before(async function () {\r\n    const PrecompileCallerFactory = await ethers.getContractFactory(\"PrecompileCaller\");\r\n    precompileCaller = await PrecompileCallerFactory.deploy();\r\n  });\r\n  \r\n  it(\"Calls foo\", async function () {\r\n    await precompileCaller.callFoo();\r\n  });\r\n});\r\n```\r\n```\r\n$ yarn hardhat test --network localhost\r\n  PrecompileCaller\r\n    ✔ Calls foo\r\n  1 passing (224ms)\r\n```\r\n\r\n怎么回事？ 这应该是不能运行的 🤔\r\n\r\n让我们看看。如果调用`foo`有效，那么调用`bar`也应该有效。我将在合约中添加一些代码来调用预编译的`bar`函数。\r\n\r\n```Solidity\r\n// SPDX-License-Identifier: UNLICENSED\r\npragma solidity 0.8.15;\r\ninterface IPrecompile {\r\n    function foo() external returns (uint256);\r\n    function bar() external;\r\n}\r\ncontract PrecompileCaller {\r\n    // Somehow this works\r\n    function callFoo() external {\r\n        uint256 result = IPrecompile(address(0x0b)).foo();\r\n        require(result == 43, \"Unexpected result\");\r\n    }\r\n    // If calling `foo` works, this should also work\r\n    function callBar() external {\r\n        IPrecompile(address(0x0b)).bar();\r\n    }\r\n}\r\n```\r\n\r\n扩展的Hardhat测试现在如下所示：\r\n\r\n```Javascript\r\nconst { expect } = require(\"chai\");\r\ndescribe(\"PrecompileCaller\", function () {\r\n  let precompileCaller;\r\n  before(async function () {\r\n    const PrecompileCallerFactory = await ethers.getContractFactory(\"PrecompileCaller\");\r\n    precompileCaller = await PrecompileCallerFactory.deploy();\r\n  });\r\n  \r\n  it(\"Calls foo\", async function () {\r\n    // This works (doesn't revert)\r\n    await precompileCaller.callFoo();\r\n  });\r\n  it(\"Calls bar\", async function () {\r\n    // This should also work. Does it?\r\n    await precompileCaller.callBar();\r\n  });\r\n});\r\n```\r\n```\r\n$ yarn hardhat test --network localhost\r\n  PrecompileCaller\r\n    ✔ Calls foo\r\n    1) Calls bar\r\n  1 passing (252ms)\r\n  1 failing\r\n  1) PrecompileCaller\r\n       Calls bar:\r\n     ProviderError: execution reverted\r\n```\r\n\r\n糟糕。\r\n\r\n## 我不知道调用是如何工作的\r\n\r\n看到了吗？我告诉过你。在写了那么多年代码后，我不知道调用是如何工作的。这是Solidity代码：\r\n\r\n```Solidity\r\n// SPDX-License-Identifier: UNLICENSED\r\npragma solidity 0.8.15;\r\ninterface IPrecompile {\r\n    function foo() external returns (uint256);\r\n    function bar() external;\r\n}\r\ncontract PrecompileCaller {\r\n    // Somehow this works\r\n    function callFoo() external {\r\n        uint256 result = IPrecompile(address(0x0b)).foo();\r\n        require(result == 43, \"Unexpected result\");\r\n    }\r\n    // Somehow this doesn't work\r\n    function callBar() external {\r\n        IPrecompile(address(0x0b)).bar();\r\n    }\r\n}\r\n```\r\n\r\n我们现在是处于简单模式。在这个例子中，这两个函数有一个明显的区别。真实的案例更难，我不太能理解。\r\n\r\n这里的区别在于返回值（returns）。声明的返回值可能与这一切有关吗？\r\n\r\n## 如果返回则不检查\r\n\r\n我了解到Solidity不总是在高级调用中包含`EXTCODESIZE`检查。\r\n\r\n让我们分析一下“PrecompileCaller”合约的函数`callFoo`和`callBar`生成的Yul代码。\r\n\r\n对于`callFoo`：\r\n\r\n```Solidity\r\nfunction fun_callFoo_32() {\r\n  // ...\r\n  let _3 := call(gas(), expr_21_address,  0,  _1, sub(_2, _1), _1, 32)\r\n```\r\n\r\n对于 `callBar`:\r\n\r\n```Solidity\r\nfunction fun_callBar_45() {\r\n  // ...\r\n  if iszero(extcodesize(expr_41_address)) { revert_error_0cc013b6b3b6beabea4e3a74a6d380f0df81852ca99887912475e1f66b2a2c20() }\r\n  // ...\r\n let _8 := call(gas(), expr_41_address,  0,  _6, sub(_7, _6), _6, 0)\r\n```\r\n\r\n在`callFoo`中，编译器在调用前没有包含`EXTCODESIZE`检查。与它在`callBar`中所做的相反。它为什么要这样做？\r\n\r\n答案隐藏在C++文件的第[2718和2719行][12]中。\r\n\r\n>如果我们期望返回数据，我们不需要检查extcodesize，因为如果没有代码，调用将返回空数据并且ABI解码器将恢复。\r\n这是什么意思？\r\n\r\n还记得我在Solidity中使用的`interface`吗：\r\n\r\n```Solidity\r\ninterface IPrecompile {\r\n    function foo() external returns (uint256);\r\n    function bar() external;\r\n}\r\n```\r\n\r\n根据这个定义，编译器期望`foo`返回一些东西（“uint256”）。 因此，它不会在调用之前进行`EXTCODESIZE`检查！\r\n\r\nSolc假设目标没有代码，实际上无论如何都不会返回数据，因此将无返回数据的 ABI 解码作为返回类型（“uint256”）将会失败。 因此，它可能会在调用之前跳过代码大小检查。\r\n\r\n更让我困惑的是，编译器并不总是这样。 当需要返回数据时，跳过外部调用的代码大小检查[在 0.8.10 中引入的][13]。 这意味着这至少是在2年前。 我想我发现得太晚了？\r\n\r\n即使在写完这篇文章后，我仍然认为文档不完整且过时。但事实并非如此。我亲爱的[matta][14]发现这种特殊行为[在另一节][15]中有记录，但我没有读过🤦\r\n\r\n该文档还有改进的空间。 所以我们提出了[一个小PR][16]，让它们更清晰、更一致。\r\n\r\n我希望我现在可以说我知道Solidity调用是如何工作的了。但也许转角处会有新的惊喜在等着我。\r\n\r\n![](https://blog.theredguild.org/content/images/2023/11/file-Luuf7zu3dIoPwrwlME2PVISM-1-1-1.png)\r\n\r\n## 想要更多故事？订阅博客！\r\n\r\n没关系，免费的。我们也不发垃圾邮件。我懒得发垃圾邮件。\r\n\r\n[1]: https://unsplash.com/@dbeamer_jpg?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit\r\n[2]: https://unsplash.com/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit\r\n[4]: https://docs.soliditylang.org/en/v0.8.24/units-and-global-variables.html#members-of-address-types\r\n[5]: https://github.com/ethereum/go-ethereum/blob/v1.13.14/core/vm/contracts.go#L84-L92\r\n[6]: https://github.com/ethereum/go-ethereum/blob/v1.13.14/core/vm/contracts.go#L188-L217\r\n[7]: https://docs.soliditylang.org/en/v0.8.24/units-and-global-variables.html#members-of-address-types\r\n[8]: https://github.com/ethereum/go-ethereum/\r\n[9]: https://docs.arbitrum.io/arbos/#precompiles\r\n[10]: https://book.getfoundry.sh/cast/\r\n[11]: https://hardhat.org/\r\n[12]: https://github.com/ethereum/solidity/blob/v0.8.15/libsolidity/codegen/ExpressionCompiler.cpp#L2718-L2719\r\n[13]: https://github.com/ethereum/solidity/commit/a1aa9d2d90f2f7e7390408e9005d62c7159d4bd4\r\n[14]: https://twitter.com/mattaereal\r\n[15]: https://docs.soliditylang.org/en/v0.8.24/control-structures.html#external-function-calls\r\n[16]: https://github.com/ethereum/solidity/pull/14931\r\n[18]: https://twitter.com/intent/tweet?text=A%20call%2C%20a%20precompile%20and%20a%20compiler%20walk%20into%20a%20bar&url=https://blog.theredguild.org/a-call-a-precompile-and-a-compiler-walk-into-a-bar/\r\n[19]: https://www.facebook.com/sharer/sharer.php?u=https://blog.theredguild.org/a-call-a-precompile-and-a-compiler-walk-into-a-bar/"},"author":{"user":"https://learnblockchain.cn/people/14524","address":null},"history":"bafkreie4z7x5zwtn3gst2wsabg2ffxzra52puh644wz7ag6yhwvofs24xa","timestamp":1713265679,"version":1}