Xác định địa chỉ symbol hàm kernel trên Raspberry Pi CM5
Để hook vào một hàm kernel cụ thể, bạn cần biết chính xác tên symbol hoặc địa chỉ của nó. Trên Raspberry Pi CM5 (ARM64), cấu trúc kernel có thể khác với x86_64, đặc biệt là khi có các bản vá bảo mật hoặc cấu hình CONFIG_KALLSYMS.
Ta sẽ xác định địa chỉ của hàm sys_write (hàm hệ thống xử lý ghi file) để làm ví dụ giám sát.
Thực hiện lệnh sau để tìm địa chỉ symbol:
grep "sys_write" /proc/kallsyms | grep T
Kết quả mong đợi: Bạn sẽ thấy một dòng chứa địa chỉ hex (ví dụ: ffff800081234567 T sys_write). Cột đầu tiên là địa chỉ, cột thứ hai là loại (T là Text/Code), cột thứ ba là tên hàm.
Trên CM5, nếu kernel đã được build với CONFIG_FTRACE, bạn cũng có thể dùng perf list để kiểm tra xem hàm đó có sẵn cho kprobe hay không.
perf list kprobe | grep sys_write
Kết quả mong đợi: Danh sách các kprobe sẵn có, xác nhận sys_write có thể được hook.
Viết chương trình eBPF: Đếm lần gọi và đo thời gian
Cấu trúc logic chương trình
Chúng ta sẽ viết một chương trình C để biên dịch thành eBPF object. Logic bao gồm:
- kprobe: Hook vào đầu hàm
sys_write để ghi lại thời gian bắt đầu và tăng bộ đếm.
- kretprobe: Hook vào điểm trả về của hàm để lấy thời gian kết thúc, tính toán delta (thời gian thực thi) và ghi vào map.
- Map: Sử dụng
BPF_MAP_TYPE_HASH để lưu số lần gọi và BPF_MAP_TYPE_PERF_EVENT_ARRAY để xuất dữ liệu thời gian ra user space.
Code eBPF (bpf_probe.c)
Tạo file /root/bpf_probe.c với nội dung hoàn chỉnh dưới đây. Code này sử dụng libbpf API và cấu trúc map chuẩn cho ARM64.
#include "vmlinux.h"
#include
#include
char LICENSE[] SEC("license") = "GPL";
// Map để đếm số lần gọi
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, u32);
__type(value, u64);
} call_count SEC(".maps");
// Map để lưu thời gian thực thi (duration) cho từng lần gọi
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, u64);
__type(value, u64);
} start_time SEC(".maps");
// Map để xuất dữ liệu ra user space (perf buffer)
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
} events SEC(".maps");
struct event {
u64 pid;
u64 duration_ns;
u64 count;
};
SEC("kprobe/sys_write")
int BPF_KPROBE(trace_write_entry)
{
u64 pid = bpf_get_current_pid_tgid();
u64 ts = bpf_ktime_get_ns();
// Tăng bộ đếm
u32 key_count = 1;
u64 *count_val = bpf_map_lookup_elem(&call_count, &key_count);
if (count_val) {
u64 new_count = *count_val + 1;
bpf_map_update_elem(&call_count, &key_count, &new_count, BPF_ANY);
}
// Lưu thời gian bắt đầu
bpf_map_update_elem(&start_time, &pid, &ts, BPF_ANY);
return 0;
}
SEC("kretprobe/sys_write")
int BPF_KRETPROBE(trace_write_exit)
{
u64 pid = bpf_get_current_pid_tgid();
u64 ts = bpf_ktime_get_ns();
u64 *start = bpf_map_lookup_elem(&start_time, &pid);
if (start) {
u64 duration = ts - *start;
struct event event = {};
event.pid = pid;
event.duration_ns = duration;
// Lấy số lần gọi hiện tại
u32 key_count = 1;
u64 *count_val = bpf_map_lookup_elem(&call_count, &key_count);
if (count_val) {
event.count = *count_val;
}
// Gửi sự kiện ra user space
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
// Xóa record start_time để tránh leak memory map
bpf_map_delete_elem(&start_time, &pid);
}
return 0;
}
Kết quả mong đợi: File source code được lưu. Không có lỗi gì xảy ra ở bước này vì chưa biên dịch.
Biên dịch và chạy chương trình trên CM5
Biên dịch sang eBPF object
Sử dụng clang và llvm-strip để biên dịch. Đảm bảo bạn đã cài đặt clang và libbpf-dev từ Phần 2 của series.
clang -g -O2 -target bpf -c /root/bpf_probe.c -o /root/bpf_probe.o
Kết quả mong đợi: Tạo ra file bpf_probe.o tại thư mục root. Nếu có lỗi, kiểm tra lại đường dẫn vmlinux.h hoặc phiên bản clang.
Chương trình User Space (C)
Cần một chương trình C để load map, attach probe và đọc dữ liệu từ perf buffer. Tạo file /root/run_probe.c.
#include
#include
#include
#include
#include
#include
#include "bpf_probe.skel.h"
static volatile bool exiting = false;
void handle_signal(int sig)
{
exiting = true;
}
int main(int argc, char **argv)
{
struct bpf_probe *skel;
int err;
struct event *event;
int map_fd;
// Setup signal handler
signal(SIGINT, handle_signal);
signal(SIGTERM, handle_signal);
printf("Starting eBPF probe on sys_write...\n");
printf("Press Ctrl+C to stop.\n");
skel = bpf_probe__open();
if (!skel) {
fprintf(stderr, "Failed to open eBPF skeleton\n");
return 1;
}
skel->bpf_obj = bpf_object__open(skel);
if (skel->bpf_obj < 0) {
fprintf(stderr, "Failed to load eBPF object\n");
bpf_probe__free(skel);
return 1;
}
skel->bpf_obj = bpf_object__load(skel->bpf_obj);
if (skel->bpf_obj < 0) {
fprintf(stderr, "Failed to load eBPF object\n");
bpf_probe__free(skel);
return 1;
}
// Attach kprobe
skel->links.trace_write_entry = bpf_program__attach(skel->progs.trace_write_entry);
if (!skel->links.trace_write_entry) {
err = libbpf_get_error(skel->links.trace_write_entry);
fprintf(stderr, "Failed to attach kprobe: %d\n", err);
bpf_object__close(skel->bpf_obj);
return 1;
}
// Attach kretprobe
skel->links.trace_write_exit = bpf_program__attach(skel->progs.trace_write_exit);
if (!skel->links.trace_write_exit) {
err = libbpf_get_error(skel->links.trace_write_exit);
fprintf(stderr, "Failed to attach kretprobe: %d\n", err);
bpf_program__detach(skel->links.trace_write_entry);
bpf_object__close(skel->bpf_obj);
return 1;
}
// Read from perf buffer
map_fd = bpf_map__fd(skel->maps.events);
while (!exiting) {
event = bpf_map_lookup_elem(map_fd, (void*)0); // Placeholder logic, actual perf buffer reading requires perf_buffer library
// For simplicity in this tutorial, we use a simpler approach with perf_buffer
// Note: In production, use libbpf's perf_buffer API
sleep(1);
}
// Cleanup
bpf_program__detach(skel->links.trace_write_exit);
bpf_program__detach(skel->links.trace_write_entry);
bpf_object__close(skel->bpf_obj);
bpf_probe__free(skel);
printf("Probe stopped.\n");
return 0;
}
Lưu ý: Code trên là khung cơ bản. Để đọc dữ liệu từ PERF_EVENT_ARRAY một cách đúng chuẩn trên ARM64, ta sẽ dùng công cụ bpftool hoặc viết lại phần user space sử dụng perf_buffer của libbpf để tránh lỗi biên dịch phức tạp. Dưới đây là cách đơn giản hơn để verify kết quả bằng bpftool trước khi chạy code C phức tạp.
Chạy nhanh bằng bpftool (Khuyến nghị cho bước verify)
Thay vì compile code C user space phức tạp ngay lập tức, ta dùng bpftool để load và attach probe, sau đó dùng cat để đọc map.
bpftool prog load /root/bpf_probe.o /sys/fs/bpf/bpf_probe map create name call_count type hash key_size 4 value_size 8 max_entries 1024 map create name start_time type hash key_size 8 value_size 8 max_entries 1024 map create name events type perf_event array max_entries 1
Để đơn giản hóa quy trình cho người mới, ta sẽ dùng clang tạo skeleton và dùng libbpf trong một script Python hoặc C đơn giản hơn. Tuy nhiên, để đảm bảo chạy được ngay trên CM5 mà không cần cài đặt thêm thư viện C++, ta dùng lệnh bpftool để attach thủ công:
bpftool prog load /root/bpf_probe.o /sys/fs/bpf/bpf_probe
Kết quả mong đợi: Loaded program 'trace_write_entry'...
bpftool prog attach type kprobe name sys_write target 0 id $(bpftool prog list | grep trace_write_entry | awk '{print $1}')
Kết quả mong đợi: Không có lỗi, probe đã được attach vào kernel.
Verify kết quả và thu thập dữ liệu
Trigger hoạt động
Để kiểm tra xem kprobe có hoạt động không, ta sẽ tạo một quá trình viết file vào stdout hoặc file hệ thống.
echo "Test data for kprobe" > /tmp/test_kprobe.log
Kết quả mong đợi: Lệnh chạy thành công. Hàm sys_write sẽ được gọi.
Đọc dữ liệu từ Map
Đọc map call_count để xem số lần gọi đã tăng chưa.
bpftool map show | grep call_count
bpftool map dump name call_count
Kết quả mong đợi: Bạn sẽ thấy key 1 có value tăng lên (ví dụ: 1: 1 hoặc cao hơn nếu có nhiều lần gọi).
Xuất log thời gian thực thi
Để xem thời gian thực thi (duration), ta cần đọc từ map events (PERF_EVENT_ARRAY). Cách đơn giản nhất là dùng perf tool.
perf trace -e bpf:sys_write -a
Chạy lệnh này trong một terminal, sau đó chạy lệnh echo "data" > /tmp/file.txt trong terminal khác.
Kết quả mong đợi: Terminal chạy perf trace sẽ in ra các dòng log với thông tin thời gian (ns) và PID của các lần gọi sys_write.
Ví dụ output:
12:34:56.789012 write(0, ..., 20) = 20 [duration: 1500 ns]
Điều này chứng tỏ kprobe và kretprobe đã hoạt động chính xác, ghi lại thời điểm vào và ra của hàm.
Tổng kết quy trình trên CM5
Quy trình đã hoàn thành:
- Xác định symbol
sys_write qua /proc/kallsyms.
- Viết code eBPF hook kprobe (entry) và kretprobe (exit).
- Biên dịch thành object file
.o.
- Load và attach vào kernel bằng
bpftool.
- Verify bằng cách trigger write và đọc map hoặc dùng
perf trace.
Bây giờ bạn đã có thể mở rộng code để hook các hàm khác như sys_read, sys_open hoặc các hàm mạng như tcp_sendmsg trên Raspberry Pi CM5.
Đ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 4: Viết và biên dịch chương trình eBPF đầu tiên (Hello World)
Phần 6: Giám sát hiệu năng mạng với BPF Tracepoint và XDP »