使用 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爲你的項目名稱

項目創建後,其結構如下圖所示:

項目結構

1. 內核態的 eBPF 程序 (xxx.bpf.c)

2. 用戶態程序 (main.rs)

3. 構建腳本 (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(preemptTimerb),即我們定義的 map 和 Ringbuffer;兩個程序(sched_switchfinish_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 實例。progsprogs_mut 方法分別用於訪問和可變訪問 eBPF 程序,而 mapsmaps_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 骨架。progsprogs_mut 方法分別用於訪問和可變訪問 eBPF 程序,mapsmaps_mut 方法用於訪問和可變訪問 eBPF 映射。attach 方法用於將 eBPF 程序附加到系統掛載點,並更新 links 字段,以便管理程序鏈接。通過這些方法,用戶可以方便地操作 eBPF 對象和配置,實現複雜的 eBPF 程序管理和交互。

  1. 使用 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

  1. 編譯運行

編譯:

 cargo build

進入 target 目錄下的 debug 目錄,可看到可執行文件,執行以下命令運行:

 sudo ./xxx
  1. 參考資料

https://github.com/libbpf/libbpf-rs

https://docs.rs/libbpf-cargo/latest/libbpf_cargo/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/sHgsPzt_14QJk0lXBnn40g