Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

libbpf 架構解析與 xgotop 實作說明

目錄


1. libbpf 是什麼

libbpf 是一個 userspace library,負責在用戶空間(userspace)和 Linux kernel 之間橋接 eBPF 程式。

核心定位

┌─────────────────────────────────────────┐
│         Your Application                │
│         (xgotop Go 程式)                 │
│  - 業務邏輯                              │
│  - 資料處理                              │
│  - UI/API                                │
└────────────────┬────────────────────────┘
                 │
                 │ 使用 libbpf API
                 ↓
┌─────────────────────────────────────────┐
│         libbpf Library                  │  ← 這裡!
│  (xgotop 使用 cilium/ebpf - Go 版本)     │
│                                         │
│  功能:                                  │
│  1. 載入 eBPF 程式到 kernel              │
│  2. Attach probe 到函數                  │
│  3. 管理 eBPF maps (kernel-user 通訊)    │
│  4. 讀取 ringbuffer/perf buffer         │
│  5. BTF 重定位 (CO-RE)                   │
└────────────────┬────────────────────────┘
                 │
                 │ syscall: bpf()
                 ↓
┌─────────────────────────────────────────┐
│         Linux Kernel                    │
│                                         │
│  - eBPF 虛擬機執行程式                   │
│  - eBPF maps 儲存                        │
│  - Ringbuffer                           │
│  - Verifier (安全性驗證)                 │
└─────────────────────────────────────────┘

2. 整體架構

2.1 開發流程

┌─────────────── 開發時 ──────────────┐
│                                    │
│  1. 生成 vmlinux.h                  │
│     bpftool btf dump file          │
│     /sys/kernel/btf/vmlinux        │
│            ↓                       │
│  2. 編寫 eBPF C 程式                │
│     #include "vmlinux.h"           │
│     xgotop.bpf.c                   │
│            ↓                       │
│  3. 編譯成 .bpf.o                   │
│     clang -target bpf              │
│     含 BTF 型別資訊                 │
│            ↓                       │
│  4. 產生 Go binding                 │
│     bpf2go (cilium/ebpf)           │
│     生成 ebpfObjects 結構           │
│                                    │
└────────────────────────────────────┘
                 │
                 │ 部署
                 ↓
┌─────────────── 運行時 ──────────────┐
│                                    │
│  1. libbpf 載入 .bpf.o              │
│     loadEbpfObjects()              │
│            ↓                       │
│  2. BTF 重定位 (CO-RE)              │
│     讀取 /sys/kernel/btf/vmlinux   │
│     調整 offset                     │
│            ↓                       │
│  3. Kernel verifier 驗證            │
│     安全性檢查                      │
│            ↓                       │
│  4. JIT 編譯並執行                  │
│     eBPF bytecode → native code    │
│            ↓                       │
│  5. Attach probe                   │
│     ex.Uprobe(symbol, prog)        │
│            ↓                       │
│  6. 事件流動                        │
│     Kernel → Ringbuffer → libbpf  │
│            → Application           │
│                                    │
└────────────────────────────────────┘

2.2 資料流動

Go Runtime (目標程式)
     │
     │ goroutine 事件發生
     │ (create/exit/alloc)
     ↓
┌──────────────────────────┐
│ eBPF Uprobe Hook         │
│ runtime.casgstatus()     │  ← xgotop.bpf.c
│ runtime.newproc1()       │
│ runtime.makeslice()      │
│ ...                      │
└────────┬─────────────────┘
         │
         │ 收集資料
         ↓
┌──────────────────────────┐
│ eBPF Program             │
│ - 讀取 g struct          │
│ - 記錄 goid, timestamp   │
│ - 檢查 sampling rate     │
└────────┬─────────────────┘
         │
         │ 寫入
         ↓
┌──────────────────────────┐
│ eBPF Ringbuffer          │  ← Kernel space
│ (環形緩衝區)              │
└────────┬─────────────────┘
         │
         │ libbpf 讀取
         │ ringbuf.NewReader()
         ↓
┌──────────────────────────┐
│ xgotop Userspace         │
│ - Event readers (goroutines)
│ - Event processors       │
└────────┬─────────────────┘
         │
         ├─→ Storage (Protobuf/JSONL)
         │
         └─→ WebSocket → Web UI

3. libbpf 核心功能

3.1 六大功能模組

功能說明API 範例xgotop 使用位置
Load載入 .bpf.o 到 kernelloadEbpfObjects()main.go:168
Verify協助 kernel verifier自動執行-
RelocateBTF-based CO-RE 重定位自動執行-
Attach附加到 hook 點ex.Uprobe()main.go:215
Maps管理 kernel-user 通訊map.Update()main.go:182
Data I/ORingbuffer/Perf bufferringbuf.NewReader()main.go:220

3.2 功能詳解

3.2.1 Load - 載入 eBPF 物件

// cmd/xgotop/main.go:167-170
objs := ebpfObjects{}
err = loadEbpfObjects(&objs, nil)  // ← libbpf API
must(err, "loading objects")
defer objs.Close()

背後發生的事:

loadEbpfObjects()
    ↓
1. 讀取 xgotop.bpf.o 檔案
2. 解析 ELF sections
3. 提取 eBPF 程式碼和 BTF 資訊
4. 呼叫 bpf(BPF_PROG_LOAD, ...)
5. Kernel verifier 驗證程式安全性
6. 初始化 eBPF maps
7. 回傳 objs 結構(包含所有程式和 maps)

3.2.2 Attach - 附加到目標函數

// cmd/xgotop/main.go:205-218
ex, err := link.OpenExecutable(executablePath)

uprobeOpts := &link.UprobeOptions{}
if *pid != 0 {
    uprobeOpts.PID = *pid  // 只監控特定 PID
}

probes := map[string]*ebpf.Program{
    "runtime.casgstatus": objs.UprobeCasgstatus,
    "runtime.makeslice":  objs.UprobeMakeslice,
    "runtime.newproc1":   objs.UprobeNewproc1,
    // ...
}

for symbol, probe := range probes {
    uprobe, err := ex.Uprobe(symbol, probe, uprobeOpts)
    //              ↑ libbpf attach API
    must(err, "attaching uprobe at "+symbol)
    defer uprobe.Close()
}

Uprobe 原理:

目標 binary (testserver)
    ↓
1. 找到 runtime.casgstatus 的位址
2. 在該位址設置 breakpoint
3. 當執行到該位址時:
   → 觸發 uprobe
   → 執行 eBPF 程式
   → 記錄事件
   → 繼續執行原程式

3.2.3 Maps - Kernel-Userspace 通訊

// cmd/xgotop/main.go:179-187
if objs.SamplingRates != nil {
    for eventType, rate := range rates {
        key := uint32(eventType)
        err := objs.SamplingRates.Update(&key, &rate, ebpf.UpdateAny)
        //                             ↑ userspace → kernel
        if err != nil {
            log.Fatalf("Failed to update sampling rate: %v", err)
        }
    }
}

Maps 類型:

// xgotop.h 中的 map 定義
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 16);
    __type(key, u32);
    __type(value, u32);
} sampling_rates SEC(".maps");

雙向通訊:

Userspace                    Kernel
    │                           │
    │  map.Update(key, value)   │
    │ ────────────────────────> │  eBPF 程式讀取
    │                           │  map_lookup_elem()
    │                           │
    │  map.Lookup(&key, &val)   │
    │ <──────────────────────── │  eBPF 程式寫入
    │                           │  map_update_elem()

3.2.4 Ringbuffer - 高效事件傳輸

// cmd/xgotop/main.go:220-222
rd, err := ringbuf.NewReader(objs.Events)
must(err, "creating events ringbuf reader")
defer rd.Close()

// 讀取事件
for {
    record, err := rd.Read()
    if err != nil {
        break
    }
    // 處理 record.RawSample
}

Ringbuffer 優勢:

傳統 Perf Buffer              Ringbuffer (現代)
    │                              │
    ├─ 每個 CPU 一個 buffer        ├─ 全域共享 buffer
    ├─ 需要輪詢所有 CPU            ├─ 單一讀取點
    ├─ 可能 reorder                ├─ 保證順序
    └─ 較高 overhead               └─ 更高效

4. xgotop 中的實際應用

4.1 完整初始化流程

// cmd/xgotop/main.go 簡化版

func main() {
    // 1. 移除記憶體鎖定限制
    err := rlimit.RemoveMemlock()
    // 允許 eBPF 鎖定記憶體,避免被 swap

    // 2. 載入 eBPF 物件
    objs := ebpfObjects{}
    err = loadEbpfObjects(&objs, nil)
    defer objs.Close()

    // 3. 設定 sampling rates (透過 map)
    for eventType, rate := range rates {
        key := uint32(eventType)
        objs.SamplingRates.Update(&key, &rate, ebpf.UpdateAny)
    }

    // 4. Attach uprobes
    ex, err := link.OpenExecutable(executablePath)
    for symbol, probe := range probes {
        uprobe, err := ex.Uprobe(symbol, probe, nil)
        defer uprobe.Close()
    }

    // 5. 建立 ringbuffer reader
    rd, err := ringbuf.NewReader(objs.Events)
    defer rd.Close()

    // 6. 啟動 event readers (goroutines)
    for i := 0; i < *readWorkers; i++ {
        go readEvents(rd, eventCh)
    }

    // 7. 啟動 event processors
    for i := 0; i < *processWorkers; i++ {
        go processEvents(eventCh, eventStore)
    }
}

4.2 事件處理流程

[Kernel] eBPF Hook 觸發
    ↓
[Kernel] 寫入 Ringbuffer
    ↓
[User] ringbuf.NewReader().Read()  ← libbpf
    ↓
[User] readEvents() goroutine
    ↓ 放入 channel
[User] eventCh := make(chan *ebpfGoRuntimeEventT, 1_000_000)
    ↓
[User] processEvents() goroutine
    ↓
[User] Storage (Protobuf/JSONL)
    ↓
[User] WebSocket → Web UI

4.3 生成的 Go Binding

# gen.go
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go \
    -type go_runtime_event_t \
    -target arm64 \
    -output-dir cmd/xgotop \
    ebpf xgotop.bpf.c

生成檔案:

cmd/xgotop/
├── ebpf_arm64_bpfeb.go     ← Big-endian
├── ebpf_arm64_bpfel.go     ← Little-endian
└── ebpf_arm64_bpfel.o      ← eBPF bytecode

自動生成的結構:

// ebpf_arm64_bpfel.go (自動生成)

type ebpfObjects struct {
    // Programs
    UprobeCasgstatus *ebpf.Program
    UprobeMakeslice  *ebpf.Program
    UprobeNewproc1   *ebpf.Program
    // ...

    // Maps
    Events         *ebpf.Map  // Ringbuffer
    SamplingRates  *ebpf.Map  // Array map
}

func loadEbpfObjects(obj *ebpfObjects, opts *ebpf.CollectionOptions) error {
    // 自動生成的載入邏輯
}

5. BCC vs libbpf 比較

5.1 開發方法對比

BCC 方法(傳統)

# BCC Python 範例
from bcc import BPF

bpf_text = """
#include <linux/sched.h>      // ← 使用 kernel headers
#include <linux/fs.h>

int trace_open(struct pt_regs *ctx) {
    struct task_struct *task =
        (struct task_struct *)bpf_get_current_task();
    bpf_trace_printk("PID: %d\\n", task->pid);
    return 0;
}
"""

b = BPF(text=bpf_text)  # ← 運行時編譯
b.attach_kprobe(event="do_sys_open", fn_name="trace_open")

特點:

  • ❌ 不需要 vmlinux.h
  • ✅ 使用 kernel headers (/usr/include/linux/*)
  • 🔄 運行時編譯(每次執行都編譯)
  • 📦 需要安裝 LLVM/Clang
  • 🐍 Python API(易學)

libbpf + CO-RE 方法(現代,xgotop 使用)

// xgotop.bpf.c
#include "vmlinux.h"           // ← 使用 vmlinux.h
#include <bpf/bpf_helpers.h>

SEC("uprobe/runtime.casgstatus")
int uprobe_casgstatus(struct pt_regs *ctx) {
    struct go_runtime_g g;
    bpf_probe_read(&g, sizeof(g), gp);
    return 0;
}
# 開發機器:預先編譯
clang -target bpf -c xgotop.bpf.c -o xgotop.bpf.o

# 目標機器:直接執行
./xgotop  # 不需要編譯器

特點:

  • 需要 vmlinux.h
  • ❌ 不使用 kernel headers
  • 預先編譯 (Compile Once, Run Everywhere)
  • 📦 只需要目標機器支援 BTF
  • 🚀 啟動快速、部署輕量

5.2 完整對比表

項目BCClibbpf + CO-RE
vmlinux.h❌ 不需要需要
Kernel Headers✅ 需要❌ 不需要
LLVM/Clang運行時需要編譯時需要
編譯時機運行時開發時
啟動速度慢(5-10秒)快(<1秒)
部署大小大(需要 headers)小(單一 binary)
學習曲線簡單(Python)較陡(C + BTF)
適用場景快速開發、實驗生產環境
跨版本相容好(CO-RE)
效能 overhead較高較低

5.3 編譯流程對比

BCC:

運行時(目標機器)
┌─────────────────────────────┐
│ 1. Python 腳本執行           │
│ 2. 讀取 kernel headers       │ ← 需要安裝
│    /usr/include/linux/*     │
│ 3. LLVM 即時編譯             │ ← 每次都編譯
│ 4. 載入到 kernel             │
└─────────────────────────────┘
   啟動時間:5-10 秒

libbpf + CO-RE:

開發時(開發機器)              運行時(目標機器)
┌─────────────────────┐       ┌──────────────────┐
│ 1. 生成 vmlinux.h    │       │ 1. 載入 .bpf.o    │
│ 2. clang 編譯        │  ──>  │ 2. BTF 重定位     │ ← 自動適配
│ 3. 產生 .bpf.o       │       │ 3. 執行           │
└─────────────────────┘       └──────────────────┘
                               啟動時間:<1 秒

6. vmlinux.h 的角色

6.1 為什麼需要 vmlinux.h

問題:Kernel 版本差異

// Kernel 5.10
struct task_struct {
    int field_a;      // offset 0
    long field_b;     // offset 4
};

// Kernel 6.14
struct task_struct {
    long new_field;   // offset 0  ← 新增!
    int field_a;      // offset 8  ← offset 改變!
    long field_b;     // offset 12
};

如果寫死 offset,程式會在不同 kernel 版本上讀到錯誤資料

6.2 vmlinux.h 生成流程

# Makefile
vmlinux:
    bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

流程圖:

/sys/kernel/btf/vmlinux
    ↓ (二進位 BTF 格式)
    │ 包含 kernel 所有型別資訊
    ↓
bpftool btf dump
    ↓ (轉換成 C header)
    │ 產生 3.8MB 的 header
    ↓
vmlinux.h
    ↓ (包含所有結構定義)
    │ struct task_struct { ... }
    │ struct pt_regs { ... }
    │ ...(數千個結構)
    ↓
#include "vmlinux.h"  ← xgotop.h

6.3 在 xgotop 的使用

// xgotop.h:4
#include "vmlinux.h"

// 使用 vmlinux.h 中的定義
struct pt_regs {  // ← 來自 vmlinux.h
    unsigned long regs[31];
    unsigned long sp;
    unsigned long pc;
    // ...
};

// xgotop.bpf.c 中使用
SEC("uprobe/runtime.casgstatus")
int BPF_KPROBE(uprobe_casgstatus,
               const void *gp,
               const u32 oldval,
               const u32 newval) {
    struct go_runtime_g g;
    // pt_regs 透過 ctx 傳入,型別來自 vmlinux.h
}

7. CO-RE 機制詳解

7.1 CO-RE 架構

CO-RE = Compile Once, Run Everywhere

┌─────────── 編譯時 ────────────┐
│                              │
│  vmlinux.h (開發機器)         │
│  struct task_struct {        │
│      int pid;  // offset 123 │  ← BTF 記錄
│  }                           │
│         ↓                    │
│  clang -target bpf           │
│         ↓                    │
│  xgotop.bpf.o                │
│  + BTF relocations           │  ← 記住 "pid" 這個欄位名稱
│                              │
└──────────────────────────────┘
            │
            │ 部署
            ↓
┌─────────── 運行時 ────────────┐
│                              │
│  /sys/kernel/btf/vmlinux     │
│  (目標機器實際 kernel)        │
│  struct task_struct {        │
│      long new_field;         │
│      int pid;  // offset 456 │  ← 不同 offset!
│  }                           │
│         ↓                    │
│  libbpf 重定位引擎            │
│         ↓                    │
│  找到 "pid" 欄位              │
│  offset 123 → 456            │  ← 自動調整
│         ↓                    │
│  載入到 kernel                │
│                              │
└──────────────────────────────┘

7.2 BTF (BPF Type Format)

BTF 儲存的資訊:

// 原始結構
struct task_struct {
    int pid;           // offset 456, size 4
    char comm[16];     // offset 460, size 16
};

BTF 格式:

BTF Type #123: struct task_struct {
    member: pid
        type: int
        offset: 456 bits
        size: 32 bits
    member: comm
        type: char[16]
        offset: 460 bits
        size: 128 bits
}

7.3 libbpf 重定位過程

1. 讀取 .bpf.o 的 BTF section
   → 知道程式想存取 task_struct.pid
   → 編譯時 offset = 123

2. 讀取 /sys/kernel/btf/vmlinux
   → 找到 task_struct 定義
   → 運行時 offset = 456

3. 修改程式碼中的 offset
   → mov r1, [r0 + 123]  改成
   → mov r1, [r0 + 456]

4. 載入到 kernel

7.4 CO-RE Helper 巨集

// xgotop.h:6
#include <bpf/bpf_core_read.h>

// 使用 CO-RE helper
if (bpf_core_type_exists(struct trace_event_raw_bpf_trace_printk___x)) {
    // 檢查結構是否存在於當前 kernel
    // 不存在則跳過,避免 crash
}

// 讀取欄位(自動處理 offset)
int pid = BPF_CORE_READ(task, pid);
//        ↑ 展開成 bpf_probe_read + offset calculation

8. 實戰範例:追蹤流程

8.1 從 Hook 到顯示的完整路徑

[Step 1] Go 程式執行
testserver 的 runtime.casgstatus() 被呼叫

    ↓

[Step 2] Uprobe 觸發
xgotop attach 的 eBPF 程式執行

    ↓

[Step 3] eBPF 程式執行
// xgotop.bpf.c:4-30
SEC("uprobe/runtime.casgstatus")
int BPF_KPROBE(uprobe_casgstatus, ...) {
    struct go_runtime_g g;
    bpf_probe_read(&g, sizeof(g), gp);  // 讀取 goroutine 結構

    // 組裝事件
    struct go_runtime_event event = {
        .event_type = EVENT_TYPE_CASGSTATUS,
        .timestamp = bpf_ktime_get_ns(),
        .goid = g.goid,
        // ...
    };

    // 寫入 ringbuffer
    bpf_ringbuf_output(&events, &event, sizeof(event), 0);
}

    ↓

[Step 4] Ringbuffer → Userspace (libbpf)
// main.go:220
rd, err := ringbuf.NewReader(objs.Events)

// 多個 reader goroutines
for i := 0; i < *readWorkers; i++ {
    go func() {
        for {
            record, err := rd.Read()  // ← libbpf API
            eventCh <- parseEvent(record.RawSample)
        }
    }()
}

    ↓

[Step 5] Event Processing
// main.go:processEvents
go func() {
    for event := range eventCh {
        // 處理事件
        processedEvent := transform(event)

        // 儲存到 storage
        eventStore.Write(processedEvent)

        // 發送到 WebSocket
        apiServer.Broadcast(processedEvent)
    }
}()

    ↓

[Step 6] Storage & Web UI
Protobuf/JSONL 寫入磁碟
WebSocket 推送到瀏覽器
Timeline 視覺化顯示

8.2 效能指標追蹤

// main.go:234-238
var probeDurationNsSum atomic.Int64     // LAT: eBPF hook 時間
var processingTimeNsSum atomic.Int64    // PRC: 處理時間

var readEventCount atomic.Uint64        // RPS: 讀取速率
var procEventCount atomic.Uint64        // PPS: 處理速率

eventCh := make(chan *ebpfGoRuntimeEventT, 1_000_000)
// ↑ EWP: len(eventCh) = 等待處理的事件數

計算公式:

LAT = probeDurationNsSum / probeDurationNsCount
RPS = readEventCount / 時間間隔
PPS = procEventCount / 時間間隔
EWP = len(eventCh)
PRC = processingTimeNsSum / processingTimeNsCount

9. 總結

9.1 關鍵概念回顧

vmlinux.h
    ↓ 提供 kernel 型別定義
    ↓
eBPF C 程式 (xgotop.bpf.c)
    ↓ clang 編譯 + BTF
    ↓
.bpf.o (eBPF bytecode)
    ↓ libbpf 載入
    ↓
Kernel (執行)
    ↓ Ringbuffer
    ↓
libbpf 讀取
    ↓
Application (xgotop)

9.2 libbpf 的價值

沒有 libbpf有 libbpf
手動 syscall(__NR_bpf, ...)loadEbpfObjects()
手動解析 BTF自動重定位
手動管理 mapsmap.Update()
手動讀取 ringbufferringbuf.NewReader()
數百行 boilerplate幾行程式碼

9.3 開發建議

使用 BCC 當:

  • 快速原型開發
  • 一次性除錯任務
  • 學習 eBPF 基礎

使用 libbpf + CO-RE 當:

  • 生產環境工具
  • 需要跨 kernel 版本分發
  • 效能要求高
  • 最小化部署依賴

9.4 參考資源

  • cilium/ebpf: https://github.com/cilium/ebpf
  • libbpf 官方文檔: https://github.com/libbpf/libbpf
  • BPF CO-RE 介紹: https://nakryiko.com/posts/bpf-portability-and-co-re/
  • xgotop 專案: https://github.com/sazak-io/xgotop

文件版本: 1.0 最後更新: 2026-01-01 適用於: xgotop (arm64), Linux kernel 5.2+