eBPF Từ Số Không
Học eBPF từ nền tảng tới tự viết chương trình thật — máy ảo eBPF, verifier, maps, các hook (XDP/tc/kprobe/tracepoint/LSM); tracing với bpftrace; viết chương trình bằng libbpf + CO-RE (C) rồi nạp từ Go (cilium/ebpf); networking, observability và security. Dùng một cụm Kubernetes thật (kernel 6.17, Cilium 1.19 eBPF kube-proxy-less với hàng trăm chương trình BPF đang chạy) làm phòng lab xuyên suốt. Test thật, bám docs chính thức (ebpf.io, kernel.org, libbpf, cilium). Mã nguồn tại github.com/nghiadaulau/ebpf-from-scratch.
eBPF Từ Số Không: Chạy Chương Trình Trong Nhân Linux
Ngay lúc này, trên một worker của cụm Kubernetes ta dựng ở series trước, có 140 chương trình eBPF đang chạy bên trong nhân Linux — định tuyến từng gói tin, kiểm soát quyền truy cập thiết bị, gom metric. eBPF cho phép nạp mã vào nhân và chạy an toàn tại các điểm móc, không sửa mã nguồn nhân, không nạp module. Bài mở đầu series giải thích eBPF là gì, vì sao nó đổi cách mở rộng nhân, và một chương trình đi từ code tới mã máy chạy trong nhân ra sao.
Máy Ảo eBPF: Thanh Ghi, Tập Lệnh và Bytecode
Bài trước thấy một chương trình eBPF có 'xlated 512B' (bytecode đã verify) và 'jited 333B' (mã máy). Bài này đi vào trong cái bytecode đó: eBPF là một máy ảo kiểu RISC với 11 thanh ghi 64-bit, một tập lệnh nhỏ, được thiết kế để vừa dịch nhanh sang mã máy gốc vừa kiểm chứng được an toàn. Ta đọc thẳng bytecode của một chương trình Cilium đang chạy, soi từng lệnh ánh xạ ra thanh ghi và lớp lệnh nào, rồi xem vì sao đúng thiết kế máy ảo này mới cho phép verifier chứng minh an toàn.
Verifier: Vì Sao eBPF Không Sập Nhân
Bài 1 nói thiết kế máy ảo eBPF cho phép verifier chứng minh an toàn. Bài này thấy nó làm thật: ta biên dịch một chương trình XDP đọc byte đầu của gói tin mà quên kiểm giới hạn, nạp vào nhân — verifier từ chối với log chỉ đích danh thanh ghi và lý do. Thêm đúng một câu kiểm data_end, verifier cho qua. Verifier là bộ chứng minh an toàn chạy lúc nạp, theo dõi trạng thái từng thanh ghi trên mọi nhánh — thứ khiến eBPF nạp được mã lạ vào nhân mà không như kernel module.
Maps: Bộ Nhớ và Cầu Nối Với Userspace
Một chương trình eBPF chạy theo từng sự kiện rồi tắt, không giữ biến giữa các lần. Maps là cách nó nhớ trạng thái và nói chuyện với userspace. Bài này viết một chương trình đếm mỗi lần có tiến trình exec vào một map, nạp vào nhân, chạy vài lệnh, rồi đọc map từ userspace bằng bpftool — thấy con số tăng thật. Kèm soi một map thật của Cilium đang giữ metric per-CPU, và phân biệt array thường với per-CPU.
Program Type và Hook: Gắn Vào Đâu, Thấy Được Gì
Một chương trình eBPF không chạy lơ lửng — nó gắn vào một hook trong nhân, và loại hook đó quyết định ba thứ: chương trình chạy lúc nào, nhận context gì, và được gọi helper nào. Bài này liệt kê các program type nhân hỗ trợ, rồi gắn một tracepoint vào syscall openat để thấy nó chạy thật trên mỗi lần mở file — tương phản với XDP nhận gói tin ở bài trước, để thấy vì sao cùng là eBPF mà mỗi loại thấy một thế giới khác nhau.
BTF và CO-RE: Một Lần Biên Dịch, Chạy Mọi Kernel
Cấu trúc dữ liệu bên trong nhân như task_struct có layout khác nhau giữa các phiên bản kernel — field nằm ở offset nào tùy bản. Vậy làm sao một chương trình eBPF biên dịch sẵn đọc đúng field trên mọi kernel? Câu trả lời là BTF và CO-RE. Bài cuối Part I sinh vmlinux.h từ BTF của nhân, viết một chương trình đọc ppid bằng cách lần qua task->real_parent->tgid, biên dịch một lần và chạy — libbpf tự tìm offset đúng theo BTF của kernel đang chạy. Đây cũng là nền cho Part III tự viết công cụ thật.
bpftrace: Viết Tracing Trong Một Dòng
Part I tự viết chương trình eBPF bằng C, clang, bpftool — nhiều bước cho một câu hỏi đơn giản như 'tiến trình nào đang mở file nào'. bpftrace là đường tắt: một dòng lệnh trả lời ngay, không C không clang. Nhưng bên dưới vẫn là eBPF — bài này chứng minh điều đó (bpftool thấy chương trình bpftrace nạp vào rồi biến mất), rồi đi qua cú pháp probe/filter/action, kho 122 nghìn probe gắn được, và các biến dựng sẵn, qua các one-liner chạy thật trên cụm.
bpftrace: Maps, Đếm và Histogram
In từng dòng ngập màn hình khi sự kiện dày. Sức mạnh thật của bpftrace là gộp dữ liệu ngay trong nhân: đếm theo khóa, lập biểu đồ phân phối, rồi chỉ trả về bản tóm tắt nhỏ. Bài này dùng map @ của bpftrace để đếm syscall theo tiến trình, rồi dựng một histogram độ trễ vfs_read thật bằng cặp kprobe/kretprobe — thấy phân phối với cột ASCII, gồm cả cái đuôi chậm mà số trung bình sẽ giấu mất.
uprobe, USDT và Soi Pod Từ Host
Tới giờ ta gắn vào nhân. eBPF còn với được vào userspace: uprobe gắn vào hàm của một chương trình hay thư viện thường, USDT gắn vào điểm tracepoint mà ứng dụng cài sẵn. Bài này trace hàm getaddrinfo trong libc để thấy chương trình nào đang phân giải tên miền gì, rồi dùng đúng kỹ thuật đó soi một pod thật trên cụm từ phía host — thấy nó gọi syscall gì mà không cần đụng vào pod. Đây là lý do eBPF thành nền quan sát cho Kubernetes.
libbpf và CO-RE: Tự Viết Một Công Cụ eBPF
bpftrace hợp cho câu hỏi nhanh. Khi cần một công cụ thật — đóng gói, phân phối, chạy lâu dài — ta viết chương trình eBPF bằng C với libbpf và CO-RE. Bài này dựng execsnoop từ đầu: chương trình nhân đẩy sự kiện exec qua ring buffer, bpftool sinh skeleton, một loader C dùng libbpf nạp và đọc sự kiện. Build đủ chuỗi clang → skeleton → link libbpf, rồi chạy thấy từng lần exec trên cụm hiện ra với pid, ppid, tên file — kèm một cái bẫy buffer thật.
cilium/ebpf: Nạp eBPF Từ Go
Bài 9 dựng execsnoop bằng C với libbpf. Bài này viết lại đúng công cụ đó nhưng nạp từ Go bằng thư viện cilium/ebpf — cách hệ sinh thái Kubernetes (Cilium, Tetragon, Falco) build ứng dụng eBPF. Phía nhân không đổi; bpf2go biên dịch nó và nhúng thẳng object vào mã Go, rồi một chương trình Go gắn tracepoint và đọc ring buffer. Kết quả là một binary tĩnh duy nhất, không phụ thuộc libbpf.so — và ta gặp đúng những cái bẫy thật khi dựng nó.
XDP: Xử Lý Gói Ở Điểm Sớm Nhất, Viết Một Firewall
XDP gắn chương trình eBPF vào driver mạng, chạy trên mỗi gói tới trước cả khi nhân cấp phát sk_buff — điểm sớm nhất có thể đụng vào một gói. Nó trả về một verdict: PASS, DROP, TX, REDIRECT. Bài này dựng một XDP firewall nhỏ drop ICMP trên một interface thật, gắn vào card mạng của node bằng bptool, rồi xem ping rớt từ 0% lên 100% loss trong khi SSH vẫn sống — và thấy nó nằm trước datapath tc của Cilium trên cùng interface ra sao.