Solana 60 天课程

2025年02月27日更新 88 人订阅
原价: ¥ 36 限时优惠
专栏简介 开始 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 中的 Ed25519 签名验证 Solana 指令自省 Rust 程序到 SBF 编译

Rust 程序到 SBF 编译

本文详细阐述了Rust程序在Solana平台上的三阶段编译过程,包括从Rust到LLVM IR,再到SBF字节码,并最终通过JIT编译在Solana验证器上高效执行,从而确保了程序的确定性和跨平台一致性。

理解 Rust 如何编译到 SBF (Solana Bytecode Format) 以及验证器如何执行它,对于构建复杂的 Solana 程序至关重要。本文解释了三阶段编译过程,帮助你理解程序大小、调试部署问题并优化性能。

Solana Rust 程序的三阶段编译过程

当你运行 cargo build-sbf 时,你的 Rust 程序会经历三个阶段:

  1. Rust 到 LLVM IR:Rust 编译器将你的代码翻译成 LLVM 中间表示 (LLVM IR)
  2. LLVM IR 到 SBF 字节码 (汇编):LLVM 将中间表示编译成 SBF 字节码(我们部署的 .so 文件)
  3. SBF 到原生代码:Solana 验证器内置了一个 即时 (JIT) 编译器,它在运行时将 SBF 字节码编译成原生机器码,从而实现接近原生的执行速度

下图总结了这一过程。

A diagram showing Rust code being compiled to LLVM IR and SBF bytecode, then translated to native machine code.

Solana Rust 程序的三阶段编译过程

现在考虑这个简单的 Rust 函数,它将两个 u64 整数相加:

pub fn add(a: u64, b: u64) -> u64 {
    a + b
}

该函数在 Solana 验证器上执行之前将经历所有三个编译阶段。我们将它作为一个贯穿始终的例子来 H说明每个阶段。

阶段 1:Rust 到 LLVM IR

Rust 编译器 (rustc) 使用 LLVM 作为其后端。LLVM 是一个编译器基础设施,它提供了一个通用的中间表示 (IR)——一种用于表示代码的平台无关格式——并应用内联和死代码消除等优化。rustc 将 Rust 源代码翻译成 LLVM IR。

使用 LLVM 的语言将其代码编译为 LLVM IR。然后 LLVM 可以将该 IR 翻译为针对不同目标(如 x86、ARM、WebAssembly、BPF 等)的机器码。这种设计允许单个编译器前端支持多种硬件架构,而无需为每种架构维护单独的后端。

要查看 Rust 代码的实际 LLVM IR,请按如下方式设置环境变量 RUSTFLAGS

RUSTFLAGS="-C debuginfo=0 --emit=llvm-ir" cargo build

这会在 target/debug/deps/ 文件夹中生成一个 LLVM IR 文件,其名称类似于 llvm-<hash>.ll

以下是为上面所示的 Rust add 函数生成的 LLVM IR。我们将在代码块之后讨论它。

; llvm::add
; Function Attrs: uwtable
define i64 @_ZN4llvm3add17h48743c4abf0c9b05E(i64 %a, i64 %b) unnamed_addr #0 {
start:
  %0 = call { i64, i1 } @llvm.uadd.with.overflow.i64(i64 %a, i64 %b)
  %_3.0 = extractvalue { i64, i1 } %0, 0
  %_3.1 = extractvalue { i64, i1 } %0, 1
  br i1 %_3.1, label %panic, label %bb1

bb1:                                              ; preds = %start
  ret i64 %_3.0

panic:                                            ; preds = %start
; call core::panicking::panic_const::panic_const_add_overflow
  call void @_ZN4core9panicking11panic_const24panic_const_add_overflow17h0235fd41b8202631E(ptr align 8 @alloc_d358b5fc6deae9ccd21c0c027d9d651f) #3
  unreachable
}

上面的代码块已精简,只显示了 add 函数的 LLVM IR。

上面的代码块已精简,只显示了 add 函数的 LLVM IR。以下是它如何映射到我们原始的 Rust 代码:

  • 函数 @_ZN4llvm3add17h48743c4abf0c9b05E 是我们 add 函数的编译器改编名称
  • i64 %ai64 %b 是两个 64 位整数参数
  • @llvm.uadd.with.overflow.i64 执行加法并检查溢出
  • 如果发生溢出(%_3.1 为真),执行分支到 panic;否则返回结果(%_3.0

LLVM IR 使用类似汇编的语法:define 声明一个函数,i64 指定 64 位整数,%a/%b虚拟寄存器(值的临时存储)。

阶段 2:LLVM IR 到 SBF 字节码

LLVM 针对不同的硬件目标(x86-64、ARM64、eBPF 等)有不同的后端。Solana 使用 eBPF 后端,但维护了一个 LLVM 分支,其中包含用于生成 SBF 字节码的自定义修改。

cargo build-sbf 命令下载 Solana 的平台工具(其中包括这个自定义的 LLVM 分支),然后使用这个自定义的 LLVM 将你的程序编译成带有 .so 文件扩展名的 SBF 字节码:

cargo build-sbf
## Output: target/deploy/program_name.so

.so 文件扩展名来自 Linux 共享库(可以在运行时被多个程序加载和共享的编译代码)。然而,在 Solana 的情况下,此文件包含 SBF 字节码,而不是原生机器码。Solana 运行时将此字节码作为一系列 64 位指令(8 字节)读取,如 eBPF 指令编码 规范中所定义。

当程序在区块链上执行时,它应在网络中的所有验证器上产生相同的结果。如果 Solana 验证器运行原生机器码(x86-64、ARM64),硬件或操作系统上的差异可能导致不确定性结果,从而破坏共识。

SBF 解决了这个问题。它是一个受限的、确定性的 eBPF 版本,运行在一个沙盒虚拟机中。 这些限制包括防止无限循环、在执行前验证指令、阻止未经授权的内存访问以及优雅地处理程序崩溃。每个验证器执行相同的字节码并获得相同的结果,无论它运行在什么 CPU 上。

正如我们所知,SBF 基于 eBPF,eBPF 使用基于寄存器的架构(我们将在本系列后续讨论)并支持即时编译。Solana 通过移除特定于内核的指令、添加 Solana 区块链系统调用 (sol_log_sol_invoke_sol_create_program_address) 和实现计算单位计量来限制执行成本,从而修改了 eBPF。

阶段 3:SBF 到原生代码 (运行时)

Solana 验证器不会逐条解释 SBF 字节码指令。它们使用即时 (JIT) 编译器将字节码翻译成原生机器码。JIT 编译发生在运行时——字节码在执行前立即编译成原生指令(特定于它所运行的硬件)。

我们在阶段 2 中提到的 LLVM 分支将你的 Rust 程序编译成 SBF 字节码。然后该字节码由 Solana 的 sbpf 虚拟机执行。

JIT 编译器 将每条 SBF 指令翻译成原生机器码;例如,一个 SBF add64(64 位整数加法)在 x86-64 或 ARM64 上变成 add,具体取决于验证器的 CPU。

在 JIT 编译器将 SBF 字节码翻译成原生机器码后,验证器会将编译后的原生代码存储在内存中。下次同一程序执行时,验证器的 程序缓存 会返回已经编译过的版本,而不是重新进行 JIT 编译。

由于这种 JIT 编译过程,SBF 字节码以原生速度运行,无论验证器硬件如何。即使在不同的 CPU 架构上,相同的字节码也会在所有验证器上产生相同的结果。

查看 SBF 字节码

在构建 Solana 程序时,你可以使用 --dump 标志输出反汇编的字节码进行检查。

假设我们有这个简单的原生 Rust 程序:

// lib.rs
use solana_program::{
    account_info::AccountInfo,
    entrypoint,
    entrypoint::ProgramResult,
    pubkey::Pubkey,
};

entrypoint!(process_instruction);

pub fn process_instruction(
    _program_id: &Pubkey,
    _accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    Ok(())
}

在我们构建和转储字节码之前,我们需要安装 rustfilt,一个将编译器生成的函数名称转换为可读名称的工具。安装它:

cargo install rustfilt

然后构建并转储字节码:

cargo build-sbf --dump

这会在 target/deploy/minimal_sbpf-dump.txt 中生成一个 .txt 文件。该文件包含程序元数据(文件结构信息、内存布局、函数名称)和反汇编的字节码。我们关心的是我们 Rust 程序中定义的反汇编 entrypoint 函数(至少是它的一部分)。这将帮助我们可视化 SBF 指令格式。

你可以在文件中搜索 <entrypoint> 来找到它,它看起来像这样:

Annotated disassembly screenshot with the entrypoint label highlighted in red, raw instruction bytes highlighted in yellow, and decoded SBF mnemonics highlighted in green.

红色框中的第一行:0000000000000168 <entrypoint> 标记了 entrypoint 函数的开始。

黄色框是指令地址(在内存中)和原始字节码。168 bf 12 00 00 00 00 00 00 表示地址 0x168 处的指令的字节码为 bf 12 00 00 00 00 00 00。每条指令是 8 字节,所以下一条在 0x170,然后是 0x178,依此类推。

绿色框是解码后的指令。mov64 r2, r1 是字节码的可读形式:“将寄存器一中的 64 位值移动到寄存器二”。

每条 sBPF 指令都遵循 eBPF 指令集 中的这种格式:

A diagram showing sBPF instruction format.

从图中:

  • opcode 表示要执行的指令
  • dest register 表示目标寄存器(结果存放的位置)
  • src register 表示源寄存器(输入来源)
  • offset 表示加载/存储操作的内存地址
  • immediate 表示嵌入指令中的常量值

以之前转储截图中的第一条指令(黄色高亮)为例:bf 12 00 00 00 00 00 00(十六进制格式):

A diagram showing sBPF opcodes

你可以在 sbpf ebpf 模块 中找到完整的操作码列表。

本文是关于 Solana 开发 的系列教程的一部分

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

0 条评论

请先 登录 后评论