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 BPF (sBPF) 虚拟机的内存布局和寄存器约定。它阐述了五种内存区域及其用途,并深入探讨了sBPF的12个寄存器各自的角色、使用规则,通过具体汇编代码和执行跟踪展示了寄存器的行为。

sBPF 内存布局和寄存器约定

本教程介绍了 Solana BPF (sBPF) 的内存布局及其虚拟机寄存器的作用。我们将演示程序如何在 sBPF 虚拟机中将数据从内存读入寄存器以及写入寄存器的约定。

Solana BPF 内存布局

sBPF 虚拟机的内存分为 5 个不同的区域。这五个区域各有其特定用途。任何试图访问这些区域之外的内存,或违反区域权限(例如写入只读数据)的行为,都将触发访问违规错误。我们稍后将展示这一点。

在我们描述每个区域之前,我们想强调的是,EVM 直接从合约代码中读取其字节码,而 SVM 在运行之前将字节码加载到内存中。

下面的地址是每个内存区域的起始地址,并在 Solana 源代码 中定义为 u64 常量。诸如 MM_BYTECODE_STARTMM_RODATA_START 等名称是 Rust 常量定义中每个区域的起始地址。这里的 MM 代表“memory map”(内存映射),而 RO 代表“read-only”(只读)。Solana 为每个内存区域保留 4GiB,以防止区域间的地址冲突,但会根据每个区域实际所需的大小进行分配。

  • 0x000000000: MM_RODATA_START — 4 GiB 用于只读 ELF 数据(常量、静态数据)
  • 0x100000000: MM_BYTECODE_START — 4 GiB 用于程序字节码区域
  • 0x200000000: MM_STACK_START — 4 GiB 用于执行栈
  • 0x300000000: MM_HEAP_START — 4 GiB 预留给堆内存区域
  • 0x400000000: MM_INPUT_START — 4 GiB 用于当前事务中序列化的输入数据(程序 ID、账户和指令数据)。这在程序启动时由运行时填充。

Solana BPF 内存布局可以如下图所示:

A diagram showing Solana BPF memory layout.

Solana 客户端定义了如下代码片段所示的常量:

Copypub const MM_RODATA_START: u64 = 0;
pub const MM_BYTECODE_START: u64 = MM_REGION_SIZE;  // = MM_REGION_SIZE * 1
pub const MM_STACK_START: u64 = MM_REGION_SIZE * 2;
pub const MM_HEAP_START: u64 = MM_REGION_SIZE * 3;
pub const MM_INPUT_START: u64 = MM_REGION_SIZE * 4;

MM_REGION_SIZE 定义了一个虚拟内存块的大小,计算方式为 $1 \ll \text{VIRTUAL\_ADDRESS\_BITS}$VIRTUAL_ADDRESS_BITS 在 sBPF 源代码 中定义为 $32$,这意味着在每个区域内,有 $2^{32}$ 个不同的字节地址,这使得每个区域拥有 4GiB 的可寻址空间。

乘数(* 2* 3* 4)使每个区域的起始地址以 MM_REGION_SIZE 的倍数递增。

A code snippet showing sBPF memory region start addresses defined as multiples of MM_REGION_SIZE

要在内存中使用数据,我们必须首先将其加载到寄存器中。虚拟机为每个寄存器分配了特定的角色,开发人员应约定俗成地遵循这些角色。下一节将通过最简单的示例描述这些角色。

Solana BPF 虚拟机如何为寄存器分配角色

sBPF 虚拟机有 $12$ 个寄存器,命名为 $r0$$r11$。寄存器 $r0$-$r10$ 对程序开放,而 $r11$ 存储程序计数器,Solana 程序既不能读取也不能写入。

$r0$ 用于保存返回值,$r1$-$r5$ 是参数寄存器,$r6$-$r9$ 是通用暂存寄存器,也称为被调用者保存寄存器(用于在跨调用中存储临时值),而 $r10$ 是当前调用栈的帧指针寄存器。

A diagram showing sBPF register layout from r0 through r11 grouped by role: return value, argument registers, callee-saved scratch registers, frame pointer, and program counter

在我们检查每个寄存器之前,让我们先设置一个环境来观察寄存器值在执行过程中如何变化。

寄存器实验设置

创建一个名为 register-experiment 的新文件夹。在此文件夹内打开终端并运行 solana-test-validator 命令。这将启动一个本地 Solana 集群并在 register-experiment 内部创建一个 test-ledger 目录

本地验证器运行后:

  • register-experiment 目录中创建一个名为 src 的文件夹。此文件夹将包含我们的汇编程序和一个跟踪文件,显示每条指令执行后寄存器状态如何变化。
  • 创建一个 src/inputs.asm 文件用于我们的汇编代码。

你的目录结构应如下所示:

Copyregister-experiment
├── src
   └── inputs.asm

我们将使用 agave-ledger-tool(随 Solana 安装提供)来运行我们的汇编代码并创建寄存器跟踪。

使用以下命令运行以下示例。它针对我们的本地测试账本运行,以 $200,000$ 计算单位限制执行汇编程序,并生成一个跟踪文件,显示寄存器值在执行期间如何变化。

Copyagave-ledger-tool program run src/inputs.asm --limit 200000 --trace src/trace.txt --ledger test-ledger

注意:在某些架构上,例如 Apple Silicon,运行此命令可能会触发 JitNotCompiled 错误。要解决此问题,请添加 --mode interpreter 标志以强制解释执行而不是 JIT 编译。

现在我们的设置已完成,我们将演示在执行过程中如何使用从 $r0$$r11$ 的每个寄存器。我们的演示将值硬编码到寄存器中以隔离其行为。我们将在下一篇文章中展示如何从内存中读取并将值加载到寄存器中。

寄存器 $r0$

程序通过写入 $r0$ 向运行时传达成功或失败。虚拟机在执行完成时读取此值。可能的结果是:

  1. 成功执行返回 $0$
  2. 可控错误返回非零错误代码(我们写入自定义错误代码)。
  3. 如果发生 panic,程序会在到达 exit 指令之前终止,因此运行时会忽略 $r0$

让我们根据上面列出的三种可能结果,演示程序如何使用 $r0$ 传达成功或失败。

1/3 一个显示成功执行返回 $0$ 的示例

src/inputs.asm 中写入一个简单的 exit 指令:

Copyexit

使用 agave-ledger-tool 命令运行它,你将得到一个成功退出并返回 $0$ 的结果:

A screenshot showing agave-ledger-tool logs

以下跟踪将在 trace.txt 中创建,显示 $r0$0000000000000000(跟踪中的第一列是 $r0$,第二列是 $r1$ ……):

CopyFrame 0
    0 [0000000000000000, 0000000400000000, ...]     0: exit

上述演示表明,如果函数成功返回,$r0$ 将保存 $0$

2/3 一个显示可控失败返回非零值的示例。

在可控失败的情况下,我们可以写入 $r0$。如果出于某种原因,我们想返回一个自定义错误代码,例如 $600$ (0x0000000000000258),程序仍然会干净地退出,但 $r0$ 将保存错误代码而不是零:

Copymov r0, 600  ; 设置自定义错误代码
exit

汇编代码示例中的注释在直接复制时可能会导致解析错误,因为 agave-ledger-tool 在有注释时会抛出错误。如果发生这种情况,请删除注释 ; Set custom error code

使用 agave-ledger-tool 运行上述代码,你将得到以下输出:

A screenshot showing agave-ledger-tool logs

跟踪显示 $r0$$0$ 开始,其当前状态为十六进制的 0000000000000258,即十进制的 $600$:


CopyFrame 0
    0 [0000000000000000, 0000000400000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000200001000]     0: lddw r...

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

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

0 条评论

请先 登录 后评论