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 汇编中的日志记录

Solana 系统调用:sBPF 汇编中的日志记录

这篇文章详细介绍了Solana运行时系统调用(syscalls)在sBPF汇编中的应用,特别是聚焦于日志记录相关的五种syscalls。文章通过具体的代码示例、内存布局图和寄存器使用说明,深入解释了如何在sBPF汇编中调用这些功能并管理计算单元。

在之前的教程中,我们学习了程序如何将内存中的数据读取到 sBPF 虚拟机寄存器中。现在,我们将在此模型的基础上,展示程序如何通过 syscalls 调用 Solana runtime 功能,并通过寄存器提供 syscall 参数。

syscall 是 Solana runtime 暴露和执行的 API,程序调用它来执行自身无法执行的操作,例如日志记录和 cross-program invocation

它的工作原理如下:

  • 如果 syscall 需要参数,程序会将值加载到参数寄存器(r1r5)中。
  • 你的程序执行 syscall 指令并将控制权转移给 Solana runtime。syscall 处理器从你加载值的寄存器中读取数据。
  • runtime 执行请求的操作,然后将控制权交还给程序。

A diagram showing the life cycle of a Solana syscall

Solana 为不同的目的提供了 syscalls,例如日志记录、密码学操作、cross-program invocation、sysvar 访问和内存操作。在本教程中,我们将重点关注日志记录 syscalls。

用于日志记录的 syscalls

程序调用日志记录 syscalls 以在执行期间打印值。共有五个日志记录 syscalls,如下所示。我们将在后面的部分详细讨论每个 syscall。

  1. fn sol_log_(message: *const u8, len: u64): 这个 syscall 将 UTF-8 文本打印到程序日志中。
  2. fn sol_log_data(data: *const u8, data_len: u64): 将任意字节数据记录到程序日志中。
  3. fn sol_log_64_(arg1: u64, arg2: u64, arg3: u64, arg4: u64, arg5: u64): 这个 syscall 记录五个 64 位整数值。
  4. fn sol_log_pubkey(pubkey_addr: *const u8): 将 public key 记录到程序日志中。
  5. 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.asm
  • 为交易数据创建一个文件 syscalls/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 汇编中执行 syscalls

sBPF 汇编中的 syscall 遵循以下模板。在调用 syscall 之前,你必须将参数值加载到寄存器中。

... ; instructions that copy arguments into registers
syscall <syscall_name>

我们将在本文的汇编代码中全程使用此模板。

在下一节中,我们将使用 sBPF 汇编调用 sol_log_ syscall。

使用 sollog 记录字符串 1/5

sol_log_ syscall 接受指向内存中消息字符串的指针作为第一个参数,消息长度作为第二个参数,然后将消息作为 UTF-8 文本记录到程序日志中。sol_log_ syscall 的 Rust 定义如下:

fn sol_log_(message: *const u8, len: u64)

让我们通过 3 个步骤演示如何使用 sol_log_ 在 sBPF 汇编中记录消息“Hello world”:

  1. 我们将把“Hello world”的 ASCII 十进制字节表示作为 instruction data 传递给程序。
  2. runtime 会将 instruction data 复制到输入内存区域,我们的汇编代码将使用 r1 作为基指针读取它,因为输入会在程序启动时加载到 r1 中。
  3. 然后,我们将记录“Hello world”消息。

首先,使用“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

让我们详细解释每条指令:

第 1/4 行,第一条指令:mov64 r3, r1

此指令将输入内存区域的起始地址(0x400000000)从 r1 复制到 r3 以保留它。我们将此值保留在 r3 中,因为我们稍后需要它来读取 instruction data length。

第 2/4 行,第二条指令:add64 r1, 16

此指令的目的是将指向 instruction data 的指针存储到 r1 中,这正是 sol_log_ syscall 所期望的。

此指令将 r1 推进到 account count 和 instruction data length 之后,使其指向 instruction data 的第一个字节,在此示例中是“Hello world”字符串的开头。

account count 字段始终存在于内存中,即使指令中没有 accounts:

  • 当 accounts 存在时,account count 后面跟着 account metadata、public keys 和 account data。
  • 当没有 accounts 时,account count 后面紧跟着 instruction data length。

在我们的示例中,没有 accounts,前 16 字节仅包含 account count(8 字节)和 instruction data length(8 字节)。

A diagram showing Solana input serialization layout without account.

因此 r1 指向内存中 instruction data 的第一个字节,并变为 0x400000000 + 1616 是十六进制的 0x10)= 0x400000010。由于我们已经知道目标内存位置,也可以使用指令 lddw r1, 0x400000010 将“Hello world”消息指针加载到 r1 中。我们使用 add64 指令是因为它更符合惯例,可以相对地导航动态内存结构。

第 3/4 行,第三条指令: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 中。

第 4/4 行,第四条指令:syscall sol_log_

这会调用 sol_log_ syscall,分别将 r1r2 作为第一个和第二个参数传递给它。

现在我们了解了程序的工作原理,使用 agave-ledger-tool 运行程序。结果应该会记录“Hello world”消息。

A screenshot showing agave-ledger-tool logs, highlighting a “Hello World” string

**如果你正在使用原生的 Rust 或 Anchor 编写 Solana 程序,你通常会使用 msg! 宏,而不是直接调用 sol_log_。Solana 的 [`ms...

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

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

0 条评论

请先 登录 后评论