这个问题比我预想的要长。由于 BPF 很新,可能没有准确的答案。
我正在尝试将“hello world”eBPF 程序加载到 XDP 连接点上。我想使用原始系统调用和 without 使用
libbpf
、bpftool
、xdp-loader
等来完成此操作。
我的内核版本是
6.5.0-9-generic
。
BPF 程序是:
#include <linux/types.h>
#include <bpf/bpf_helpers.h>
#include <linux/bpf.h>
#include <linux/version.h>
SEC("xdp")
int xdp_prog_simple(struct xdp_md *ctx)
{
bpf_printk("In xdp_prog_simple\n");
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
__u32 _version SEC("version") = LINUX_VERSION_CODE;
并编译为:
clang -O2 -g -Wall -target bpf -c bpf.c -o bpf.o
llvm-objdump --no-show-raw-insn --dr bpf.o
的输出是:
Disassembly of section xdp:
0000000000000000 <xdp_prog_simple>:
0: r1 = 0x0 ll
0000000000000000: R_BPF_64_64 .rodata
2: r2 = 0x14
3: call 0x6
4: r0 = 0x2
5: exit
我正在尝试使用 Rust 程序加载这个
bpf.o
文件。我从 xdp
部分提取字节,并在传递给 BPF_PROG_LOAD
系统调用的 SYS_bpf
命令的属性中引用这些字节:
(这段代码不是问题。将其包含在上下文中,并且因为我在互联网上没有看到很多 Rust 示例,所以它可能对未来的读者有所帮助)
use std::mem::size_of;
use crate::ffi::bpf;
use crate::ffi::syscall::check_err;
use crate::Error;
use object::{Object, ObjectSection};
const LOG_BUF_SIZE: usize = 65536;
static mut BPF_LOG_BUF: [u8; LOG_BUF_SIZE] = [0; LOG_BUF_SIZE];
pub fn load_xdp_program(elf: &[u8], section: &str) -> Result<i32, Error> {
let obj = object::File::parse(elf).map_err(|err| Error::Boxed(Box::new(err)))?;
// Extract bytes from the program section ("xdp" in this example)
let text = obj
.section_by_name(section)
.ok_or_else(|| Error::NotFound("program section not found"))?
.data()
.map_err(|err| Error::Boxed(Box::new(err)))?;
// Extract the license bytes
let license = obj
.section_by_name("license")
.ok_or_else(|| Error::NotFound("section 'license' not found"))?
.data()
.map_err(|err| Error::Boxed(Box::new(err)))?;
// Extract the kernel version bytes
let version = {
let data: [u8; 4] = obj
.section_by_name("version")
.ok_or_else(|| Error::NotFound("section 'version' not found"))?
.data()
.map_err(|err| Error::Boxed(Box::new(err)))?
.try_into()
.map_err(|err| Error::Boxed(Box::new(err)))?;
u32::from_le_bytes(data)
};
let attr = &bpf::ProgramAttr {
prog_type: XDP_PROG_TYPE,
insn_cnt: (text.len() / size_of::<bpf::Insn>()) as u32,
insns: ptr_to_u64(text),
license: ptr_to_u64(license),
log_level: 1,
log_size: LOG_BUF_SIZE as u32,
log_buf: ptr_to_u64(unsafe { &BPF_LOG_BUF }),
kern_version: version,
prog_flags: 0,
prog_name: *b"xdp_prog_simple\0",
prog_ifindex: 0,
expected_attach_type: 37, // BPF_XDP
prog_btf_fd: 0,
func_info_rec_size: 0,
func_info: 0,
func_info_cnt: 0,
line_info_rec_size: 0,
line_info: 0,
line_info_cnt: 0,
attach_btf_id: 0,
attach: bpf::AttachFd { object_fd: 0 },
core_relo_cnt: 0,
fd_array: 0,
core_relos: 0,
core_relo_rec_size: 0,
};
let prog_fd = check_err(unsafe {
libc::syscall(
libc::SYS_bpf,
bpf::cmd::PROG_LOAD,
attr as *const _ as *const libc::c_void,
size_of::<bpf::BpfAttr>(),
) as i32
});
match prog_fd {
Ok(fd) => Ok(fd),
Err(err) => {
let msg = String::from_utf8_lossy(unsafe { &BPF_LOG_BUF });
eprintln!("Failed to load BPF program. Log buffer:\n\n{msg}");
Err(err)
}
}
}
我构建并将其称为
sudo
以确保它具有 root 权限:
cargo build --release --bin xdp-loader
sudo ./target/release/xdp-loader bpf.o
这会从日志缓冲区返回错误:
0: R1=ctx(off=0,imm=0) R10=fp0
0: (18) r1 = 0x0 ; R1_w=0
2: (b7) r2 = 20 ; R2_w=20
3: (85) call bpf_trace_printk#6
R1 type=scalar expected=fp, pkt, pkt_meta, map_key, map_value, mem, ringbuf_mem, buf, trusted_ptr_
processed 3 insns (limit 1000000) max_states_per_insn 0 total_states 0 peak_states 0 mark_read 0
BPF 系统调用返回错误代码 13:权限被拒绝。
这似乎暗示寄存器
r1
是一种类型scalar
,而它应该是类型fp
。这让我很困惑。据我了解,根据这些文档,r1
应该将地址携带到上下文结构struct xdp_md *ctx
。我没有执行任何指针算术,因此不应将其转换为标量。
我觉得这是因为struct xdp_md *ctx
没有被使用,所以编译出来了。相反,它用于 BPF 调用约定来调用
bpf_printk
帮助器,这实际上是一个被
bpf_trace_printk
替换的宏。调用时,寄存器
r1
用于指向
"In xdp_prog_simple\n"
字符串。它将立即 64 位
0x0
地址(零偏移地址)加载到包含该字符串的
.rodata
部分中。这是
bpf_printk
的第一个论点。寄存器
r2
还加载立即值
20
,因为这是
"In xdp_prog_simple\n"
字符串的长度 – 它是
bpf_trace_printk
的第二个参数。但这并不能解释错误:
R1 type=scalar expected=fp, pkt, pkt_meta, map_key, map_value, mem, ringbuf_mem, buf, trusted_ptr_
它期望 r1
是一个帧指针,但它是一个标量。我
认为这是因为当我加载包含程序字节代码的xdp
部分时,我没有加载
.rodata
部分,所以这不指向任何地方,并且验证器解释了
0x0
地址作为标量。似乎需要一个中间链接/加载步骤才能使 BPF JIT 可以访问
.rodata
。这个 SO 答案证实了这一点,这意味着
libbpf
会自动执行此操作。如果我重写我的代码:
SEC("xdp")
int xdp_prog_simple(struct xdp_md *ctx)
{
char msg[] = "In xdp_prog_simple\n";
bpf_trace_printk(msg, sizeof(msg));
return XDP_PASS;
}
字符串内联存储,而不是存储在不同的部分中:
Disassembly of section xdp:
0000000000000000 <xdp_prog_simple>:
0: r1 = 0xa656c
1: *(u32 *)(r10 - 0x8) = r1
2: r1 = 0x706d69735f676f72 ll
4: *(u64 *)(r10 - 0x10) = r1
5: r1 = 0x705f706478206e49 ll
7: *(u64 *)(r10 - 0x18) = r1
8: r1 = r10
9: r1 += -0x18
10: r2 = 0x14
11: call 0x6
12: r0 = 0x2
13: exit
程序加载成功!在我的内核版本上,
bpf_printk
/* Helper macro to print out debug messages */
#define bpf_printk(fmt, args...) ___bpf_pick_printk(args)(fmt, ##args)
我的问题:如何自动执行此操作,而不需要拆分字符串的声明和 bpf_trace_printk
的调用?我相信我需要以某种方式链接/加载由反汇编中的
.rodata
重定位条目标识的
R_BPF_64_64
部分中的字符串:(之前的片段)
0000000000000000 <xdp_prog_simple>:
0: r1 = 0x0 ll
0000000000000000: R_BPF_64_64 .rodata
以便它与字节码内联存储。那是对的吗?如果是这样,我该怎么做?(我的想法是,我需要扫描 ELF 文件中的重定位条目,并使用它们以某种方式定位 .rodata 部分中的项目,然后将它们直接插入字节码中)
我知道 BPF 相当新并且有锋利的边缘。对任何为我指明正确方向的答案感到满意。
加载eBPF程序的过程中,Aya会首先解析ELF文件并找到.rodata
部分、
.bss
部分、
.data
部分等等。之后,Aya 将为这些部分创建地图。这些地图的类型通常是
BPF_MAP_TYPE_ARRAY
。这一步完成后,程序中访问这些段内容的指令需要重定位。核心重定位逻辑如下:
if !map.data().is_empty() {
instructions[ins_index].set_src_reg(BPF_PSEUDO_MAP_VALUE as u8);
instructions[ins_index + 1].imm = instructions[ins_index].imm + sym.address as i32;
} else {
instructions[ins_index].set_src_reg(BPF_PSEUDO_MAP_FD as u8);
}
instructions[ins_index].imm = *fd;
您可以在 内核文档中了解为什么需要这样做。