cilium/ebpf: Nạp eBPF Từ Go
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-go là mộ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-stripthiếu: bpf2go strip object sau khi biên dịch, cầnllvm-strip(góillvm, không có trongclangmột mình). Thiếu thìgo generatebáoexec: "llvm-strip": executable file not found.//go:build ignoretrên.bpf.c: như đã nói, không có thìgo buildbá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.