Program Type và Hook: Gắn Vào Đâu, Thấy Được Gì

K
Kai··4 min read

Các bài trước chạy hai loại chương trình rất khác nhau: một XDP nhận gói tin (Bài 2), một tracepoint nhận sự kiện exec (Bài 3). Cả hai đều là eBPF, nhưng thấy được những thứ hoàn toàn khác. Lý do nằm ở program type — và đây là khái niệm quyết định một chương trình eBPF làm được gì.

Program type quyết định ba thứ

Khi nạp một chương trình, ta khai nó thuộc một program type. Type đó quyết định:

  1. Hook — chương trình gắn vào đâu, tức chạy lúc nào: gói tới card mạng, syscall được gọi, hàm nhân chạy, thao tác bảo mật diễn ra...
  2. Context — đối số r1 lúc bắt đầu trỏ vào cấu trúc gì (Bài 1): gói tin? tham số syscall? thanh ghi CPU?
  3. Helper được phép — mỗi type chỉ gọi được một tập helper hợp lý với nó, và verifier (Bài 2) áp ràng buộc riêng theo type.

Nhân hỗ trợ hàng chục type. Soi danh sách trên node:

sudo bpftool feature probe | grep 'program_type .* is available'
program_type socket_filter is available
program_type kprobe is available
program_type sched_cls is available      # tc — datapath Cilium dùng (Bài 0)
program_type tracepoint is available
program_type xdp is available
program_type perf_event is available
program_type cgroup_skb is available
program_type sock_ops is available
program_type sk_skb is available
... (và nhiều nữa)

Gom lại theo nhóm: mạng (xdp, sched_cls/tc, socket_filter, sk_skb, sock_ops), tracing (kprobe, tracepoint, perf_event, uprobe), cgroup (cgroup_skb, cgroup_sock...), và bảo mật (lsm). Cilium ở cụm này dùng chủ yếu sched_cls (tc) cho datapath và cgroup_* (Bài 0).

Một hook khác: tracepoint trên syscall

Bài 2 dùng XDP — context là gói tin. Giờ thử một type ở đầu kia: tracepoint, gắn vào syscall openat (mở file). Context khác hẳn, và ta lấy thông tin sự kiện qua helper thay vì đọc gói:

SEC("tracepoint/syscalls/sys_enter_openat")
int on_openat(void *ctx)
{
    char comm[16];
    bpf_get_current_comm(comm, sizeof(comm));            // helper: tên tiến trình
    __u32 pid = bpf_get_current_pid_tgid() >> 32;        // helper: pid
    bpf_printk("openat by %s (pid %d)", comm, pid);      // helper: in ra trace_pipe
    return 0;
}

SEC("tracepoint/syscalls/sys_enter_openat") khai cả type (tracepoint) lẫn hook cụ thể. Nạp và tự gắn:

clang -O2 -g -target bpf -I/usr/include/x86_64-linux-gnu -c openat_trace.bpf.c -o openat_trace.bpf.o
sudo bpftool prog loadall openat_trace.bpf.o /sys/fs/bpf/openat autoattach

bpf_printk ghi vào trace_pipe của nhân. Đọc nó vài giây trong khi hệ thống mở file:

sudo timeout 2 cat /sys/kernel/debug/tracing/trace_pipe | grep 'openat by'
   basename-355997 [000] ....1 47718.657120: bpf_trace_printk: openat by basename (pid 355997)
   basename-355997 [000] ....1 47718.657129: bpf_trace_printk: openat by basename (pid 355997)
   basename-355997 [000] ....1 47718.657133: bpf_trace_printk: openat by basename (pid 355997)

Chương trình chạy mỗi lần bất kỳ tiến trình nào trên máy gọi openat — đây là sức mạnh quan sát của tracepoint: gắn một chỗ, thấy toàn hệ thống. Context của nó không phải gói tin mà là sự kiện syscall; ta lấy tên tiến trình và pid qua helper.

Cùng là eBPF, mỗi type thấy một thế giới

Đặt cạnh nhau ba type đã/sẽ gặp:

   type        hook (chạy khi)              context (r1 trỏ vào)
   ─────────   ──────────────────────       ─────────────────────────
   xdp         gói tin tới card mạng        struct xdp_md (data, data_end)
   tracepoint  một tracepoint nhân nổ       cấu trúc sự kiện (vd tham số syscall)
   kprobe      một hàm nhân được gọi        struct pt_regs (thanh ghi CPU lúc đó)

Cùng một máy ảo (Bài 1), cùng verifier (Bài 2), cùng cơ chế maps (Bài 3) — nhưng XDP "thấy" gói tin và sửa được đường đi của nó, còn tracepoint "thấy" sự kiện và chỉ quan sát. Verifier cũng siết khác nhau: chương trình XDP đọc gói phải kiểm data_end (Bài 2), còn tracepoint không có khái niệm đó. Chọn program type chính là chọn đứng ở đâu trong nhân để nhìn và tác động. Phần còn lại của series khai thác đúng các type này: Part IV networking (xdp, tc), Part V security (lsm), Part VI observability (kprobe, tracepoint, perf_event).

🧹 Dọn dẹp

sudo rm -rf /sys/fs/bpf/openat
rm -f /tmp/openat_trace.bpf.*

Gỡ pin là chương trình tự gỡ khỏi tracepoint; node về 140 chương trình. Mã nguồn ở github.com/nghiadaulau/ebpf-from-scratch, thư mục 04-program-types.

Tổng kết

Program type là thứ quyết định một chương trình eBPF gắn vào hook nào (chạy khi nào), nhận context gì (r1 trỏ vào cấu trúc nào), và được gọi helper nào — với ràng buộc verifier riêng theo type. Nhân hỗ trợ hàng chục type chia theo nhóm mạng (xdp, tc/sched_cls, socket), tracing (kprobe, tracepoint, perf_event), cgroup, lsm. Ta gắn một tracepoint vào sys_enter_openat và thấy nó chạy thật trên mỗi lần mở file (trace_pipe in openat by basename), lấy comm/pid qua helper — context hoàn toàn khác XDP nhận gói ở Bài 2. Cùng máy ảo, cùng verifier, cùng maps, nhưng mỗi type đứng ở một điểm khác trong nhân và thấy một thế giới khác.

Còn một mảnh nền tảng trước khi tự viết công cụ thật: làm sao một chương trình đọc được cấu trúc dữ liệu bên trong nhân (như task_struct) khi layout của chúng đổi theo từng phiên bản kernel? Bài 5 — BTF và CO-RE — trả lời, và mở đường sang Part III.

Related Posts