bpftrace: Viết Tracing Trong Một Dòng
Part I tự viết chương trình eBPF bằng C: clang dịch sang bytecode, bpftool nạp, verifier kiểm. Đầy đủ và sâu, nhưng để trả lời một câu hỏi nhanh — "tiến trình nào đang mở file nào?" — thì nhiều bước. Part II dùng bpftrace: một dòng lệnh trả lời ngay. Mở đầu bằng bpftrace vì nó là cách nhanh nhất để thấy eBPF làm việc, trước khi ta tự viết công cụ phức tạp ở Part III.
bpftrace vẫn là eBPF
Điểm cần rõ ngay: bpftrace không phải công nghệ khác — nó là một ngôn ngữ tracing cấp cao mà bpftrace tự compile thành bytecode eBPF rồi nạp vào nhân, đúng quy trình Part I, chỉ là ta không viết C tay. Chứng minh: đếm chương trình eBPF trước/trong/sau khi chạy một one-liner bpftrace:
sudo bpftool prog show | grep -c '^[0-9]*:' # 140 (chỉ Cilium)
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { @ = count(); }' & # chạy nền
sudo bpftool prog show | grep -c '^[0-9]*:' # 141 !
# ...Ctrl-C bpftrace...
sudo bpftool prog show | grep -c '^[0-9]*:' # 140 trở lại
140 # trước
141 # trong khi bpftrace chạy: +1 chương trình (type tracepoint)
140 # bpftrace thoát: tự gỡ
Một chương trình eBPF (type tracepoint) xuất hiện đúng lúc bpftrace chạy và biến mất khi nó thoát. bpftrace lấy script của ta, compile sang eBPF, nạp + attach, gom dữ liệu qua maps (Bài 3), rồi dọn khi xong — tất cả tự động. Ta được cú pháp ngắn gọn mà vẫn là eBPF thật bên dưới.
Cú pháp: probe / filter / action
Một chương trình bpftrace là một hay nhiều khối theo khuôn:
probe /filter/ { action }
- probe — gắn vào đâu (chính là hook/program type của Bài 4, viết dạng chuỗi).
- filter (predicate, tùy chọn) — điều kiện để action chạy.
- action — làm gì khi probe nổ.
Một one-liner hay dùng: tiến trình nào mở file nào.
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%-16s %s\n", comm, str(args.filename)); }'
bash /dev/null
cat /etc/ld.so.cache
cat /lib/x86_64-linux-gnu/libc.so.6
cat /usr/lib/locale/C.UTF-8/LC_IDENTIFICATION
Đọc câu lệnh: probe là tracepoint:syscalls:sys_enter_openat (Bài 4 — ta đã gắn đúng tracepoint này bằng C, giờ một dòng); action in comm (biến dựng sẵn: tên tiến trình) và str(args.filename). args là context của probe — với tracepoint syscall, nó là các tham số; args.filename là con trỏ tên file, str() đọc chuỗi ra an toàn. bpftrace cho sẵn nhiều biến dựng sẵn: comm, pid, tid, args, nsecs, cpu, kstack...
Kho probe: gắn được vào đâu
bpftrace gắn được vào rất nhiều điểm. Liệt kê và đếm theo loại trên node:
sudo bpftrace -l | wc -l
sudo bpftrace -l | sed 's/:.*//' | sort | uniq -c | sort -rn
122991 # tổng số probe gắn được
61331 kprobe # mọi hàm nhân có thể chèn
58834 kfunc # hàm nhân (kiểu BTF, có typed args)
1756 tracepoint # điểm tracepoint ổn định
1026 rawtracepoint
18 iter
14 software
12 hardware
Hơn 120 nghìn điểm để quan sát. tracepoint là giao diện ổn định nhà phát triển kernel cam kết giữ; kprobe chèn được vào gần như mọi hàm nhân (linh hoạt hơn nhưng tên hàm có thể đổi giữa các bản kernel). bpftrace -l 'mẫu*' để dò probe theo tên — như bpftrace -l 'tracepoint:syscalls:sys_enter_open*' ra sys_enter_openat và sys_enter_openat2.
Filter: lọc đúng cái cần
Thêm predicate để action chỉ chạy khi thỏa điều kiện — ví dụ chỉ quan tâm file mà lệnh cat mở:
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat /comm == "cat"/ { printf("%s opened %s\n", comm, str(args.filename)); }'
cat opened /etc/ld.so.cache
cat opened /lib/x86_64-linux-gnu/libc.so.6
cat opened /usr/lib/locale/C.UTF-8/LC_IDENTIFICATION
/comm == "cat"/ là filter: chương trình vẫn nổ trên mọi openat của toàn hệ thống, nhưng action chỉ chạy khi tiến trình là cat. Lọc ngay trong nhân như vậy rẻ hơn nhiều so với in tất rồi grep ở userspace.
🧹 Dọn dẹp
bpftrace tự gỡ chương trình + maps khi thoát (Ctrl-C) — bpftool prog show về lại 140. Không có gì để dọn tay. Lệnh trong bài ở github.com/nghiadaulau/ebpf-from-scratch, thư mục 06-bpftrace-basics.
Tổng kết
bpftrace là một ngôn ngữ tracing cấp cao: ta viết script ngắn, bpftrace tự compile sang bytecode eBPF, nạp + attach, gom dữ liệu qua maps rồi dọn khi thoát — chứng minh bằng bpftool thấy chương trình xuất hiện (140→141) lúc chạy và biến mất lúc thoát. Cú pháp gồm ba phần: probe /filter/ { action } — probe là hook (Bài 4) viết dạng chuỗi, filter là điều kiện, action dùng các biến dựng sẵn (comm, pid, args, str()...). Nhân cho hơn 120 nghìn probe (kprobe gắn mọi hàm nhân, tracepoint là giao diện ổn định), bpftrace -l để dò. Predicate lọc ngay trong nhân. Tất cả không cần C, clang hay viết loader — đường nhanh để quan sát hệ thống.
In từng dòng thì hợp khi sự kiện ít; với sự kiện dày (mọi syscall, mọi gói) thì ngập màn hình. Bài 7 dùng khả năng chính của bpftrace: maps và phép gộp — đếm, lập biểu đồ phân phối (histogram) ngay trong nhân, chỉ trả về kết quả tổng hợp.