Whistler: Lập trình eBPF trực tiếp từ REPL Lisp thông thường
Whistler: Live eBPF Programming from the Common Lisp REPL
Whistler là một dự án mới cho phép lập trình eBPF trực tiếp từ Common Lisp REPL. Dự án này biên dịch một DSL (domain-specific language) dựa trên Lisp thành bytecode eBPF được tối ưu hóa cao, mà không cần đến toolchain clang/llvm. Thậm chí, Whistler còn cho phép nhúng eBPF code trực tiếp vào các chương trình Lisp để thực hiện hot-swapping. Điều này mang lại cho các developer một cách thức phát triển ứng dụng eBPF tương tác và cô đọng hơn, cho phép lặp lại và gỡ lỗi nhanh chóng, tương tự như cách phát triển truyền thống trong Lisp. Nhờ đó, các developer có thể viết và thử nghiệm chương trình eBPF một cách gọn gàng hơn, với phản hồi tức thì.
Một chuỗi thử nghiệm gần đây xung quanh khả năng quan sát và bảo mật của các hệ thống AI tác nhân đã khiến tôi rơi vào tình trạng khó khăn về eBPF. Khi tôi xuất hiện, tôi đã quay lại với trình biên dịch tối ưu hóa đầy đủ cho Common...
Một chuỗi thử nghiệm gần đây về khả năng quan sát và bảo mật cho hệ thống AI tác nhân đã dẫn tôi xuống hố thỏ eBPF. Khi tôi nổi lên, Tôi đã quay lại với một trình biên dịch tối ưu hóa đầy đủ cho Common Lisp DSL dành cho eBPF có tên là Whistler.
Whistler cho phép bạn viết mã ngắn hơn, ít nghi thức hơn eBPF C mã và vẫn tạo ra đầu ra eBPF được tối ưu hóa cao, tương đương hoặc tốt hơn tiếng kêu. Và Whistler tạo ra các tệp ELF eBPF đó trực tiếp mà không cần bất kỳ chuỗi công cụ eBPF clang+llvm nào.
Ngoài việc tạo trực tiếp các tệp mã đối tượng và tải chúng theo cách truyền thống, bạn thực sự có thể nội tuyến mã Whistler trực tiếp trong các chương trình Common Lisp của bạn và biên dịch/tải/dỡ chúng dưới dạng một phần của quy trình REPL truyền thống của bạn, nơi không có tệp đối tượng nào được đưa vào trên đĩa.
Một hương vị
Đây là một kprobe đếm mọi lệnh gọi execve trên hệ thống:
(with-bpf-session ()
(bpf:bộ đếm bản đồ :type :hash :key-size 4 :value-size 8 :max-entries 1)
(bpf:prog trace (:type :kprobe
:phần "kprobe/__x64_sys_execve"
:giấy phép "GPL")
(incf (bộ đếm getmap 0))
0)
(bpf:đính kèm dấu vết "__x64_sys_execve")
(vòng lặp (ngủ 1)
(định dạng t "số lượng thực thi: ~d~%" (bpf:bộ đếm bản đồ-ref 0))))
Đó là một chương trình hoàn chỉnh và có thể chạy được. Phần thân bpf:prog biên dịch thành
Mã byte eBPF trong quá trình mở rộng macro. Mã byte được nhúng dưới dạng
nghĩa đen trong việc mở rộng. Khi chạy, bản đồ được tạo, chương trình
được tải vào kernel và đầu dò được gắn vào. Vòng lặp tại
phía dưới là Common Lisp đơn giản, thăm dò bản đồ mỗi giây.
Một ví dụ thực tế
Đây là một điều quan trọng hơn — một cuộc cải tiến theo dõi mọi
Lệnh gọi ffi_call trong libffi, đếm các cuộc gọi theo tên chương trình và
chữ ký hàm:
(with-bpf-session ()
;; Phía BPF — được biên dịch thành mã byte tại thời điểm macroexpand
(bpf:map stats :type :hash :key-size 40 :value-size 8 :max-entries 10240)
(bpf:prog ffi_call_tracker (:type :kprobe
:section "uprobe/ffi_call"
:giấy phép "GPL")
(let ((cif (make-ffi-cif))
(ft (make-ffi-type))
(khóa (make-stats-key)))
(người dùng thăm dò cif (sizeof ffi-cif) (pt-regs-parm1))
(người dùng thăm dò ft (sizeof ffi-type) (ffi-cif-rtype cif))
(setf (khóa thống kê-khóa-rtype) (ffi-type-type-code ft)
(khóa thống kê-key-abi) (ffi-cif-abi cif)
(khóa stats-key-nargs) (ffi-cif-nargs cif))
(get-current-comm (khóa thống kê-comm-ptr) 16)
(khóa bộ nhớ 16 #xFF 16)
(do-user-ptrs (atype-ptr (ffi-cif-arg-types cif)
(ffi-cif-nargs cif) +max-args+ :index i)
(người dùng thăm dò ft (sizeof ffi-type) atype-ptr)
(setf (khóa Stats-key-arg-types i) (ffi-type-type-code ft)))
(incf (khóa thống kê getmap)))
0)
;; Phía không gian người dùng — mã CL bình thường, chạy khi chạy
(bpf:đính kèm ffi_call_tracker "/lib64/libffi.so.8" "ffi_call")
(định dạng t "Theo dõi ffi_call. Nhấn Ctrl-C để kết xuất số liệu thống kê.~%")
(trường hợp xử lý (vòng lặp (ngủ 1))
(sb-sys:interactive-interrupt ()
;; Lặp lại bản đồ và in kết quả
...)))
Kết quả:
Biên dịch và đang tải chương trình BPF...
Đang gắn uprobe vào ffi_call in /lib64/libffi.so.8...
Theo dõi ffi_call. Nhấn Ctrl-C để kết xuất số liệu thống kê.
C
COUNT CHỮ KÝ COMM
-------- ---------------- ---------
880 void void(ptr, ptr, u32) [unix64]
384 gnome-shell void(ptr, ptr, u32) [unix64]
352 gnome-shell void(ptr, ptr, ptr, s64, ptr) [unix64]
224 gnome-shell void(ptr, ptr) [unix64]
176 void void(ptr, ptr) [unix64]
...
Mọi thứ diễn ra bên trong một quy trình SBCL. Mã byte BPF không bao giờ đĩa đã chạm.
Cách thức hoạt động
Tiền tố bpf: là ranh giới giữa kernel và không gian người dùng.
Các biểu mẫu có tiền tố bpf: là các khai báo cho trình biên dịch BPF:
bpf:map— khai báo bản đồ BPF (được biên dịch tại thời điểm mở rộng macro)bpf:prog— khai báo một chương trình BPF (được biên dịch tại thời điểm mở rộng macro)bpf:attach— tạo ra các cuộc gọiperf_event_open(chạy trong thời gian chạy)bpf:map-ref— tạo ra các cuộc gọibpf_map_lookup_elem(chạy trong thời gian chạy)
Mọi thứ khác đều bình thường. Ranh giới là cú pháp, không phải ngữ nghĩa — cả hai bên đều chia sẻ cùng một hình ảnh Lisp.
Thông tin chi tiết quan trọng: trình biên dịch Whistler chạy trong quá trình mở rộng macro. Bởi
thời điểm SBCL biên dịch biểu mẫu with-bpf-session, mã byte eBPF
đã là một hằng số - được nhúng dưới dạng một mảng byte trong
mở rộng. Mã thời gian chạy chỉ tạo bản đồ, vá các vị trí di chuyển FD,
và gọi bpf(BPF_PROG_LOAD, ...). Và bởi vì tất cả đều chạy trong
mở rộng macro, bạn sẽ gặp lỗi thời gian biên dịch với ngữ cảnh:
lỗi: loại hẹp U8 được chuyển dưới dạng con trỏ tới PROBE-READ
trong: (THĂM DÒ-ĐỌC SỰ KIỆN 8 PTR)
dự kiến: giá trị con trỏ u64
gợi ý: Giá trị U8 là 0-255, không con trỏ hợp lệ — sử dụng (tải u64 ...) để đọc
Một cấu trúc, cả hai bên
whistler:defstruct tạo các trình truy cập cho cả BPF và CL:
(whistler:defstruct stats-key
(comm (mảng u8 16))
(arg-types (mảng u8 16))
(nargs u16)
(rtype u8)
(abi u8)
(pad u32))
Về phía BPF, điều này mang lại cho bạn (make-stats-key), (stats-key-rtype ptr),
(setf (stats-key-rtype ptr) val) — phân bổ ngăn xếp và tải/lưu trữ trực tiếp
với sự bù đắp thời gian biên dịch.
Về phía CL, nó tạo ra stats-key-record (CL defstruct),
decode-stats-key (byte → struct) và encode-stats-key (struct →
byte). Mã lặp bản đồ không gian người dùng sử dụng cùng tên trường:
(let ((key (decode-stats-key raw-bytes)))
(khóa stats-key-record-nargs) ;; → 3
(khóa stats-key-record-rtype) ;; → 0
(khóa thống kê-khóa-record-comm)) ;; → #(112 121 116 104 111 110 51 0 ...)
Không có phân tích cú pháp bù byte thủ công. Một định nghĩa phục vụ cả kernel và không gian người dùng.
Hạt nhân trong tầm tay bạn
Whistler có thể nhập định nghĩa trực tiếp từ hạt nhân đang chạy.
deftracepoint đọc các tệp định dạng tracepoint từ tracefs:
(deftracepoint sched/sched-switch prev-pid prev-state next-pid)
;; Tạo: (tp-prev-pid) → (ctx-load u32 24)
;; (tp-prev-state) → (ctx-load u64 32)
import-kernel-struct đọc BTF của kernel:
(import-kernel-struct task_struct pid tgid flags)
;; Tạo: (task-struct-pid ptr) → (tải u32 ptr 2768)
;; +task-struct-size+ → 9856
Các lỗi lệch được giải quyết từ kernel đang chạy của bạn tại thời điểm biên dịch — kernel các tiêu đề và vmlinux.h là không cần thiết.
Trình tải cũng hoàn toàn là CL
whistler/loader là một trình tải không gian người dùng BPF hoàn chỉnh được viết bằng Common
Lisp không phụ thuộc C. Nó sử dụng sb-alien của SBCL để trực tiếp
quyền truy cập tòa nhà:
- Trình phân tích cú pháp ELF cho các tệp
.bpf.o - Việc tạo và vận hành bản đồ BPF (tra cứu, cập nhật, xóa, lặp lại)
- Vá tái định vị bản đồ FD
- Tải chương trình với tính năng chụp lỗi trình xác minh
- Kprobe, uprobe và tệp đính kèm XDP
- Người sử dụng bộ đệm vòng thông qua mmap + epoll
Dành cho quy trình làm việc dựa trên tệp:
(với-bpf-object (obj "my-probes.bpf.o")
(đính kèm-obj-kprobe obj "trace_execve" "__x64_sys_execve")
...)
Hoặc bỏ qua toàn bộ tệp bằng with-bpf-session.
Không gian người dùng Polyglot
Không phải mọi thứ đều phải là Lisp. Nếu bạn muốn viết không gian người dùng
bên trong Go, C, Rust hoặc Python, Whistler có thể tạo cấu trúc phù hợp
các định nghĩa từ cùng các khai báo defstruct được sử dụng trong BPF
chương trình:
whistler biên dịch thăm dò.lisp --gen c go Rust python
Điều này tạo ra các tệp tiêu đề với bố cục cấu trúc được đảm bảo phù hợp với Bên BPF, vì cả hai đều có nguồn gốc từ cùng một nguồn. Bạn biên soạn BPF với Whistler và viết trình tải bằng bất kỳ ngôn ngữ nào mà nhóm của bạn đã sử dụng.
Quyền không cần root
Bạn không cần root. Cấp khả năng cho SBCL:
sudo setcap cap_bpf,cap_perfmon+ep /usr/bin/sbcl
Bây giờ sbcl --load my-bpf-program.lisp hoạt động như người dùng thông thường của bạn.
Các tệp định dạng tracepoint cần chmod a+r để cho phép không phải root
biên dịch bằng deftracepoint.
Tại sao điều này lại quan trọng
Quy trình làm việc eBPF truyền thống là: viết C cho phía BPF, biên dịch bằng tiếng kêu, sau đó viết Go hoặc Rust hoặc Python cho phía không gian người dùng. Hai ngôn ngữ, các bước xây dựng riêng biệt, nhiều quy trình.
Với Whistler 1.0, quy trình làm việc là: viết Lisp. Trình biên dịch, trình tải, và ứng dụng không gian người dùng chia sẻ một quy trình. Bạn có thể phát triển tại REPL — sửa đổi đầu dò, đánh giá lại biểu mẫu, xem kết quả ngay lập tức. Sự phản hồi vòng lặp diễn ra ngay lập tức.
Hãy xem thử trên GitHub.
Tác giả: varjag