Solana 60 天课程

2025年02月27日更新 89 人订阅
原价: ¥ 53 限时优惠
专栏简介 开始 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 指令输入

本文深入探讨了如何使用Solana sBPF汇编语言读取Solana指令输入,包括账户数量、标志、公钥、Lamports、账户数据及程序ID等。文章通过ldxdw指令配合具体的内存偏移量,详细展示了这些数据在Solana程序执行时内存中的布局和读取机制,并提供了详尽的汇编代码示例和执行跟踪分析,帮助读者理解底层交互。

在之前的教程中,我们介绍了 sBPF 的内存布局,并解释了程序执行期间每个寄存器的作用。

在本教程中,我们将演示如何使用 sBPF 汇编读取指令输入字段,例如账户密钥、程序 ID 和指令数据。在此过程中,我们将观察它们在内存中的布局方式。

编写汇编代码从 sBPF 内存读取数据

当 Solana 程序运行时,运行时会将程序的指令输入(账户、指令数据、程序 ID)序列化,并将其加载到起始地址为 0x400000000 的输入内存区域。

我们将编写简单的汇编程序,从这个输入内存区域将数据读取到寄存器中。

设置

创建一个名为 assembly-experiment 的新文件夹。在该文件夹中打开终端并运行 solana-test-validator。这会启动一个本地 Solana 集群并创建一个 test-ledger 目录来存储账本数据。

assembly-experiment 目录中创建以下文件夹和文件:

  • 一个 src 文件夹,用于存放你的汇编程序和跟踪输出。
  • 一个 src/inputs.asm 文件,用于你的汇编代码。
  • 一个 src/instructions.json 文件,用于将要序列化并发送到程序的交易数据。

创建文件夹和文件后,assembly-experiment 文件夹应如下所示:

assembly-experiment/
├── test-ledger/
└── src/
    ├── inputs.asm
    └── instructions.json

指令序列化布局参考图

还记得之前教程中的指令序列化图吗?它显示了内存中每个序列化字段的字节偏移量。我们将使用这些偏移量从汇编代码的输入区域读取特定数据。

instruction serialization diagram

使用 ldxdw 指令将数据从内存加载到寄存器

在我们的汇编程序中,我们将使用 sBPF 指令 ldxdw 从内存中加载数据。此指令执行索引加载,其中最终地址由基址寄存器加上 $offset$ 计算得出。

该指令的每个部分含义如下:

  • ldx 表示使用寄存器加偏移量来计算地址(索引加载)从内存中加载。例如:[r1 + offset]。变量 $offset$ 将被替换为序列化输入中特定字段的偏移量。
  • dw 表示加载的宽度是一个双字 (double-word),即 64 位或 8 字节。

每个实验都必须使用如下所示的汇编程序结构,将值从内存加载到寄存器中。此示例中使用的寄存器是任意的;任何寄存器都可以工作。我们在此处使用 r1 是因为它在入口处包含指令输入,而 r2 没有特殊含义,仅用于演示。

ldxdw r2, [r1 + offset]
exit

这个程序将从内存地址 [r1 + $offset$] 加载 8 字节到寄存器 r2 中,然后退出。寄存器 r1 指向序列化指令输入的起始地址 0x400000000。我们将用实际的字节偏移量替换 $offset$,以读取内存中的任何字段。

为了了解 ldxdw 指令如何从内存加载值,请考虑下面所示的指令输入序列化布局的这一部分:

A diagram showing the account metadata and flags section of the instruction serialization format.

在此布局中,账户数量 (Num Accounts) 从偏移量 0x00 开始,而重复标志 (duplicate flag) 从偏移量 0x08 开始。要加载每个值,请将 $offset$ 替换为字段的字节偏移量,VM 将从相对于存储在 r1 中的输入基址的位置读取。

与 EVM 从 calldata 访问指令输入不同,Solana 在执行开始前将指令输入加载到内存中。实际操作如下:

  • 要读取账户数量,使用偏移量 0x00 并通过 ldxdw r2, [r1 + 0x00] 将其从内存加载到 r2。这会从地址 0x400000000 (0x400000000 + 0) 读取。如果指令包含两个账户,r2 将包含 2
  • 要读取重复标志,使用偏移量 0x08 并通过 ldxdw r2, [r1 + 0x08] 加载。这会从地址 0x400000008 (0x400000000 + 8) 读取,该地址是标志字段的起始位置。

在本文中,我们将使用不同的偏移值来检查运行时存储序列化指令输入的内存区域的各个部分。

现在我们已经为理解如何使用 ldxdw 指令从内存中读取奠定了基础,接下来创建我们的测试数据。

设置测试输入

我们将创建一个测试交易指令,其中 accounts 数组中包含一个由 BPF Loader 拥有的账户。这将允许我们说明 VM 如何读取序列化的指令输入。

以下测试数据包括:

  • 一个 accounts 数组,其格式如下:
    • 公钥
    • 所有者
    • 账户标志:不是签名者、可写、不可执行
    • 1,000 lamports 余额
    • 4 字节账户数据:[0, 0, 0, 3]
  • 一个程序 ID
  • 4 字节指令数据:[2, 0, 0, 0]

这是测试数据,将其粘贴到 src/instructions.json 中。运行时将使用它来将指令加载到内存中。在本教程中,我们将研究 sBPF VM 如何从内存中读取指令。

{
  "accounts": [
    {
      "key": "524HMdYYBy6TAn4dK5vCcjiTmT2sxV6Xoue5EXrz22Ca",
      "owner": "BPFLoaderUpgradeab1e11111111111111111111111",
      "is_signer": false,
      "is_writable": true,
      "lamports": 1000,
      "data": [0, 0, 0, 3]
    }
  ],
  "program_id": "HTpqQdG7f44su3QsV3HHurraR1ZNjHAdArCy3qHKyKBC",
  "instruction_data": [2, 0, 0, 0]
}

下面是关于如何运行 agave-ledger-tool 的快速提醒。

运行我们的汇编代码

我们将使用 agave-ledger-tool 来运行我们的汇编代码,并在每条指令后跟踪寄存器状态。这个工具随你的 Solana 开发安装预装。

agave-ledger-tool 生成显示寄存器状态转换的执行跟踪。由于我们无法直接查看内存内容,我们将把内存中的值复制到寄存器中,并在跟踪输出中检查这些寄存器。

以下是我们将用于执行汇编程序的 agave-ledger-tool 命令。它将以 200,000 计算单位限制执行我们的程序,写入一个显示寄存器状态的跟踪文件,使用我们的本地测试账本,并从我们的 instructions.json 文件获取输入:

agave-ledger-tool program run inputs/inputs.asm --limit 200000 --trace inputs/trace.txt --ledger test-ledger --input inputs/instructions.json

读取测试输入中的账户数量

根据我们前面展示的序列化格式图,前 8 字节包含账户数量,偏移量为 0x00。我们的输入参数在 accounts 列表中只包含一个账户:

{
  "accounts": [
    {
      "key": "524HMdYYBy6TAn4dK5vCcjiTmT2sxV6Xoue5EXrz22Ca",
      "owner": "BPFLoaderUpgradeab1e11111111111111111111111",
      "is_signer": false,
      "is_writable": true,
      "lamports": 1000,
      "data": [0, 0, 0, 3]
    }
  ],
  ... // other input parameters
}

为了演示这一点,请在汇编程序中将 $offset$ 替换为 0x00。这会将地址 r1 + 0x00(即 0x400000000)的 8 字节加载到 r2 中。r2 的选择是任意的,这里可以使用任何其他参数寄存器。

ldxdw r2, [r1 + 0x00]
exit

使用 agave-ledger-tool 命令运行程序,然后打开 inputs/trace.txt 查看执行跟踪:

Frame 0
    0 [0000000000000000, 0000000400000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000200001000]     0: ldxdw r2, [r1+0x0]
    1 [0000000000000000, 0000000400000000, 0000000000000001, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000200001000]     1: exit

跟踪显示了每条指令执行前后的寄存器状态。数组按顺序显示寄存器:[r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10]

A trace diagram showing stack frame count and registers

  • 第 0 行显示初始状态r1 包含 0x400000000(输入基址),r2 为零。
  • 第 1 行显示 ldxdw r2, [r1+0x0] 指令执行后的状态r2 现在包含 1 (0000000000000001),这是我们交易输入中的账户数量。

读取账户标志和填充区域

接下来的 8 字节,从偏移量 0x08 开始,包含 4 个单字节标志,后跟 4 字节填充,如下图所示。由于寄存器可容纳 8 字节,我们将一次性将所有标志和填充加载到一个寄存器中。

A diagram showing the account metadata and flags section

重复标志 (duplicate flag) 是一个位于偏移量 0x08 的单字节,具有以下属性:

  • 如果账户是唯一的,重复标志将是 0xFF 字节。
  • 如果账户是账户数组中早期账户的重复,重复标志应是原始账户的索引。

我们的测试交易数据只有一个账户,因此它不可能是重复的。

我们可以通过使用 ldxdw r2, [r1 + $offset$] 指令将这些标志的内容加载到寄存器中来查看内存中的内容。将 $offset$ 变量替换为我们打算开始读取的偏移量。

让我们来展示重复标志的值是 0xFF(表示一个唯一的账户):

ldxdw r2, [r1 + 0x08]
exit

请记住,ldxdw 加载 8 字节,而不仅仅是 1 字节。因此,即使重复标志只在偏移量 0x08 处是第一个字节,此指令也会将重复标志以及其后的 7 字节加载到 r2 中。这意味着我们还将看到占据偏移量 0x080x0F 的其他账户标志和填充。

运行程序并检查 src/trace.txt

Frame 0
    0 [0000000000000000, 0000000400000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000200001000]     0: ldxdw r2, [r1+0x8]
    1 [0000000000000000, 0000000400000000, **00000000000100FF**, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000200001000]     1: exit

跟踪显示寄存器 r2 现在包含 00000000000100FF(以小端序格式——最右边的字节 FF 位于最低内存地址 0x08),因此我们将反向读取高亮显示的字节,这意味着:

  • FF(字节 0,偏移量 0x08):重复标志,0xFF 表示它不是重复的
  • 00(字节 1,偏移量 0x09):账户不是签名者。在我们的交易中,我们将其设置为 false,这转换为 0
  • 01(字节 2,偏移量 0x0A):账户可写。在我们的交易中,我们将其设置为 1
  • 00(字节 3,偏移量 0x0B):账户不可执行。我们没有设置此项,它默认为 false
  • 00000000(字节 4-7,偏移量 0x0C - 0x0F):账户填充

读取账户公钥

序列化指令输入的接下来 32 字节包含账户的公钥,接着是所有者的公钥(将在下一节讨论)的另外 32 字节。

A diagram showing the account and owner public keys.

一个寄存器容纳 8 字节。由于我们尝试加载超过 8 字节的数据,我们必须使用多个寄存器。我们将把账户公钥分四块加载到寄存器 r2r3r4r5 中。

以下程序将内存中的四个 8 字节块加载到寄存器中。用以下代码替换 src/inputs.asm 的内容:

ldxdw r2, [r1+16]   ; Load bytes 16-23 (first 8 bytes of public key) into r2  ; 将字节 16-23(公钥的前 8 字节)加载到 r2 中
ldxdw r3, [r1+24]   ; Load bytes 24-31 (next 8 bytes) into r3  ; 将字节 24-31(接下来的 8 字节)加载到 r3 中
ldxdw r4, [r1+32]   ; Load bytes 32-39 (next 8 bytes) into r4  ; 将字节 32-39(接下来的 8 字节)加载到 r4 中
ldxdw r5, [r1+40]   ; Load bytes 40-47 (last 8 bytes) into r5  ; 将字节 40-47(最后的 8 字节)加载到 r5 中
exit

使用 agave-ledger-tool 运行程序。跟踪文件现在应该包含以下内容:


Frame 0
    0 [0000000000000000, 0000000400000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000000000000, 0000000200001000]     0: ldxdw r2, [r1+0x10]
    1 [0000000000000000, 0000000400000000, FA44AE351B0AB43B, 0000000000000000...

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

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

0 条评论

请先 登录 后评论