cilium/ebpf: Nạp eBPF Từ Go

K
Kai··4 min read

Bài 9 dựng execsnoop bằng C: clang biên dịch phía nhân, bpftool sinh skeleton, loader C link libbpf. Bài này dựng đúng công cụ đó nhưng loader viết bằng Go với thư viện cilium/ebpf. Đây là cách phần lớn công cụ eBPF trong thế giới Kubernetes được viết — Cilium, Tetragon, Falco đều dùng Go — vì Go là ngôn ngữ chung của hệ sinh thái k8s và cho ra một binary tĩnh dễ phân phối.

Phía nhân không đổi, chỉ đổi loader

Điểm quan trọng: exec.bpf.c (phía nhân, Bài 9) giữ nguyên — vẫn là tracepoint exec đẩy sự kiện qua ring buffer. Chỉ phần userspace đổi từ C sang Go. eBPF là eBPF; thay đổi chỉ ở ai nạp và đọc nó.

bpf2go: biên dịch và sinh binding Go

Trong C, ta chạy clang rồi bpftool gen skeleton tay. Trong Go, công cụ bpf2go làm cả hai qua một directive go:generate:

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang exec exec.bpf.c -- -I.
go generate
exec_bpfel.go    exec_bpfel.o      # little-endian: Go binding + object nhúng
exec_bpfeb.go    exec_bpfeb.o      # big-endian

bpf2go chạy clang biên dịch exec.bpf.c, rồi sinh file Go (exec_bpfel.go) nhúng thẳng bytecode object vào mã Go, kèm các kiểu có sẵn: execObjects (gom map + program), loadExecObjects(), trường HandleExec (program), Events (map). Tên suy từ tiền tố exec + tên hàm/map trong C. Đây là tương đương của skeleton ở Bài 9, nhưng là mã Go.

Một chi tiết bắt buộc: thêm //go:build ignore lên đầu exec.bpf.c. Đó là constraint của Go (clang coi là comment, bỏ qua) để go build không cố biên dịch file C đó như mã Go — không có nó, build báo lỗi C source files not allowed.

main.go: nạp, gắn, đọc

import (
    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/ringbuf"
    "github.com/cilium/ebpf/rlimit"
)

type event struct {                 // khớp struct trong exec.h
    Pid, Ppid uint32
    Comm      [16]byte
    Filename  [64]byte
}

func main() {
    rlimit.RemoveMemlock()                                   // nới giới hạn bộ nhớ khóa
    var objs execObjects
    loadExecObjects(&objs, nil)                              // nạp + verifier + CO-RE + JIT
    defer objs.Close()

    tp, _ := link.Tracepoint("sched", "sched_process_exec", objs.HandleExec, nil)  // gắn
    defer tp.Close()

    rd, _ := ringbuf.NewReader(objs.Events)                  // mở ring buffer
    defer rd.Close()

    var e event
    for {
        rec, err := rd.Read()                            // chờ sự kiện kế
        if errors.Is(err, ringbuf.ErrClosed) { return }
        binary.Read(bytes.NewReader(rec.RawSample), binary.LittleEndian, &e)
        fmt.Printf("%-16s %-8d %-8d %s\n", cstr(e.Comm[:]), e.Pid, e.Ppid, cstr(e.Filename[:]))
    }
}

Đối chiếu với loader C Bài 9: loadExecObjects thay exec_bpf__open_and_load (cùng việc: nạp, CO-RE relocate, verifier, JIT); gói link thay lệnh attach; ringbuf.Reader thay ring_buffer__poll. Sự kiện đọc về là byte thô — binary.Read giải vào struct event (phải khớp layout C). rlimit.RemoveMemlock() là bước chuẩn của ứng dụng eBPF Go.

Build và chạy

go generate              # bpf2go: clang + nhúng object + sinh binding
go build -o execsnoop-go .
sudo ./execsnoop-go
COMM             PID      PPID     FILENAME
iptables         372461   213711   /usr/sbin/iptables
iptables         372462   213711   /usr/sbin/iptables
iptables         372463   213711   /usr/sbin/iptables

Cùng kết quả Bài 9 — stream mọi lần exec, thấy cilium-agent (ppid 213711) liên tục gọi iptables. Nhưng khác biệt then chốt: execsnoop-gomột binary tĩnh 5.4MB đã nhúng object eBPF bên trong. Không cần ship file .o hay skeleton riêng, không phụ thuộc libbpf.so lúc chạy — chỉ một file, scp sang máy khác (cùng kiến trúc, kernel có BTF) là chạy, CO-RE lo phần khác kernel. Đây là lý do hệ k8s chuộng Go cho eBPF: gói gọn một binary, dễ đóng container, dễ phân phối.

Hai cái bẫy khi dựng

  • llvm-strip thiếu: bpf2go strip object sau khi biên dịch, cần llvm-strip (gói llvm, không có trong clang một mình). Thiếu thì go generate báo exec: "llvm-strip": executable file not found.
  • //go:build ignore trên .bpf.c: như đã nói, không có thì go build báo lỗi C source.

🧹 Dọn dẹp

go run github.com/cilium/ebpf/cmd/bpf2go ...   # các file sinh ra: xóa hoặc giữ trong git
rm -rf /tmp/goebpf /tmp/gopath                 # project + module cache (cache cần sudo nếu read-only)

Chương trình tự gỡ khi thoát; node về 140 chương trình. Mã nguồn (exec.bpf.c, main.go, exec.h, go.mod/go.sum) ở github.com/nghiadaulau/ebpf-from-scratch, thư mục 10-cilium-ebpf-go; chạy go generate để sinh binding rồi go build.

Tổng kết

Cùng một chương trình eBPF phía nhân (Bài 9), loader đổi từ C sang Go với cilium/ebpf. bpf2go (qua go:generate) biên dịch exec.bpf.c bằng clang và nhúng object vào mã Go kèm binding có kiểu (execObjects, loadExecObjects, HandleExec, Events) — tương đương skeleton nhưng là Go. main.go dùng loadExecObjects (nạp + CO-RE + verifier + JIT), gói link để gắn tracepoint, ringbuf.Reader để đọc sự kiện, giải byte thô vào struct khớp layout C. Kết quả là một binary tĩnh duy nhất nhúng sẵn eBPF — không cần libbpf.so hay file rời, dễ phân phối, đúng lý do Cilium/Tetragon chọn Go. Bẫy thực tế: cần llvm-strip (gói llvm) và //go:build ignore trên file .bpf.c.

Part III khép lại — ta đã tự viết công cụ eBPF đầy đủ, bằng cả C (libbpf) lẫn Go (cilium/ebpf). Part IV bước vào lãnh địa eBPF nổi tiếng nhất: networking — XDP xử lý gói ở mức sớm nhất, tc, và mổ thẳng datapath Cilium đang chạy trên cụm để thấy 74 chương trình sched_cls định tuyến từng gói pod ra sao.

Related Posts