Solana eBPF 虚拟机

本文详细介绍了Solana的rBPF虚拟机,包括其架构、工作原理以及如何通过Agave验证器执行Solana程序。文章还探讨了Berkeley Packet Filter (BPF) 和Extended Berkeley Packet Filter (eBPF) 的背景,以及Solana虚拟机的指令集架构 (ISA)。

或许你已经看到最近关于 Solana 虚拟机 (SVM) 的所有热议,以及在其上构建的所有令人兴奋的项目。也许你甚至阅读了我们关于 SVM API 的 之前的博客文章

无论如何,你可能在寻找关于驱动 Solana 的虚拟机的更多信息。此指南将带你了解 Agave 验证者如何使用 rBPF 虚拟机,解释它是什么,它是如何工作的,以及验证者如何利用它来执行 Solana 程序。

rBPF 虚拟机

rBPF 虚拟机 是一个实现了扩展的伯克利数据包过滤器 (eBPF) 虚拟机的 Rust 实现,由 Quentin Monnet 创建。在 Solana 的早期,rBPF 项目在 Solana Labs 下被分叉,并稍作修改以支持特定于 Solana 的功能,本指南后面会详细介绍这些。如今,rBPF 分支由 Anza 工程师维护。

正如代码库的自述文件中所提到的,rBPF VM 被设计为在用户空间中运行,而不是在内核中运行。这使得 rBPF 成为 Solana 验证者用来执行程序的虚拟机的理想候选者,因为验证者的运行时是在节点的用户空间中运行。

流行术语“SVM”实际上是一个误称。在整个生态系统中,当 Solana 开发人员提到 Solana 虚拟机 (SVM) 时,他们通常是指整个交易处理管道,也就是 Solana 运行时中的执行层。然而,负责执行 Solana 节目的实际 虚拟机 是一个 eBPF VM,受到由 Solana 虚拟机指令集架构 (SVM ISA) 施加的约束。

Solana rBPF 是实现 SVM ISA 的 Rust 虚拟机,由 Agave 验证者使用。例如,Firedancer 具有一个完全重新实现的遵循 SVM ISA 的虚拟机版本。

虚拟机本身也可以访问一组由 Solana 协议定义的系统调用(“sycalls”),这些将会在后面的章节中讨论。这些 syscalls 也是施加在底层 Solana 虚拟机环境上的约束的一部分。

伯克利数据包过滤器

Solana 程序被编译成 伯克利数据包过滤器 (BPF) 格式。BPF 最初是为 伯克利软件分发 (BSD) Unix 系统设计的,用于在操作系统的内核中过滤网络数据包。该格式利用了 限定符,类似于区分符,允许有效地过滤数据包而无需复制数据。

BPF 程序使用这些限定符定义捕获或丢弃数据包的条件。它们定义了一套在寄存器、内存或数据包数据上运行的指令 (操作码)。

BPF 最终演变为 扩展伯克利数据包过滤器 (eBPF) 格式,这正是 Solana 的 LLVM 今日编译的目标。eBPF 允许程序配置一个受到特定于安全内核执行设计的受限指令集和约束。这对于 Solana 程序非常有用,因为它防止验证者崩溃,并为所有程序创建一致的环境。

Solana 程序通常是用 Rust 编写的,随后被 Solana 平台工具 编译为 eBPF。然而,程序也可以用 Zig、C 或汇编语言编写。平台工具确保程序被编译为正确的 eBPF 格式,遵守 Solana VM 所施加的 eBPF 限制,详情将在下一部分讨论。

Solana rBPF ISA

如前面章节所述,eBPF 允许平台——如虚拟机——为 eBPF 程序施加严格的指令集和约束。Solana rBPF 仓库的指令集架构 就正是描述了这一点。所有 Solana 虚拟机都必须遵守这一 ISA,以便符合 Solana 协议。

第一部分涵盖了 rBPF VM 支持的 寄存器。它们是 64 位宽的,意味着它们可以存储 64 位整数或 地址

|  名称 | 特性集 | 种类            | Solana ABI
|------:|:------------|:----------------|:----------
|  `r0` | 全部         | GPR             | 返回值
|  `r1` | 全部         | GPR             | 参数 0
|  `r2` | 全部         | GPR             | 参数 1
|  `r3` | 全部         | GPR             | 参数 2
|  `r4` | 全部         | GPR             | 参数 3
|  `r5` | 全部         | GPR             | 参数 4 <br/>或堆栈溢出指针
|  `r6` | 全部         | GPR             | 调用保留
|  `r7` | 全部         | GPR             | 调用保留
|  `r8` | 全部         | GPR             | 调用保留
|  `r9` | 全部         | GPR             | 调用保留
| `r10` | 全部         | 栈指针         | 系统寄存器
| `r11` | 从 v2 开始 | 栈指针         | 系统寄存器
|  `pc` | 全部         | 程序计数器     | 隐藏寄存器

寄存器是存储当前正在操作的数据的小内存位置。ISA 定义了十个通用寄存器 (GPR)。

  • r0 存储函数的返回数据。
  • r1r5 存储函数参数,r5 实际上可以存储“溢出”数据,用某个堆栈数据的指针表示。
  • r6r9 是调用保留寄存器,意味着它们的值在函数调用之间得到保存。

除了 GPR,框架指针 (r10) 参考内存中当前的堆栈帧,栈指针 (r11) 跟踪堆栈顶部的位置,程序计数器 (pc) 则保持当前正在执行指令的地址。

下一部分涵盖了 指令布局。正如文档中所述,字节码以 64 位槽编码,指令可以占用一个或两个槽,用于指令的第一个槽的操作码来指示。

+-------+--------+---------+---------+--------+-----------+
| 类别  | 操作码 | 目的寄存器 | 源寄存器 | 偏移  | 立即数    |
|  0..3 |  3..8  |  8..12  |  12..16 | 16..32 |   32..64  | 位数
+-------+--------+---------+---------+--------+-----------+
低字节                                          高字节
| 位索引 | 含义
| --------- | -------
| 0..=2     | 指令类别
| 3..=7     | 操作码
| 8..=11    | 目的寄存器
| 12..=15   | 源寄存器
| 16..=31   | 偏移
| 32..=63   | 立即数

指令布局准确地覆盖了指令在 VM 中如何编码及其每个位的含义。

  • 指令类别: 确定指令类型(算术、内存访问等)。
  • 操作码: 具体操作本身。
  • 目的寄存器: 存储操作结果的寄存器。
  • 源寄存器: 操作输入数据源的寄存器。
  • 偏移: 用于内存访问或跳转偏移。
  • 立即数: 常量值。

下一部分涵盖了所有 rBPF VM 支持的 操作码。文档中提供的表格详细列出了 VM 支持的每一个操作码,其中行标签是操作码的前四位,列标签是操作码的后四位。

在操作码之后,ISA 定义了 “按类别的指令”部分,定义了特定操作及其约束的细节。例如,它覆盖了 32 位和 64 位算术、乘法、除法、余数、内存访问和控制流。对于每个部分,都会提供有关期望恐慌的特定信息。这些都是前面提到的 eBPF 约束,并在 SVM ISA 中进行了明确的定义。

请注意,ISA 中存在 32 位和 64 位算术定义并不意味着虚拟机可以在 32 位和 64 位架构上进行操作。这些部分专门定义算术操作,可以使用 32 位进行内存优化,或者在必要时使用 64 位。

ISA 中定义的恐慌情况相对直接。对于除法,它定义了除以零和负溢出作为恐慌情况。对于内存访问,它引用了越界或访问违规(即写入只读部分)。最后,对于控制流,提到了越界跳转、引用未注册函数和堆栈溢出。

最后,验证部分定义了验证 eBPF 程序的规则,这涉及程序二进制文件的静态分析。这使得整体形成 Solana 虚拟机全部 eBPF VM ISA 定义。

Solana VM 内置程序 (加载器)

编译的 eBPF 程序中的函数在二进制文件被 eBPF VM 加载时被读取到一个称为 函数注册表 的结构中。然而,rBPF VM 支持某种称为 “内置程序” 的功能,它们也拥有自己的函数注册表。

你可能对这个术语很熟悉,因为 Solana 运行时使用内置(有时称为“N体”)程序。由于这两个程序共享相同的一些行为,因此术语被设计为相同。Solana 原生程序为运行时提供的功能与 rBPF 虚拟机内置程序为执行 BPF 程序提供的作用相似:访问内置于执行环境中的函数。

当 Solana 运行时遇到内置程序的指令——例如系统转账——时,它不会加载和执行某个编译好的 BPF 程序,而是简单地调用运行时内置的函数来执行转账。这个内置函数就是系统程序,其代码实际上伴随 Solana 运行时一起提供,并且是其环境的一个组成部分。这些程序不在链上存在,而是在其对应的地址上有链上占位符。

同样,在 rBPF 虚拟机环境中,正在执行的程序实际上可以定义调用内置函数的指令。这些函数——就像运行时的内置程序一样——是内置于虚拟机中的。

// <https://github.com/solana-labs/rbpf/blob/9d1a9a0c394e65a322be2826144b64f00fbce1a4/src/vm.rs#L365>
impl<'a, C: ContextObject> EbpfVm<'a, C> {
    /* ... */
    pub fn execute_program(
      &mut self,
      executable: &Executable<C>,
      interpreted: bool,
  ) -> (u64, ProgramResult)
}

// <https://github.com/solana-labs/rbpf/blob/7364447cba1319e8b63d54b521776439181853a7/src/elf.rs#L249>
pub struct Executable<C: ContextObject> {
 /* ... */
 function_registry: FunctionRegistry<usize>,
 loader: Arc<BuiltinProgram<C>>,
}

// <https://github.com/solana-labs/rbpf/blob/7364447cba1319e8b63d54b521776439181853a7/src/program.rs#L214>
pub struct BuiltinProgram<C: ContextObject> {
    /* ... */
    functions: FunctionRegistry<BuiltinFunction<C>>,
}

VM 层的内置程序——它赋予可执行体访问内置函数集的能力——被称为 加载器。VM 内置函数有很多类型,但 VM 加载器提供的主要函数是系统调用(或“syscalls”)。

Solana syscalls 允许执行 eBPF 程序调用内置于虚拟机中的编译字节码之外的函数,进行许多操作,例如:

  • 打印日志消息
  • 调用其他 Solana 程序 (CPI)
  • 执行加密算术操作

类似于虚拟机的 ISA,所有 Solana syscalls 都是 Solana 协议的一部分,并具有 明确定义的接口。对这些接口的任何更改,以及新 syscalls 的引入,均由 Solana 改进文档 (SIMD) 流程管理。

Agave 验证者 实现了所有 Solana syscalls 在 BPF Loader 上,这是提供给 VM 的加载机制。目前存在多个版本的 BPF Loader,包括当前开发中的 Loader v4

Solana BPF Loaders 也是运行时内置程序——类似于系统程序——由运行时调用。事实上,当通过指令调用链上 eBPF 程序时,运行时实际上会调用其所属的 BPF Loader 程序以执行它。关于这一点之后会进一步讨论。

程序执行

rBPF VM 库可以通过解释器或将程序 JIT 编译为 x86_64 机器代码来执行 eBPF 程序。

解释器逐条遍历每条指令,运行时解释每条并执行。这样可能会增加轻微的运行时开销,因为解释器必须在运行时确定每条指令的操作,再执行,但好处是大大减少了加载时间。

相反,即时编译 (JIT) 将程序编译为 x86_64 机器代码,使程序执行变得更快,但代价是加载时间明显延长,产生初始编译的结果。

Agave 目前出于几个原因使用 JIT 编译。首先,系统调用当前是动态注册的,这意味着它们不能通过 静态分析 步骤进行处理,而是标记为“未知”的外部函数调用。系统调用函数在程序二进制文件中的引用在 JIT 编译步骤期间与其注册的内置函数链接。

Agave 之旅

在获得了关于 rBPF 虚拟机如何工作的所有一般性背景之后,是时候通过 Agave 验证者看看 rBPF VM 如何被用来执行当用户发送包含链上程序指令的交易时的实际工作。

程序部署

在进入 Agave 的指令处理管道之前,了解开发者部署 Solana 程序时会发生什么是很重要的。

程序部署是通过调用 BPF Loader 程序实现的,该程序,如前所述,是一个内置程序。它作为内置程序的身份使程序能够访问额外的计算资源,这使得一些关键步骤得以在请求部署程序时验证该程序。

例如,当你运行 CLI 命令 solana program deploy 时,CLI 将发送一组交易,首先分配一个 缓冲账户 并将你程序的 ELF 文件写入其中。ELF 文件通常较大,因此这一过程需要分多次交易,每次将 ELF 分块。当缓冲区包含整个 ELF 时,程序就可以被“部署”(最终的 CLI 指令)。

当调用 BPF Loader 程序的 “deploy” 指令 时,它会尝试 验证存储在提供的缓冲账户中的 ELF,如果成功,就将 ELF 移动到程序账户并将其标记为可执行。只有通过这种成功验证后,程序才能通过 Solana 交易指令被调用。

程序 ELF 的验证在 BPF Loader 的 deploy_program! 中很好地封装。步骤如下:

  1. 将程序加载为具有“严格”运行时环境的 eBPF 可执行文件。在该步骤中,严格环境的目的是防止部署具有过时的 ELF 头或系统调用的程序。使用 rBPF 的 load 方法 来验证 ELF 文件结构并执行指令重定位。
  2. 验证加载的执行程序字节与 ISA 的一致性。使用 rBPF 的 verify 方法
  3. 使用当前的运行时环境重新加载程序。

ELF 验证是程序部署中非常重要的一步,因为它直接涉及虚拟机 ISA 设定的期望。BPF Loader 程序将实际使用 rBPF 库提供的 eBPF 验证工具来验证程序二进制文件,以确保其不违反任何约束。

这意味着只有有效的 Solana eBPF 程序二进制文件才能成为活跃的 Solana 程序。从性能的角度来看,这使得运行时能够通过简简单单检查它是否是可执行程序,快速丢弃无效的 Solana 程序二进制文件,因为程序在部署后只有通过验证才能变为可执行状态。

交易管道

如前所述,运行时只有在一个 BPF 程序被成功部署并验证后,才能接触到这个可执行的 BPF 程序。在假设这一点的基础上,有效的链上 BPF 程序的交易指令生命周期可以追溯到 Agave 中的交易管道。

交易通过调度程序时间表进行处理,最终通过 Bank 实例进行处理。Bank 通过 SVM API 的交易批处理器处理交易,具体是 load_and_execute_sanitized_transactions 方法

在批处理交易的过程中,处理器首先会评估请求支付交易费用的必要账户 (check transaction fee)。接着,会 过滤所有可执行的程序账户,以利于被程序 JIT 缓存加载。

程序 JIT 缓存 实际上是之前已经经过 JIT 编译为 x86_64 机器代码的可执行程序的缓存。实际的主要任务是加载跨分叉的正确版本,考虑创建或关闭结果可能会产生的冲突版本。

随后,所有的必要 账户被加载,准备处理交易。这时,交易被处理,如果它是有效的交易,就被 执行。在执行交易过程中存在许多小的相邻步骤,但在这个练习中,可以主要关注关键信息的执行路径,即针对加载的 BPF 程序的指令执行路径。

首先需要一个 InvokeContext 实例。这是 Agave 特定的上下文对象,包含许多 Solana 协议特定的上下文配置,rBPF VM 需要这些配置。事实上,rBPF VM 本身是 针对某种上下文对象的通用实现

/// 从运行时到程序执行的主管道。
pub struct InvokeContext<'a> {
    /// 有关当前执行交易的信息。
    pub transaction_context: &'a mut TransactionContext,
    /// 交易批处理的本地程序缓存。
    pub program_cache_for_tx_batch: &'a mut ProgramCacheForTxBatch,
    /// 用于配置调用环境的运行时配置。
    pub environment_config: EnvironmentConfig<'a>,
    /// 当前调用的计算预算。
    compute_budget: ComputeBudget,
    /// 指令计算仪表,用于追踪在程序执行期间消耗的计算单位与设计的计算预算的对应关系。
    compute_meter: RefCell<u64>,
    log_collector: Option<Rc<RefCell<LogCollector>>>,
    /// 尚未被[ExecuteDetailsTimings::execute_us]累积的最近测量
    pub execute_time: Option<Measure>,
    pub timings: ExecuteDetailsTimings,
    pub syscall_context: Vec<Option<SyscallContext>>,
    traces: Vec<Vec<[u64; 12]>>,
}

在这个调用上下文下,交易的消息被处理,每条指令被逐一执行。对于每条指令,目标程序使用 eBPF VM 被调用,无论是直接调用还是间接调用。调用风格之间的关系将在下一部分中讨论。

调用 BPF 程序

在运行时调用 BPF 程序的过程相当复杂。然而,本节将分解该过程,以便揭示 Agave 源代码中的各种细微差别。

首先,需要再次关注Solana的内置程序。这些程序,如前所述内置于运行时,因此它们无需 eBPF 虚拟机便可执行。然而,仍然使用了一个

通过使用 eBPF VM 来执行运行时内置程序,主要是为了强制执行内置程序和 BPF 程序之间的一致接口。这种通用接口被称为程序 入口点

// 伪代码 Rust 接口
fn rust(
    vm: &mut ContextObject,
    arg_a: u64,
    arg_b: u64,
    arg_c: u64,
    arg_d: u64,
    arg_e: u64,
    memory_mapping: &mut MemoryMapping,
) -> Result

回到 Agave 的交易管道巡回演讲,我们在 InvokeContext 上下文中停止了。所有指令都通过 process_executable_chain 方法由 InvokeContext 处理。在此方法内部,仅内置程序被直接调用。

首先,运行时将 确定哪个加载器 归属目标程序。如果它是原生加载器,则目标程序为内置。如果是 BPF 加载器之一(所有 BPF 程序都由之所有),则实际上调用该特定 BPF Loader 内置程序来调用目标 BPF 程序。此步骤仅获取适当加载器 ID 以使用。

let builtin_id = {
    let borrowed_root_account = instruction_context
        .try_borrow_program_account(self.transaction_context, 0)
        .map_err(|_| InstructionError::UnsupportedProgramId)?;
    let owner_id = borrowed_root_account.get_owner();
    if native_loader::check_id(owner_id) {
        *borrowed_root_account.get_key()
    } else {
        *owner_id
    }
};

随后,创建一个对 加载器的入口点函数(之前涵盖的接口)的引用,从其函数注册表中获取。这将被用于调用加载器内置。

// Murmur3 哈希值(由 RBPF 使用)字符串“entrypoint”
const ENTRYPOINT_KEY: u32 = 0x71E3CF81;
let entry = self
    .program_cache_for_tx_batch
    .find(&builtin_id)
    .ok_or(InstructionError::UnsupportedProgramId)?;
let function = match &entry.program {
    ProgramCacheEntryType::Builtin(program) => program
        .get_function_registry()
        .lookup_by_key(ENTRYPOINT_KEY)
        .map(|(_name, function)| function),
    _ => None,
}
.ok_or(InstructionError::UnsupportedProgramId)?;

几行下就是 eBPF VM 最终创建的位置。然而,这个 VM 只是一个模拟对象。使用模拟的 VM 强制遵守接口并允许运行时将内置程序调用作为 rBPF 内置函数(或系统调用)。

let mock_config = Config::default();
let empty_memory_mapping =
    MemoryMapping::new(Vec::new(), &mock_config, &SBPFVersion::V1).unwrap();
let mut vm = EbpfVm::new(
    self.program_cache_for_tx_batch
        .environments
        .program_runtime_v2
        .clone(),
    &SBPFVersion::V1,
    // 移除生命周期跟踪
    unsafe { std::mem::transmute::<&mut InvokeContext, &mut InvokeContext>(self) },
    empty_memory_mapping,
    0,
);
vm.invoke_function(function);

在 rBPF 中,为 EbpfVminvoke_function 方法直接调用 Rust 接口,而不必知道它所调用实体的任何信息。在这种情况下,它是一个 Solana 的内置程序。

/// 调用内置函数
pub fn invoke_function(&mut self, function: BuiltinFunction<C>) {
    function(
        unsafe {
            std::ptr::addr_of_mut!(*self)
                .cast::<u64>()
                .offset(get_runtime_environment_key() as isize)
                .cast::<Self>()
        },
        self.registers[1],
        self.registers[2],
        self.registers[3],
        self.registers[4],
        self.registers[5],
    );
}

你可能会在这个时候想:如果只有内置程序通过运行时被调用,使用一个被模拟的 eBPF VM,那么我的指令的实际目标 BPF 程序又是如何被调用的?这个答案并未在运行时代码中立即清晰,因为机制实际上是 BPF Loader 程序处理程序的一部分。

如上所述,当指令针对某个 BPF 程序时,运行时将调用其拥有者,这就是其中的 BPF Loader 程序。BPF Loader 程序的处理器将决定收到的指令类型。这可以是程序账户管理指令(即升级、关闭)或调用 BPF 程序。

如果 BPF Loader 程序的账户出现在 指令上下文中,处理器按推导推断该指令为 BPF Loader 程序。相反,如果目标程序被推测为 BPF 程序,则调用 BPF Loader 的 execute 函数。

pub fn process_instruction_inner(
    invoke_context: &mut InvokeContext,
) -> Result<u64, Box<dyn std::error::Error>> {
    /* ... */
    let program_account =
        instruction_context.try_borrow_last_program_account(transaction_context)?;

    // 程序管理指令
    if native_loader::check_id(program_account.get_owner()) {
        /* ... */
        return {
         /* 更多逻辑 ... */
         process_loader_upgradeable_instruction(invoke_context)
        }
    }

    // 如果程序账户不是 BPF Loader 程序,
    // 执行 BPF 程序。
}

BPF Loader 的 execute 函数包含了设置 真实 eBPF VM 的所有步骤,后者将执行目标 BPF 程序。

执行 BPF 程序

如前所述,在用 eBPF VM 调用 Solana 内置程序时,调用 invoke_function 方法直接调用内置函数。然而,在执行 BPF 程序时,运行时实际上将调用 VM 的 execute_program 方法。这是将 VM 正确设置为最重要的步骤。

对于 Agave 的运行时巡回演讲,我们在上一节中停留在 BPF Loader 的 execute 函数位置。在此函数中,将配置出一个适当的 eBPF VM,并利用其来执行 BPF 程序。设置 VM 涉及四个重要步骤。

  1. 参数序列化
  2. 栈和堆供应
  3. 内存映射配置
  4. 系统调用上下文配置

参数序列化 是将规范程序参数序列化到 VM 的内存区域(程序 ID、账户信息、指令数据)。在此步骤中,所有账户、指令数据、程序 ID 被序列化,它们最终将由 SDK 的 entrypoint! 宏反序列化,该宏为大多数 Solana 开发者所知。

let (parameter_bytes, regions, accounts_metadata) = serialization::serialize_parameters(
    invoke_context.transaction_context,
    instruction_context,
    !direct_mapping,
)?;

接下来,程序的内存的栈和堆被供应。开发人员可以使用计算预算程序要求更多的堆空间。

macro_rules! create_vm {
    /* ... */
    let stack_size = $program.get_config().stack_size();
    let heap_size = invoke_context.get_compute_budget().heap_size;
    let heap_cost_result = invoke_context.consume_checked($crate::calculate_heap_cost(
        heap_size,
        invoke_context.get_compute_budget().heap_cost,
    ));
    /* ... */
}

在参数已序列化到内存区域、栈和堆已供应的情况下,所有这些区域将用以构建 内存映射,将这些新供应区域与程序的 ELF 区域结合起来,以形成一个完整的映射,供 VM 使用。

最后,在调用上下文中设置 系统调用上下文,用于存储账户字段的内存地址,以提供更好的错误堆栈跟踪。

pub struct SyscallContext {
    pub allocator: BpfAllocator,
    pub accounts_metadata: Vec<SerializedAccountMetadata>,
    pub trace_log: Vec<[u64; 12]>,
}
pub struct SerializedAccountMetadata {
    pub original_data_len: usize,
    pub vm_data_addr: u64,
    pub vm_key_addr: u64,
    pub vm_lamports_addr: u64,
    pub vm_owner_addr: u64,
}
impl<'a> ContextObject for InvokeContext<'a> {
    fn trace(&mut self, state: [u64; 12]) {
        self.syscall_context
            .last_mut()
            .unwrap()
            .as_mut()
            .unwrap()
            .trace_log
            .push(state);
    }
fn consume(&mut self, amount: u64) {
        // 1 到 1 指令与计算单位映射
        // 忽略溢出,Ebpf 会在超出时报错
        let mut compute_meter = self.compute_meter.borrow_mut();
        *compute_meter = compute_meter.saturating_sub(amount);
    }

    fn get_remaining(&self) -> u64 {
        *self.compute_meter.borrow()
    }
}

rBPF 的 ContextObject 特性在 InvokeContext 结构上的实现(如前所述)显示了一处系统调用上下文用于提供跟踪。InvokeContext 还负责对整个交易进行计算单位(CUs)计量。ContextObject 方法 consume 在每个程序执行结束时被调用,减少剩余交易的 CU 计量。

虚拟机在每条指令上都有 CU 计量溢出检查,因此一旦达到最大 CU 预算,下一条指令将以 Error::ExceededMaxInstructions 被中止。这防止了任何长期运行的进程或无限循环通过恶意程序进程对 Solana 验证者进行拒绝服务攻击。当 CUs 超出时,也会返回错误,导致交易失败。

完成这四个基本步骤后, eBPF 虚拟机可以得到正确配置 并且 程序可以被执行

Ok(EbpfVm::new(
    program.get_loader().clone(),
    program.get_sbpf_version(),
    invoke_context,
    memory_mapping,
    stack_size,
))
let (compute_units_consumed, result) = vm.execute_program(executable, !use_jit);

在 rBPF 中执行程序仅仅是执行程序二进制中的每个操作码,直到程序执行完成。如前一部分所述,这可以通过 解释或执行 JIT 编译的二进制文件 来完成。完成后,虚拟机将返回一个 u64 代码,其中 零表示程序成功

虚拟机中的恐慌由 rBPF 库中的 EbpfError 处理。在 Agave 运行时,这些错误被 转换为运行时InstructionError 类型。例如,如果一个系统调用抛出 InstructionError,该错误通过 EbpfError::SyscallError(..) 通过虚拟机传递,然后在运行时重铸回正确的 InstructionError。对于大多数其他 EbfError 变体,会抛出著名的 InstructionError::ProgramFailedToComplete 错误。

最后,虚拟机返回的代码通过运行时传播,最终得出交易指令的程序结果。这导致了许多 Solana 开发者常见的交易结果,因此结束了对 Agave 的运行时和虚拟机的全面介绍!

随着我们继续发展和优化运行时,Anza 仍然专注于提高性能、优化计算单位和扩展功能。通过在 Solana 改进文档 过程中发表你的意见,参与讨论吧!

  • 原文链接: medium.com/anza-xyz/the-...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
jcaulfield135
jcaulfield135
江湖只有他的大名,没有他的介绍。