本文深入探讨了如何使用Solana sBPF汇编语言读取Solana指令输入,包括账户数量、标志、公钥、Lamports、账户数据及程序ID等。文章通过ldxdw指令配合具体的内存偏移量,详细展示了这些数据在Solana程序执行时内存中的布局和读取机制,并提供了详尽的汇编代码示例和执行跟踪分析,帮助读者理解底层交互。
在之前的教程中,我们介绍了 sBPF 的内存布局,并解释了程序执行期间每个寄存器的作用。
在本教程中,我们将演示如何使用 sBPF 汇编读取指令输入字段,例如账户密钥、程序 ID 和指令数据。在此过程中,我们将观察它们在内存中的布局方式。
当 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
指令序列化布局参考图
还记得之前教程中的指令序列化图吗?它显示了内存中每个序列化字段的字节偏移量。我们将使用这些偏移量从汇编代码的输入区域读取特定数据。
在我们的汇编程序中,我们将使用 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 指令如何从内存加载值,请考虑下面所示的指令输入序列化布局的这一部分:

在此布局中,账户数量 (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 数组,其格式如下:
[0, 0, 0, 3][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]。

r1 包含 0x400000000(输入基址),r2 为零。ldxdw r2, [r1+0x0] 指令执行后的状态:r2 现在包含 1 (0000000000000001),这是我们交易输入中的账户数量。接下来的 8 字节,从偏移量 0x08 开始,包含 4 个单字节标志,后跟 4 字节填充,如下图所示。由于寄存器可容纳 8 字节,我们将一次性将所有标志和填充加载到一个寄存器中。

重复标志 (duplicate flag) 是一个位于偏移量 0x08 的单字节,具有以下属性:
0xFF 字节。我们的测试交易数据只有一个账户,因此它不可能是重复的。
我们可以通过使用 ldxdw r2, [r1 + $offset$] 指令将这些标志的内容加载到寄存器中来查看内存中的内容。将 $offset$ 变量替换为我们打算开始读取的偏移量。
让我们来展示重复标志的值是 0xFF(表示一个唯一的账户):
ldxdw r2, [r1 + 0x08]
exit
请记住,ldxdw 加载 8 字节,而不仅仅是 1 字节。因此,即使重复标志只在偏移量 0x08 处是第一个字节,此指令也会将重复标志以及其后的 7 字节加载到 r2 中。这意味着我们还将看到占据偏移量 0x08 到 0x0F 的其他账户标志和填充。
运行程序并检查 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,这转换为 001(字节 2,偏移量 0x0A):账户可写。在我们的交易中,我们将其设置为 100(字节 3,偏移量 0x0B):账户不可执行。我们没有设置此项,它默认为 false00000000(字节 4-7,偏移量 0x0C - 0x0F):账户填充序列化指令输入的接下来 32 字节包含账户的公钥,接着是所有者的公钥(将在下一节讨论)的另外 32 字节。

一个寄存器容纳 8 字节。由于我们尝试加载超过 8 字节的数据,我们必须使用多个寄存器。我们将把账户公钥分四块加载到寄存器 r2、r3、r4 和 r5 中。
以下程序将内存中的四个 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... 如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!