在雲原生技術蓬勃發展的當下,eBPF (extended Berkeley Packet Filter) 已然成為近年來最熱門的技術話題之一。這項技術正在徹底改變我們對 Linux 核心功能擴充套件的認知,並為系統觀測、網路功能和安全防護帶來前所未有的可能性。

作為一個深入研究核心技術多年的開發者,我發現 eBPF 的獨特之處在於它能讓開發者在核心空間安全地執行自定義程式,同時避免了傳統核心模組開發的許多風險與複雜性。這種能力為基礎設施工具帶來了質的飛躍,使其在效能和準確性上遠超前代產品。

為何 eBPF 如此引人注目?

eBPF 的魅力在於它打破了使用者空間和核心空間之間的傳統界限,同時保持了系統的安全性與穩定性。當我第一次接觸 eBPF 時,立即被它的設計理念所吸引:在核心中安全地執行自定義程式,而不需要修改核心原始碼或載入可能導致系統當機的核心模組。

在技術領域,很少有技術能同時應用於如此廣泛的場景:

  • 系統觀測:捕捉系統呼叫、追蹤函式執行、分析效能瓶頸
  • 網路功能:高效率封包處理、自定義負載平衡、網路安全策略
  • 安全監控:實時檢測可疑行為、防止許可權提升攻擊
  • 資源控制:精細化資源分配與限制

作為一個經常需要深入系統底層解決問題的技術人員,我認為理解 eBPF 的工作原理不僅能幫助我們更好地使用根據 eBPF 的工具,還能開啟全新的思路來解決長期以來困擾系統工程師的問題。

誰應該瞭解 eBPF?

無論你是系統管理員、網路工程師、安全工作者還是應用開發者,eBPF 都可能對你的工作產生深遠影響:

  1. 系統管理員與維運人員:瞭解 eBPF 能幫助你更有效地使用新一代的監控與診斷工具
  2. 安全工程師:掌握 eBPF 安全工具的內部機制,避免安全策略中的盲點
  3. 網路工程師:利用 eBPF 實作更高效、更靈活的網路功能
  4. 應用開發者:使用 eBPF 工具進行更精確的效能分析與調優

即使你不打算親自編寫 eBPF 程式,理解其基本原理也能讓你在評估和使用根據 eBPF 的工具時做出更明智的決策。在技術工作中,我常發現那些瞭解底層技術原理的工程師能更有效地診斷問題並提出創新解決方案。

在這篇技術中,我將帶你從零開始瞭解 eBPF,探索它的基本概念、工作原理以及實際應用場景。無論你是剛接觸 eBPF 的新手,還是想深入瞭解其內部機制的資深工程師,希望這篇文章都能為你提供有價值的見解。

eBPF 基礎:核心可程式化的新正規化

eBPF 代表「extended Berkeley Packet Filter」,它源自於 BSD 系統中用於網路封包過濾的 BPF 技術。然而,現代 eBPF 已經遠超越了其最初的設計目標,發展成為 Linux 核心中的一個通用執行引擎。

eBPF 的技術本質

從技術角度看,eBPF 提供了一個在核心中安全執行沙箱程式的虛擬機器制。這些程式能夠在特定事件(如系統呼叫、函式進入/結束、網路事件等)觸發時執行。關鍵在於,這些程式在載入核心前會經過嚴格的驗證,確保它們不會導致系統當機或無限迴圈。

在我的理解中,eBPF 的核心價值可以概括為三點:

  1. 安全的核心可程式化:不同於傳統核心模組,eBPF 程式經過驗證器嚴格審查,確保不會破壞系統穩定性
  2. 高效能的事件處理:直接在核心中處理事件,避免了使用者空間與核心空間的頻繁切換
  3. 動態的功能擴充套件:無需重啟系統或重新編譯核心即可增加新功能

這種設計使 eBPF 成為核心功能擴充套件的理想選擇,特別是在不能輕易重啟的生產環境中。

eBPF 與傳統方法的對比

為了更清晰地理解 eBPF 的優勢,我整理了它與其他核心擴充套件方法的對比:

1. eBPF vs 核心模組

傳統核心模組雖然功能強大,但存在明顯缺點:

  • 核心模組擁有完全的核心存取許可權,錯誤可能導致系統當機
  • 需要與特定核心版本比對,更新核心後通常需要重新編譯
  • 在生產環境中載入核心模組通常被視為風險操作

相比之下,eBPF 程式:

  • 經過驗證器嚴格檢查,確保安全執行
  • 透過 CO-RE (Compile Once, Run Everywhere) 技術可實作跨核心版本相容
  • 可以在生產環境中安全動態載入

2. eBPF vs 系統呼叫跟蹤

傳統的系統呼叫跟蹤工具(如 strace)存在效能開銷大的問題:

  • 每次系統呼叫都需要在使用者空間和核心空間之間切換
  • 對高頻呼叫的應用產生顯著效能影響
  • 難以實作複雜的過濾邏輯

而 eBPF 跟蹤:

  • 直接在核心中執行,避免了空間切換的開銷
  • 可以實作精確的過濾,只關注真正需要的事件
  • 能夠聚合資料,進一步減少對系統的影響

從實際應用經驗來看,eBPF 技術讓我們能夠以最小的系統開銷取得最精確的訊息,這在高負載生產環境中尤為重要。

eBPF 程式的生命週期

理解 eBPF 程式的生命週期對於掌握這項技術至關重要。一個典型的 eBPF 程式經歷以下階段:

  1. 編寫:使用 C 語言的受限子集編寫 eBPF 程式
  2. 編譯:使用 LLVM/Clang 編譯為 eBPF 位元組碼
  3. 載入:使用者空間程式將 eBPF 位元組碼提交給核心
  4. 驗證:核心驗證器檢查程式的安全性
  5. JIT 編譯:將位元組碼轉換為本機指令以提高效能
  6. 附加:將程式附加到特定的事件或掛鉤點
  7. 執行:當指定事件發生時,程式被觸發執行
  8. 互動:透過 eBPF 對映 (maps) 與使用者空間程式交換資料
  9. 解除安裝:當不再需要時,從核心中解除安裝程式

在我的開發實踐中,我發現理解這個生命週期對於診斷 eBPF 程式的問題非常有幫助,特別是在驗證階段遇到的各種限制。

eBPF 程式型別概覽

eBPF 支援多種程式型別,每種型別對應不同的掛鉤點和用途。以下是一些常見型別:

  • kprobe/kretprobe:跟蹤核心函式的進入和回傳
  • uprobe/uretprobe:跟蹤使用者空間程式的函式
  • tracepoint:跟蹤核心中預定義的跟蹤點
  • XDP (eXpress Data Path):網路封包的高效處理
  • socket:通訊端層的過濾和操作
  • cgroup:容器和資源控制相關的掛鉤點
  • perf_event:效能監控事件

每種程式型別都有其特定的上下文和能力,選擇合適的程式型別是 eBPF 開發中的重要決策。在實際應用中,我發現針對不同問題選擇最適合的程式型別能顯著提升解決方案的效能和可靠性。

在下一部分,我們將透過實際的「Hello World」範例,開始動手探索 eBPF 程式的開發過程。

eBPF 入門:你的第一個 eBPF 程式

理論知識固然重要,但真正掌握 eBPF 還是需要親自動手實踐。在這一部分,我將帶你建立並執行一個簡單的 eBPF 程式,這將幫助你理解 eBPF 開發的基本流程和概念。

環境準備

在開始之前,需要確保你的系統滿足以下條件:

  1. Linux 核心版本 >= 4.9(建議 5.4 或更高版本以獲得更完整的 eBPF 功能)
  2. 安裝必要的開發工具:
    • LLVM 和 Clang(用於編譯 eBPF 程式)
    • libelf-dev(處理 ELF 檔案)
    • zlib1g-dev(壓縮函式庫)
    • Linux 核心頭檔案

在 Ubuntu 系統上,可以使用以下命令安裝這些依賴:

sudo apt update
sudo apt install -y build-essential clang llvm libelf-dev zlib1g-dev linux-headers-$(uname -r)

“Hello World” eBPF 程式

我們的第一個 eBPF 程式將非常簡單:每當系統上發生 execve 系統呼叫(即執行新程式)時,記錄下該事件並列印一條訊息。這個例子雖小,但包含了 eBPF 程式的基本元素。

1. eBPF 程式部分(核心空間)

首先建立一個名為 hello.bpf.c 的檔案:

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

SEC("tracepoint/syscalls/sys_enter_execve")
int hello_execve(void *ctx)
{
    char msg[] = "Hello, eBPF! Someone executed a program.\n";
    bpf_trace_printk(msg, sizeof(msg));
    return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";

這段程式碼雖然簡短,但包含了幾個重要概念:

  1. SEC("tracepoint/syscalls/sys_enter_execve") - 這個巨集定義了程式的型別和附加點。在這個例子中,我們使用 tracepoint 型別,附加到系統呼叫 execve 的入口點。

  2. hello_execve(void *ctx) - 這是我們的 eBPF 程式主體,當 execve 系統呼叫發生時會被執行。引數 ctx 包含了關於該事件的上下文訊息。

  3. bpf_trace_printk() - 這是一個輔助函式,用於將訊息列印到 trace pipe,是 eBPF 程式中最簡單的除錯方法。

  4. char LICENSE[] SEC("license") = "Dual BSD/GPL"; - 這行定義了程式的許可證,這對於決定程式可以使用哪些 eBPF 輔助函式很重要。某些輔助函式僅適用於 GPL 相容的程式。

2. 使用者空間載入程式

接下來,我們需要一個使用者空間程式來載入並附加這個 eBPF 程式。建立一個名為 hello.py 的 Python 檔案:

#!/usr/bin/python3
from bcc import BPF

# 載入 eBPF 程式
bpf_text = """
#include <linux/sched.h>

int hello_execve(void *ctx) {
    char msg[] = "Hello, eBPF! Someone executed a program.\\n";
    bpf_trace_printk(msg, sizeof(msg));
    return 0;
}
"""

# 編譯並載入 eBPF 程式
b = BPF(text=bpf_text)
b.attach_tracepoint(tp="syscalls:sys_enter_execve", fn_name="hello_execve")

# 輸出結果
print("eBPF program loaded! Tracing execve() calls... Press Ctrl+C to exit.")
try:
    b.trace_print()
except KeyboardInterrupt:
    pass

這個 Python 程式使用 BCC (BPF Compiler Collection) 框架來簡化 eBPF 程式的編譯和載入:

  1. 我們直接在 Python 程式中嵌入了 eBPF C 程式碼
  2. BPF(text=bpf_text) 負責將 C 程式碼編譯為 eBPF 位元組碼並載入到核心
  3. attach_tracepoint() 將程式附加到指定的 tracepoint
  4. trace_print() 從 trace pipe 讀取輸出並顯示

執行你的第一個 eBPF 程式

讓我們執行這個程式並觀察結果:

sudo python3 hello.py

你應該會看到類別似下面的輸出:

eBPF program loaded! Tracing execve() calls... Press Ctrl+C to exit.
b'           <...>-1234    [001] d...1  1234.123456: bpf_trace_printk: Hello, eBPF! Someone executed a program.'

每當系統中有程式執行時,你都會看到一條新訊息。這表明你的 eBPF 程式成功附加到了 execve 系統呼叫,並在每次呼叫時執行。

使用 BPF 對映(Maps)

eBPF 程式的能力不僅限於列印訊息。一個關鍵功能是透過 BPF 對映在核心空間程式和使用者空間程式之間分享資料。讓我們擴充套件我們的例子,計算不同程式的執行次數。

修改 hello.py 如下:

#!/usr/bin/python3
from bcc import BPF
import time

# 載入 eBPF 程式
bpf_text = """
#include <linux/sched.h>
#include <linux/string.h>

BPF_HASH(exec_counter, u32, u32);

int hello_execve(void *ctx) {
    u32 pid = bpf_get_current_pid_tgid();
    u32 count = 0;
    
    // 查詢或初始化計數器
    u32 *counter = exec_counter.lookup(&pid);
    if (counter) {
        count = *counter + 1;
    } else {
        count = 1;
    }
    exec_counter.update(&pid, &count);
    
    return 0;
}
"""

# 編譯並載入 eBPF 程式
b = BPF(text=bpf_text)
b.attach_tracepoint(tp="syscalls:sys_enter_execve", fn_name="hello_execve")

# 定期列印統計訊息
print("Counting execve() calls by PID... Press Ctrl+C to exit.")
try:
    while True:
        time.sleep(1)
        print("--- Execve counts per PID ---")
        for k, v in b["exec_counter"].items():
            print(f"PID {k.value}: {v.value} executions")
except KeyboardInterrupt:
    pass

這個更新後的程式引入了 BPF 對映的概念:

  1. BPF_HASH(exec_counter, u32, u32) 建立了一個雜湊對映,用 PID (u32) 作為鍵,計數器 (u32) 作為值
  2. exec_counter.lookup() 查詢特定 PID 的計數值
  3. exec_counter.update() 更新計數值
  4. 使用者空間程式透過 b["exec_counter"] 存取對映資料

BPF 對映是 eBPF 程式與使用者空間通訊的主要方式,也是儲存持久資料的關鍵機制。

eBPF 程式的限制

在開發 eBPF 程式時,瞭解其限制非常重要:

  1. 有限的程式大小:eBPF 程式通常限制在 4096 條指令以內(較新核心可能支援更多)
  2. 無限迴圈禁止:迴圈必須有確定的上限,否則驗證器會拒絕程式
  3. 有限的堆積積堆疊空間:通常為 512 位元組(較新核心中可能更大)
  4. 受限的函式呼叫:只能呼叫特定的 BPF 輔助函式,不能呼叫任意核心函式
  5. 指標操作限制:指標算術受到嚴格限制,以防止非法記憶體存取

這些限制確保了 eBPF 程式的安全性,但也增加了開發的複雜性。隨著時間推移和核心版本的更新,這些限制正在逐漸放寬,但安全性仍然是首要考慮因素。

在開發過程中,我發現最常見的挑戰是處理驗證器的錯誤訊息,這些訊息有時候不太直觀。掌握常見模式和技巧能大提高開發效率。

eBPF 程式型別與掛鉤點詳解

eBPF 的強大之處在於它可以附加到 Linux 核心中的多種不同位置。每種附加點都提供了不同的功能和上下文,選擇合適的掛鉤點對於解決特定問題至關重要。

核心探測點 (Kprobes 和 Kretprobes)

Kprobes 允許我們動態地附加到幾乎任何核心函式的入口點,而 Kretprobes 則附加到函式的回傳點。這是最靈活的跟蹤方式,但也有一些缺點。

Kprobe 範例:跟蹤檔案開啟操作

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

SEC("kprobe/do_sys_open")
int kprobe_open(struct pt_regs *ctx)
{
    // 取得檔案名指標 (第二個引數)
    char *filename = (char *)PT_REGS_PARM2(ctx);
    
    // 列印檔案名
    bpf_trace_printk("Opening file: %s\\n", filename);
    
    return 0;
}

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

這個程式使用 kprobe 附加到 do_sys_open 核心函式,該函式處理檔案開啟操作。當函式被呼叫時:

  1. struct pt_regs *ctx 包含函式呼叫時的暫存器狀態
  2. PT_REGS_PARM2(ctx) 是一個巨集,用於取得函式的第二個引數(在這個例子中是檔案名)
  3. 程式列印出正在被開啟的檔案名

Kprobes 的優點是靈活性高,幾乎可以附加到任何核心函式。缺點是它們依賴於核心內部實作,在不同核心版本間可能不相容。

跟蹤點 (Tracepoints)

Tracepoints 是核心開發者在程式碼中預先定義的靜態跟蹤點,提供了穩定的 API。相較於 Kprobes,它們在核心版本間更加穩定。

Tracepoint 範例:跟蹤程式建立

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

struct sched_process_exec_args {
    __u64 pad;
    char *filename;
    int pid;
    int old_pid;
};

SEC("tracepoint/sched/sched_process_exec")
int trace_exec(struct sched_process_exec_args *ctx)
{
    char comm[16];
    int pid;
    
    // 取得程式 ID 和名稱
    pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&comm, sizeof(comm));
    
    bpf_trace_printk("Process %s (PID: %d) executed\\n", comm, pid);
    
    return 0;
}

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

這個程式使用 tracepoint 附加到 sched_process_exec 事件,該事件在程式執行新程式時觸發:

  1. struct sched_process_exec_args *ctx 是這個特定 tracepoint 的上下文結構
  2. bpf_get_current_pid_tgid() 取得當前程式的 PID 和 TGID
  3. bpf_get_current_comm() 取得當前程式的名稱

Tracepoints 的主要優勢是穩定性和可維護性,缺點是隻能使用核心開發者預先定義的跟蹤點。

使用者空間探測點 (Uprobes 和 Uretprobes)

Uprobes 和 Uretprobes 類別似於 Kprobes 和 Kretprobes,但它們附加到使用者空間程式的函式,而不是核心函式。

Uprobe 範例:跟蹤函式庫函式呼叫

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

SEC("uprobe/libc:malloc")
int trace_malloc(struct pt_regs *ctx)
{
    size_t size = PT_REGS_PARM1(ctx);
    bpf_trace_printk("malloc(%lu) called\\n", size);
    return 0;
}

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

這個程式使用 uprobe 附加到 libc 函式庫中的 malloc 函式:

  1. 當任何使用 libc 的程式呼叫 malloc 時,這個 eBPF 程式會被觸發
  2. PT_REGS_PARM1(ctx) 取得 malloc 的第一個引數,即請求分配的記憶體大小
  3. 程式列印出 malloc 呼叫的大小

Uprobes 非常適合分析應用程式行為,而無需修改其原始碼。

網路相關掛鉤點

eBPF 在網路領域有多種強大的掛鉤點,其中最重要的是 XDP(eXpress Data Path)和 TC(Traffic Control)。

XDP 範例:簡單封包過濾

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>

SEC("xdp")
int xdp_filter(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data = (void *)(long)ctx->data;
    
    // 確保我們可以安全讀取乙太網頭部
    struct ethhdr *eth = data;
    if (data + sizeof(*eth) > data_end)
        return XDP_PASS;
    
    // 只關注 IPv4 封包
    if (eth->h_proto != __constant_htons(ETH_P_IP))
        return XDP_PASS;
    
    // 取得 IP 頭部
    struct iphdr *iph = data + sizeof(*eth);
    if ((void*)iph + sizeof(*iph) > data_end)
        return XDP_PASS;
    
    // 丟棄來自特定 IP 的封包 (範例: 192.168.1.100)
    if (iph->saddr == __constant_htonl(0xC0A80164))
        return XDP_DROP;
    
    return XDP_PASS;
}

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

這個 XDP 程式在網路封包到達網路卡後立即處理:

  1. struct xdp_md *ctx 包含了封包資料的指標和邊界
  2. 程式首先檢查封包是否是 IPv4 封包
  3. 然後檢查源 IP 地址,如果比對特定 IP (192.168.1.100),則丟棄封包
  4. 其他封包正常透過

XDP 的主要優勢是極高的效能,因為它在網路堆積疊的最早期階段處理封包,甚至在分配 SKB(Socket Buffer)之前。

安全相關掛鉤點

eBPF 提供了多種掛鉤點用於安全監控和強制執行,例如 LSM(Linux Security Module)BPF 和 seccomp-BPF。

LSM BPF 範例:監控檔案建立

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

SEC("lsm/file_open")
int BPF_PROG(file_open, struct file *file)
{
    char comm[16];
    bpf_get_current_comm(&comm, sizeof(comm));
    
    // 記錄哪個程式開啟了檔案
    bpf_trace_printk("Process %s opened a file\\n", comm);
    
    // 允許操作繼續
    return 0;
}

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

這個 LSM BPF 程式附加到檔案開啟操作的安全檢查點:

eBPF程式碼範例與實作

在探索eBPF技術的過程中,實際動手寫程式碼是最有效的學習方式。透過親自實作各種範例,我們能更深入理解eBPF的運作機制和強大功能。這篇文章將介紹多種eBPF程式碼範例,並提供實用的實作指導。

程式碼實作與練習資源

要跟著本文練習eBPF程式設計,你可以在GitHub上找到完整的程式碼範例和詳細說明。這些範例涵蓋了從基礎到進階的eBPF應用,每個章節結尾還附有練習題,幫助你透過擴充套件範例或撰寫自己的程式來深化理解。

練習是鞏固eBPF知識的最佳方式。透過修改現有程式碼或從頭開始編寫新的eBPF程式,你將能夠更好地掌握這項技術的精髓。每個練習都經過精心設計,引導你探索eBPF不同的功能和應用場景。

核心版本與功能相容性

eBPF技術持續快速演進,可用的功能取決於你執行的核心版本。較新的核心版本通常會解除或放寬早期版本的各種限制。IO Visor專案提供了一個有用的概述,說明不同BPF功能是在哪個核心版本中增加的。

本文中的範例是在Linux核心5.15版本上測試的。如果你使用的是較舊的核心版本,某些功能可能無法正常運作。這是因為許多流行的Linux發行版可能尚未支援如此新的核心版本。在生產環境中實作eBPF解決方案時,核心版本相容性是一個重要考量因素。

eBPF不僅限於Linux

雖然eBPF最初是為Linux開發的,但這種技術方法並非只能在Linux上使用。事實上,微軟已經在為Windows開發eBPF實作。不過,本文將主要聚焦於Linux實作,所有範例都根據Linux環境。

隨著eBPF技術的普及,我們可以預期更多作業系統會採用類別似的方法。這種跨平台的發展趨勢將使eBPF成為更加通用的技術解決方案。

程式碼慣例與格式說明

為了讓程式碼範例更加清晰易懂,本文使用了以下排版慣例:

  • 斜體:用於表示新術語、URL、電子郵件地址、檔案名稱和檔案副檔名。

  • 等寬字型:用於程式碼列表,以及在段落中參照程式元素,如變數或函式名稱、資料函式庫、資料型別、環境變數、陳述式和關鍵字。

  • 粗體等寬字型:顯示使用者應該逐字輸入的命令或其他文字。

  • 斜體等寬字型:顯示應該由使用者提供的值或由上下文決定的值。

在文章中,我會使用以下元素來標示特殊資訊:

提示:這個元素表示一個小技巧或建議。

注意:這個元素表示一般性的注意事項。

警告:這個元素表示警告或需要注意的事項。

程式碼範例的使用

所有程式碼範例都可以在GitHub上找到。如果你在使用程式碼範例時遇到技術問題,可以透過相關社群或論壇尋求協助。

這些程式碼範例主要用於學習和參考。在實際應用中,你可能需要根據自己的需求和環境進行調整。值得注意的是,eBPF程式碼通常需要根據特定的核心版本和系統環境進行微調。

eBPF的本質與重要性

eBPF是一項革命性的核心技術,允許開發者編寫可以動態載入到核心中的自定義程式碼,從而改變核心的行為方式。這使得新一代高效能的網路、可觀測性和安全工具成為可能。

使用eBPF,你可以實作以下功能:

  • 對系統幾乎任何方面進行效能追蹤
  • 高效能網路處理,具有內建的可見性
  • 檢測並(可選)防止惡意活動

eBPF的起源:Berkeley封包過濾器

今天我們所稱的"eBPF"起源於BSD封包過濾器(Berkeley Packet Filter),這項技術最初由勞倫斯伯克利國家實驗室的Steven McCanne和Van Jacobson於1993年在一篇論文中描述。該論文討論了一種偽機器,可以執行過濾器程式,這些程式用於決定是接受還是拒絕網路封包。

這些程式使用BPF指令集編寫,這是一種通用的32位元指令集,與組合語言非常相似。以下是直接從該論文中摘取的一個範例:

ldh [12]
jeq #ETHERTYPE_IP, L1, L2
L1: ret #TRUE
L2: ret #0

這段簡短的程式碼用於過濾出非IP封包。該過濾器的輸入是一個乙太網封包,第一條指令(ldh)從封包的第12位元組開始載入一個2位元組的值。在下一條指令(jeq)中,該值與表示IP封包的值進行比較。如果比對,執行跳轉到標記為L1的指令,並回傳TRUE值,表示接受該封包;否則,跳轉到L2並回傳0,表示拒絕該封包。

這個簡單的例子展示了BPF的基本工作原理 - 檢查網路封包的特定部分,然後根據預定義的規則決定如何處理它。這種方法在網路封包過濾中非常高效,因為它避免了將所有封包複製到使用者空間進行處理的開銷。

eBPF的演進與擴充套件

從最初的網路封包過濾器,eBPF已經演變成一個功能強大的技術平台。現代eBPF不再僅限於網路封包過濾,它已經擴充套件到系統呼叫追蹤、效能分析、安全監控等多個領域。

關鍵的擴充套件包括:

  1. 指令集擴充:從原始的32位元擴充套件到64位元,支援更複雜的操作

  2. 掛鉤點增加:不僅可以附加到網路堆積積疊,還可以附加到系統呼叫、函式入口/出口、核心事件等

  3. 對映(maps)引入:允許在核心空間和使用者空間之間分享資料的高效機制

  4. 幫助函式(helper functions):提供安全存取核心功能的介面

  5. 即時編譯(JIT):將eBPF位元組碼編譯成本機器碼,提高執行效率

這些擴充套件使eBPF成為一個通用的核心程式設計平台,能夠安全地擴充套件核心功能,而無需修改核心原始碼或載入核心模組。

實際應用範例

讓我們來看一些實際的eBPF應用範例,這些範例展示了eBPF在不同領域的強大功能。

網路封包過濾與處理

以下是一個簡單的eBPF程式,用於過濾TCP封包:

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>

SEC("socket")
int socket_filter(struct __sk_buff *skb) {
    // 檢查是否為IPv4封包
    if (skb->protocol != htons(ETH_P_IP))
        return 0;
    
    // 取得IP標頭
    struct iphdr *ip = (struct iphdr *)(skb->data + ETH_HLEN);
    if (ip->protocol != IPPROTO_TCP)
        return 0;
    
    // 是TCP封包,接受它
    return -1;
}

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

這個eBPF程式附加到socket介面,用於過濾TCP封包。程式首先檢查封包是否為IPv4型別(透過比較skb->protocol與ETH_P_IP),如果不是則回傳0(表示不接受)。然後,程式取得IP標頭並檢查協定是否為TCP(IPPROTO_TCP)。如果是TCP封包,則回傳-1,表示接受該封包。

最後的license宣告是必需的,因為eBPF程式需要在GPL許可下才能使用某些核心功能。

系統呼叫追蹤

下面是一個追蹤系統開啟檔案操作的eBPF程式:

#include <linux/bpf.h>
#include <linux/ptrace.h>
#include <linux/sched.h>
#include <linux/fs.h>

struct event {
    u32 pid;
    u8 filename[256];
};

BPF_PERF_OUTPUT(events);

SEC("tracepoint/syscalls/sys_enter_open")
int trace_open(struct syscall_trace_enter *ctx) {
    struct event event = {};
    
    // 取得當前程式ID
    event.pid = bpf_get_current_pid_tgid() >> 32;
    
    // 取得檔案名稱
    bpf_probe_read_user_str(event.filename, sizeof(event.filename), (void *)ctx->args[0]);
    
    // 送出事件到使用者空間
    events.perf_submit(ctx, &event, sizeof(event));
    
    return 0;
}

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

這個eBPF程式附加到sys_enter_open系統呼叫的追蹤點,用於監控開啟檔案的操作。當系統呼叫發生時,程式取得當前程式的ID以及被開啟的檔案名稱,然後透過BPF_PERF_OUTPUT對映將這些資訊送到使用者空間。

bpf_get_current_pid_tgid()是一個eBPF幫助函式,回傳當前程式的PID和TGID,右移32位取得PID。bpf_probe_read_user_str()用於安全地從使用者空間讀取字串(檔案名稱)。

這種追蹤方式不需要修改被監控的應用程式,也不會顯著影響系統效能,這正是eBPF的優勢所在。

效能分析

以下是一個測量函式執行時間的eBPF程式:

#include <linux/bpf.h>
#include <linux/ptrace.h>

struct key_t {
    u32 pid;
    char function[64];
};

struct data_t {
    u64 count;
    u64 total_ns;
};

BPF_HASH(start, u32, u64);
BPF_HASH(stats, struct key_t, struct data_t);

SEC("kprobe/some_kernel_function")
int entry(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 ts = bpf_ktime_get_ns();
    
    start.update(&pid, &ts);
    return 0;
}

SEC("kretprobe/some_kernel_function")
int return(struct pt_regs *ctx) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 *tsp = start.lookup(&pid);
    
    if (tsp == 0)
        return 0;
    
    u64 delta = bpf_ktime_get_ns() - *tsp;
    
    struct key_t key = {};
    key.pid = pid;
    bpf_get_current_comm(&key.function, sizeof(key.function));
    
    struct data_t *data = stats.lookup(&key);
    if (data == 0) {
        struct data_t new_data = {1, delta};
        stats.update(&key, &new_data);
    } else {
        data->count++;
        data->total_ns += delta;
        stats.update(&key, data);
    }
    
    start.delete(&pid);
    return 0;
}

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

這個eBPF程式使用kprobe和kretprobe來測量核心函式的執行時間。它在函式入口點(kprobe)記錄當前時間戳,然後在函式回傳點(kretprobe)計算執行時間差。

程式使用兩個BPF對映:start用於儲存入口時間戳,stats用於累計統計資料。對於每次函式呼叫,程式記錄程式ID、函式名稱、呼叫次數和總執行時間。

bpf_ktime_get_ns()用於取得高精確度時間戳,bpf_get_current_comm()用於取得當前程式的名稱。這種方法可以提供詳細的函式效能分析,幾乎不影響被分析函式的執行效率。

跨平台考量

雖然eBPF最初是為Linux開發的,但這種技術方法正在擴充套件到其他作業系統。微軟已經開始為Windows開發eBPF實作,這表明eBPF的理念具有廣泛的適用性。

在不同平台上實作eBPF時,需要考慮以下因素:

  1. 核心/作業系統整合:不同作業系統的核心架構差異可能影響eBPF的實作方式

  2. 安全模型:各平台的安全模型不同,影響eBPF程式的許可權和能力

  3. 效能考量:不同平台的效能特性可能需要針對性的最佳化

  4. API相容性:跨平台eBPF實作可能需要提供相容層以支援現有的eBPF程式

隨著eBPF技術的成熟和普及,我們可以預期會看到更多的跨平台標準化努力,使開發者能夠編寫一次eBPF程式,在多個平台上執行。

設定開發環境

要開始eBPF程式設計,你需要設定適當的開發環境。以下是基本步驟:

  1. 安裝相依套件
sudo apt-get update
sudo apt-get install -y build-essential clang llvm libelf-dev linux-headers-$(uname -r)
  1. 安裝BPF編譯工具
sudo apt-get install -y libbpf-dev bpfcc-tools
  1. 設定編譯環境

建立一個簡單的Makefile來編譯你的eBPF程式:

TARGET = my_ebpf_program

BPF_PROG = ${TARGET}.bpf.c
BPF_OBJ = ${TARGET}.bpf.o
USER_PROG = ${TARGET}.c
USER_OBJ = ${TARGET}.o
BINARY = ${TARGET}

CLANG = clang
CFLAGS = -g -O2 -Wall

all: ${BINARY}

${BPF_OBJ}: ${BPF_PROG}
	${CLANG} -target bpf -D__TARGET_ARCH_x86 -I/usr/include/bpf -g -O2 -c $< -o $@

${USER_OBJ}: ${USER_PROG}
	${CC} ${CFLAGS} -c $< -o $@

${BINARY}: ${USER_OBJ} ${BPF_OBJ}
	${CC} ${CFLAGS} ${USER_OBJ} -lbpf -lelf -o $@

clean:
	rm -f ${BPF_OBJ} ${USER_OBJ} ${BINARY}
  1. 驗證環境

編寫一個簡單的eBPF程式來驗證你的環境設定是否正確:

// hello.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

SEC("tracepoint/syscalls/sys_enter_execve")
int hello(void *ctx) {
    char msg[] = "Hello, eBPF!";
    bpf_trace_printk(msg, sizeof(msg));
    return 0;
}

char LICENSE[] SEC("license") = "GPL";
// hello.c
#include <stdio.h>
#include <unistd.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>

int main() {
    struct bpf_object *obj;
    int prog_fd;
    
    obj = bpf_object__open_file("hello.bpf.o", NULL);
    if (!obj) {
        printf("Failed to open BPF object\n");
        return 1;
    }
    
    if (bpf_object__load(obj)) {
        printf("Failed to load BPF object\n");
        return 1;
    }
    
    struct bpf_program *prog = bpf_object__find_program_by_name(obj, "hello");
    if (!prog) {
        printf("Failed to find BPF program\n");
        return 1;
    }
    
    prog_fd = bpf_program__fd(prog);
    
    printf("eBPF program loaded successfully! Run 'sudo cat /sys/kernel/debug/tracing/trace_pipe' to see output\n");
    
    // 保持程式執行
    while (1) {
        sleep(1);
    }
    
    return 0;
}

編譯並執行:

make
sudo ./hello

在另一個終端:

sudo cat /sys/kernel/debug/tracing/trace_pipe

當你執行任何命令時,應該能看到"Hello, eBPF!“訊息。

深入理解eBPF程式碼範例

讓我們深入分析一些更複雜的eBPF程式碼範例,以便更全面地理解eBPF的強大功能和應用場景。

網路流量分析

以下是一個分析TCP連線狀態的eBPF程式:

#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>

struct conn_key {
    u32 saddr;
    u32 daddr;
    u16 sport;
    u16 dport;
};

struct conn_info {
    u64 rx_packets;
    u64 rx_bytes;
    u64 tx_packets;
    u64 tx_bytes;
    u8 state;
};

BPF_HASH(connections, struct conn_key, struct conn_info, 10240);

SEC("socket")
int socket_stats(struct __sk_buff *skb) {
    u8 *cursor = 0;
    
    struct ethhdr *ethernet = (struct ethhdr *)cursor_advance(cursor, sizeof(*ethernet));
    if (ethernet->h_proto != htons(ETH_P_IP))
        return 0;
    
    struct iphdr *ip = (struct iphdr *)cursor_advance(cursor, sizeof(*ip));
    if (ip->protocol != IPPROTO_TCP)
        return 0;
    
    struct tcphdr *tcp = (struct tcphdr *)cursor_advance(cursor, sizeof(*tcp));
    
    struct conn_key key = {};
    key.saddr = ip->saddr;
    key.daddr = ip->daddr;
    key.sport = tcp->source;
    key.dport = tcp->dest;
    
    struct conn_info *info = connections.lookup(&key);
    if (!info) {
        struct conn_info new_info = {};
        if (tcp->syn && !tcp->ack) {
            new_info.state = 1; // SYN_SENT
        }
        connections.update(&key, &new_info);
    } else {
        // 更新連線狀態
        if (tcp->syn && tcp->ack && info->state == 1) {
            info->state = 2; // SYN_RECEIVED
        } else if (tcp->ack && !tcp->syn && !tcp->fin && info->state == 2) {
            info->state = 3; // ESTABLISHED
        } else if (tcp->fin && info->state == 3) {
            info->state = 4; // FIN_WAIT
        } else if (tcp->fin && tcp->ack && info->state == 4) {
            info->state = 5; // LAST_ACK
        } else if (tcp->ack && info->state == 5) {
            info->state = 6; // CLOSED
        }
        
        // 更新統計資料
        if (ip->saddr == key.saddr) {
            info->tx_packets++;
            info->tx_bytes += skb->len;
        } else {
            info->rx_packets++;
            info->rx_bytes += skb->len;
        }
        
        connections.update(&key, info);
    }
    
    return 0;
}

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

這個eBPF程式用於分析TCP連線的狀態和流量統計。它附加到網路堆積積疊,檢查每個經過的封包,並追蹤TCP連線的狀態變化和流量統計。

程式首先檢查封包是否為IPv4和TCP型別。然後,它使用源IP、目標IP、源埠和目標埠作為鍵值,在BPF_HASH對映中查詢或建立連線記錄。

對於每個TCP封包,程式根據TCP標誌(SYN, ACK, FIN)更新連線狀態,並累計傳輸和接收的封包數量和位元組數。這種方法可以提供詳細的網路連線檢視,而無需修改應用程式或網路堆積積疊。

系統安全監控

以下是一個監控可疑係統呼叫的eBPF程式:

#include <linux/bpf.h>
#include <linux/ptrace.h>
#include <linux/sched.h>

struct event_t {
    u32 pid;
    u32 uid;
    char comm[16];
    char syscall[32];
    char path[256];
};

BPF_PERF_OUTPUT(events);
BPF_HASH(suspicious_paths, const char *, u8, 1024);

static int init_suspicious_paths() {
    const char *paths[] = {
        "/etc/passwd",
        "/etc/shadow",
        "/etc/ssh/sshd_config",
        "/var/log/auth.log"
    };
    
    u8 val = 1;
    for (int i = 0; i < sizeof(paths)/sizeof(paths[0]); i++) {
        suspicious_paths.update(&paths[i], &val);
    }
    
    return 0;
}

SEC("tracepoint/syscalls/sys_enter_open")
int trace_open(struct syscall_trace_enter *ctx) {
    struct event_t event = {};
    const char *path = (const char *)ctx->args[0];
    u8 *is_suspicious;
    
    // 檢查路徑是否在可疑列表中
    is_suspicious = suspicious_paths.lookup(&path);
    if (!is_suspicious)
        return 0;
    
    // 收集事件資訊
    event.pid = bpf_get_current_pid_tgid() >> 32;
    event.uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;
    bpf_get_current_comm(&event.comm, sizeof(event.comm));
    __builtin_memcpy(event.syscall, "open", 5);
    bpf_probe_read_user_str(event.path, sizeof(event.path), (void *)path);
    
    // 送出事件到使用者空間
    events.perf_submit(ctx, &event, sizeof(event));
    
    return 0;
}

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

這個eBPF程式用於監控對敏感檔案的存取。它附加到open系統呼叫的追蹤點,檢查被開啟的檔案路徑是否在預定義的可疑路徑列表中。

程式維護一個可疑路徑的雜湊表,當檢測到對這些路徑的存取時,收集程式ID、使用者ID、程式名稱、系統呼叫名稱和檔案路徑等資訊,並透過perf事件送到使用者空間進行進一步分析或警示。

這種監控方法可以即時檢測潛在的安全威脅,而無需修改應用程式或安裝額外的代理程式。它在核心級別運作,難以被繞過或停用。

效能熱點分析

以下是一個分析CPU使用熱點的eBPF程式:

#include <linux/bpf.h>
#include <linux/ptrace.h>
#include <linux/sched.h>

#define STACK_STORAGE_SIZE 16384

struct key_t {
    u32 pid;
    u64 kernel_ip;
    u64 kernel_ret_ip;
    int user_stack_id;
    int kernel_stack_id;
    char name[TASK_COMM_LEN];
};

BPF_HASH(counts, struct key_t);
BPF_STACK_TRACE(stack_traces, STACK_STORAGE_SIZE);

SEC("perf_event")
int on_cpu(struct bpf_perf_event_data *ctx) {
    u64 id = bpf_get_current_pid_tgid();
    u32 pid = id >> 32;
    u32 tgid = id;
    
    if (pid == 0)
        return 0;
    
    struct key_t key = {};
    key.pid = tgid;
    key.kernel_ip = PT_REGS_IP(&ctx->regs);
    key.kernel_ret_ip = 0;
    
    // 取得使用者和核心堆積積疊
    key.user_stack_id = stack_traces.get_stackid(&ctx->regs, BPF_F_USER_STACK);
    key.kernel_stack_id = stack_traces.get_stackid(&ctx->regs, 0);
    
    if (key.user_stack_id >= 0) {
        u64 *val, zero = 0;
        val = counts.lookup_or_init(&key, &zero);
        (*val)++;
    }
    
    return 0;
}

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

這個eBPF程式用於分析CPU使用的熱點。它附加到perf事件,在CPU週期取樣時收集當前執行的程式堆積積疊資訊。

程式為每個取樣點收集程式ID、核心指令指標、使用者堆積積疊和核心堆積積疊等資訊。透過BPF_STACK_TRACE對映,它可以取得完整的堆積積疊追蹤,這對於識別效能瓶頸非常有用。

BPF_HASH對映用於計數每個獨特堆積積疊的出現