{"content":{"title":"Solidity 编译器：内存安全","body":"探索 Solidity 编译管道、优化假设，以及它们如何与内存安全汇编相关。\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2023/08/eFRos47464ccd9efd4e15.png)\r\n\r\n## 介绍\r\n\r\n`memory-safe` 是什么意思？当你处理内联汇编时，Solidity 会提供哪些保证？这个文档[提出了一些要求](https://docs.soliditylang.org/en/v0.8.20/assembly.html#memory-safety)，但是[违反这些要求](https://github.com/Vectorized/solady/blob/main/src/utils/SafeTransferLib.sol#L165-L166)的生产代码一定不安全吗？\r\n\r\n在本文中，我们对 Solidity 编译器进行了高级概述。我们还将深入探讨优化管道、语言规范，并就内存安全的实际含义提出论证。\r\n\r\n## 编译器管道(Compiler Pipeline)\r\n\r\n[为了简洁起见，我们只介绍 v0.8.13 版本的](https://blog.soliditylang.org/2022/03/16/solidity-0.8.13-release-announcement/)YUL IR Solidity 编译管道。编译分为[两个主要步骤](https://github.com/ethereum/solidity/blob/4a8d6618f5b398077f694835905f93d0a3890289/libsolidity/interface/CompilerStack.cpp#L684)：\r\n\r\n1. Solidity 到 YUL IR  \r\n2. YUL IR 到 EVM 操作码\r\n\r\n```assembly\r\n    if (m_viaIR || m_generateIR || m_generateEwasm)\r\n        generateIR(*contract);\r\n    if (m_generateEvmBytecode)\r\n    {\r\n        if (m_viaIR)\r\n            generateEVMFromIR(*contract);\r\n        else\r\n            compileContract(*contract, otherCompilers);\r\n    }\r\n```\r\n\r\n每个步骤都会应用自己的一组优化。入口点位于[YulStack::optimize](https://github.com/ethereum/solidity/blob/fd9ac9abed2049a4b8134d39e178275c8aad75b6/libyul/YulStack.cpp#L92)和[Assembly::optimize](https://github.com/ethereum/solidity/blob/4a8d6618f5b398077f694835905f93d0a3890289/libevmasm/Assembly.cpp#L336)。\r\n\r\n总共有四个步骤：\r\n\r\n1. Solidity 到 YUL IR \r\n2. YUL IR 优化\r\n3. YUL IR 到 EVM 操作码\r\n4. EVM 操作码优化\r\n\r\n正如 v0.8.13 版本公布中提到的，YUL 优化器能够执行更复杂的优化。与 Solidity 相比，YUL 包含详细的语义信息，理论上比操作码更容易进行优化。\r\n\r\n> 新管道的性能并不总是优于旧管道，但它可以跨函数进行更高级别的优化，所以请尝试一下并给我们反馈！\r\n\r\n重要的是，每个步骤都是独立发生的，并且不保留前一阶段的信息。\r\n\r\n优化器无法更改生成的 IR 的行为。这意味着我们不需要担心潜在的棘手优化，例如函数重新排序、删除未使用的赋值或将堆栈变量移至内存。\r\n\r\n当谈到安全性时，我们只需要考虑IR的生成。但这里的保证到底是什么？\r\n\r\n## 保证\r\n\r\nSolidity[内存布局](https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_memory.html)仅在 YUL IR 生成时存在。YUL 优化器和后续步骤没有有关此布局的信息。\r\n\r\n如果优化器想要使用内存进行优化传递怎么办？它如何知道 IR 生成器使用了哪些槽位？\r\n\r\n引入`memoryguard`. 如果你曾经查看过`solc --ir`的输出，这个调用可能会很熟悉。它用于初始化空闲内存指针。\r\n\r\n```\r\n    /// @src 0:26:371  \"contract XXX {...\"\r\n    store(64, memoryguard(0x80))\r\n```\r\n\r\n根据[文档](https://solidity.readthedocs.io/en/latest/yul.html#memoryguard)说明\r\n\r\n> 调用者使用`let ptr := memoryguard(size)`（其中 size 必须是常量数字）承诺只在范围 `[0, size)` 或以 `ptr` 为起点的无界范围内使用内存。\r\n\r\n例如，如果 YUL 优化器需要 32 字节内存，它可以让 `memoryguard` 返回 `size + 32`。优化器获得了一个保证不会被触及的内存区域！\r\n\r\n实际中使用这种优化的一个例子是 [StackLimitEvader](https://github.com/ethereum/solidity/blob/1633e367c90aed7a6a14d84e2c288e6a8ab93304/libyul/optimiser/StackLimitEvader.cpp)，它将变量从栈移动到内存中。顺便说一下，这也是目前*唯一*依赖于 `memoryguard` 传递的语义信息的优化传递。\r\n\r\n不同编译阶段之间的模块化设计也意味着我们不会被绑定到任何特定的内存布局。在一些应用中，将整个内存字节用于空闲内存指针可能并不合理。\r\n\r\n不用担心，我们可以完全移除这个指针，并改为调用`memoryguard(0x60)`。其余的管道仍然可以正常工作。\r\n\r\n## 内存安全\r\n\r\n那么内存安全是什么意思呢？\r\n\r\nSolidity 文档提供了[一组约束](https://docs.soliditylang.org/en/v0.8.20/assembly.html#memory-safety)，而不是定义：\r\n\r\n> 特别说明，内存安全汇编块只能访问以下内存范围：\r\n>\r\n> 1. 通过类似上述分配函数的机制自行分配的内存。\r\n> 2. 由 Solidity 分配的内存，例如你引用的内存数组范围内的内存\r\n> 3. 上述提到的从内存偏移 0 到 64 之间的临时空间。\r\n> 4. 位于汇编块开头的空闲内存指针值之后的临时内存，即在空闲内存指针处“分配”的内存，而不更新空闲内存指针。\r\n\r\n从编译器的角度来看，内存不安全的汇编代码的存在似乎会[清除内存保护](https://github.com/ethereum/solidity/blob/a297a687261a1c634551b1dac0e36d4573c19afe/libsolidity/codegen/ir/IRGenerator.cpp#L210) 。\r\n\r\n> 注：有趣的是，`memoryguard` 是一个不透明的函数，它阻止优化推理空闲内存指针。这导致一些相当反直觉的行为 -- `memory-unsafe` 代码可以减少 gas 的消耗， 尤其是在 YUL 头中。[参考](https://osec.io/blog/2023-07-28-solidity-compilers-memory-safety#user-content-fn-1)\r\n\r\n```cpp\r\n// bool creationInvolvesMemoryUnsafeAssembly = m_context.memoryUnsafeInlineAssemblySeen();\r\n// t(\"memoryInitCreation\", memoryInit(!creationInvolvesMemoryUnsafeAssembly));\r\n\r\nstring IRGenerator::memoryInit(bool _useMemoryGuard)\r\n{\r\n // This function should be called at the beginning of the EVM call frame\r\n // and thus can assume all memory to be zero, including the contents of\r\n // the \"zero memory area\" (the position CompilerUtils::zeroPointer points to).\r\n return\r\n  Whiskers{\r\n   _useMemoryGuard ?\r\n   \"mstore(<memPtr>, memoryguard(<freeMemoryStart>))\" :\r\n   \"mstore(<memPtr>, <freeMemoryStart>)\"\r\n  }\r\n```\r\n\r\n`solc --ir`命令将不再像预期那样包含`memoryguard(0x80)`。\r\n\r\n```cpp\r\n    /// @src 0:26:371  \"contract XXX {...\"\r\n    mstore(64, 128)\r\n```\r\n\r\n从语义上讲，缺少`memoryguard`意味着 IR 生成器告诉优化器它无法保证`memoryguard`不变性。\r\n\r\n> 调用`let ptr := memoryguard(size)`（其中 size 必须是一个字符数字）的调用者承诺只使用范围为`[0, size)`或从`ptr`开始的无界范围的内存。\r\n\r\n这是有道理的。在程序员没有更严格的保证，内存不安全的汇编可以随意访问内存的任何位置。因为优化器不再具备这个保证，它不能在任何优化过程中使用内存。\r\n\r\n## 未定义行为\r\n\r\n内存安全有多严格？就`memoryguard`而言，只有在 0x80 之后访问内存似乎很重要。在`[0x40, 0x7f]`范围内访问内存的`memory-safe`带注释汇编真的安全吗？\r\n\r\n[Solidity文档](https://buildmedia.readthedocs.org/media/pdf/solidity/develop/solidity.pdf)中三次提到了未定义行为。\r\n\r\n1. 悬空引用的存在\r\n\r\n2. 使用 verbatim  不当\r\n\r\n   > 很不幸，文档只提供了对 verbatim 字节码的“非穷尽列表限制”。实际上，使用不透明字节似乎很难保证行为。[参考](https://osec.io/blog/2023-07-28-solidity-compilers-memory-safety#user-content-fn-2)\r\n\r\n3. 使用标记为\"memory-safe\" 内联汇编违反内存模型。\r\n\r\n这为什么重要？\r\n\r\n假设程序代码可以实现强大的优化 - 这就是为什么[有符号整数溢出是未定义的原因](https://kristerw.blogspot.com/2016/02/how-undefined-signed-overflow-enables.html)。严格遵循编译器模型是至关重要的。未定义行为会在[多年后](https://blog.regehr.org/archives/1307)显现为棘手的错误。\r\n\r\n回到 Solidity，规范明确指出了[这一点](https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html)。**不得修改零槽**。\r\n\r\n> 零槽用作动态内存数组的初始值，不应对其进行写入（空闲内存指针最初指向 0x80）。\r\n\r\n任何触及 0x60 处零槽的代码都明显违反了规范。但这有关系吗？这就是 Solidity 和 YUL 之间的语义变得棘手的地方。回想一下，零槽是 *Solidity* 中的用来构造。\r\n\r\n即使在生成过程中没有明确保证内联汇编将被逐字地发出\r\n\r\n1. 它在今天显然是[正确的](https://github.com/ethereum/solidity/blob/a297a687261a1c634551b1dac0e36d4573c19afe/libsolidity/codegen/ir/IRGeneratorForStatements.cpp#L2216)。\r\n\r\n```cpp\r\nbool IRGeneratorForStatements::visit(InlineAssembly const& _inlineAsm)\r\n{\r\n    setLocation(_inlineAsm);\r\n    if (*_inlineAsm.annotation().hasMemoryEffects && !_inlineAsm.annotation().markedMemorySafe)\r\n        m_context.setMemoryUnsafeInlineAssemblySeen();\r\n    CopyTranslate bodyCopier{_inlineAsm.dialect(), m_context, _inlineAsm.annotation().externalReferences};\r\n\r\n    yul::Statement modified = bodyCopier(_inlineAsm.operations());`\r\n```\r\n\r\n2. 要在优化之前有意义地修改汇编语句，需要一个相当精心设计的编译器实现。\r\n\r\n只要在汇编块执行之前和之后保持不变性，代码就可能是安全的。\r\n\r\n## 结束语\r\n\r\n在这篇博文中，我们介绍了一个关于 Solidity 编译器的探索。这旨在为好奇者提供有用的参考。编译器非常复杂，有隐含和显式的假设。当有疑问时，请阅读源代码。那么什么是内存安全性呢？\r\n\r\n它是 YUL 生成和优化之间的承诺。\r\n\r\n\r\n\r\n>原文链接：https://osec.io/blog/2023-07-28-solidity-compilers-memory-safety"},"author":{"user":"https://learnblockchain.cn/people/3291","address":null},"history":null,"timestamp":1694529330,"version":1}