前言AI的冲击下,公司都转AI,Web3岗位大幅减少.找不到合适的EVM相关的工作,写的OpTrace也没人用.心灰意冷,决定学习Solana.还是从底层看起,这次看的sBPF.GitHub,我看的是0.19.0版本.基本概念sBPF基于eBPF进行
AI 的冲击下, 公司都转 AI, Web3 岗位大幅减少.
找不到合适的 EVM 相关的工作, 写的 OpTrace 也没人用.
心灰意冷, 决定学习 Solana.还是从底层看起,这次看的 sBPF.
GitHub, 我看的是 0.19.0 版本.
sBPF 基于 eBPF 进行扩展, 是专门为 Solana 定制的一套指令集.
最初是用在 Linux 内核沙箱 和 网络包过滤, Solana 官方对其调整, 用于智能合约.
和 EVM 不同, EVM 是基于堆栈, 指令操作的数据和结果, 都来源于和保存到堆栈.
而 sBPF 是基于寄存器, 拥有 11 个寄存器可以用于指令集.
从这就能看出性能上的一点差距.
一个只能操作一个栈, 所有的结果中间值也必须保存到栈上.你要进行下一步必须往下压或者弹出.
一个能操作一堆寄存器, 结果中间值可以保存到多个,不要反复 PUSH 和 POP.
sBPF 采用的精简指令集(RISC), 这里的精简不是少,而是精细.类似 EVM 中接触到的,一条指令只负责一个小的工作,例如 加减乘除,读取内存,写入内存.
采用 寄存器 + RISC 的好处是, 后期字节码通过 JIT 编译成底层原生机器码足够的方便.性能再进一步提高.
和 EVM 指令的定义不同, EVM 中的指令只定义了名称.
sBPF 不止名称. 还带了寄存器的位置, 数据在内存中的位置, 立即数.
┌────────┬──────┬──────┬──────────┬──────────┐
│ Opcode │ Dst │ Src │ Offset │ Imm │
├────────┼──────┼──────┼──────────┼──────────┤
│ 1 byte │ 4 b │ 4 b │ 2 bytes │ 4 bytes │
└────────┴──────┴──────┴──────────┴──────────┘
OpCode 操作码,这里是1个字节,但又细分为3, 1, 4位.
低3位 指令类型,例如内存访问、算术中1位 源操作数标志, 指示是寄存器还是立即数高4位 具体指令,例如算术的 ADD, DIV,内存的 LOADDst/Src 这里是位(bit),一定要看清楚.指定目标寄存器和源寄存器的编号(r0 - r10).Offset 有符号整数,在内存指令表示计算地址偏移,跳转中则表示相对偏移量Imm 立即数, 存放常量、系统调用号或直接作为内存操作的绝对地址.这里有个特殊的指令 LDDW,, 它实际占用的是16个字节,属于双槽指令.
LDDW 的作用是将一个 64 位(8个字节)立即数加载到寄存器中.
低位4个字节存在LDDW指令中的Imm,高位4个字节存在第二槽位的Imm,第二槽位前面的部分为空.
r0 通常用于存放函数的返回值。r1 - r5 用于传递函数参数(临时寄存器)。r6 - r9 被调用者保存寄存器,用于跨函数调用保存数据(中间结果).r10 只读的栈帧指针,永远指向当前栈空间的顶部
sBPF 程序运行在虚拟的64位地址空间内.
相当于系统对sBPF虚拟机 虚拟了一遍.sBPF虚拟机对sBPF字节码(合约)又虚拟了一遍.
内存被分成五大区域.每个区域有 4GB 的预留空间.
只读数据区(0x000000000): 存放硬编码常量.代码区(0x100000000) : 存放程序的指令.栈区(0x200000000) : 用于函数调用和局部变量堆区(0x300000000) : 用于程序的动态内存分配输入数据区(0x400000000) : 存放 Solana 传入的交易输入(如账户信息、指令数据).操作码的个数太多,这里就不贴出来.
具体定义在 sBPF操作码.
操作码名称后面的 REG 和 IMM 对应的是寄存器还是立即数.REG 的中间是 BPF_X,IMM 是 BPF_K.
WORD 对应的是 4个字节, HALF-WORD 是2个字节.
里面的函数都比较简单,就不解析了.关键是理解了格式的定义.
大部分的文件里面内容比较少.后续可能按文件也可能按模块进行解释.
Program.rs
里面大部分都是类型定义.
分为 SBPFVersion, FunctionRegistry, BuiltinFunction, BuiltinProgram, BuiltinFunctionDefinition, BuiltinCodegen.
SBPFVersion 是 Enum 类型, 指示当前版本.
内容非常简单.就是根据版本判断支持的功能.
FunctionRegistry函数符号注册表.用于保存函数符号.
底层使用的 BtreeMap . Key 通常是函数名的 hash.
Value 是程序中函数的 PC地址 或者 内置函数的 Rust侧 + JIT侧 实现.
它的函数都是增删查改,就不展开了.
pub struct FunctionRegistry<T> {
pub(crate) map: BTreeMap<u32, (Vec<u8>, T)>,
}
内置函数处理器类型: VM 调用内置函数时的函数指针签名.
EncryptedHostAddressToEbpfVm<C> 后续讲 VM 的时候再讲.先当指针记住.
后面5个参数对应sBPF r1 - r5寄存器.
pub type BuiltinFunction<C> = fn(EncryptedHostAddressToEbpfVm<C>, u64, u64, u64, u64, u64);
JIT 代码生成函数类型,JIT相关的后面再讲
#[cfg(not(all(feature = "jit", not(target_os = "windows"), target_arch = "x86_64")))]
pub struct JitCompiler<'a, C> {
_phantom: std::marker::PhantomData<&'a C>,
}
#[cfg(not(all(feature = "jit", not(target_os = "windows"), target_arch = "x86_64")))]
impl<'a, C: ContextObject> JitCompiler<'a, C> {
#[allow(dead_code)]
pub fn emit_external_call(&mut self, _function: BuiltinFunction<C>) {}
}
pub type BuiltinCodegen<C> = fn(&mut JitCompiler<C>);
前面的几个类型,在这里结合起来了.
内置程序,对固定功能函数集合的封装.
这个有个要注意的.
sparse_registry 固定是内置函数集合.
如果一个 BuiltinProgram 携带了 Config ,也就是 Config 不为空,那它是 loader程序.
loader程序负责合约代码安装、更新.
如果没有 Config ,则就是纯内置函数集合.
主要是Solana 提供给合约的系统API,让合约能读写账户、记录日志、获取时间等.
ContextObject 宿主(Solana)环境上下文接口.
pub struct BuiltinProgram<C: ContextObject> {
/// 若为 loader 程序,包含 VM 配置;纯 syscall 集合则为 None
config: Option<Box<Config>>,
/// 稀疏函数注册表:key=hash,value=(名称, (Rust实现, JIT codegen))
sparse_registry: FunctionRegistry<(BuiltinFunction<C>, BuiltinCodegen<C>)>,
}
// src/vm.rs
pub trait ContextObject {
/// 消耗指定数量的指令预算
fn consume(&mut self, amount: u64);
/// 获取剩余可用指令数
fn get_remaining(&self) -> u64;
/// 返回当前活跃的 MemoryMapping 的可变指针
fn active_mapping_ptr(&mut self) -> ptr::NonNull<MemoryMapping>;
}
内置函数定义 trait:声明一个可在 VM 和 JIT 中调用的内置函数。
这里面只需要 rust 方法. vm 在后面讲了 vm 相关的再说.
pub trait BuiltinFunctionDefinition<C>
where
C: crate::vm::ContextObject,
{
/// 该内置函数返回的错误类型
type Error: Into<Box<dyn core::error::Error>>;
// 这是唯一必须实现的方法。
fn rust(
vm: &mut C,
arg_a: u64,
arg_b: u64,
arg_c: u64,
arg_d: u64,
arg_e: u64,
) -> Result<u64, Self::Error>;
#[expect(clippy::arithmetic_side_effects)]
fn vm(mut vm: EncryptedHostAddressToEbpfVm<C>, a: u64, b: u64, c: u64, d: u64, e: u64) {
unsafe {
vm.with_vm(|vm| {
let enable_insn_meter = vm.loader.get_config().enable_instruction_meter;
if enable_insn_meter {
let used_cus = vm.previous_instruction_meter - vm.due_insn_count;
vm.context().consume(used_cus);
}
let converted_result: crate::error::ProgramResult =
Self::rust(vm.context(), a, b, c, d, e)
.map_err(|err| crate::error::EbpfError::SyscallError(err.into()))
.into();
vm.program_result = converted_result;
if enable_insn_meter {
vm.previous_instruction_meter = vm.context().get_remaining();
}
})
}
}
fn codegen(jit: &mut JitCompiler<C>) {
jit.emit_external_call(Self::vm);
}
fn register(program: &mut BuiltinProgram<C>, name: &str) -> Result<(), ElfError>
where
Self: Sized,
{
program.register_definition::<Self>(name)
}
}
生成一个内置函数的宏.
大致是生成空一个空钩体,然后实现 BuiltinFunctionDefinition, 这里不展开了.
ELF(Executable and Linkable Format,可执行与可链接格式)是类 Unix 系统中二进制文件的标准容器.
在 sBPF 中,它是编译器(如 LLVM)输出、虚拟机(如 sbpf 库)输入的中间桥梁.
一个 ELF 格式的文件大概长下面的样子.
┌─────────────────────────────────┐ ← 文件偏移 0
│ ELF 文件头 (64 字节) │
├─────────────────────────────────┤
│ 程序头表 (Program Headers) │ ← 用于执行视图(Segment)
├─────────────────────────────────┤
│ │
│ 各种 节区数据 (Section Data) │
│ - .text 的机器码 │ ← 这里才是真正的代码!
│ - .rodata 的字符串 │
│ - .symtab 的符号表 │
│ - .strtab 的字符串表 │
│ - .rel.text 的重定位条目 │
│ ... │
│ │
├─────────────────────────────────┤
│ 节区头表 (Section Headers) │ ← 位于文件末尾(通常)
│ 每个条目描述一个节区的元数据 │
└─────────────────────────────────┘
具体定义在:ELF-TYPES.太长了,我就不贴出来了.
对 ELF文件 格式感兴趣的也可以点进去看看.
ElfIdent 文件头的标识符结构体,包含魔数(判断是否ELF文件),字节序,规范版本等.Elf64Ehdr 文件头结构体Elf64Phdr 程序头结构体,节区头是一串结构体的数组.Elf64Shdr 节区头结构体,节区头是一串结构体的数组.Elf64Sym 符号表条目结构体Elf64Dyn 动态链接表条目Elf64Rel 隐式重定位条目把 Types 大概扫一遍, 再看 ELF_Parse 的代码就会清晰很多.
都是在处理数据的读取,麻烦的是一些边界问题.
pub struct Elf64<'a> {
/// 原始 ELF 文件字节切片
elf_bytes: &'a [u8],
/// 文件头指针
file_header: &'a Elf64Ehdr,
/// 程序头表
program_header_table: &'a [Elf64Phdr],
/// 节区头表
section_header_table: &'a [Elf64Shdr],
/// .shstrtab 节区头(节区名字符串表)
section_names_section_header: Option<&'a Elf64Shdr>,
/// .symtab 节区头, 用于获取符号表
symbol_section_header: Option<&'a Elf64Shdr>,
/// .strtab 节区头,用于获取字符串表
symbol_names_section_header: Option<&'a Elf64Shdr>,
/// 动态链接表展开数组
dynamic_table: [Elf64Xword; DT_NUM],
/// 动态重定位表(.rel.dyn)
dynamic_relocations_table: Option<&'a [Elf64Rel]>,
/// 动态符号表(.dynsym)
dynamic_symbol_table: Option<&'a [Elf64Sym]>,
/// .dynstr 节区头
dynamic_symbol_names_section_header: Option<&'a Elf64Shdr>,
}
从字节切片中获取文件头.
pub fn parse_file_header(
elf_bytes: &'a [u8],
) -> Result<(std::ops::Range<usize>, &'a Elf64Ehdr), ElfParserError> {
// Elf64Ehdr是64个字节,所以这里是0..64
let file_header_range = 0..mem::size_of::<Elf64Ehdr>();
// 获取64个字节长度的字节切片
let file_header_bytes = elf_bytes
.get(file_header_range.clone())
.ok_or(ElfParserError::OutOfBounds)?;
let ptr = file_header_bytes.as_ptr();
// mem::align_of::<Elf64Ehdr>(),按结构体中最大的基本类型进行对齐.
// 最大的是u64,64位,所以这里是8.按字节算
// checked_rem 计算指针地址除以对齐值的余数
// 这里是确保remaining为0,确保指针对齐.
if (ptr as usize)
.checked_rem(mem::align_of::<Elf64Ehdr>())
.map(|remaining| remaining != 0)
.unwrap_or(true)
{
return Err(ElfParserError::InvalidAlignment);
}
// 用ptr.cast转换类型并返回
let file_header = unsafe { &*ptr.cast::<Elf64Ehdr>() };
Ok((file_header_range, file_header))
}
解析程序头表
e_phnum 定义在文件头中,表示程序头表条目数e_phoff 程序头表在文件中的偏移check_that_there_is_no_overlap 判断范围是否重叠,多个地方会用到.所以程序头表在文件中的范围为 [e_phoff , e_phoff + e_phnum * sizeof(Elf64Phdr)]
`
pub fn parse_program_header_table(
elf_bytes: &'a [u8],
file_header_range: std::ops::Range<usize>,
file_header: &Elf64Ehdr,
) -> Result<(std::ops::Range<usize>, &'a [Elf64Phdr]), ElfParserError> {
let program_header_table_range = file_header.e_phoff as usize
..mem::size_of::<Elf64Phdr>()
.err_checked_mul(file_header.e_phnum as usize)?
.err_checked_add(file_header.e_phoff as usize)?;
check_that_there_is_no_overlap(&file_header_range, &program_header_table_range)?;
let program_header_table =
Self::slice_from_bytes::<Elf64Phdr>(elf_bytes, program_header_table_range.clone())?;
Ok((program_header_table_range, program_header_table))
}
节区表的计算和程序头表差不多,略过
解析节区头
fn parse_sections(&mut self) -> Result<(), ElfParserError> {
macro_rules! section_header_by_name {
($self:expr, $section_header:expr, $section_name:expr,
$($name:literal => $field:ident,)*) => {
match $section_name {
$($name => {
if $self.$field.is_some() {
return Err(ElfParserError::InvalidSectionHeader);
}
$self.$field = Some($section_header);
})*
_ => {}
}
}
}
let section_names_section_header = self
.section_names_section_header
.ok_or(ElfParserError::NoSectionNameStringTable)?;
// 找出节区名,并根据节区名把节区放入指定的字段中.
for section_header in self.section_header_table.iter() {
let section_name = Self::get_string_in_section(
self.elf_bytes,
section_names_section_header,
section_header.sh_name,
SECTION_NAME_LENGTH_MAXIMUM,
)?;
section_header_by_name!(
self, section_header, section_name,
b".symtab" => symbol_section_header,
b".strtab" => symbol_names_section_header,
b".dynstr" => dynamic_symbol_names_section_header,
)
}
Ok(())
}
// 获取节区中的字符串(节区名)
pub fn get_string_in_section(
elf_bytes: &'a [u8],
section_header: &Elf64Shdr,
offset_in_section: Elf64Word,
maximum_length: usize,
) -> Result<&'a [u8], ElfParserError> {
if section_header.sh_type != SHT_STRTAB {
return Err(ElfParserError::InvalidSectionHeader);
}
// 节区在文件中的偏移 + 节区名在节区中的偏移 = 节区名的全局偏移
let offset_in_file =
(section_header.sh_offset as usize).err_checked_add(offset_in_section as usize)?;
// 这里计算的是整个节区的范围
let string_range = offset_in_file
..(section_header.sh_offset as usize)
.err_checked_add(section_header.sh_size as usize)?
.min(offset_in_file.err_checked_add(maximum_length)?);
// 写得跟条贪吃蛇一样.其实是找出整个节区中第一个null的位置.
// 然后截取从0到null之前位置的字符串,因为sh_name在结构体中第一个,所以从0截取.
// 也就是找出节区名
let unterminated_string_bytes = elf_bytes
.get(string_range)
.ok_or(ElfParserError::OutOfBounds)?;
unterminated_string_bytes
.iter()
.position(|byte| *byte == 0x00)
.and_then(|string_length| unterminated_string_bytes.get(0..string_length))
.ok_or_else(|| {
ElfParserError::StringTooLong(
String::from_utf8_lossy(unterminated_string_bytes).to_string(),
maximum_length,
)
})
}
其他的解析不细讲了,都是差不多的.
将 ELF 文件加载成可执行的 sBPF 程序.
这部分我只大概讲下,我想了解的是整个vm的运行.这部分没那么相关,只粗略看了下.
pub struct Executable<C: ContextObject> {
// 字节对齐的elf字节缓冲区
elf_bytes: AlignedMemory<{ HOST_ALIGN }>,
// 版本
sbpf_version: SBPFVersion,
// 合并后的只读节数据
ro_section: Section,
// .text 在节区中的起始位置
text_section_vaddr: u64,
// .text 在节区中的范围
text_section_range: Range<usize>,
// 入口pC
entry_pc: usize,
// 函数映射表
function_registry: FunctionRegistry<usize>,
// 加载器的内置程序
loader: Arc<BuiltinProgram<C>>,
#[cfg(all(feature = "jit", not(target_os = "windows"), target_arch = "x86_64"))]
compiled_program: std::sync::Mutex<Option<Arc<JitProgram>>>,
}
.text 节保存的就是可执行的 sBPF 字节码指令.
在后面的 load 函数中会可以看到有两个版本的加载器.
load_with_strict_parser 严格解析, 省略了重定位load_with_lenient_parser 宽松解析,需要重定位.重定位: 讲起来太复杂了,就只要知道编译过程中你要调用其他的函数,但是你还不知道他们在地址中的具体位置.先放个东西占位,后期链接器链接的时候,再把占位符修改成实际的地址.或者在解析的时候修改.
先看下严格解析的部分.严格模式有严格的格式要求.sBPF v3+ 用的是这个加载解析器
为什么它不需要在加载过程中进行重定位,是因为它要求加载的 ELF 文件已经是在链接过程中已经提前重定位好.
要求的格式大致如下(简略版)
[ ELF 文件 ]
│
├─ ELF Header
│ ├─ 魔数、64位、小端、版本、机器=EM_BPF 等 → 固定值
│ ├─ e_phoff = sizeof(Ehdr)
│ └─ e_phnum ≥ 1
│
├─ Program Headers (紧跟在Ehdr后)
│ ├─ 段0: 可选只读数据段 (PF_R, vaddr=0)
│ └─ 段1: 字节码段 (PF_X, vaddr=0x100000000)
│ 每个段必须: PT_LOAD, 对齐8, filesz=memsz, 不越界
│
├─ 文件内容 (按p_offset定位)
│ ├─ 只读数据 (若存在)
│ └─ 字节码 (必须存在)
│
├─ 可选: 节头表 (用于符号表解析)
│ ├─ .symtab
│ └─ .strtab
│
└─ 结果: SBPFProgram { ro_section, text_section_range, entry_pc, function_registry, ... }
点进去看下代码.根据文件头判断 ELF 格式是否正确.
if file_header.e_ident.ei_mag != ELFMAG // 魔数必须为 \x7fELF
|| file_header.e_ident.ei_class != ELFCLASS64 // 必须是 64 位
|| file_header.e_ident.ei_data != ELFDATA2LSB // 小端序
|| file_header.e_ident.ei_version != EV_CURRENT as u8 // ELF 版本必须是 1
|| file_header.e_ident.ei_osabi != ELFOSABI_NONE // OS ABI 必须是通用
|| file_header.e_ident.ei_abiversion != 0x00 // ABI 版本必须为 0
|| file_header.e_ident.ei_pad != [0x00; 7] // 填充必须全为 0
// file_header.e_type 忽略
|| file_header.e_machine != EM_BPF // 目标机器必须是 EM_BPF
|| file_header.e_version != EV_CURRENT // ELF 结构版本必须为 1
// file_header.e_entry 忽略
|| file_header.e_phoff != mem::size_of::<Elf64Ehdr>() as u64 // 程序头表紧随文件头
// file_header.e_shoff / e_flags 忽略
|| file_header.e_ehsize != mem::size_of::<Elf64Ehdr>() as u16 // 文件头大小正确
|| file_header.e_phentsize != mem::size_of::<Elf64Phdr>() as u16 // 程序头条目大小固定
|| file_header.e_phnum == 0 // 必须至少有一个程序头
|| program_header_table_range.end > elf_bytes.len() // 程序头表不能超出文件
{
return Err(ElfParserError::InvalidFileHeader);
}
检测程序头的布局是否符合要求.
这两个是在前面介绍的内存布局内容中的划分.拉到最上面看看.
MM_RODATA_START 只读数据区(0x000000000)MM_BYTECODE_START 代码区(0x100000000)// 期望的程序头表格式
const EXPECTED_PROGRAM_HEADERS: [(u32, u64); 2] = [
(PF_R, ebpf::MM_RODATA_START),
(PF_X, ebpf::MM_BYTECODE_START),
];
// 截取指定范围的字节并转换成Elf64Phdr,也就是获取程序头表
let program_header_table =
Elf64::slice_from_bytes::<Elf64Phdr>(elf_bytes, program_header_table_range.clone())?;
let mut expected_program_headers = EXPECTED_PROGRAM_HEADERS.iter();
// 若第一个段不是 PF_R,说明没有只读数据段,跳过该期望
let skip_rodata_program_header =
program_header_table[0].p_flags != EXPECTED_PROGRAM_HEADERS[0].0;
if skip_rodata_program_header {
// 只有字节码段,期望序列从 PF_X 开始
expected_program_headers.next();
} else if file_header.e_phnum < 2 {
// 有只读数据段就必须同时有字节码段
return Err(ElfParserError::InvalidFileHeader);
}
检查程序头
// 逐一验证每个程序头的各字段
for (program_header, (p_flags, p_vaddr)) in
program_header_table.iter().zip(expected_program_headers)
{
if program_header.p_type != PT_LOAD // 必须是 LOAD 类型
|| program_header.p_flags != *p_flags // 标志需匹配期望
|| program_header.p_offset != expected_offset // 偏移必须紧随前一个段
|| program_header.p_offset >= elf_bytes.len() as u64 // 不能超出文件
|| program_header.p_offset.checked_rem(ebpf::INSN_SIZE as u64) != Some(0) // 对齐指令大小
|| program_header.p_vaddr != *p_vaddr // 虚拟地址必须匹配预设
|| program_header.p_paddr != *p_vaddr // 物理地址同虚拟地址
|| program_header.p_filesz != program_header.p_memsz // 文件大小=内存大小(无 BSS)
|| program_header.p_filesz
> (elf_bytes.len() as u64).saturating_sub(program_header.p_offset) // 不越界
|| program_header.p_filesz.checked_rem(ebpf::INSN_SIZE as u64) != Some(0) // 大小对齐
|| program_header.p_memsz >= ebpf::MM_REGION_SIZE // 单段不得占满或超过一个 MM 区域(4 GiB)
{
return Err(ElfParserError::InvalidProgramHeader);
}
// 下一个段紧随当前段之后
expected_offset = expected_offset.saturating_add(program_header.p_filesz);
}
// 找出只读程序头和可执行程序头
let (ro_section_range, bytecode_header) = if skip_rodata_program_header {
(
program_header_table_range.end..program_header_table_range.end,
&program_header_table[0],
)
} else {
(
// 第一个程序头覆盖只读数据段,第二个覆盖字节码段
program_header_table[0].file_range().unwrap_or_default(),
&program_header_table[1],
)
};
后面的不讲了,都是一些类似的解析.属于繁杂容易出错但逻辑比较清晰的.
使用宽松解析器加载带重定位的 ELF.sBPF V0 - V2 用的是这个加载解析器.
这部分不是特别感兴趣可以跳过,内容太多,而且后续估计也用不到了.
在 load_with_strict_parser 中, 只用了 Elf64::parse_file_header 解析文件头.
而在这里,直接使用了 Elf64::parse 解析了整个文件.
这里也会有一个简单的验证部分,在 validate 函数中.
// 确保text节只有一个
let num_text_sections =
elf.section_header_table()
.iter()
.fold(0, |count: usize, section_header| {
if let Ok(this_name) = elf.section_name(section_header.sh_name) {
if this_name == b".text" {
return count.saturating_add(1);
}
}
count
});
if 1 != num_text_sections {
return Err(ElfError::NotOneTextSection);
}
// 检查是否存在可写节(sBPF 不支持运行时可写数据段,不然瞎改其他内存就就出问题了)
for section_header in elf.section_header_table().iter() {
if let Ok(name) = elf.section_name(section_header.sh_name) {
if name.starts_with(b".bss") // .bss 节(未初始化全局数据)不允许
|| (section_header.is_writable()
&& (name.starts_with(b".data") && !name.starts_with(b".data.rel")))
// .data 节允许只读重定位(.data.rel.ro),但不允许普通可写 .data
{
return Err(ElfError::WritableSectionNotSupported(
String::from_utf8_lossy(name).to_string(),
));
}
}
}
// 验证每个节的文件范围在 elf_bytes 切片内,防止越界访问
for section_header in elf.section_header_table().iter() {
let start = section_header.sh_offset as usize;
let end = section_header
.sh_offset
.checked_add(section_header.sh_size)
.ok_or(ElfError::ValueOutOfBounds)? as usize;
let _ = elf_bytes
.get(start..end)
.ok_or(ElfError::ValueOutOfBounds)?;
}
// 验证入口点虚拟地址在 .text 节的虚拟地址范围内
let text_section = get_section(elf, b".text")?;
if !text_section.vm_range().contains(&header.e_entry) {
return Err(ElfError::EntrypointOutOfBounds);
}
接着是关键的重定位函数函数 relocate.
这部分的内容很长,会分成几部分来说.
重定位主要修改两类指令指令的立即数字段(imm).
在前面的指令集部分可以看出,imm是指令后面的4个字节.
所以修改立即数就是修改指令所在索引+4的位置的4个字节.
这里有个要非常注意的点.sBPF规定,使用相对偏移的跳转/调用指令,他们的imm或offset的值,都是以 下一条指令 为基准的相对偏移.
搞清这个,下面 CALL_IMM 的处理计算才能看懂.
第一步,处理CALL_IMM:
let config = loader.get_config();
// 找出text节区的字节.
let text_bytes = elf_bytes
.get_mut(text_section.file_range().unwrap_or_default())
.ok_or(ElfError::ValueOutOfBounds)?;
// 每个指令都是8个字节,这里计算出指令数量
let instruction_count = text_bytes
.len()
.checked_div(ebpf::INSN_SIZE)
.ok_or(ElfError::ValueOutOfBounds)?;
for i in 0..instruction_count {
let insn = ebpf::get_insn(text_bytes, i);
// 找出所有的CALL_IMM
if insn.opc == ebpf::CALL_IMM && insn.imm != -1 {
// 按下条指令的偏移计算,这里是找出要调用的函数的pc.
let target_pc = (i as isize)
.saturating_add(1)
.saturating_add(insn.imm as isize);
if target_pc < 0 || target_pc >= instruction_count as isize {
return Err(ElfError::RelativeJumpOutOfBounds(i));
}
let name = if config.enable_symbol_and_section_labels {
format!("function_{target_pc}")
} else {
String::default()
};
// 注册函数并获取32位的key
let key = function_registry.register_function_hashed_legacy(
loader,
true,
name.as_bytes(),
target_pc as usize,
)?;
// 找出指令中的立即数在text节中的offset
let offset = i.saturating_mul(ebpf::INSN_SIZE).saturating_add(4);
// 获取立即数的可变引用,并写入前面注册的key.
// 后续解释器看到是key就不根据相对偏移找函数,而是直接去已注册里的函数找
let checked_slice = text_bytes
.get_mut(offset..offset.saturating_add(4))
.ok_or(ElfError::ValueOutOfBounds)?;
LittleEndian::write_u32(checked_slice, key);
}
}
第二步,处理动态重定位表.
分为三部分:
BpfRelocationType::R_Bpf_64_64 绝对地址重定位BpfRelocationType::R_Bpf_64_Relative 相对地址重定位BpfRelocationType::R_Bpf_64_32 函数调用重定位需要注意的点:
r_offset 表示从 ELF 文件起始的字节偏移具体看代码吧.主要是处理格式,麻烦的地方主要是不熟悉sBPF的规定.
我也不熟悉,只是大概了看下源码.
for relocation in elf.dynamic_relocations_table().unwrap_or_default().iter() {
let r_offset = relocation.r_offset as usize;
match BpfRelocationType::from_x86_relocation_type(relocation.r_type()) {
Some(BpfRelocationType::R_Bpf_64_64) => {
// R_Bpf_64_64:lddw 加载绝对地址(基于符号 + 加数)
// imm 字段在指令中的字节偏移
let imm_offset = r_offset.saturating_add(BYTE_OFFSET_IMMEDIATE);
// 读取指令 imm 中预存的加数(低 32 位),与符号值相加得到 VM 虚拟地址
let checked_slice = elf_bytes
.get(imm_offset..imm_offset.saturating_add(BYTE_LENGTH_IMMEDIATE))
.ok_or(ElfError::ValueOutOfBounds)?;
let refd_addr = LittleEndian::read_u32(checked_slice) as u64;
// 从动态符号表中查找重定位条目引用的符号
let symbol = elf
.dynamic_symbol_table()
.and_then(|table| table.get(relocation.r_sym() as usize).cloned())
.ok_or_else(|| ElfError::UnknownSymbol(relocation.r_sym() as usize))?;
// 最终地址 = 符号值(st_value)+ 隐含加数
let mut addr = symbol.st_value.saturating_add(refd_addr);
// 若地址仍落在「第 0 号」MM 区域(小于 MM_REGION_SIZE),则加上 MM_BYTECODE 区基址
if addr < ebpf::MM_REGION_SIZE {
addr = ebpf::MM_REGION_SIZE.saturating_add(addr);
}
let imm_low_offset = imm_offset;
// lddw 占两个指令槽,第二槽的 imm 紧随第一槽之后
let imm_high_offset = imm_low_offset.saturating_add(INSN_SIZE);
// 将地址低 32 位写入第一个指令槽的 imm 字段
let imm_slice = elf_bytes
.get_mut(
imm_low_offset..imm_low_offset.saturating_add(BYTE_LENGTH_IMMEDIATE),
)
.ok_or(ElfError::ValueOutOfBounds)?;
LittleEndian::write_u32(imm_slice, (addr & 0xFFFFFFFF) as u32);
// 将地址高 32 位写入第二个指令槽的 imm 字段
let imm_slice = elf_bytes
.get_mut(
imm_high_offset..imm_high_offset.saturating_add(BYTE_LENGTH_IMMEDIATE),
)
.ok_or(ElfError::ValueOutOfBounds)?;
LittleEndian::write_u32(
imm_slice,
addr.checked_shr(32).unwrap_or_default() as u32,
);
}
Some(BpfRelocationType::R_Bpf_64_Relative) => {
// R_Bpf_64_Relative:跨节相对地址重定位(目标内存不关联具体符号,
// 例如编译器自动生成的 rodata 引用)
// imm 字段在文件中的字节偏移
let imm_offset = r_offset.saturating_add(BYTE_OFFSET_IMMEDIATE);
if text_section
.file_range()
.unwrap_or_default()
.contains(&r_offset) // 重定位目标在 .text 节内
{
// lddw 双槽指令:64 位地址被拆分存入两个 imm 字段
let imm_low_offset = imm_offset;
let imm_high_offset = r_offset
.saturating_add(INSN_SIZE)
.saturating_add(BYTE_OFFSET_IMMEDIATE);
// 读取第一槽 imm(地址低 32 位)
let imm_slice = elf_bytes
.get(
imm_low_offset
..imm_low_offset.saturating_add(BYTE_LENGTH_IMMEDIATE),
)
.ok_or(ElfError::ValueOutOfBounds)?;
let va_low = LittleEndian::read_u32(imm_slice) as u64;
// Read the high side of the address
let imm_slice = elf_bytes
.get(
imm_high_offset
..imm_high_offset.saturating_add(BYTE_LENGTH_IMMEDIATE),
)
.ok_or(ElfError::ValueOutOfBounds)?;
let va_high = LittleEndian::read_u32(imm_slice) as u64;
// Put the address back together
let mut refd_addr = va_high.checked_shl(32).unwrap_or_default() | va_low;
if refd_addr == 0 {
return Err(ElfError::InvalidVirtualAddress(refd_addr));
}
if refd_addr < ebpf::MM_REGION_SIZE {
refd_addr = ebpf::MM_REGION_SIZE.saturating_add(refd_addr);
}
// Write back the low half
let imm_slice = elf_bytes
.get_mut(
imm_low_offset
..imm_low_offset.saturating_add(BYTE_LENGTH_IMMEDIATE),
)
.ok_or(ElfError::ValueOutOfBounds)?;
// 将低 32 位写回第一个指令槽的 imm
LittleEndian::write_u32(imm_slice, (refd_addr & 0xFFFFFFFF) as u32);
// 将高 32 位写回第二个指令槽的 imm
let imm_slice = elf_bytes
.get_mut(
imm_high_offset
..imm_high_offset.saturating_add(BYTE_LENGTH_IMMEDIATE),
)
.ok_or(ElfError::ValueOutOfBounds)?;
LittleEndian::write_u32(
imm_slice,
refd_addr.checked_shr(32).unwrap_or_default() as u32,
);
} else {
// 旧版工具链 bug 兼容:
// 在 solana-labs/llvm-project#35 修复之前,编译器对 64 位重定位
// 仅编码了低 32 位并左移 32 位存储。为向后兼容,保留此处理逻辑:
// - 从 imm 字段读出低 32 位值(即旧格式的"高32位")
// - 加上 MM_REGION_SIZE 基址后写回为完整的 64 位地址
let addr_slice = elf_bytes
.get(imm_offset..imm_offset.saturating_add(BYTE_LENGTH_IMMEDIATE))
.ok_or(ElfError::ValueOutOfBounds)?;
let mut refd_addr = LittleEndian::read_u32(addr_slice) as u64;
refd_addr = ebpf::MM_REGION_SIZE.saturating_add(refd_addr);
// 以 64 位整数写回到 r_offset 处(覆盖两个槽的连续 8 字节)
let addr_slice = elf_bytes
.get_mut(r_offset..r_offset.saturating_add(mem::size_of::<u64>()))
.ok_or(ElfError::ValueOutOfBounds)?;
LittleEndian::write_u64(addr_slice, refd_addr);
}
}
Some(BpfRelocationType::R_Bpf_64_32) => {
// R_Bpf_64_32:.text 节中有未解析的 call 指令
// 计算符号名哈希或目标 PC,写入 call 指令 imm 字段。
// 解释器/JIT 运行时用该哈希查找实际函数地址。
// imm 字段在文件中的字节偏移
let imm_offset = r_offset.saturating_add(BYTE_OFFSET_IMMEDIATE);
// 从动态符号表中查找重定位引用的符号
let symbol = elf
.dynamic_symbol_table()
.and_then(|table| table.get(relocation.r_sym() as usize).cloned())
.ok_or_else(|| ElfError::UnknownSymbol(relocation.r_sym() as usize))?;
// 从动态字符串表中获取符号名称字节
let name = elf
.dynamic_symbol_name(symbol.st_name as Elf64Word)
.map_err(|_| ElfError::UnknownSymbol(symbol.st_name as usize))?;
// 判断是 BPF-to-BPF 调用(符号有值且是函数类型)还是系统调用
let key = if symbol.is_function() && symbol.st_value != 0 {
// BPF-to-BPF 调用:符号已在 .text 中定义
if !text_section.vm_range().contains(&symbol.st_value) {
return Err(ElfError::ValueOutOfBounds); // 目标地址超出 .text 范围
}
// 计算目标函数的 PC(相对于 .text 起始的指令偏移)
let target_pc = (symbol.st_value.saturating_sub(text_section.sh_addr)
as usize)
.checked_div(ebpf::INSN_SIZE)
.unwrap_or_default();
// 注册函数并返回哈希键
function_registry
.register_function_hashed_legacy(loader, true, name, target_pc)?
} else {
// 系统调用:符号未定义或不是函数,按名称哈希识别
let hash = *syscall_cache
.entry(symbol.st_name)
.or_insert_with(|| ebpf::hash_symbol_name(name)); // 缓存哈希,避免重复计算
// reject_broken_elfs:未在 loader 中注册的 syscall 视为错误
if config.reject_broken_elfs
&& loader.get_function_registry().lookup_by_key(hash).is_none()
{
return Err(ElfError::UnresolvedSymbol(
String::from_utf8_lossy(name).to_string(),
r_offset.checked_div(ebpf::INSN_SIZE).unwrap_or(0),
r_offset,
));
}
hash
};
// 将哈希键写入 call 指令的 imm 字段
let checked_slice = elf_bytes
.get_mut(imm_offset..imm_offset.saturating_add(BYTE_LENGTH_IMMEDIATE))
.ok_or(ElfError::ValueOutOfBounds)?;
LittleEndian::write_u32(checked_slice, key);
}
// 未知重定位类型,返回错误
_ => return Err(ElfError::UnknownRelocation(relocation.r_type())),
}
}
第三步:
if config.enable_symbol_and_section_labels {
for symbol in elf.symbol_table().ok().flatten().unwrap_or_default().iter() {
// 只处理 st_info & 0xEF == STT_FUNC(2) 的符号
if symbol.st_info & 0xEF != 0x02 {
continue;
}
// 函数必须在 .text 节虚拟地址范围内
if !text_section.vm_range().contains(&symbol.st_value) {
return Err(ElfError::ValueOutOfBounds);
}
// 计算函数 PC
let target_pc = (symbol.st_value.saturating_sub(text_section.sh_addr) as usize)
.checked_div(ebpf::INSN_SIZE)
.unwrap_or_default();
// 从静态字符串表中获取符号名
let name = elf
.symbol_name(symbol.st_name as Elf64Word)
.map_err(|_| ElfError::UnknownSymbol(symbol.st_name as usize))?;
// 注册函数(使用名称哈希,不覆盖动态重定位已注册的条目)
function_registry.register_function_hashed_legacy(loader, true, name, target_pc)?;
}
}
load_with_lenient_parser 剩下的部分不讲了,都是些跟前面差不多的解析函数.
字节码合法性验证器,在程序运行和JIT 编译之前,对 sBPF 字节码进行静态检查,
确保每条指令都是合法的、安全的,防止运行时崩溃或安全漏洞。
主要检查项:
代码部分都比较简单,感兴趣的自己去看下
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!