使用 libbpf 和 Rust 開發 ebpf 程序
作者簡介:
張新誼,西安郵電大學計算機專業研一在讀,目前在學習操作系統底層原理和內核編程。
使用本教程前需有 C 語言的 libbpf 開發經驗,以及 rust 語言基礎。
01
環境搭建
安裝 Rust 編程環境
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Cargo 是 Rust 的包管理器和構建系統,在安裝 Rust 時會自動安裝 Cargo。
安裝 libbpf 庫
sudo apt-get install libbpf-dev
安裝 clang 和 llvm
sudo apt-get install clang llvm
02
創建項目與結構解讀
要使用 libbpf 和 Rust 開發 ebpf 程序,首先使用以下命令創建新的 Rust 包:
cargo new xxx //xxx爲你的項目名稱
項目創建後,其結構如下圖所示:
-
src
目錄用於存放內核態和用戶態代碼。 -
Cargo.toml
是項目的配置文件。
項目結構
1. 內核態的 eBPF 程序 (xxx.bpf.c
):
- 使用 C 語言編寫,放置於
src
目錄中。
2. 用戶態程序 (main.rs
):
- 使用 Rust 語言編寫,放置於
src
目錄中。
3. 構建腳本 (build.rs
):
-
相當於 Makefile,用於使用
libbpf-cargo
依賴庫構建xxx.bpf.c
並生成xxx.skel.rs
。 -
Cargo build
會自動運行build.rs
中的代碼。
最終的目錄結構如下圖所示:
03
build.rs 及 libbpf-cargo
build.rs 用於構建骨架文件,而 libbpf-cargo 是由 Rust 庫提供的,用於簡化在 Rust 項目中使用 eBPF 程序的工作流程。它提供了一些工具,可以幫助編譯和生成 BPF 程序的骨架代碼。
1. 如何在 Rust 項目中使用 libbpf-cargo
要在我們的 Rust 項目中使用 libbpf-cargo
,需要在 Cargo.toml
文件中添加以下依賴項:
[build dependencies]
libbpf-cargo = "0.13"
下圖爲 Rust 和 eBPF 項目文件調用關係圖,內核態的 eBPF 程序用 C 語言編寫在 xxx.bpf.c
文件中。用戶態的 build.rs
構建腳本通過調用 libbpf-cargo
編譯 xxx.bpf.c
文件並生成相應的骨架文件 xxx.skel.rs
。在用戶態的主程序 main.rs
中,通過 libbpf-rs
庫調用生成的 xxx.skel.rs
文件中的函數,加載並運行 eBPF 程序,從而實現內核態和用戶態之間的交互。
骨架文件 xxx.skel.rs 生成流程:
build.rs:
use libbpf_cargo::SkeletonBuilder;//引入了 SkeletonBuilder,它是 libbpf-cargo 提供的一個工具,用於在構建時生成 eBPF 程序的骨架代碼。
SkeletonBuilder::new()//創建實例
.source(SRC)//指定ebpf源文件
.clang_args(format!(
"-I{}",
Path::new("src/bpf")
.join(match arch.as_ref() {
"aarch64" => "arm64",
"loongarch64" => "loongarch",
"powerpc64" => "powerpc",
"riscv64" => "riscv",
"x86_64" => "x86",
_ => &arch,
})//使用 clang_args 方法向 Clang 編譯器傳遞編譯參數。
.display()
))
.build_and_generate(out)//調用 build_and_generate 方法,生成骨架代碼並將其輸出到 out 指定的位置。
.unwrap();
build.rs 使用 libbpf-cargo
提供的 SkeletonBuilder
工具來配置並生成 eBPF 程序的骨架代碼。
首先創建了 SkeletonBuilder
實例,然後指定 eBPF 源文件路徑。接着向 Clang 編譯器傳遞編譯參數,特別是頭文件的路徑,根據當前系統架構動態確定路徑中的子目錄。最後,調用 build_and_generate
方法生成骨架代碼,並將生成的代碼輸出到指定位置。
骨架代碼生成流程:
用戶代碼 build.rs
↓
libbpf-cargo:SkeletonBuilder()
↓
編譯 eBPF 源文件 *.bpf.c
↓
生成骨架代碼 .output/*.skel.rs
2.xxx.skel.rs 舉例解析
下面是對生成的 xxx.skel.rs 舉例分析:
① 構建骨架配置
fn build_skel_config() -> libbpf_rs::Result<libbpf_rs::skeleton::ObjectSkeletonConfig<'static>> {
let mut builder = libbpf_rs::skeleton::ObjectSkeletonConfigBuilder::new(DATA);
builder
.name("xxx_bpf")
.map("preemptTime", false)//你創建的map
.map("rb", false)//創建的環形緩衝區
.prog("sched_switch")//tracepoint掛載點
.prog("finish_task_switch");//kprobe掛載點
builder.build()
}
通過這個函數構建 eBPF 骨架配置,DATA
是 eBPF 對象文件的字節數組。通過 ObjectSkeletonConfigBuilder
,可以配置骨架的名稱、map 和程序。這裏配置了兩個映 map(preemptTime
和 rb
),即我們定義的 map 和 Ringbuffer;兩個程序(sched_switch
和 finish_task_switch
), 即我們定義的掛載點。
② 骨架構建器
pub struct xxxSkelBuilder {
pub obj_builder: libbpf_rs::ObjectBuilder,
}
impl<'a> xxxSkelBuilder {
pub fn open(mut self) -> libbpf_rs::Result<OpenxxxSkel<'a>> {
let mut skel_config = build_skel_config()?;
let open_opts = self.obj_builder.opts(std::ptr::null());
let ret =unsafe { libbpf_sys::bpf_object__open_skeleton(skel_config.get(), &open_opts) };
if ret != 0 {
return Err(libbpf_rs::Error::System(-ret));
}
let obj = unsafe { libbpf_rs::OpenObject::from_ptr(skel_config.object_ptr())? };
Ok(OpenxxxSkel { obj, skel_config })
}
pub fn open_opts(
self,
open_opts: libbpf_sys::bpf_object_open_opts,
) -> libbpf_rs::Result<OpenxxxSkel<'a>> {
let mut skel_config = build_skel_config()?;
let ret =
unsafe { libbpf_sys::bpf_object__open_skeleton(skel_config.get(), &open_opts) };
if ret != 0 {
return Err(libbpf_rs::Error::System(-ret));
}
let obj = unsafe { libbpf_rs::OpenObject::from_ptr(skel_config.object_ptr())? };
Ok(OpenxxxSkel { obj, skel_config })
}
}
這段代碼定義了 xxxSkelBuilder
結構體及其方法,用於通過 libbpf-rs
構建和打開 eBPF 骨架。xxxSkelBuilder
提供了兩種方法來打開骨架:open
方法使用默認選項,open_opts
方法允許自定義打開選項。兩者均通過調用 build_skel_config
構建骨架配置,並使用 bpf_object__open_skeleton
函數打開 eBPF 對象文件,最後將對象指針轉換爲 OpenObject
實例,並返回包含骨架配置的 OpenxxxSkel
實例。
③ 打開骨架
pub struct OpenxxxSkel<'a> {
pub obj: libbpf_rs::OpenObject,
skel_config: libbpf_rs::skeleton::ObjectSkeletonConfig<'a>,
}
impl<'a> OpenxxxSkel<'a> {
pub fn load(mut self) -> libbpf_rs::Result<xxxSkel<'a>> {
let ret = unsafe { libbpf_sys::bpf_object__load_skeleton(self.skel_config.get()) };
if ret != 0 {
return Err(libbpf_rs::Error::System(-ret));
}
let obj = unsafe { libbpf_rs::Object::from_ptr(self.obj.take_ptr())? };
Ok(xxxSkel {
obj,
skel_config: self.skel_config,
links: xxxLinks::default(),
})
}
pub fn progs(&self) -> OpenxxxProgs {
OpenxxxProgs { inner: &self.obj }
}
pub fn progs_mut(&mut self) -> OpenxxxProgsMut {
OpenxxxProgsMut {
inner: &mut self.obj,
}
}
pub fn maps(&self) -> OpenxxxMaps {
OpenxxxMaps { inner: &self.obj }
}
pub fn maps_mut(&mut self) -> OpenxxxMapsMut {
OpenxxxMapsMut {
inner: &mut self.obj,
}
}
}
這部分定義了 OpenxxxSkel
結構體及其方法,用於管理和操作已打開的 eBPF 骨架。load
方法加載 eBPF 骨架,並返回一個 xxxSkel
實例。progs
和 progs_mut
方法分別用於訪問和可變訪問 eBPF 程序,而 maps
和 maps_mut
方法則用於訪問和可變訪問 eBPF map。
④ 加載骨架
pub struct xxxSkel<'a> {
pub obj: libbpf_rs::Object,
skel_config: libbpf_rs::skeleton::ObjectSkeletonConfig<'a>,
pub links: xxxLinks,
}
unsafe impl<'a> Send for xxxSkel<'a> {}
impl<'a> xxxSkel<'a> {
pub fn progs(&self) -> xxxProgs {
xxxProgs { inner: &self.obj }
}
pub fn progs_mut(&mut self) -> xxxProgsMut {
xxxProgsMut {
inner: &mut self.obj,
}
}
pub fn maps(&self) -> xxxMaps {
xxxMaps { inner: &self.obj }
}
pub fn maps_mut(&mut self) -> xxxMapsMut {
xxxMapsMut {
inner: &mut self.obj,
}
}
pub fn attach(&mut self) -> libbpf_rs::Result<()> {
let ret = unsafe { libbpf_sys::bpf_object__attach_skeleton(self.skel_config.get()) };
if ret != 0 {
return Err(libbpf_rs::Error::System(-ret));
}
self.links = xxxLinks {
sched_switch: (|| {
let ptr = self.skel_config.prog_link_ptr(0)?;
if ptr.is_null() {
Ok(None)
} else {
Ok(Some(unsafe { libbpf_rs::Link::from_ptr(ptr) }))
}
})()?,
finish_task_switch: (|| {
let ptr = self.skel_config.prog_link_ptr(1)?;
if ptr.is_null() {
Ok(None)
} else {
Ok(Some(unsafe { libbpf_rs::Link::from_ptr(ptr) }))
}
})()?,
};
Ok(())
}
}
這部分定義了 xxxSkel
結構體及其方法,用於管理、操作和附加 eBPF 骨架。progs
和 progs_mut
方法分別用於訪問和可變訪問 eBPF 程序,maps
和 maps_mut
方法用於訪問和可變訪問 eBPF 映射。attach
方法用於將 eBPF 程序附加到系統掛載點,並更新 links
字段,以便管理程序鏈接。通過這些方法,用戶可以方便地操作 eBPF 對象和配置,實現複雜的 eBPF 程序管理和交互。
- 使用 libbpf-rs 進行 eBPF 程序用戶態交互
main.rs
是我們 eBPF 程序的用戶態部分,負責從內核提取數據、處理並輸出。libbpf-rs
提供了一組功能強大且易於使用的 Rust 接口,用於加載、管理和與 eBPF 程序交互。
libbpf-rs
的入口點是 ObjectBuilder
,用於打開 BPF 對象文件。打開對象文件後,將返回一個 OpenObject
。在這個階段可以執行所有預加載操作。預加載是指在內核加載和驗證任何 BPF 映射或 BPF 程序之前進行的操作。加載 BPF 對象後,會返回一個 Object
實例,在其中可以讀取 / 寫入 BPF 映射、附加 BPF 程序到鉤子等操作。
04
示例代碼
以下是一個示例,說明如何在用戶態使用 libbpf-rs
與內核態進行交互:
1. 如何加載和附加 eBPF 程序
首先導入處理 eBPF 骨架和 Ringbuffer 的接口:
use libbpf_rs::RingBufferBuilder;//使用Ringbuffer
use libbpf_rs::MapFlags;;//使用map
use xxx::xxxSkelBuilder;
接着在用戶態代碼中,首先創建 xxxSkelBuilder 實例,再調用 open 和 load 方法打開並加載 eBPF 骨架,最後調用 attach()
方法,將 eBPF 程序附加到適當的內核鉤子上,使其開始運行:
let skel_builder = xxxSkelBuilder::default(); // 創建 xxxSkelBuilder 實例
let open_skel = skel_builder.open()?; // 打開 BPF 骨架
let mut skel = open_skel.load()?; // 加載 BPF 骨架
skel.attach()?; // 附加 BPF 程序
2. 如何與內核態進行交互
① 使用 Ringbuffer
let mut ring_buffer_builder = RingBufferBuilder::new();
ring_buffer_builder.add(skel.maps().rb(), handle_event)?;
let ring_buffer = ring_buffer_builder.build()?;
while !unsafe { EXITING } {
ring_buffer.poll(Duration::from_millis(100))?;
sleep(Duration::from_millis(100));
}
首先創建一個新的 RingBufferBuilder
實例,然後將事件處理器(handle_event
函數)添加到 RingBufferBuilder
中。skel.maps().rb()
返回 eBPF 程序中定義的 ring buffer 映射。handle_event
是一個用於處理從 ring buffer 中讀取的數據的回調函數。當有數據到達時,RingBufferBuilder
會調用 handle_event
來處理這些數據。我們可以自定義 handle_event
函數,以實現如何處理從 ring buffer 接收到的數據。
**完整示例代碼可參考:**https://github.com/libbpf/libbpf-rs/tree/master/examples/tc_port_whitelist
② 使用 map
如果在內核態使用了 map 作爲與用戶態進行交互的數據結構,在用戶態則要使用如下處理方式:
let map_fd = skel.maps().ports().fd();
fn read_bpf_map(fd: i32) -> Result<HashMap<u32, u16>> {
let mut map = HashMap::new();
let mut key = -1;
let mut next_key = 0;
let mut value = 0u16;
while unsafe { libbpf_rs::libbpf_sys::bpf_map_get_next_key(fd, &key as *const _ as *const _, &next_key as *mut _ as *mut _) } == 0 {
if unsafe { libbpf_rs::libbpf_sys::bpf_map_lookup_elem(fd, &next_key as *const _ as *const _, &mut value as *mut _ as *mut _) } == 0 {
map.insert(next_key, value);
}
key = next_key;
}
Ok(map)
}//讀取map數據
**完整示例代碼可參考:**使用 map
- 編譯運行
編譯:
cargo build
進入 target 目錄下的 debug 目錄,可看到可執行文件,執行以下命令運行:
sudo ./xxx
- 參考資料
https://github.com/libbpf/libbpf-rs
https://docs.rs/libbpf-cargo/latest/libbpf_cargo/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/sHgsPzt_14QJk0lXBnn40g