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

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 条指令:

这为我们提供了足够的信息来分析和理解程序及其消耗的计算单元。
让我们看看每列显示了什么:
现在让我们浏览跟踪输出的前 6 条指令,以熟悉执行期间寄存器值的变化。让我们将图像分割开,使寄存器的值更清晰可见:

![显示执行跟踪的图表。](https://img.learnblockchain.cn/2026/02/28/59b362208a49...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!