环境及依赖
代码库
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
| 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。
最后编译:
不出意外,我们的BPF程序编译好后会在这里:
target/bpf/programs/m1/m1.elf
用户端
新建个普通项目,这次cargo new 就可以了。不再需要cargo bpf,注意bpf_project跟刚才的内核态项目的相对路径。
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"
[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参数。
编译用户端
执行用户端需要root权限,所以:
1 2
| sudo target/debug/bpf_user
|
一切工作正常的话,我们将可以看到其他进程调用了open函数打印出来。