ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Solidity 编译器输出分析:使用 `solc --asm` 解读 EVM 汇编、部署代码与 auxdata

Solidity 编译器输出分析:使用 `solc --asm` 解读 EVM 汇编、部署代码与 auxdata Solidity 编译器输出分析使用solc --asm解读 EVM 汇编、部署代码与 auxdata【免费下载链接】soliditySolidity, the Smart Contract Programming Language项目地址: https://gitcode.com/GitHub_Trending/so/solidity导读本篇文章基于 Solidity 官方文档 docs/analysing-compilation-output.rst 展开系统讲解如何通过solc --asm查看和分析编译器生成的 EVM 汇编输出。你将掌握如何区分创建代码creation code与部署代码deploy code/runtime code、如何读懂汇编注释中的源码位置信息、#utility.yul内部工具函数文件的含义、auxdata与合约元数据metadata的关系以及如何借助--optimize和diff对比两份源码的优化后汇编。读完本文你可以在不依赖图形化工具的情况下直接通过命令行深入分析任何 Solidity 合约的底层机器码行为。一、为什么要分析编译输出合约编译后得到的二进制字节码即solc --bin contract.sol的输出通常是难以直接阅读的十六进制字符串。而在开发智能合约时查看编译器生成的汇编代码往往是极有价值的调试手段确认某个语法结构最终生成的机器码是否符合预期对比修改前后合约的行为差异对大型合约观察汇编的视觉 diff 往往非常具有启发性验证两个不同写法是否编译出相同的代码例如(a * b) / c与a * b / c理解 EVM 的执行模型栈、跳转、内存与调用数据calldata。官方文档给出的建议是使用--asm标志来输出汇编见 docs/analysing-compilation-output.rst。该标志在 solc/CommandLineParser.cpp 中被定义为输出组件之一Output Components: --asm EVM assembly of the contracts. --asm-json EVM assembly of the contracts in JSON format. --opcodes Opcodes of the contracts. --bin Binary of the contracts in hex. --bin-runtime Binary of the runtime part of the contracts in hex.可以看到除文本汇编外编译器还提供 JSON 格式的汇编--asm-json、操作码列表--opcodes以及二进制--bin与运行时二进制--bin-runtime输出便于不同场景下的分析。注意--asm输出并不是为机器可读而设计的。solc 的次要版本minor version之间输出格式可能发生破坏性变更详见原文档末尾的 note因此不要编写依赖该文本格式的自动化脚本机器可读请使用--asm-json或标准 JSON 接口。二、第一个示例solc --asm contract.sol输出逐段解读考虑下面的合约假设文件名为contract.sol// SPDX-License-Identifier: GPL-3.0 pragma solidity 0.5.0 0.9.0; contract C { function one() public pure returns (uint) { return 1; } }执行solc --asm contract.sol得到如下输出 contract.sol:C EVM assembly: /* contract.sol:0:86 contract C {... */ mstore(0x40, 0x80) callvalue dup1 iszero tag_1 jumpi 0x00 dup1 revert tag_1: pop dataSize(sub_0) dup1 dataOffset(sub_0) 0x00 codecopy 0x00 return stop sub_0: assembly { /* contract.sol:0:86 contract C {... */ mstore(0x40, 0x80) callvalue dup1 iszero tag_1 jumpi 0x00 dup1 revert tag_1: pop jumpi(tag_2, lt(calldatasize, 0x04)) shr(0xe0, calldataload(0x00)) dup1 0x901717d1 eq tag_3 jumpi tag_2: 0x00 dup1 revert /* contract.sol:17:84 function one() public pure returns (uint) {... */ tag_3: tag_4 tag_5 jump // in tag_4: mload(0x40) tag_6 swap2 swap1 tag_7 jump // in tag_6: mload(0x40) dup1 swap2 sub swap1 return tag_5: /* contract.sol:53:57 uint */ 0x00 /* contract.sol:76:77 1 */ 0x01 /* contract.sol:69:77 return 1 */ swap1 pop /* contract.sol:17:84 function one() public pure returns (uint) {... */ swap1 jump // out /* #utility.yul:7:125 */ tag_10: /* #utility.yul:94:118 */ tag_12 /* #utility.yul:112:117 */ dup2 /* #utility.yul:94:118 */ tag_13 jump // in tag_12: /* #utility.yul:89:92 */ dup3 /* #utility.yul:82:119 */ mstore /* #utility.yul:72:125 */ pop pop jump // out /* #utility.yul:131:353 */ tag_7: 0x00 /* #utility.yul:262:264 */ 0x20 /* #utility.yul:251:260 */ dup3 /* #utility.yul:247:265 */ add /* #utility.yul:239:265 */ swap1 pop /* #utility.yul:275:346 */ tag_15 /* #utility.yul:343:344 */ 0x00 /* #utility.yul:332:341 */ dup4 /* #utility.yul:328:345 */ add /* #utility.yul:319:325 */ dup5 /* #utility.yul:275:346 */ tag_10 jump // in tag_15: /* #utility.yul:229:353 */ swap3 swap2 pop pop jump // out /* #utility.yul:359:436 */ tag_13: 0x00 /* #utility.yul:425:430 */ dup2 /* #utility.yul:414:430 */ swap1 pop /* #utility.yul:404:436 */ swap2 swap1 pop jump // out auxdata: 0xa2646970667358221220a5874f19737ddd4c5d77ace1619e5160c67b3d4bedac75fce908fed32d98899864736f6c637827302e382e342d646576656c6f702e323032312e332e33302b636f6d6d69742e65613065363933380058 }2.1 结构总览创建代码与部署代码仔细观察可以发现--asm输出由两个主要部分组成这正是原文档指出的要点创建/构造代码creation / constructor code即输出开头到stop指令之前的部分。它负责初始化空闲内存指针mstore(0x40, 0x80)检查callvalue附带 value 的调用直接 revert然后通过dataSize(sub_0)、dataOffset(sub_0)、codecopy把子对象sub object中的部署代码复制到内存最后return。这就是创建合约时实际执行、并把运行时代码作为返回值的部分。部署代码deploy code / runtime code作为子对象sub_0提供即sub_0: assembly { ... }花括号中的内容。这才是合约部署后、每次外部调用时实际执行的代码。它先校验 calldata 长度是否小于 4 字节lt(calldatasize, 0x04)小于则跳转到tag_2直接 revert再用shr(0xe0, calldataload(0x00))取出函数选择器与0x901717d1即one()的 Keccak-256 哈希前 4 字节比较命中则跳入函数体tag_3。2.2 汇编注释源码位置信息输出中大量的/* contract.sol:0:86 contract C {... */之类的注释表示该条指令对应的源码位置源文件、字节偏移区间以及对应的源码片段。例如/* contract.sol:0:86 contract C {... */对应整个合约声明/* contract.sol:17:84 function one() public pure returns (uint) {... */对应one函数声明/* contract.sol:76:77 1 */对应字面量1/* contract.sol:69:77 return 1 */对应return 1表达式。这些注释是进行代码级 diff 时的重要参照——当然如果你只想比较两条汇编的逻辑而非源码位置可以先用工具剥离这些注释行下文 4.2 节会给出做法。2.3 auxdata 与合约元数据auxdata字段对应合约的元数据哈希metadata hash它被附加在部署代码的末尾。元数据中编码了编译器版本、源码哈希、ABI 等信息详情参见 docs/metadata.rst 的 Encoding of the Metadata Hash in the Bytecode 一节。字段中的 CBOR 编码包含了solc版本信息本例输出中可以看到0.8.4-dev与 commit 哈希这也是链上合约身份证明的一部分。2.4 #utility.yul内部生成的工具函数汇编注释中的/* #utility.yul:7:125 */等标记表明这些指令来自编译器内部生成的 Yul 工具函数文件#utility.yul而非用户源码。这些工具函数如本示例中tag_10、tag_7、tag_13对应的mstore/内存拷贝/栈整理逻辑用于 ABI 编码、内存布局等通用操作。你可以通过如下命令获取这些内部生成的源码原文档给出的精确做法solc --combined-json generated-sources,generated-sources-runtime contract.sol从 libsolidity/interface/CompilerStack.h 的接口注释可以看到其返回格式[ { name: string, id: number, language: Yul, contents: string }, ... ]即一个数组每项包含工具文件名称name、编号id、语言固定为Yul和内容contents。generatedSources对应创建代码部分generated-sources-runtime对应部署代码部分CompilerStack::generatedSources(contractName, _runtime)的第二个布尔参数正是用来区分这两种情况见 libsolidity/interface/CompilerStack.cpp。在标准 JSON 输出中它们分别挂在evm.bytecode.generatedSources与evm.deployedBytecode.generatedSources之下见 libsolidity/interface/StandardCompiler.cpp。2.5 在 Remix 中获取相同输出上述汇编输出也可以在 Remix IDE 中获得编译合约后在Compilation Details编译详情选项下即可查看。这与命令行输出等价适合不熟悉命令行的读者作为辅助验证手段原文档对此有明确说明。三、--asm输出的底层实现路径理解输出背后对应源码中的哪条调用链有助于你判断输出格式的稳定性与可用性。从 solc/CommandLineInterface.cpp 可以看到命令行对--asm/--asm-json的处理逻辑为if (!m_options.compiler.outputs.asm_ !m_options.compiler.outputs.asmJson) return; std::string assembly; if (m_options.compiler.outputs.asmJson) assembly util::jsonPrint(m_assemblyStack-assemblyJSON(_contract), m_options.formatting.json); else assembly m_assemblyStack-assemblyString(_contract, m_fileReader.sourceUnits()); if (!m_options.output.dir.empty()) createFile( m_compiler-filesystemFriendlyName(_contract) (m_options.compiler.outputs.asmJson ? _evm.json : .evm), assembly );要点解读未指定输出目录时汇编直接打印到标准输出指定了--output-dir时文本汇编写入合约名.evm文件JSON 汇编写入合约名_evm.json文件文本汇编由CompilerStack::assemblyString生成JSON 汇编由CompilerStack::assemblyJSON生成两个接口都声明在 libsolidity/interface/CompilerStack.h文本输出的真正写入动作发生在 libevmasm/Assembly.h 的Assembly::assemblyStream(std::ostream _out, ...)中它接受一个DebugInfoSelection参数来控制注释中包含哪些调试信息源码位置、栈布局等这也解释了为什么输出中会出现/* ...:start:end ... */形式的注释。四、实战对比优化后汇编验证语义等价性4.1 开启优化器很多时候我们更关心优化后的代码。开启优化器即可solc --optimize --asm contract.sol--optimize在 solc/CommandLineParser.cpp 中会将优化设置切换为标准优化OptimiserSettings::standard()。对比默认输出与优化输出可以直观看到优化器如何消除冗余指令、简化栈操作。4.2 用 diff 判断两段源码是否生成相同字节码原文档给出了一个非常实用的场景判断表达式(a * b) / c与a * b / c是否生成完全相同的优化后汇编。具体做法分别对两份源码执行solc --optimize --asm由于汇编注释中带有源码位置信息如/* contract.sol:0:86 ... */两份输出的注释必然不同需要先剥离这些注释行再比较对处理后的输出执行diff。如果 diff 为空则说明两个写法在优化后生成了等价的字节码。这种汇编级等价性验证无需部署合约、无需运行链上测试成本极低非常适合在代码评审code review和重构时快速判断这次改写是否改变了行为。# 示例伪代码流程 solc --optimize --asm a.sol | grep -v ^\s*/\* a.asm solc --optimize --asm b.sol | grep -v ^\s*/\* b.asm diff a.asm b.asm提醒grep -v仅作为示例演示剥离注释的思路。若两文件的源码位置范围不同注释行中的start:end区间也会不同务必在 diff 前统一过滤。五、注意事项与适用前提输出格式不保证稳定原文档明确警告--asm文本输出不是机器可读格式solc 次要版本之间可能发生破坏性变更。长期依赖该格式的脚本应在 CI 中固定 solc 版本或改用--asm-json/ 标准 JSON 输出。创建代码 vs 部署代码分析时务必分清两段代码的职责——创建代码只执行一次部署时部署代码才是链上长期运行的部分审计业务逻辑时重点看sub_0之后的运行时汇编。工具函数归属看到#utility.yul注释时不要困惑它表示这段指令来自编译器自动生成的 Yul 工具代码而不是你的源码需要审计其内容时用--combined-json generated-sources,generated-sources-runtime导出。版本前提示例合约使用pragma solidity 0.5.0 0.9.0示例汇编输出来自 0.8.x 开发版本auxdata 中可看到版本标识。不同版本的 solc 对同一源码可能生成不同的汇编分析时应使用与部署环境一致的编译器版本。六、延伸阅读docs/metadata.rst了解 auxdata 中元数据哈希的完整编码规范docs/assembly.rstSolidity 内联汇编inline assembly语法与语义docs/yul.rst中间语言 Yul 的设计与用途solc/CommandLineParser.cpp查看--asm之外更多输出组件--opcodes、--bin-runtime、--abi等的帮助说明。【免费下载链接】soliditySolidity, the Smart Contract Programming Language项目地址: https://gitcode.com/GitHub_Trending/so/solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表