eBPF 完全入門指南(萬字長文)

eBPF 源於 BPF[1],本質上是處於內核中的一個高效與靈活的虛類虛擬機組件,以一種安全的方式在許多內核 hook 點執行字節碼。BPF 最初的目的是用於高效網絡報文過濾,經過重新設計,eBPF 不再侷限於網絡協議棧,已經成爲內核頂級的子系統,演進爲一個通用執行引擎。開發者可基於 eBPF 開發性能分析工具、軟件定義網絡、安全等諸多場景。本文將介紹 eBPF 的前世今生,並構建一個 eBPF 環境進行開發實踐,文中所有的代碼可以在我的 Github[2] 中找到。

技術背景

發展歷史

BPF,是類 Unix 系統上數據鏈路層的一種原始接口,提供原始鏈路層封包的收發。1992 年,Steven McCanne 和 Van Jacobson 寫了一篇名爲 The BSD Packet Filter: A New Architecture for User-level Packet Capture[3] 的論文。在文中,作者描述了他們如何在 Unix 內核實現網絡數據包過濾,這種新的技術比當時最先進的數據包過濾技術快 20 倍。

BPF 在數據包過濾上引入了兩大革新:

由於這些巨大的改進,所有的 Unix 系統都選擇採用 BPF 作爲網絡數據包過濾技術,直到今天,許多 Unix 內核的派生系統中(包括 Linux 內核)仍使用該實現。tcpdump 的底層採用 BPF 作爲底層包過濾技術,我們可以在命令後面增加 -d 來查看 tcpdump 過濾條件的底層彙編指令。

$ tcpdump -d 'ip and tcp port 8080'
(000) ldh      [12]
(001) jeq      #0x800           jt 2 jf 12
(002) ldb      [23]
(003) jeq      #0x6             jt 4 jf 12
(004) ldh      [20]
(005) jset     #0x1fff          jt 12 jf 6
(006) ldxb     4*([14]&0xf)
(007) ldh      [x + 14]
(008) jeq      #0x1f90          jt 11 jf 9
(009) ldh      [x + 16]
(010) jeq      #0x1f90          jt 11 jf 12
(011) ret      #262144
(012) ret      #0

2014 年初,Alexei Starovoitov 實現了 eBPF(extended Berkeley Packet Filter)。經過重新設計,eBPF 演進爲一個通用執行引擎,可基於此開發性能分析工具、軟件定義網絡等諸多場景。eBPF 最早出現在 3.18 內核中,此後原來的 BPF 就被稱爲經典 BPF,縮寫 cBPF(classic BPF),cBPF 現在已經基本廢棄。現在,Linux 內核只運行 eBPF,內核會將加載的 cBPF 字節碼透明地轉換成 eBPF 再執行

eBPF 與 cBPF

eBPF 新的設計針對現代硬件進行了優化,所以 eBPF 生成的指令集比舊的 BPF 解釋器生成的機器碼執行得更快。擴展版本也增加了虛擬機中的寄存器數量,將原有的 2 個 32 位寄存器增加到 10 個 64 位寄存器。由於寄存器數量和寬度的增加,開發人員可以使用函數參數自由交換更多的信息,編寫更復雜的程序。總之,這些改進使 eBPF 版本的速度比原來的 BPF 提高了 4 倍。

5olDOK

2014 年 6 月,eBPF 擴展到用戶空間,這也成爲了 BPF 技術的轉折點。正如 Alexei 在提交補丁的註釋中寫到:「這個補丁展示了 eBPF 的潛力」。當前,eBPF 不再侷限於網絡棧,已經成爲內核頂級的子系統。

eBPF 與內核模塊

對比 Web 的發展,eBPF 與內核的關係有點類似於 JavaScript 與瀏覽器內核的關係,eBPF 相比於直接修改內核和編寫內核模塊提供了一種新的內核可編程的選項。eBPF 程序架構強調安全性和穩定性,看上去更像內核模塊,但與內核模塊不同,eBPF 程序不需要重新編譯內核,並且可以確保 eBPF 程序運行完成,而不會造成系統的崩潰。

sgJYIO

eBPF 架構

eBPF 分爲用戶空間程序和內核程序兩部分:

eBPF 整體結構圖如下:

用戶空間程序與內核中的 BPF 字節碼交互的流程主要如下:

  1. 使用 LLVM 或者 GCC 工具將編寫的 BPF 代碼程序編譯成 BPF 字節碼

  2. 使用加載程序 Loader 將字節碼加載至內核

  3. 內核使用驗證器(Verfier) 組件保證執行字節碼的安全性,以避免對內核造成災難,在確認字節碼安全後將其加載對應的內核模塊執行

  4. 內核中運行的 BPF 字節碼程序可以使用兩種方式將數據回傳至用戶空間

eBPF 限制

eBPF 技術雖然強大,但是爲了保證內核的處理安全和及時響應,內核中的 eBPF 技術也給予了諸多限制,當然隨着技術的發展和演進,限制也在逐步放寬或者提供了對應的解決方案。

eBPF 實戰

在深入介紹 eBPF 特性之前,讓我們 Get Hands Dirty,切切實實的感受 eBPF 程序到底是什麼,我們該如何開發 eBPF 程序。隨着 eBPF 生態的演進,現在已經有越來越多的工具鏈用於開發 eBPF 程序,在後文也會詳細介紹:

內核源碼編譯

系統環境如下,採用騰訊雲 CVM,Ubuntu 20.04,內核版本 5.4.0

$ uname -a
Linux VM-1-3-ubuntu 5.4.0-42-generic #46-Ubuntu SMP Fri Jul 10 00:24:02 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

首先安裝必要依賴:

$ sudo apt install -y bison build-essential cmake flex git libedit-dev pkg-config libmnl-dev \
   python zlib1g-dev libssl-dev libelf-dev libcap-dev libfl-dev llvm clang pkg-config \
   gcc-multilib luajit libluajit-5.1-dev libncurses5-dev libclang-dev clang-tools

一般情況下推薦採用 apt 方式的安裝源碼,安裝簡單而且只安裝當前內核的源碼,源碼的大小在 200M 左右。

$ apt-cache search linux-source
$ apt install linux-source-5.4.0

源碼安裝至 /usr/src/ 目錄下。

$ ls -hl
total 4.0K
drwxr-xr-x 4 root root 4.0K Nov  9 13:22 linux-source-5.4.0
lrwxrwxrwx 1 root root   45 Oct 15 10:28 linux-source-5.4.0.tar.bz2 -> linux-source-5.4.0/linux-source-5.4.0.tar.bz2
$ tar -jxvf linux-source-5.4.0.tar.bz2
$ cd linux-source-5.4.0

$ cp -v /boot/config-$(uname -r) .config # make defconfig 或者 make menuconfig
$ make headers_install
$ make modules_prepare
$ make scripts     # 可選
$ make M=samples/bpf  # 如果配置出錯,可以使用 make oldconfig && make prepare 修復

編譯成功後,可以在 samples/bpf 目錄下看到一系列的目標文件和二進制文件。

Hello World

前面說到 eBPF 通常由內核空間程序和用戶空間程序兩部分組成,現在 samples/bpf 目錄下有很多這種程序,內核空間程序以 _kern.c 結尾,用戶空間程序以 _user.c 結尾。先不看這些複雜的程序,我們手動寫一個 eBPF 程序的 Hello World。

內核中的程序 hello_kern.c

#include <linux/bpf.h>
#include "bpf_helpers.h"

#define SEC(NAME) __attribute__((section(NAME), used))

SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx)
{
    char msg[] = "Hello BPF from houmin!\n";
    bpf_trace_printk(msg, sizeof(msg));
    return 0;
}

char _license[] SEC("license") = "GPL";

函數入口

上述代碼和普通的 C 語言編程有一些區別。

  1. 程序的入口通過編譯器的 pragama __section("tracepoint/syscalls/sys_enter_execve") 指定的。

  2. 入口的參數不再是 argc, argv, 它根據不同的 prog type 而有所差別。我們的例子中,prog type 是 BPF_PROG_TYPE_TRACEPOINT, 它的入口參數就是 void *ctx

頭文件

#include <linux/bpf.h>

這個頭文件的來源是 kernel source header file 。它安裝在 /usr/include/linux/bpf.h中。

它提供了 bpf 編程需要的很多 symbol。例如

  1. enum bpf_func_id 定義了所有的 kerne helper function 的 id

  2. enum bpf_prog_type 定義了內核支持的所有的 prog 的類型。

  3. struct __sk_buff 是 bpf 代碼中訪問內核 struct sk_buff 的接口。

等等

#include “bpf_helpers.h”

來自 libbpf ,需要自行安裝。我們引用這個頭文件是因爲調用了 bpf_printk()。這是一個 kernel helper function。

程序解釋

這裏我們簡單解讀下內核態的 ebpf 程序,非常簡單:

加載 BPF 代碼

用戶態程序 hello_user.c

#include <stdio.h>
#include "bpf_load.h"

int main(int argc, char **argv)
{
    if(load_bpf_file("hello_kern.o") != 0)
    {
        printf("The kernel didn't load BPF program\n");
        return -1;
    }

    read_trace_pipe();
    return 0;
}

在用戶態 ebpf 程序中,解讀如下:

修改 samples/bpf 目錄下的 Makefile 文件,在對應的位置添加以下三行:

hostprogs-y += hello
hello-objs := bpf_load.o hello_user.o
always += hello_kern.o

重新編譯,可以看到編譯成功的文件

$ make M=samples/bpf
$ ls -hl samples/bpf/hello*
-rwxrwxr-x 1 ubuntu ubuntu 404K Mar 30 17:48 samples/bpf/hello
-rw-rw-r-- 1 ubuntu ubuntu  317 Mar 30 17:47 samples/bpf/hello_kern.c
-rw-rw-r-- 1 ubuntu ubuntu 3.8K Mar 30 17:48 samples/bpf/hello_kern.o
-rw-rw-r-- 1 ubuntu ubuntu  246 Mar 30 17:47 samples/bpf/hello_user.c
-rw-rw-r-- 1 ubuntu ubuntu 2.2K Mar 30 17:48 samples/bpf/hello_user.o

進入到對應的目錄運行 hello 程序,可以看到輸出結果如下:

$ sudo ./hello
           <...>-102735 [001] ....  6733.481740: 0: Hello BPF from houmin!

           <...>-102736 [000] ....  6733.482884: 0: Hello BPF from houmin!

           <...>-102737 [002] ....  6733.483074: 0: Hello BPF from houmin!

代碼解讀

前面提到 load_bpf_file 函數將 LLVM 編譯出來的 eBPF 字節碼加載進內核,這到底是如何實現的呢?

static int load_and_attach(const char *event, struct bpf_insn *prog, int size)
{
  bool is_socket = strncmp(event, "socket", 6) == 0;
 bool is_kprobe = strncmp(event, "kprobe/", 7) == 0;
 bool is_kretprobe = strncmp(event, "kretprobe/", 10) == 0;
 bool is_tracepoint = strncmp(event, "tracepoint/", 11) == 0;
 bool is_raw_tracepoint = strncmp(event, "raw_tracepoint/", 15) == 0;
 bool is_xdp = strncmp(event, "xdp", 3) == 0;
 bool is_perf_event = strncmp(event, "perf_event", 10) == 0;
 bool is_cgroup_skb = strncmp(event, "cgroup/skb", 10) == 0;
 bool is_cgroup_sk = strncmp(event, "cgroup/sock", 11) == 0;
 bool is_sockops = strncmp(event, "sockops", 7) == 0;
 bool is_sk_skb = strncmp(event, "sk_skb", 6) == 0;
 bool is_sk_msg = strncmp(event, "sk_msg", 6) == 0;

  //...

 fd = bpf_load_program(prog_type, prog, insns_cnt, license, kern_version,
         bpf_log_buf, BPF_LOG_BUF_SIZE);
 if (fd < 0) {
  printf("bpf_load_program() err=%d\n%s", errno, bpf_log_buf);
  return -1;
 }
  //...
}

eBPF 特性

Hook Overview

eBPF 程序都是事件驅動的,它們會在內核或者應用程序經過某個確定的 Hook 點的時候運行,這些 Hook 點都是提前定義的,包括系統調用、函數進入 / 退出、內核 tracepoints、網絡事件等。

如果針對某個特定需求的 Hook 點不存在,可以通過 kprobe 或者 uprobe 來在內核或者用戶程序的幾乎所有地方掛載 eBPF 程序。

Verification

With great power there must also come great responsibility.

每一個 eBPF 程序加載到內核都要經過 Verification,用來保證 eBPF 程序的安全性,主要包括:

echo 1 > /proc/sys/kernel/unprivileged_bpf_disabled

JIT Compilation

Just-In-Time(JIT) 編譯用來將通用的 eBPF 字節碼翻譯成與機器相關的指令集,從而極大加速 BPF 程序的執行:

64 位的 x86_64arm64ppc64s390xmips64sparc64 和 32 位的 armx86_32 架構都內置了 in-kernel eBPF JIT 編譯器,它們的功能都是一樣的,可以用如下方式打開:

echo 1 > /proc/sys/net/core/bpf_jit_enable

32 位的 mipsppcsparc 架構目前內置的是一個 cBPF JIT 編譯器。這些只有 cBPF JIT 編譯器的架構,以及那些甚至完全沒有 BPF JIT 編譯器的架構,需要通過內核中的解釋器(in-kernel interpreter)執行 eBPF 程序。

要判斷哪些平臺支持 eBPF JIT,可以在內核源文件中 grep HAVE_EBPF_JIT

$ git grep HAVE_EBPF_JIT arch/
arch/arm/Kconfig:       select HAVE_EBPF_JIT   if !CPU_ENDIAN_BE32
arch/arm64/Kconfig:     select HAVE_EBPF_JIT
arch/powerpc/Kconfig:   select HAVE_EBPF_JIT   if PPC64
arch/mips/Kconfig:      select HAVE_EBPF_JIT   if (64BIT && !CPU_MICROMIPS)
arch/s390/Kconfig:      select HAVE_EBPF_JIT   if PACK_STACK && HAVE_MARCH_Z196_FEATURES
arch/sparc/Kconfig:     select HAVE_EBPF_JIT   if SPARC64
arch/x86/Kconfig:       select HAVE_EBPF_JIT   if X86_64

Maps

BPF Map 是駐留在內核空間中的高效 Key/Value store,包含多種類型的 Map,由內核實現其功能,具體實現可以參考 我的這篇博文 [10]。

BPF Map 的交互場景有以下幾種:

共享 map 的 BPF 程序不要求是相同的程序類型,例如 tracing 程序可以和網絡程序共享 map,單個 BPF 程序目前最多可直接訪問 64 個不同 map

當前可用的 通用 map 有:

以上 map 都使用相同的一組 BPF 輔助函數來執行查找、更新或刪除操作,但各自實現了不同的後端,這些後端各有不同的語義和性能特點。隨着多 CPU 架構的成熟發展,BPF Map 也引入了 per-cpu 類型,如BPF_MAP_TYPE_PERCPU_HASHBPF_MAP_TYPE_PERCPU_ARRAY等,當你使用這種類型的 BPF Map 時,每個 CPU 都會存儲並看到它自己的 Map 數據,從屬於不同 CPU 之間的數據是互相隔離的,這樣做的好處是,在進行查找和聚合操作時更加高效,性能更好,尤其是你的 BPF 程序主要是在做收集時間序列型數據,如流量數據或指標等。

當前內核中的 非通用 map 有:

Helper Calls

eBPF 程序不能夠隨意調用內核函數,如果這麼做的話會導致 eBPF 程序與特定的內核版本綁定,相反它內核定義的一系列 Helper functionsHelper functions 使得 BPF 能夠通過一組內核定義的穩定的函數調用來從內核中查詢數據,或者將數據推送到內核。所有的 BPF 輔助函數都是核心內核的一部分,無法通過內核模塊來擴展或添加當前可用的 BPF 輔助函數已經有幾十個,並且數量還在不斷增加,你可以在 Linux Manual Page: bpf-helpers[11] 看到當前 Linux 支持的 Helper functions

不同類型的 BPF 程序能夠使用的輔助函數可能是不同的,例如:

所有的輔助函數都共享同一個通用的、和系統調用類似的函數方法,其定義如下:

u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)

內核將輔助函數抽象成 BPF_CALL_0()BPF_CALL_5() 幾個宏,形式和相應類型的系統調用類似,這裏宏的定義可以參見 include/linux/filter.h[12] 。以 bpf_map_update_elem[13] 爲例,可以看到它通過調用相應 map 的回調函數完成更新 map 元素的操作:

/* /kernel/bpf/helpers.c */
BPF_CALL_4(bpf_map_update_elem, struct bpf_map *, map, void *, key,
           void *, value, u64, flags)
{
    WARN_ON_ONCE(!rcu_read_lock_held());
    return map->ops->map_update_elem(map, key, value, flags);
}

const struct bpf_func_proto bpf_map_update_elem_proto = {
    .func           = bpf_map_update_elem,
    .gpl_only       = false,
    .ret_type       = RET_INTEGER,
    .arg1_type      = ARG_CONST_MAP_PTR,
    .arg2_type      = ARG_PTR_TO_MAP_KEY,
    .arg3_type      = ARG_PTR_TO_MAP_VALUE,
    .arg4_type      = ARG_ANYTHING,
};

這種方式有很多優點:

雖然 cBPF 允許其加載指令(load instructions)進行超出範圍的訪問(overload),以便從一個看似不可能的包偏移量(packet offset)獲取數據以喚醒多功能輔助函數,但每個 cBPF JIT 仍然需要爲這個 cBPF 擴展實現對應的支持。而在 eBPF 中,JIT 編譯器會以一種透明和高效的方式編譯新加入的輔助函數,這意味着 JIT 編 譯器只需要發射(emit)一條調用指令(call instruction),因爲寄存器映射的方式使得 BPF 排列參數的方式(assignments)已經和底層架構的調用約定相匹配了。這使得基於輔助函數擴展核心內核(core kernel)非常方便。所有的 BPF 輔助函數都是核心內核的一部分,無法通過內核模塊(kernel module)來擴展或添加

前面提到的函數簽名還允許校驗器執行類型檢測(type check)。上面的 struct bpf_func_proto 用於存放校驗器必需知道的所有關於該輔助函數的信息,這 樣校驗器可以確保輔助函數期望的類型和 BPF 程序寄存器中的當前內容是匹配的。

參數類型範圍很廣,從任意類型的值,到限制只能爲特定類型,例如 BPF 棧緩衝區(stack buffer)的 pointer/size 參數對,輔助函數可以從這個位置讀取數據或向其寫入數據。對於這種情況,校驗器還可以執行額外的檢查,例如,緩衝區是否已經初始化過了。

Tail Calls

尾調用的機制是指:一個 BPF 程序可以調用另一個 BPF 程序,並且調用完成後不用返回到原來的程序。

BPF to BPF Calls

除了 BPF 輔助函數和 BPF 尾調用之外,BPF 核心基礎設施最近剛加入了一個新特性:BPF to BPF calls。** 在這個特性引入內核之前,典型的 BPF C 程序必須 將所有需要複用的代碼進行特殊處理,例如,在頭文件中聲明爲 always_inline**。當 LLVM 編譯和生成 BPF 對象文件時,所有這些函數將被內聯,因此會在生成的對象文件中重 復多次,導致代碼尺寸膨脹:

#include <linux/bpf.h>

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif

#ifndef __inline
# define __inline                         \
   inline __attribute__((always_inline))
#endif

static __inline int foo(void)
{
    return XDP_DROP;
}

__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
    return foo();
}

char __license[] __section("license") = "GPL";

之所以要這樣做是因爲 BPF 程序的加載器、校驗器、解釋器和 JIT 中都缺少對函數調用的支持。從 Linux 4.16LLVM 6.0 開始,這個限制得到了解決,BPF 程序不再需要到處使用 always_inline 聲明瞭。因此,上面的代碼可以更自然地重寫爲:

#include <linux/bpf.h>

#ifndef __section
# define __section(NAME)                  \
   __attribute__((section(NAME), used))
#endif

static int foo(void)
{
    return XDP_DROP;
}

__section("prog")
int xdp_drop(struct xdp_md *ctx)
{
    return foo();
}

char __license[] __section("license") = "GPL";

BPF 到 BPF 調用是一個重要的性能優化,極大減小了生成的 BPF 代碼大小,因此 對 CPU 指令緩存(instruction cache,i-cache)更友好

BPF 輔助函數的調用約定也適用於 BPF 函數間調用:

當前,BPF 函數間調用和 BPF 尾調用是不兼容的,因爲後者需要複用當前的棧設置( stack setup),而前者會增加一個額外的棧幀,因此不符合尾調用期望的佈局。

BPF JIT 編譯器爲每個函數體發射獨立的鏡像(emit separate images for each function body),稍後在最後一通 JIT 處理(final JIT pass)中再修改鏡像中函數調用的地址 。已經證明,這種方式需要對各種 JIT 做最少的修改,因爲在實現中它們可以將 BPF 函數間調用當做常規的 BPF 輔助函數調用。

Object Pinning

BPF map 和程序作爲內核資源只能通過文件描述符訪問,其背後是內核中的匿名 inode。 這帶來了很多優點:

但同時,文件描述符受限於進程的生命週期,使得 map 共享之類的操作非常笨重,這給某些特定的場景帶來了很多複雜性。

例如 iproute2,其中的 tc 或 XDP 在準備環境、加載程序到內核之後最終會退出。在這種情況下,從用戶空間也無法訪問這些 map 了,而本來這些 map 其實是很有用的。例如,在 data path 的 ingress 和 egress 位置共享的 map(可以統計包數、字節數、PPS 等信息)。另外,第三方應用可能希望在 BPF 程序運行時監控或更新 map。

爲了解決這個問題,內核實現了一個最小內核空間 BPF 文件系統,BPF map 和 BPF 程序 都可以 pin 到這個文件系統內,這個過程稱爲 object pinning。BPF 相關的文件系統不是單例模式(singleton),它支持多掛載實例、硬鏈接、軟連接等等。

相應的,BPF 系統調用擴展了兩個新命令,如下圖所示:

Hardening

Protection Execution Protection

爲了避免代碼被損壞,BPF 會在程序的生命週期內,在內核中將 BPF 解釋器解釋後的整個鏡像struct bpf_prog)和 JIT 編譯之後的鏡像struct bpf_binary_header)鎖定爲只讀的。在這些位置發生的任何數據損壞(例如由於某些內核 bug 導致的)會觸發通用的保護機制,因此會造成內核崩潰而不是允許損壞靜默地發生。

查看哪些平臺支持將鏡像內存(image memory)設置爲只讀的,可以通過下面的搜索:

$ git grep ARCH_HAS_SET_MEMORY | grep select
arch/arm/Kconfig:    select ARCH_HAS_SET_MEMORY
arch/arm64/Kconfig:  select ARCH_HAS_SET_MEMORY
arch/s390/Kconfig:   select ARCH_HAS_SET_MEMORY
arch/x86/Kconfig:    select ARCH_HAS_SET_MEMORY

CONFIG_ARCH_HAS_SET_MEMORY 選項是不可配置的,因此平臺要麼內置支持,要麼不支持,那些目前還不支持的架構未來可能也會支持。

Mitigation Against Spectre

爲了防禦 👉Spectre v2 攻擊,Linux 內核提供了 CONFIG_BPF_JIT_ALWAYS_ON 選項,打開這個開關後 BPF 解釋器將會從內核中完全移除,永遠啓用 JIT 編譯器:

/proc/sys/net/core/bpf_jit_harden 設置爲 1 會爲非特權用戶的 JIT 編譯做一些額外的加固工作。這些額外加固會稍微降低程序的性能,但在有非受信用戶在系統上進行操作的情況下,能夠有效地減小潛在的受攻擊面。但與完全切換到解釋器相比,這些性能損失還是比較小的。對於 x86_64 JIT 編譯器,如果設置了 CONFIG_RETPOLINE,尾調用的間接跳轉( indirect jump)就會用 retpoline 實現。寫作本文時,在大部分現代 Linux 發行版上這個配置都是打開的。

Constant Blinding

當前,啓用加固會在 JIT 編譯時盲化(blind)BPF 程序中用戶提供的所有 32 位和 64 位常量,以防禦 JIT spraying 攻擊,這些攻擊會將原生操作碼作爲立即數注入到內核。這種攻擊有效是因爲:立即數駐留在可執行內核內存(executable kernel memory)中,因此某些內核 bug 可能會觸發一個跳轉動作,如果跳轉到立即數的開始位置,就會把它們當做原生指令開始執行。

盲化 JIT 常量通過對真實指令進行隨機化(randomizing the actual instruction)實現 。在這種方式中,通過對指令進行重寫,將原來基於立即數的操作轉換成基於寄存器的操作。指令重寫將加載值的過程分解爲兩部分:

  1. 加載一個盲化後的(blinded)立即數 rnd ^ imm 到寄存器

  2. 將寄存器和 rnd 進行異或操作(xor)

這樣原始的 imm 立即數就駐留在寄存器中,可以用於真實的操作了。這裏介紹的只是加載操作的盲化過程,實際上所有的通用操作都被盲化了。下面是加固關閉的情況下,某個程序的 JIT 編譯結果:

echo 0 > /proc/sys/net/core/bpf_jit_harden

  ffffffffa034f5e9 + <x>:
  [...]
  39:   mov    $0xa8909090,%eax
  3e:   mov    $0xa8909090,%eax
  43:   mov    $0xa8ff3148,%eax
  48:   mov    $0xa89081b4,%eax
  4d:   mov    $0xa8900bb0,%eax
  52:   mov    $0xa810e0c1,%eax
  57:   mov    $0xa8908eb4,%eax
  5c:   mov    $0xa89020b0,%eax
  [...]

加固打開之後,以上程序被某個非特權用戶通過 BPF 加載的結果(這裏已經進行了常量盲化):

echo 1 > /proc/sys/net/core/bpf_jit_harden

  ffffffffa034f1e5 + <x>:
  [...]
  39:   mov    $0xe1192563,%r10d
  3f:   xor    $0x4989b5f3,%r10d
  46:   mov    %r10d,%eax
  49:   mov    $0xb8296d93,%r10d
  4f:   xor    $0x10b9fd03,%r10d
  56:   mov    %r10d,%eax
  59:   mov    $0x8c381146,%r10d
  5f:   xor    $0x24c7200e,%r10d
  66:   mov    %r10d,%eax
  69:   mov    $0xeb2a830e,%r10d
  6f:   xor    $0x43ba02ba,%r10d
  76:   mov    %r10d,%eax
  79:   mov    $0xd9730af,%r10d
  7f:   xor    $0xa5073b1f,%r10d
  86:   mov    %r10d,%eax
  89:   mov    $0x9a45662b,%r10d
  8f:   xor    $0x325586ea,%r10d
  96:   mov    %r10d,%eax
  [...]

兩個程序在語義上是一樣的,但在第二種方式中,原來的立即數在反彙編之後的程序中不再可見。同時,加固還會禁止任何 JIT 內核符合(kallsyms)暴露給特權用戶,JIT 鏡像地址不再出現在 /proc/kallsyms 中。

Offloads

BPF 網絡程序,尤其是 tc 和 XDP BPF 程序在內核中都有一個 offload 到硬件的接口,這樣就可以直接在網卡上執行 BPF 程序。

當前,Netronome 公司的 nfp 驅動支持通過 JIT 編譯器 offload BPF,它會將 BPF 指令翻譯成網卡實現的指令集。另外,它還支持將 BPF maps offload 到網卡,因此 offloaded BPF 程序可以執行 map 查找、更新和刪除操作。

eBPF 接口

BPF 系統調用

eBPF 提供了 bpf()[14] 系統調用來對 BPF Map 或 程序進行操作,其函數原型如下:

#include <linux/bpf.h>
int bpf(int cmd, union bpf_attr *attr, unsigned int size);

函數有三個參數,其中:

cmd 可以爲一下幾種類型,基本上可以分爲操作 eBPF Map 和操作 eBPF 程序兩種類型:

bpf_attr union 的結構如下所示,根據不同的 cmd 可以填充不同的信息。

union bpf_attr {
  struct {    /* Used by BPF_MAP_CREATE */
    __u32         map_type;
    __u32         key_size;    /* size of key in bytes */
    __u32         value_size;  /* size of value in bytes */
    __u32         max_entries; /* maximum number of entries in a map */
  };

  struct {    /* Used by BPF_MAP_*_ELEM and BPF_MAP_GET_NEXT_KEY commands */
    __u32         map_fd;
    __aligned_u64 key;
    union {
      __aligned_u64 value;
      __aligned_u64 next_key;
    };
    __u64         flags;
  };

  struct {    /* Used by BPF_PROG_LOAD */
    __u32         prog_type;
    __u32         insn_cnt;
    __aligned_u64 insns;      /* 'const struct bpf_insn *' */
    __aligned_u64 license;    /* 'const char *' */
    __u32         log_level;  /* verbosity level of verifier */
    __u32         log_size;   /* size of user buffer */
    __aligned_u64 log_buf;    /* user supplied 'char *' buffer */
    __u32         kern_version; /* checked when prog_type=kprobe (since Linux 4.1) */
  };
} __attribute__((aligned(8)));

使用 eBPF 程序的命令

BPF_PROG_LOAD 命令用於校驗和加載 eBPF 程序,其需要填充的參數 bpf_xattr,下面展示了在 libbpfbpf_load_program[15] 的實現,可以看到最終是調用了 bpf 系統調用。

/* /tools/lib/bpf/bpf.c */
int bpf_load_program(enum bpf_prog_type type, const struct bpf_insn *insns,
       size_t insns_cnt, const char *license,
       __u32 kern_version, char *log_buf,
       size_t log_buf_sz)
{
 struct bpf_load_program_attr load_attr;

 memset(&load_attr, 0, sizeof(struct bpf_load_program_attr));
 load_attr.prog_type = type;
 load_attr.expected_attach_type = 0;
 load_attr.name = NULL;
 load_attr.insns = insns;
 load_attr.insns_cnt = insns_cnt;
 load_attr.license = license;
 load_attr.kern_version = kern_version;

 return bpf_load_program_xattr(&load_attr, log_buf, log_buf_sz);
}

int bpf_load_program_xattr(const struct bpf_load_program_attr *load_attr,
      char *log_buf, size_t log_buf_sz)
{
  // ...
  fd = sys_bpf_prog_load(&attr, sizeof(attr));
 if (fd >= 0)
  return fd;
  // ...
}

static inline int sys_bpf_prog_load(union bpf_attr *attr, unsigned int size)
{
 int fd;

 do {
  fd = sys_bpf(BPF_PROG_LOAD, attr, size);
 } while (fd < 0 && errno == EAGAIN);

 return fd;
}

使用 eBPF Map 的命令

和前面一樣,查看 libbpfbpf_create_map[16] 的實現,可以看到最終也調用了 bpf 系統調用:

/* /tools/lib/bpf/bpf.c */
int bpf_create_map(enum bpf_map_type map_type, int key_size,
     int value_size, int max_entries, __u32 map_flags)
{
 struct bpf_create_map_attr map_attr = {};

 map_attr.map_type = map_type;
 map_attr.map_flags = map_flags;
 map_attr.key_size = key_size;
 map_attr.value_size = value_size;
 map_attr.max_entries = max_entries;

 return bpf_create_map_xattr(&map_attr);
}

int bpf_create_map_xattr(const struct bpf_create_map_attr *create_attr)
{
 union bpf_attr attr;

 memset(&attr, '\0', sizeof(attr));

 attr.map_type = create_attr->map_type;
 attr.key_size = create_attr->key_size;
 attr.value_size = create_attr->value_size;
 attr.max_entries = create_attr->max_entries;
 attr.map_flags = create_attr->map_flags;
 if (create_attr->name)
  memcpy(attr.map_name, create_attr->name,
         min(strlen(create_attr->name), BPF_OBJ_NAME_LEN - 1));
 attr.numa_node = create_attr->numa_node;
 attr.btf_fd = create_attr->btf_fd;
 attr.btf_key_type_id = create_attr->btf_key_type_id;
 attr.btf_value_type_id = create_attr->btf_value_type_id;
 attr.map_ifindex = create_attr->map_ifindex;
 attr.inner_map_fd = create_attr->inner_map_fd;

 return sys_bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
}

libbpfbpf_map_lookup_elem[17] 的實現:

/* /tools/lib/bpf/bpf.c */
int bpf_map_lookup_elem(int fd, const void *key, void *value)
{
 union bpf_attr attr;

 memset(&attr, 0, sizeof(attr));
 attr.map_fd = fd;
 attr.key = ptr_to_u64(key);
 attr.value = ptr_to_u64(value);

 return sys_bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}

libbpfbpf_map_update_elem[18] 的實現:

/* /tools/lib/bpf/bpf.c */
int bpf_map_update_elem(int fd, const void *key, const void *value,
   __u64 flags)
{
 union bpf_attr attr;

 memset(&attr, 0, sizeof(attr));
 attr.map_fd = fd;
 attr.key = ptr_to_u64(key);
 attr.value = ptr_to_u64(value);
 attr.flags = flags;

 return sys_bpf(BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
}

libbpfbpf_map_delete_elem[19] 的實現:

/* /tools/lib/bpf/bpf.c */
int bpf_map_delete_elem(int fd, const void *key)
{
 union bpf_attr attr;

 memset(&attr, 0, sizeof(attr));
 attr.map_fd = fd;
 attr.key = ptr_to_u64(key);

 return sys_bpf(BPF_MAP_DELETE_ELEM, &attr, sizeof(attr));
}

libbpfbpf_map_get_next_key[20] 的實現:

/* /tools/lib/bpf/bpf.c */
int bpf_map_get_next_key(int fd, const void *key, void *next_key)
{
 union bpf_attr attr;

 memset(&attr, 0, sizeof(attr));
 attr.map_fd = fd;
 attr.key = ptr_to_u64(key);
 attr.next_key = ptr_to_u64(next_key);

 return sys_bpf(BPF_MAP_GET_NEXT_KEY, &attr, sizeof(attr));
}

注意,這裏的 libbpf 函數和之前提到的 helper functions 還不太一樣,你可以在 Linux Manual Page: bpf-helpers[21] 看到當前 Linux 支持的 Helper functions。以 bpf_map_update_elem 爲例,eBPF 程序通過調用 helper function,其參數如下:

struct msg {
 __s32 seq;
 __u64 cts;
 __u8 comm[MAX_LENGTH];
};

struct bpf_map_def SEC("maps") map = {
 .type = BPF_MAP_TYPE_PERF_EVENT_ARRAY,
 .key_size = sizeof(int),
 .value_size = sizeof(__u32),
 .max_entries = 0,
};

void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)

這裏的第一個參數來自於 SEC(".maps") 語法糖創建的 bpf_map

對於用戶態程序,則其函數原型如下,其中通過 fd 來訪問 eBPF map。

int bpf_map_lookup_elem(int fd, const void *key, void *value)

BPF 程序類型

函數BPF_PROG_LOAD加載的程序類型規定了四件事:

實際上,程序類型本質上定義了一個 API。甚至還創建了新的程序類型,以區分允許調用的不同的函數列表(比如BPF_PROG_TYPE_CGROUP_SKB 對比 BPF_PROG_TYPE_SOCKET_FILTER)。

bpf 程序會被 hook 到內核不同的 hook 點上。不同的 hook 點的入口參數,能力有所不同。因而定義了不同的 prog type。不同的 prog type 的 bpf 程序能夠調用的 kernel function 集合也不一樣。當 bpf 程序加載到內核時,內核的 verifier 程序會根據 bpf prog type,檢查程序的入口參數,調用了哪些 helper function。

目前內核支持的 eBPF 程序類型列表如下所示:

隨着新程序類型的添加,內核開發人員同時發現也需要添加新的數據結構。

舉個例子 BPF_PROG_TYPE_SCHED_CLS bpf prog , 能夠訪問哪些 bpf helper function 呢?讓我們來看看源代碼是如何實現的。

每一種 prog type 會定義一個 struct bpf_verifier_ops 結構體。當 prog load 到內核時,內核會根據它的 type,調用相應結構體的 get_func_proto 函數。

const struct bpf_verifier_ops tc_cls_act_verifier_ops = {
        .get_func_proto         = tc_cls_act_func_proto,
    .convert_ctx_access     = tc_cls_act_convert_ctx_access,
};

對於 BPF_PROG_TYPE_SCHED_CLS 類型的 BPF 代碼,verifier 會調用 tc_cls_act_func_proto ,以檢查程序調用的 helper function 是否都是合法的。

BPF 代碼調用時機

每一種 prog type 的調用時機都不同。

BPF_PROG_TYPE_SCHED_CLS

BPF_PROG_TYPE_SCHED_CLS 的調用過程如下。

Egress 方向

egress 方向上,tcp/ip 協議棧運行之後,有一個 hook 點。這個 hook 點可以 attach BPF_PROG_TYPE_SCHED_CLS type 的 egress 方向的 bpf prog。在這段 bpf 代碼執行之後,纔會運行 qos,tcpdump, xmit 到網卡 driver 的代碼。在這段 bpf 代碼中你可以修改報文裏面的內容,地址等。修改之後,通過 tcpdump 可以看到,因爲 tcpdump 代碼在此之後才執行。

static int __dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)

{
   skb = sch_handle_egress(skb, &rc, dev);
   // enqueue tc qos
   // dequeue tc qos
   // dev_hard_start_xmit
   // tcpdump works here! dev_queue_xmit_nit
   // nic driver->ndo_start_xmit
}
Ingress 方向

ingress 方向上,在 deliver to tcp/ip 協議棧之前,在 tcpdump 之後,有一個 hook 點。這個 hook 點可以 attach BPF_PROG_TYPE_SCHED_CLS type 的 ingress 方向的 bpf prog。在這裏你也可以修改報文。但是修改之後的結果在 tcpdump 中是看不到的。

static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
                                    struct packet_type **ppt_prev)
{
  // generic xdp bpf hook
  // tcpdump
  // tc ingress hook
  skb = sch_handle_ingress(skb, &pt_prev, &ret, orig_dev, &another);
  // deliver to tcp/ip stack or bridge/ipvlan device
}
執行入口 cls_bpf_classify

無論 egress 還是 ingress 方向,真正執行 bpf 指令的入口都是 cls_bpf_classify。它遍歷 tcf_proto 中的 bpf prog link list, 對每一個 bpf prog 執行 BPF_PROG_RUN(prog->filter, skb)

static int cls_bpf_classify(struct sk_buff *skb, const struct tcf_proto *tp,
                            struct tcf_result *res)
{
  struct cls_bpf_head *head = rcu_dereference_bh(tp->root);
  struct cls_bpf_prog *prog;

  list_for_each_entry_rcu(prog, &head->plist, link) {
                int filter_res;
        if (tc_skip_sw(prog->gen_flags)) {
                        filter_res = prog->exts_integrated ? TC_ACT_UNSPEC : 0;
                } else if (at_ingress) {
                        /* It is safe to push/pull even if skb_shared() */
                        __skb_push(skb, skb->mac_len);
                        bpf_compute_data_pointers(skb);
                        filter_res = BPF_PROG_RUN(prog->filter, skb);
                        __skb_pull(skb, skb->mac_len);
                } else {
                        bpf_compute_data_pointers(skb);
                        filter_res = BPF_PROG_RUN(prog->filter, skb);
                }
}

BPF_PROG_RUN 會執行 JIT compile 的 bpf 指令,如果內核不支持 JIT,則會調用解釋器執行 bpf 的 byte code。

BPF_PROG_RUN 傳給 bpf prog 的入口參數是 skb,其類型是 struct sk_buff, 定義在文件 include/linux/skbuff.h 中。

但是在 bpf 代碼中,爲了安全,不能直接訪問 sk_buff。bpf 中是通過訪問 struct __sk_buff 來訪問 struct sk_buff 的。__sk_buffsk_buff 的一個子集,是 sk_buff 面向 bpf 程序的接口。bpf 代碼中對 __sk_buff 的訪問會在 verifier 程序中翻譯成對 sk_buff 相應 fileds 的訪問。

在加載 bpf prog 的時候,verifier 會調用上面 tc_cls_act_verifier_ops 結構體裏面的 tc_cls_act_convert_ctx_access 的鉤子。它最終會調用下面的函數修改 ebpf 的指令,使得對 __sk_buff 的訪問變成對 struct sk_buff 的訪問。

BPF Attach type

一種 type 的 bpf prog 可以掛到內核中不同的 hook 點,這些不同的 hook 點就是不同的 attach type。

其對應關係在 下面函數 [22] 中定義了。

attach_type_to_prog_type(enum bpf_attach_type attach_type)
{
        switch (attach_type) {
        case BPF_CGROUP_INET_INGRESS:
        case BPF_CGROUP_INET_EGRESS:
                return BPF_PROG_TYPE_CGROUP_SKB;
        case BPF_CGROUP_INET_SOCK_CREATE:
        case BPF_CGROUP_INET_SOCK_RELEASE:
        case BPF_CGROUP_INET4_POST_BIND:
        case BPF_CGROUP_INET6_POST_BIND:
                return BPF_PROG_TYPE_CGROUP_SOCK;
     .....
}

當 bpf prog 通過系統調用 bpf() attach 到具體的 hook 點時,其入口參數中就需要指定 attach type。

有趣的是,BPF_PROG_TYPE_SCHED_CLS 類型的 bpf prog 不能通過 bpf 系統調用來 attach,因爲它沒有定義對應的 attach type。故它的 attach 需要通過 netlink interface 額外的實現,還是非常複雜的。

常用 prog type 介紹

內核中的 prog type 目前有 30 種。每一種 type 能做的事情有所差異,這裏只講講我平時工作用過的幾種。

理解一種 prog type 的最好的方法是

include/uapi/linux/bpf.h

enum bpf_prog_type {
}

BPF_PROG_TYPE_SOCKET_FILTER

是第一個被添加到內核的程序類型。當你 attach 一個 bpf 程序到 socket 上,你可以獲取到被 socket 處理的所有數據包。socket 過濾不允許你修改這些數據包以及這些數據包的目的地。僅僅是提供給你觀察這些數據包。在你的程序中可以獲取到諸如 protocol type 類型等。

以 tcp 爲 example,調用的地點是 tcp_v4_rcv->tcp_filter->sk_filter_trim_cap 作用是過濾報文,或者 trim 報文。udp, icmp 中也有相關的調用。

BPF_PROG_TYPE_SOCK_OPS

在 tcp 協議 event 發生時調用的 bpf 鉤子,定義了 15 種 event。這些 event 的 attach type 都是 BPF_CGROUP_SOCK_OPS。不同的調用點會傳入不同的 enum, 比如:

主要作用:tcp 調優,event 統計等。

BPF_PROG_TYPE_SOCK_OPS 這種程序類型,允許你當數據包在內核網絡協議棧的各個階段傳輸的時候,去修改套接字的鏈接選項。他們 attach 到 cgroups 上,和 BPF_PROG_TYPE_CGROUP_SOCK 以及 BPF_PROG_TYPE_CGROUP_SKB 很像,但是不同的是,他們可以在整個連接的生命週期內被調用好多次。你的 bpf 程序會接受到一個 op 的參數,該參數代表內核將通過套接字鏈接執行的操作。因此,你知道在鏈接的生命週期內何時調用該程序。另一方面,你可以獲取 ip 地址,端口等。你還可以修改鏈接的鏈接的選項以設置超時並更改數據包的往返延遲時間。

舉個例子,Facebook 使用它來爲同一數據中心內的連接設置短恢復時間目標(RTO)。RTO 是一種時間,它指的是網絡在出現故障後的恢復時間,這個指標也表示網絡在受到不可接受到情況下的,不能被使用的時間。Facebook 認爲,在同一數據中心中,應該有一個很短的 RTO,Facebook 修改了這個時間,使用 bpf 程序。

BPF_PROG_TYPE_CGROUP_SOCK_ADDR

它對應很多 attach type,一般在 bind, connect 時調用, 傳入 sock 的地址。

主要作用:例如 cilium 中 clusterip 的實現,在主動 connect 時,修改了目的 ip 地址,就是利用這個。

BPF_PROG_TYPE_CGROUP_SOCK_ADDR,這種類型的程序使您可以在由特定 cgroup 控制的用戶空間程序中操縱 IP 地址和端口號。在某些情況下,當您要確保一組特定的用戶空間程序使用相同的 IP 地址和端口時,系統將使用多個 IP 地址. 當您將這些用戶空間程序放在同一 cgroup 中時,這些 BPF 程序使您可以靈活地操作這些綁定。這樣可以確保這些應用程序的所有傳入和傳出連接均使用 BPF 程序提供的 IP 和端口。

BPF_PROG_TYPE_SK_MSG

BPF_PROG_TYPE_SK_MSG, These types of programs let you control whether a message sent to a socket should be delivered. 當內核創建了一個 socket,它會被存儲在前面提到的 map 中。當你 attach 一個程序到這個 socket map 的時候,所有的被髮送到那些 socket 的 message 都會被 filter。在 filter message 之前,內核拷貝了這些 data,因此你可以讀取這些 message,而且可以給出你的決定:例如,SK_PASS 和 SK_DROP。

BPF_PROG_TYPE_SK_SKB

調用點:tcp sendmsg 時會調用。

主要作用:做 sock redir 用的。

BPF_PROG_TYPE_SK_SKB,這類程序可以讓你獲取 socket maps 和 socket redirects。socket maps 可以讓你獲得一些 socket 的引用。當你有了這些引用,你可以使用相關的 helpers,去重定向一個 incoming 的 packet ,從一個 socket 去另外一個 scoket. 這在使用 BPF 來做負載均衡時是非常有用的。你可以在 socket 之間轉發網絡數據包,而不需要離開內核空間。Cillium 和 facebook 的 Katran 廣泛的使用這種類型的程序去做流量控制。

BPF_PROG_TYPE_CGROUP_SOCKOPT

調用點:getsockopt, setsockopt

BPF_PROG_TYPE_KPROBE

類似 ftrace 的 kprobe,在函數出入口的 hook 點,debug 用的。

BPF_PROG_TYPE_TRACEPOINT

類似 ftrace 的 tracepoint。

BPF_PROG_TYPE_SCHED_CLS

如上面的例子

BPF_PROG_TYPE_XDP

網卡驅動收到 packet 時,尚未生成 sk_buff 數據結構之前的一個 hook 點。

BPF_PROG_TYPE_XDP 允許你的 bpf 程序,在網絡數據包到達 kernel 很早的時候。在這樣的 bpf 程序中,你僅僅可能獲取到一點點的信息,因爲 kernel 還沒有足夠的時間去處理。因爲時間足夠的早,所以你可以在網絡很高的層面上去處理這些 packet。

XDP 定義了很多的處理方式,例如

BPF_PROG_TYPE_CGROUP_SKB

BPF_PROG_TYPE_CGROUP_SKB 允許你過濾整個 cgroup 的網絡流量。在這種程序類型中,你可以在網絡流量到達這個 cgoup 中的程序前做一些控制。內核試圖傳遞給同一 cgroup 中任何進程的任何數據包都將通過這些過濾器之一。同時,您可以決定 cgroup 中的進程通過該接口發送網絡數據包時該怎麼做。其實,你可以發現它和 BPF_PROG_TYPE_SOCKET_FILTER 的類型很類似。最大的不同是 cgroup_skb 是 attach 到這個 cgroup 中的所有進程,而不是特殊的進程。在 container 的環境中,bpf 是非常有用的。

BPF_PROG_TYPE_CGROUP_SOCK

在 sock create, release, post_bind 時調用的。主要用來做一些權限檢查的。

BPF_PROG_TYPE_CGROUP_SOCK,這種類型的 bpf 程序允許你,在一個 cgroup 中的任何進程打開一個 socket 的時候,去執行你的 Bpf 程序。這個行爲和 CGROUP_SKB 的行爲類似,但是它是提供給你 cgoup 中的進程打開一個新的 socket 的時候的情況,而不是給你網絡數據包通過的權限控制。這對於爲可以打開套接字的程序組提供安全性和訪問控制很有用,而不必分別限制每個進程的功能。

eBPF 工具鏈

bcc

BCC 是 BPF 的編譯工具集合,前端提供 Python/Lua API,本身通過 C/C++ 語言實現,集成 LLVM/Clang 對 BPF 程序進行重寫、編譯和加載等功能, 提供一些更人性化的函數給用戶使用。

雖然 BCC 竭盡全力地簡化 BPF 程序開發人員的工作,但其 “黑魔法” (使用 Clang 前端修改了用戶編寫的 BPF 程序)使得出現問題時,很難找到問題的所在以及解決方法。必須記住命名約定和自動生成的跟蹤點結構 。且由於 libbcc 庫內部集成了龐大的 LLVM/Clang 庫,使其在使用過程中會遇到一些問題:

  1. 在每個工具啓動時,都會佔用較高的 CPU 和內存資源來編譯 BPF 程序,在系統資源已經短缺的服務器上運行可能引起問題;

  2. 依賴於內核頭文件包,必須將其安裝在每個目標主機上。即便如此,如果需要內核中未 export 的內容,則需要手動將類型定義複製 / 粘貼到 BPF 代碼中;

  3. 由於 BPF 程序是在運行時才編譯,因此很多簡單的編譯錯誤只能在運行時檢測到,影響開發體驗。

隨着 BPF CO-RE 的落地,我們可以直接使用內核開發人員提供的 libbpf 庫來開發 BPF 程序,開發方式和編寫普通 C 用戶態程序一樣:一次編譯生成小型的二進制文件。Libbpf 作爲 BPF 程序加載器,接管了重定向、加載、驗證等功能,BPF 程序開發者只需要關注 BPF 程序的正確性和性能即可。這種方式將開銷降到了最低,且去除了龐大的依賴關係,使得整體開發流程更加順暢。

性能優化大師 Brendan Gregg 在用 libbpf + BPF CO-RE 轉換一個 BCC 工具後給出了性能對比數據:

As my colleague Jason pointed out, the memory footprint of opensnoop as CO-RE is much lower than opensnoop.py. 9 Mbytes for CO-RE vs 80 Mbytes for Python.

我們可以看到在運行時相比 BCC 版本,libbpf + BPF CO-RE 版本節約了近 9 倍的內存開銷,這對於物理內存資源已經緊張的服務器來說會更友好。

關於 BCC 可以參考 我的這篇文章介紹 [23]

bpftrace

bpftrace is a high-level tracing language for Linux eBPF and available in recent Linux kernels (4.x). bpftrace uses LLVM as a backend to compile scripts to eBPF bytecode and makes use of BCC for interacting with the Linux eBPF subsystem as well as existing Linux tracing capabilities: kernel dynamic tracing (kprobes), user-level dynamic tracing (uprobes), and tracepoints. The bpftrace language is inspired by awk, C and predecessor tracers such as DTrace and SystemTap.

eBPF Go Library

libbpf

參考資料

相關文章推薦

引用鏈接

[1] BPF: https://en.wikipedia.org/wiki/Berkeley_Packet_Filter

[2] Github: https://github.com/

[3] The BSD Packet Filter: A New Architecture for User-level Packet Capture: http://www.tcpdump.org/papers/bpf-usenix93.pdf

[4] include/linux/filter.h: https://github.com/torvalds/linux/blob/v5.8/include/linux/filter.h

[5] include/linux/bpf.h: https://github.com/torvalds/linux/blob/v5.8/include/linux/bpf.h

[6] 詳見這裏: https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md#1-bpf_trace_printk

[7] read_trace_pipe: https://elixir.bootlin.com/linux/latest/source/tools/testing/selftests/bpf/trace_helpers.c#L120

[8] bpf_load.c: https://elixir.bootlin.com/linux/v5.4/source/samples/bpf/bpf_load.c#L659

[9] load_and_attach: https://elixir.bootlin.com/linux/v5.4/source/samples/bpf/bpf_load.c#L76

[10] 我的這篇博文: https://houmin.cc

[11] Linux Manual Page: bpf-helpers: https://man7.org/linux/man-pages/man7/bpf-helpers.7.html

[12] include/linux/filter.h: https://elixir.bootlin.com/linux/v5.4/source/include/linux/filter.h#L479

[13] bpf_map_update_elem: https://elixir.bootlin.com/linux/v5.4/source/kernel/bpf/helpers.c#L41

[14] bpf(): https://man7.org/linux/man-pages/man2/bpf.2.html

[15] bpf_load_program: https://elixir.bootlin.com/linux/v5.4/source/tools/lib/bpf/bpf.c#L316

[16] bpf_create_map: https://elixir.bootlin.com/linux/v5.4/source/tools/lib/bpf/bpf.c#L123

[17] bpf_map_lookup_elem: https://elixir.bootlin.com/linux/v5.4/source/tools/lib/bpf/bpf.c#L371

[18] bpf_map_update_elem: https://elixir.bootlin.com/linux/v5.4/source/tools/lib/bpf/bpf.c#L357

[19] bpf_map_delete_elem: https://elixir.bootlin.com/linux/v5.4/source/tools/lib/bpf/bpf.c#L408

[20] bpf_map_get_next_key: https://elixir.bootlin.com/linux/v5.4/source/tools/lib/bpf/bpf.c#L419

[21] Linux Manual Page: bpf-helpers: https://man7.org/linux/man-pages/man7/bpf-helpers.7.html

[22] 下面函數: https://elixir.bootlin.com/linux/v5.17-rc8/source/kernel/bpf/syscall.c#L3137

[23] 我的這篇文章介紹: https://houmin.cc/posts/6a8748a1/

[24] The BSD Packet Filter: A New Architecture for User-level Packet Capture, Steven McCanne and Van Jacobso, December 19, 1992: http://www.tcpdump.org/papers/bpf-usenix93.pdf

[25] eBPF Documentation: What is eBPF?: https://ebpf.io/what-is-ebpf/

[26] LWN: A thorough introduction to eBPF: https://lwn.net/Articles/740157/

[27] Cilium Documentation: BPF and XDP Reference Guide: https://docs.cilium.io/en/stable/bpf/

[28] eBPF summit: The Future of eBPF based Networking and Security: https://www.youtube.com/watch?v=slBAYUDABDA

[29] eBPF - The Future of Networking & Security: https://cilium.io/blog/2020/11/10/ebpf-future-of-networking/

[30] eBPF - Rethinking the Linux Kernel: https://www.youtube.com/watch?v=f-oTe-dmfyI

[31] Linux Manual Page: bpf(2): https://man7.org/linux/man-pages/man2/bpf.2.html

[32] Linux Manual Page: bpf-helpers: https://man7.org/linux/man-pages/man7/bpf-helpers.7.html

[33] Linux Kernel Documentation: Linux Socket Filtering aka Berkeley Packet Filter (BPF): https://www.kernel.org/doc/Documentation/networking/filter.txt

[34] Dive into BPF: a list of reading material: https://qmonnet.github.io/whirl-offload/2016/09/01/dive-into-bpf/

[35] LWN: eBPF materials: https://lwn.net/Kernel/Index/#Berkeley_Packet_Filter

[36] 基於 Ubuntu 20.04 的 eBPF 環境搭建: https://www.ebpf.top/post/ebpf_c_env/

[37] eBPF 指令集: https://houmin.cc/posts/5150fab3/

[38] eBPF tc 子系統: https://houmin.cc/posts/28ca4f79/

[39] Linux Traffic Control: https://houmin.cc/posts/8278f23c/

[40] 網卡聚合 Bonding: https://houmin.cc/posts/e9c4d3e9/

[41] Linux 網絡包收發流程: https://houmin.cc/posts/941301e/

原文鏈接:https://houmin.cc/posts/2c811c2c/

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