Cấu trúc chương trình eBPF Hello World
Bước đầu tiên là xây dựng file nguồn C định nghĩa map để lưu kết quả và function entry point để kernel gọi khi kích hoạt kprobe.
Chúng ta tạo file /root/hello-bpf.c với nội dung hoàn chỉnh sau. File này khai báo một map loại PERCPU_ARRAY để mỗi CPU có không gian riêng, giúp giảm contention trên Raspberry Pi CM5.
Function bpf_prog1 là entry point được kernel gọi mỗi khi hàm sys_enter (hệ thống gọi) được kích hoạt. Nó sẽ ghi số lượng lần gọi vào map.
File /root/hello-bpf.c:
#include "vmlinux.h"
#include
#include
// Khai báo map: mỗi CPU có một phần tử, giá trị là unsigned long long
char LICENSE[] SEC("license") = "GPL";
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, u32);
__type(value, u64);
} bpf_map SEC("maps");
// Entry point: kprobe vào sys_enter (hệ thống gọi)
SEC("kprobe/sys_enter")
int bpf_prog1(struct pt_regs *ctx)
{
u32 key = 0;
u64 *value;
// Lấy con trỏ đến giá trị trong map
value = bpf_map_lookup_elem(&bpf_map, &key);
if (value) {
// Tăng giá trị lên 1 mỗi lần gọi
__atomic_add_fetch(value, 1, __ATOMIC_RELAXED);
}
return 0;
}
Kết quả mong đợi: File C được lưu thành công, chuẩn bị sẵn sàng cho bước biên dịch. Code này tuân thủ các quy tắc an toàn của kernel (không dùng vòng lặp vô hạn, không dùng pointer trực tiếp).
Verify cấu trúc map và license
Trước khi biên dịch, hãy đảm bảo bạn đã hiểu rõ các thành phần: SEC("license") là bắt buộc để kernel cho phép tải chương trình. SEC("kprobe/sys_enter") xác định điểm hook vào kernel.
Chạy lệnh kiểm tra file đã tồn tại:
ls -l /root/hello-bpf.c
Kết quả mong đợi: Xuất hiện dòng file với kích thước khoảng 500-800 bytes.
Biên dịch sang BPF Bytecode với Clang
Sau khi có file C, ta cần dùng LLVM Clang để biên dịch thành object file ELF chứa BPF bytecode. Kernel Linux không hiểu C, nó chỉ hiểu bytecode eBPF.
Lệnh biên dịch sử dụng target bpf để sinh code cho architecture eBPF, không phải ARM64 trực tiếp. File vmlinux.h đã được tạo sẵn trong Phần 2 để cung cấp định nghĩa cấu trúc kernel.
Chạy lệnh biên dịch sau trong thư mục chứa file nguồn:
clang -g -O2 -target bpf -D__TARGET_ARCH_arm64 -I/usr/include/bpf -c /root/hello-bpf.c -o /root/hello-bpf.o
Giải thích các cờ: -g giữ debug info, -O2 tối ưu hóa, -target bpf xác định kiến trúc đích, -D__TARGET_ARCH_arm64 bắt buộc cho Raspberry Pi CM5 (ARM64).
Kết quả mong đợi: File /root/hello-bpf.o được tạo ra. Đây là file ELF chứa bytecode eBPF, có thể kiểm tra bằng lệnh file.
Verify file object
Kiểm tra file object vừa tạo để đảm bảo nó là ELF chứa BPF bytecode, không phải ARM64 machine code thông thường.
file /root/hello-bpf.o
Kết quả mong đợi: Dòng output chứa "ELF 64-bit LSB relocatable, ARM aarch64" hoặc "BPF". Nếu thấy "relocatable", đây là file chuẩn để load vào kernel.
Tải chương trình vào Kernel và Đọc Map
Bây giờ file bytecode đã sẵn sàng. Chúng ta sẽ dùng bpftool để load chương trình vào kernel, tạo map, và attach kprobe vào điểm sys_enter.
Trước tiên, tạo map từ file object và lưu lại file descriptor (fd) của map để truy cập sau này. Command này tạo map trong kernel và trả về fd.
bpftool map create /root/hello-bpf.o bpf_map 1024 1024 1
Giải thích: 1024 là key size (4 bytes cho u32, nhưng bpftool cần giá trị byte), 1024 là value size (8 bytes cho u64, nhưng thực tế là 8 bytes, bpftool tự tính), 1 là max_entries (số phần tử). Tuy nhiên, cách chính xác hơn với map đã định nghĩa trong C là load trực tiếp từ file object.
Thay vào đó, chúng ta load toàn bộ chương trình từ file object. Lệnh này sẽ tự động tạo map và load program, trả về fd của program.
bpftool prog load /root/hello-bpf.o /sys/fs/bpf/hello-bpf type kprobe name hello-bpf
Kết quả mong đợi: Không có lỗi, xuất hiện thông báo "loaded program". File /sys/fs/bpf/hello-bpf được tạo để chứa fd của program.
Attach kprobe vào sys_enter
Sau khi load program, ta cần "móc" (attach) nó vào điểm kprobe sys_enter để kernel gọi hàm bpf_prog1 mỗi khi hệ thống gọi xảy ra.
bpftool prog attach type kprobe name hello-bpf target /usr/bin/ls func sys_enter
Giải thích: target là binary cần hook (ở đây là /usr/bin/ls để test đơn giản), func là tên function trong kernel. Tuy nhiên, để hook toàn hệ thống (không chỉ ls), ta dùng target vmlinux.
Sửa lại lệnh để hook toàn hệ thống (khuyên dùng cho test Hello World):
bpftool prog attach type kprobe name hello-bpf target vmlinux func sys_enter
Kết quả mong đợi: Không có lỗi. Chương trình eBPF giờ đang chạy trong kernel và sẵn sàng đếm số lần gọi hệ thống.
Trigger chương trình và đọc dữ liệu từ Map
Để thấy kết quả, ta cần kích hoạt một hệ thống gọi. Chạy lệnh ls hoặc echo để tạo ra sys_enter.
ls /root
Giải thích: Lệnh này kích hoạt kernel gọi sys_enter, hàm bpf_prog1 trong eBPF sẽ chạy và tăng giá trị trong map lên 1.
Bây giờ đọc giá trị từ map bằng bpftool map lookup. Key là 0 (như khai báo trong C), map name là bpf_map (tên trong code).
bpftool map lookup name hello-bpf key 0
Hoặc nếu map chưa có tên rõ ràng, dùng fd từ file object (thường là 0 hoặc 1, nhưng bpftool tự quản lý). Dùng lệnh bpftool map list để tìm tên map chính xác.
bpftool map list
Kết quả mong đợi: Danh sách map hiện có. Tìm map có type PERCPU_ARRAY và max_entries 1.
Giả sử tên map là hello-bpf (bpftool tự đặt tên từ file object), chạy lookup:
bpftool map lookup name hello-bpf key 0
Kết quả mong đợi: Output hiển thị giá trị > 0 (ví dụ: value: 10 nếu chạy ls nhiều lần). Giá trị này là số lần sys_enter đã được gọi.
Verify kết quả bằng cách tăng tải
Để chắc chắn chương trình hoạt động, chạy lệnh tạo nhiều hệ thống gọi và kiểm tra lại map.
for i in {1..100}; do echo "test"; done
Sau đó đọc lại map:
bpftool map lookup name hello-bpf key 0
Kết quả mong đợi: Giá trị trong map tăng lên đáng kể (ví dụ từ 10 lên 110+), chứng tỏ kprobe đang hoạt động và đếm chính xác trên Raspberry Pi CM5.
Xóa chương trình khi hoàn thành
Để dọn dẹp, detach kprobe và unload program khỏi kernel.
bpftool prog detach type kprobe name hello-bpf target vmlinux func sys_enter
Sau đó xóa program:
bpftool prog delete name hello-bpf
Và xóa map:
bpftool map delete name hello-bpf
Kết quả mong đợi: Không còn chương trình eBPF nào liên quan đến hello-bpf trong hệ thống.
Điều hướng series:
Mục lục: Series: Tối ưu hóa Linux Kernel cho Raspberry Pi CM5 với BPF và eBPF
« Phần 3: Giới thiệu kiến trúc eBPF và cơ chế hoạt động trên ARM64
Phần 5: Sử dụng kprobe và kretprobe để giám sát hệ thống trên CM5 »