这篇文章详细介绍了Solana运行时系统调用(syscalls)在sBPF汇编中的应用,特别是聚焦于日志记录相关的五种syscalls。文章通过具体的代码示例、内存布局图和寄存器使用说明,深入解释了如何在sBPF汇编中调用这些功能并管理计算单元。
在之前的教程中,我们学习了程序如何将内存中的数据读取到 sBPF 虚拟机寄存器中。现在,我们将在此模型的基础上,展示程序如何通过 syscalls 调用 Solana runtime 功能,并通过寄存器提供 syscall 参数。
syscall 是 Solana runtime 暴露和执行的 API,程序调用它来执行自身无法执行的操作,例如日志记录和 cross-program invocation。
它的工作原理如下:
r1 到 r5)中。syscall 指令并将控制权转移给 Solana runtime。syscall 处理器从你加载值的寄存器中读取数据。
Solana 为不同的目的提供了 syscalls,例如日志记录、密码学操作、cross-program invocation、sysvar 访问和内存操作。在本教程中,我们将重点关注日志记录 syscalls。
程序调用日志记录 syscalls 以在执行期间打印值。共有五个日志记录 syscalls,如下所示。我们将在后面的部分详细讨论每个 syscall。
fn sol_log_(message: *const u8, len: u64): 这个 syscall 将 UTF-8 文本打印到程序日志中。fn sol_log_data(data: *const u8, data_len: u64): 将任意字节数据记录到程序日志中。fn sol_log_64_(arg1: u64, arg2: u64, arg3: u64, arg4: u64, arg5: u64): 这个 syscall 记录五个 64 位整数值。fn sol_log_pubkey(pubkey_addr: *const u8): 将 public key 记录到程序日志中。fn sol_log_compute_units_(): 这个 syscall 记录执行时剩余的 compute units 数量。它不接受任何参数。其中四个 syscalls 需要参数。程序在调用 syscall 并将控制权交给 runtime 之前,将这些参数加载到寄存器中。sol_log_compute_units_ 不接受任何参数,因为它只查询 runtime 的内部状态。
就像 EVM 中的 opcodes 一样,每个 syscall 都有一个在 客户端源代码 中记录的 compute cost。虽然 sol_log_64_ 等 syscalls 具有固定的 compute unit 成本,但处理可变长度数据的 sol_log_ 等 syscalls 的成本取决于其输入的大小。开发者可以使用 sol_log_compute_units_ syscall 来测量消耗的 compute unit 数量。
接下来,我们将设置环境,以实验如何将数据从内存加载到寄存器中,并使用 syscalls 进行日志记录。
确保 solana-test-validator 正在运行。完成以下设置步骤:
syscalls 的文件夹syscalls/syscalls.asmsyscalls/instructions.json,并添加以下 JSON。在我们的演示中,我们不需要 accounts。我们的重点将放在 instruction_data 上。我们将在此过程中更新 instruction_data 的内容:{
"accounts": [],
"program_id": "HTpqQdG7f44su3QsV3HHurraR1ZNjHAdArCy3qHKyKBC",
"instruction_data": []
}
我们将使用之前教程中相同的命令,并仅修改 syscalls 目录的文件路径。
agave-ledger-tool program run syscalls/syscalls.asm --limit 200000 --trace syscalls/trace.txt --ledger test-ledger --input syscalls/instructions.json
现在我们的设置已完成,让我们展示如何在 sBPF 汇编中执行 syscalls。
sBPF 汇编中的 syscall 遵循以下模板。在调用 syscall 之前,你必须将参数值加载到寄存器中。
... ; instructions that copy arguments into registers
syscall <syscall_name>
我们将在本文的汇编代码中全程使用此模板。
在下一节中,我们将使用 sBPF 汇编调用 sol_log_ syscall。
sol_log_ syscall 接受指向内存中消息字符串的指针作为第一个参数,消息长度作为第二个参数,然后将消息作为 UTF-8 文本记录到程序日志中。sol_log_ syscall 的 Rust 定义如下:
fn sol_log_(message: *const u8, len: u64)
让我们通过 3 个步骤演示如何使用 sol_log_ 在 sBPF 汇编中记录消息“Hello world”:
r1 作为基指针读取它,因为输入会在程序启动时加载到 r1 中。首先,使用“Hello world”的 ASCII 十进制字节表示更新 instructions.json 中的 instruction_data:
{
"accounts": [],
"program_id": "HTpqQdG7f44su3QsV3HHurraR1ZNjHAdArCy3qHKyKBC",
"instruction_data": [72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]
}
请注意,sol_log_ syscall 要求 r1 包含指向内存中消息字符串的指针,r2 包含其长度(字符串“Hello world”长 11 字节)。
下面是一个程序,它将使用 sol_log_ syscall 打印“Hello world”消息。将其复制到 syscalls/syscalls.asm 文件中。
mov64 r3, r1 ; Copy r1 (0x400000000) to r3 to preserve base pointer
add64 r1, 16 ; r1 now points to first byte of "Hello world" (0x400000010)
ldxdw r2, [r3+8] ; Load instruction data length (11) from memory into r2
syscall sol_log_ ; Invoke syscall: r1=message pointer, r2=length
exit
让我们详细解释每条指令:
mov64 r3, r1此指令将输入内存区域的起始地址(0x400000000)从 r1 复制到 r3 以保留它。我们将此值保留在 r3 中,因为我们稍后需要它来读取 instruction data length。
add64 r1, 16此指令的目的是将指向 instruction data 的指针存储到 r1 中,这正是 sol_log_ syscall 所期望的。
此指令将 r1 推进到 account count 和 instruction data length 之后,使其指向 instruction data 的第一个字节,在此示例中是“Hello world”字符串的开头。
account count 字段始终存在于内存中,即使指令中没有 accounts:
在我们的示例中,没有 accounts,前 16 字节仅包含 account count(8 字节)和 instruction data length(8 字节)。

因此 r1 指向内存中 instruction data 的第一个字节,并变为 0x400000000 + 16(16 是十六进制的 0x10)= 0x400000010。由于我们已经知道目标内存位置,也可以使用指令 lddw r1, 0x400000010 将“Hello world”消息指针加载到 r1 中。我们使用 add64 指令是因为它更符合惯例,可以相对地导航动态内存结构。
ldxdw r2, [r3+8]此指令的目的是将 instruction data length 加载到 r2 中,这正是 sol_log_ syscall 所期望的第二个参数。
请记住,我们在第一条指令中特意将基指针保留在 r3 中,正是为了这个目的。
ldxdw 指令从内存中加载一个 8 字节的值。instruction data length 字段位于基指针之后 8 字节处,因此其地址为 0x400000000 + 8 = 0x400000008。因此,ldxdw r2, [r3+8] 将存储在 0x400000008 处的 8 字节值加载到 r2 中。
在我们的示例中,这将值 11(“Hello world”的长度)加载到 r2 中。
syscall sol_log_这会调用 sol_log_ syscall,分别将 r1 和 r2 作为第一个和第二个参数传递给它。
现在我们了解了程序的工作原理,使用 agave-ledger-tool 运行程序。结果应该会记录“Hello world”消息。

**如果你正在使用原生的 Rust 或 Anchor 编写 Solana 程序,你通常会使用 msg! 宏,而不是直接调用 sol_log_。Solana 的 [`ms...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!