Solana 60 天课程

2025年02月27日更新 89 人订阅
原价: ¥ 66 限时优惠
专栏简介 开始 Solana - 安装与故障排除 Solana 和 Rust 中的算术与基本类型 Solana Anchor 程序 IDL Solana中的Require、Revert和自定义错误 Solana程序是可升级的,并且没有构造函数 Solidity开发者的Rust基础 Rust不寻常的语法 Rust 函数式过程宏 Rust 结构体与属性式和自定义派生宏 Rust 和 Solana 中的可见性与“继承” Solana时钟及其他“区块”变量 Solana 系统变量详解 Solana 日志、“事件”与交易历史 Tx.origin、msg.sender 和 onlyOwner 在 Solana 中:识别调用者 Solana 计算单元与交易费用介绍 在 Solana 和 Anchor 中初始化账户 Solana 计数器教程:在账户中读写数据 使用 Solana web3 js 和 Anchor 读取账户数据 在Solana中创建“映射”和“嵌套映射” Solana中的存储成本、最大存储容量和账户调整 在 Solana 中读取账户余额的 Anchor 方法:address(account).balance 功能修饰符(view、pure、payable)和回退函数在 Solana 中不存在的原因 在 Solana 上实现 SOL 转账及构建支付分配器 使用不同签名者修改账户 PDA(程序派生地址)与 Solana 中的密钥对账户 理解 Solana 中的账户所有权:从PDA中转移SOL Anchor 中的 Init if needed 与重初始化攻击 Solana 中的多重调用:批量交易与交易大小限制 Solana 中的所有者与权限 在Solana中删除和关闭账户与程序 在 Anchor 中:不同类型的账户 在链上读取另一个锚点程序账户数据 在 Anchor 中的跨程序调用(CPI) SPL Token 的运作方式 使用 Anchor 和 Web3.js 转移 SPL Token Solana 教程 - 如何实现 Token 出售 基础银行教程 Metaplex Token 元数据工作原理 使用Metaplex实施代币元数据 使用 LiteSVM 进行时间旅行测试 Solana Token-2022 标准规范 生息代币第一部分 计息代币第二部分 Solana 指令自省 Solana 中的 Ed25519 签名验证 Solana - Switchboard 预言机使用 原生Solana:程序入口与执行 原生 Solana :读取账户数据 原生 Solana :Borsh 序列化 原生 Solana:使用 invoke 和 invoke signed 进行跨程序调用 原生 Solana :创建存储账户 (一) 原生 Solana:创建存储账户 二 原生 Solana: 函数分发 原生 Solana:关键安全检查 Rust 程序到 SBF 编译 sBPF 虚拟机和指令集介绍 跟踪 sBPF 指令执行和计算成本 Solana 程序执行与输入序列化 指令处理器和运行时设置 sBPF 内存布局和寄存器约定 使用 sBPF 汇编读取 Solana 指令输入 Solana 系统调用:sBPF 汇编中的日志记录

跟踪 sBPF 指令执行和计算成本

文章详细介绍了如何通过Solana的agave-ledger-tool工具跟踪sBPF指令的执行和计算成本。它通过一个简单的Anchor程序示例,演示了如何反汇编程序、生成执行跟踪、分析寄存器变化,并手动计算程序的计算单元(Compute Units)消耗,包括指令执行和系统调用(syscall)的成本,深入揭示了Solana程序在虚拟机层面的运行机制。

追踪 SBF 指令执行和计算成本

在上一篇文章中,我们介绍了 sBPF 虚拟机架构、寄存器约定和指令集。现在,我们将使用 agave-ledger-tool(Solana 工具链中附带的 CLI 工具)来分析实际的字节码执行,生成执行跟踪并手动计算程序消耗的计算单元。

尽管手动跟踪操作码具有挑战性,但我们可以自动生成一个可视化跟踪,显示每个寄存器如何随着每次操作码执行而更新。这使我们能够准确地看到哪些指令运行以及计算单元如何累积。

分析一个简单程序

让我们分析一个简单的 Anchor 程序的字节码,看看每个部分如何转换为 SBF 指令以及它们如何使用寄存器。这还将使我们能够手动计算计算单元成本。

项目设置

首先,使用以下命令初始化一个新项目:

Copyanchor init compute_unit
cd compute_unit

programs/compute_unit/src/lib.rs 中的代码替换为下面的最小程序。我们使用一个空的 initialize 函数,以便我们可以专注于测量基线计算成本,而不包含任何业务逻辑:

Copyuse anchor_lang::prelude::*;

declare_id!("CR33kP6d39mBZv1ryjufVXoRm6djnWW8uKoQXwU5kgDV");

##[program]
pub mod compute_unit {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        Ok(())
    }
}

##[derive(Accounts)]
pub struct Initialize {}

我们将在本地验证器上运行 initialize 函数(我们将在后续看到如何操作),并使用 solana logs 来查看它消耗的计算单元。然后我们将反汇编程序并生成执行跟踪以观察哪些 SBF 操作码运行。通过这样做,我们可以手动计算每条指令如何累加到总计算单元成本。

构建并启动一个本地验证器:

Copyanchor keys sync
anchor build

然后在一个新的终端中:

Copysolana-test-validator

这会启动一个本地验证器并创建一个 test-ledger/ 目录——agave-ledger-tool 使用此目录在生成程序跟踪时加载账本状态。

在另一个终端中运行 solana logs,然后在单独的终端中运行 anchor test --skip-local-validator,以准确查看 initialize 函数使用了多少计算单元。如果我们这样做,我们会得到 272 个计算单元。我们稍后会回到这一点。

显示程序日志消息高亮显示 272 个计算单元的屏幕截图。

反汇编程序

Solana 字节码分析的第一步是将可执行的 Solana 程序二进制文件(通常存储在 target/deploy/<project_name>.so 文件中)转换为我们可以更好地理解的字节码助记符。助记符只是二进制/十六进制操作码的人类可读表示,例如,对于 EVM,0x60 = PUSH1,0x52 = MSTORE 等。

我们需要项目根目录中包含 genesis.bin 文件的 test-ledger 文件夹。我们之抢跑的 solana-test-validator 会自动生成这些文件。agave-ledger-tool(Solana 工具链的一部分)使用此文件在反汇编程序时加载账本状态。

我们现在可以使用以下命令反汇编我们的程序:

Copyagave-ledger-tool program --ledger test-ledger disassemble target/deploy/compute_unit.so --output json > output.txt

这会将汇编助记符转储到 output.txt。你会看到类似这样的内容:

Copyfunction_0:
    mov64 r0, r2
    and64 r0, 1
    jeq r0, 0, lbb_32
    mov64 r0, 0
    jslt r5, 0, lbb_34
    stxdw [r10-0x8], r3
    jeq r5, 0, lbb_41
    ...
    ...

function_0: 这样的标签是字节码中的跳转目标,其他指令可以使用 call 指令跳转到这些目标。Rust 函数会编译成一系列指令,编译器会生成这些标签来标记函数入口或内部代码块。所以当你看到 call function_11561 这样的内容时,执行会跳转到字节码中的该偏移量并运行那里的指令。

这是我们的 Solana 程序的助记符。然而,我们无法用它做太多事情,它非常庞大且手动分析极其困难。

生成执行跟踪

要生成跟踪,我们需要告诉 agave-ledger-tool 调用我们程序中的哪个函数。我们通过创建一个 instructions.json 文件来做到这一点,我们很快就会看到如何将其传递给工具。

现在,在我们的项目根文件夹中创建一个 instructions.json 文件并粘贴以下代码:

Copy{
  "accounts": [],
  "program_id": <program_id>,
  "instruction_data": [175, 175, 109, 31, 13, 152, 155, 237]
}

上述参数表示:accounts 列表(此处为空,因为我们的 initialize 函数不带任何账户),要调用的 program_id(将 <program_id> 替换为你的真实程序 ID),以及指令数据。

instruction_data 字段仅包含 initialize 函数的 8 字节判别符(没有额外的参数,因为我们的函数不带任何参数)。Anchor 通过获取 sha256("<namespace>:<function_name>") 的前 8 字节来生成这些判别符。在我们的例子中,那是 sha256("global:initialize")。命名空间是 global,因为我们的程序包含在代码库的最外层作用域中。

现在让我们生成跟踪:

Copyagave-ledger-tool program run target/deploy/compute_unit.so --limit 200000 --trace trace.txt --ledger test-ledger --input instructions.json

这会使用我们的指令数据(initialize 函数)运行程序,并将执行跟踪输出到 trace.txt.0--limit 标志设置了计算单元限制。它不是必需的,但对于测试很有用。

注意:如果出现错误 Err(JitNotCompiled)(例如,在使用 ARM MacBook 时),请将 --mode interpreter 添加到命令中,以使用解释器模式而不是默认的 JIT 编译模式。

阅读执行跟踪

trace.txt.0 文件看起来像这样。我们为了清晰起见标记了每个部分,并且只显示了前 6 条指令:

显示执行跟踪的图表。

这为我们提供了足够的信息来分析和理解程序及其消耗的计算单元。

让我们看看每列显示了什么:

  • 执行计数列(第一列):执行计数器。
  • r-前缀列(第 2-12 列):显示 Solana VM 的 11 个寄存器($r0 - r10$)中每个寄存器在执行其上方指令后的状态/值。
  • 程序计数器列(第 13 列):给定指令/操作码在程序二进制文件中的程序计数器 (PC) 或索引。
  • 指令列(第 14 列):要执行的下一条指令/操作码及其操作数。

现在让我们浏览跟踪输出的前 6 条指令,以熟悉执行期间寄存器值的变化。让我们将图像分割开,使寄存器的值更清晰可见:

显示执行跟踪的图表。

![显示执行跟踪的图表。](https://img.learnblockchain.cn/2026/02/28/59b362208a49...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论