详解 Solana eBPF 虚拟机

  • Anza
  • 发布于 2天前
  • 阅读 319

本文详细介绍了Solana虚拟机(SVM)及其基础的rBPF虚拟机,包括其工作原理、系统调用、程序执行流程等。通过对rBPF虚拟机的分析,文章探讨了如何通过独特的指令集架构(ISA)与安全机制,确保Solana程序的高效和安全执行。本指南为对Solana和eBPF感兴趣的开发者提供了深入的技术背景资料和实践指南。

也许你已经注意到关于 Solana 虚拟机(SVM)的最新热潮以及上面构建的所有令人兴奋的项目。也许你甚至读过我们关于 SVM API 的 之前的博客文章

无论如何,你可能正在寻找有关驱动 Solana 的虚拟机的更多信息。本文将向你介绍 Agave 验证者如何使用 rBPF 虚拟机,解释它是什么、如何工作,以及验证者如何利用它来执行 Solana 程序。

rBPF VM

rBPF 虚拟机 是一个由 Quentin Monnet 创建的 Rust 实现的扩展伯克利数据包过滤器(eBPF)虚拟机。在 Solana 的早期,rBPF 项目在 Solana Labs 下被分叉并稍作修改,以支持自定义的 Solana 特定功能,这些将在后面的部分中讨论。今天,rBPF 分支由 Anza 工程师维护。

正如存储库的 README 文件中所提到的,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 系统设计的,旨在过滤操作系统内核中的网络数据包。该格式利用了类似于区分符的 qualifiers,允许在无需复制数据的情况下高效地过滤数据包。

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

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

Solana 程序通常使用 Rust 编写,然后通过 Solana 平台工具 编译为 eBPF。然而,程序也可以用 Zig、C 或汇编语言编写。平台工具确保程序编译到适当的 eBPF 格式,同时遵守 Solana VM 施加的 eBPF 限制,下一节中将会介绍。

Solana rBPF ISA

正如在前一节中提到的,eBPF 允许平台——例如虚拟机——为 eBPF 程序施加严格的指令集和约束。Solana rBPF 存储库的 指令集架构(ISA)正是对此的定义。所有 Solana 虚拟机都必须遵循此 ISA,以符合 Solana 协议。

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

|  name | feature set | kind            | Solana ABI
|-------:|:-----------|:----------------|:----------
|  `r0` | all         | GPR             | 返回值
|  `r1` | all         | GPR             | 参数 0
|  `r2` | all         | GPR             | 参数 1
|  `r3` | all         | GPR             | 参数 2
|  `r4` | all         | GPR             | 参数 3
|  `r5` | all         | GPR             | 参数 4 <br/>或堆栈溢出指针
|  `r6` | all         | GPR             | 调用保存
|  `r7` | all         | GPR             | 调用保存
|  `r8` | all         | GPR             | 调用保存
|  `r9` | all         | GPR             | 调用保存
| `r10` | all         | 框架指针        | 系统寄存器
| `r11` | from v2     | 堆栈指针        | 系统寄存器
|  `pc` | all         | 程序计数器      | 隐藏寄存器

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

  • r0 保存函数的返回数据。

  • r1r5 存储函数参数,r5 实际上可以存储“溢出”数据,通过指向一些堆栈数据的指针表示。

  • r6r9 是调用保存寄存器,意味着它们的值在函数调用之间保持不变。

除了 GPR,还有一个框架指针 (r10) 引用内存中的当前堆栈帧,还有一个堆栈指针 (r11) 跟踪堆栈顶部的位置,以及程序计数器 (pc),保存当前正在执行的指令的地址。

下一节涵盖了 指令布局。正如文档所提到的,字节码编码在 64 位插槽中,指令可以占用一个或两个插槽,由第一个插槽的操作码指示。

+-------+--------+---------+---------+--------+-----------+
| class | opcode | dst reg | src reg | offset | immediate |
|  0..3 |   3..8 |   8..12 |  12..16 | 16..32 |    32..64 | 位
+-------+--------+---------+---------+--------+-----------+
低字节                                          高字节
| bit index | meaning
| --------- | -------
| 0..=2     | 指令类别
| 3..=7     | 操作码
| 8..=11    | 目标寄存器
| 12..=15   | 源寄存器
| 16..=31   | 偏移
| 32..=63   | 立即数

指令布局准确地说明了指令在虚拟机中的编码方式以及每个比特的含义。

  • 指令类别: 确定指令类型(算术、内存访问等)。

  • 操作码: 特定操作本身。

  • 目标寄存器: 存储操作结果的寄存器。

  • 源寄存器: 操作输入数据的寄存器。

  • 偏移: 用于内存访问或跳转偏移。

  • 立即数: 常数值。

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

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

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

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

最后,验证部分定义了验证 eBPF 程序的规则,涉及对程序二进制的静态分析。综上所述,这构成了 Solana 虚拟机 eBPF VM ISA 定义的整体。

Solana VM 内置程序(加载器)

当编译后的 eBPF 程序中的函数被 eBPF VM 加载二进制文件时,它们将读取到被称为 函数注册表 的地方。然而,rBPF VM 也支持称为 “内置程序” 的东西,它们也有自己的函数注册表。

你可能会对这个术语感到熟悉,因为 Solana 运行时使用内置(有时称为“原生”)程序。术语设计为相同,因为两者共享一些相似的行为。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 级别的内置程序——它为可执行文件提供对内置函数集合的访问——被称为 loader。有许多类型的 VM 内置函数,但 VM 加载器提供的主要功能是系统调用(或“syscalls”)。

Solana syscalls 允许执行 eBPF 程序调用直接内置于虚拟机之外的函数,以实现许多功能,如:

  • 打印日志消息

  • 调用其他 Solana 程序(CPI)

  • 执行密码算术运算

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

Agave 验证者在 BPF Loader 上 实现了所有 Solana syscalls,这是提供给虚拟机的加载机制。BPF Loader 已有几个版本,包括当前正在开发的 Loader v4

Solana BPF 加载器也被视为运行时内置程序——类似于系统程序——由运行时调用。实际上,当链上的 eBPF 程序通过指令被调用时,运行时会实际调用拥有它的 BPF Loader 程序以执行它。稍后将会详细讨论。

程序执行

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

解释器逐条遍历每条指令,解释每一条并在运行时执行。由于解释器必须在运行时确定每条指令的作用,因此这可能会增加一定的运行时开销,但好处是大幅降低了加载时间。

相反,即时编译(JIT)将程序编译为 x86_64 机器代码,从而使程序的执行速度更快,但以初始编译的时间为代价,导致加载时间变得更长。

Agave 目前使用 JIT 编译有几个原因。首先也是最重要的是,syscalls 目前是动态注册的,这意味着在静态分析阶段无法根据验证步骤处理它们,因此被标记为“未知”外部函数调用。在 JIT 编译阶段,程序二进制中的 syscall 函数引用会链接到其注册的内置函数。

走进 Agave

在了解了 rBPF 虚拟机的工作原理的所有一般背景之后,是时候走进 Agave 验证者,看看在用户发送包含按链程序指令的交易时,rBPF VM 如何用于执行 Solana 程序。

程序部署

在开始讨论 Agave 的指令处理管道之前,了解开发者部署 Solana 程序时发生的事情是很重要的。

程序部署是通过调用 BPF Loader 程序来完成的,正如前面提到的,它是一个内置程序。它作为内置程序的身份使其能够访问额外的计算资源,这使得一些关键步骤成为可能,这对于验证请求部署的程序是必要的。

例如,当你运行 CLI 命令 solana program deploy 时,CLI 将发送一组交易,首先为 缓冲帐户分配空间,并将你的程序的 ELF 文件写入其中。ELF 文件通常很大,因此这是通过数个交易完成的,在这些交易中,ELF 被分块处理。在缓冲区包含整个 ELF 后,程序可以被“部署”(最终 CLI 指令)。

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

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

  1. 将程序作为 eBPF 可执行文件加载到“严格”的运行时环境中。在这一步骤中“严格”环境的目的在于防止部署具有过期 ELF 标头或 syscalls 的程序。这使用来自 rBPF 的 load 方法,用于验证 ELF 文件结构并执行指令的重定位。

  2. 验证加载的执行器字节与 ISA 的一致性。这使用来自 rBPF 的 verify 方法

  3. 使用当前运行时环境重新加载程序。

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

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

交易管道

如前所述,运行时只有在成功部署并因此验证后,才能遇到可执行的 BPF 程序。基于这个假设,链上有效 BPF 程序的交易指令的生命周期可以在 Agave 的交易管道中 traced。

交易由调度器调度处理,并最终通过 银行(Bank) 实例处理。银行通过 SVM API 的交易批处理器,具体是 load_and_execute_sanitized_transactions 方法 处理交易。

给定一批交易,处理器将首先评估任何必要的账户以确保能够 支付交易费用。然后,它将 过滤出任何可执行程序账户,这些程序将被加载到程序 JIT 快捷缓存中。

程序 JIT 快捷缓存 只是已经经过 JIT 编译为 x86_64 机器代码的程序的缓存,正如之前所详细说明的,它们已经做好被执行的准备。程序缓存的最大责任实际上是跨分叉加载正确版本的程序,考虑到由于部署或关闭导致的潜在版本冲突。

很快,所有必要的 账户被加载 以处理交易。然后,交易被处理,如果这是一笔有效的交易,将 执行。执行交易涉及很多小的相邻步骤,但在此练习中主要关注的是指令在加载的 BPF 程序上的执行路径。

第一件事情是需要一个 InvokeContext 实例。这是特定于 Agave 的上下文对象,包含许多需要由 rBPF VM 使用的 Solana 协议特定的上下文配置。事实上,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,
) ->

回到对 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 内置函数(或 syscall)调用内置程序。

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::<

在 rBPF 中,invoke_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 proper 的关键所在。

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,
    ));
    /* ... */
}

现在,参数已被序列化到内存区域,堆栈和堆也已配置,这些区域可用于构建 内存映射,以便将主机内存与 VM 内存相结合。这将结合这些新配置的区域与程序的 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 在每个程序执行结束时被调用,减少剩余计算单位计量器的值。

VM在每个指令处都有计算单位计量器溢出的检查,所以一旦达到最大的CU预算,下一条指令将会被终止并返回 Error::ExceededMaxInstructions。这防止任何长时间运行的过程或无限循环通过恶意程序过程对Solana验证者进行拒绝服务攻击。当计算单位超出时,它也会返回错误,导致交易失败。

完成这四个基本步骤后,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编译的二进制文件来完成。完成后,VM将返回一个 u64 代码,其中零表示程序成功

VM中的崩溃通过rBPF库中的 EbpfError 进行处理。在Agave运行时,这些错误被转换为运行时InstructionError 类型,由 InvokeContext 来处理。例如,如果一个系统调用抛出 InstructionError,该错误通过 EbpfError::SyscallError(..) 传递到VM中,然后在运行时重新转换为其正确的 InstructionError。对于大多数其他 EbpfError 变体,将抛出臭名昭著的 InstructionError::ProgramFailedToComplete 错误。

最后,虚拟机返回的返回代码通过运行时传播,最终产生交易指令的程序结果。这产生了许多Solana开发者共同熟悉的交易结果,从而完成了对Agave运行时和虚拟机的介绍!

随着我们不断发展和优化运行时,Anza将继续高度关注性能提升、计算单位优化和功能扩展。通过在Solana改进文档过程中发表意见,参与讨论!

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

0 条评论

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