用Rust玩eBPF —— RedBpf

环境及依赖

代码库

1
2
git clone https://github.com/foniod/redbpf.git 

安装llvm

1
2
3
4
5
6
7
8
9
10
11
12
安装LLVM
sudo apt-get update \
&& sudo apt-get -y install \
wget \
build-essential \
software-properties-common \
lsb-release \
libelf-dev \
linux-headers-generic \
pkg-config \
&& wget https://apt.llvm.org/llvm.sh && chmod +x llvm.sh && ./llvm.sh 13 && rm -f ./llvm.sh

安装cargo-bpf

首先配置下 这个环境变量

1
export LLVM_SYS_130_PREFIX=/usr/lib/llvm-13

这里 llvm 配置你自己的安装路径。
执行:

1
cargo install cargo-bpf

如果遇到这个坑:

1
No suitable version of LLVM was found system-wide or pointed               to by LLVM_SYS_120_PREFIX 

说明安装的llvm版本和要求不一致,可能是使用了镜像crates,更新不及时的缘故。解决方法:
直接进项目编译

1
2
cd cargo-bpf
cargo install --path .

HelloWorld

BPF程序分为内核端和用户端。内核执行咱们编译的BPF字节码采集追踪数据,然后和用户端通过Map共享数据。
我们先来实现下内核端:

内核端

新建项目

1
2

cargo bpf new bpf_project

新增一个bpf模块 :

1
2
cd bpf_project
cargo bpf add m1

生成了这样的目录结构

1
2
3
4
5
6
7
8
9
├── Cargo.lock
├── Cargo.toml
└── src
├── lib.rs
└── m1
├── main.rs
└── mod.rs


首先在mod.rs中定义我们要给用户端共享的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pub const PATHLEN: usize = 256;

#[repr(C)]
#[derive(Debug, Clone)]
pub struct OpenPath {
pub filename: [u8; PATHLEN],
}

impl Default for OpenPath {
fn default() -> Self {
OpenPath {
filename: [0; PATHLEN],
}
}
}

后面我们将在此结构体中保存所有调用open的文件名,

repr[c] 代表此结构体按照C标准对齐。

然后在main.rs中定义我们的BPF程序 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#[map]
static mut OPEN_PATHS: PerfMap<OpenPath> = PerfMap::with_max_entries(1024);

#[kprobe]
fn do_sys_open(regs: Registers) {

let mut path = OpenPath::default();
unsafe {
// 从这个参数中取出文件名
let filename = regs.parm2() as *const u8;
if bpf_probe_read_user_str(
path.filename.as_mut_ptr() as *mut _,
path.filename.len() as u32,
filename as *const _,
) <= 0
{
bpf_trace_printk(b"error on bpf_probe_read_user_str\0");
return;
}
OPEN_PATHS.insert(regs.ctx, &path);
}
}


#[map] 宏定义了我们用来跟用户端共享的map。do_sys_open 代表此函数被附着 到do_sys_open 上。

bpf_probe_read_user_str 是内核提供的供BPF程序使用的辅助函数,用于复制字符串。

复制完成后插入map。

最后编译:

1
cargo bpf build 

不出意外,我们的BPF程序编译好后会在这里:

target/bpf/programs/m1/m1.elf

用户端

新建个普通项目,这次cargo new 就可以了。不再需要cargo bpf,注意bpf_project跟刚才的内核态项目的相对路径。

1
2
3
cargo new bpf_user 


cargo.toml里这样配置下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[package]
name = "bpf_user"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
bpf_project = { path="../bpf_project" }
redbpf = { version = "2.3.0", features = ["load"] }
tokio = { version = "1.0", features = ["rt", "signal", "time", "io-util", "net", "sync"] }
tracing-subscriber = "0.2"
tracing = "0.1"
futures = "0.3"

日志,tokio都安排上。

main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
use tracing_subscriber::FmtSubscriber;

#[tokio::main(flavor = "current_thread")]
async fn main() {
let subscriber = FmtSubscriber::builder()
.with_max_level(Level::WARN)
.finish();
tracing::subscriber::set_global_default(subscriber).unwrap();

let mut loaded = Loader::load(probe_code()).expect("Error on Loader load");

let probes = loaded
.kprobe_mut("do_sys_open")
.expect("Error on Loaded::kprobe_mut");

probes
.attach_kprobe("do_sys_open", 0)
.expect("Error on Attach probe");

probes
.attach_kprobe("do_sys_openat2", 0)
.expect("Error on Load do_sys_openat2");

while let Some((map_name, events)) = loaded.events.next().await {
if map_name == "OPEN_PATHS" {
for event in events {
let open_path = unsafe { ptr::read(event.as_ptr() as *const OpenPath) };
unsafe {
let cfilename = CStr::from_ptr(open_path.filename.as_ptr() as *const _);
println!("{}", cfilename.to_string_lossy());
};
}
}
}
}

fn probe_code() -> &'static [u8] {
include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"../bpf_project/m1/target/bpf/programs/m1/m1.elf"
))
}

probe_code 配置编译好的elf的路径。附着系统调用配置个do_sys_open,do_sys_openat2 两个就可以了。最后我们从封装好的EVENT中取出内核端定义的MAP,从中读数据就可以了,此时将打印出其他调用open的进程传入的path参数。

编译用户端

1
cargo build

执行用户端需要root权限,所以:

1
2

sudo target/debug/bpf_user

一切工作正常的话,我们将可以看到其他进程调用了open函数打印出来。