【eBPF-01】初见:基于 BCC 框架的第一个 eBPF 程序
閑言少敘,本文記錄了如何零基礎(chǔ)通過(guò) BCC 框架,入門(mén) eBPF 程序的開(kāi)發(fā),并實(shí)現(xiàn)幾個(gè)簡(jiǎn)易的程序。
有關(guān) eBPF 的介紹,網(wǎng)絡(luò)上的資料有很多,本文暫且先不深入討論,后面會(huì)再出一篇文章詳細(xì)分析其原理和功能。
我們目前只需要知道,eBPF 實(shí)際上是一種過(guò)濾器,這種過(guò)濾器幾乎可以插入內(nèi)核源碼的任意的流程和環(huán)節(jié)中,實(shí)現(xiàn)自定義的邏輯。由于 eBPF 自身的若干限制,使它最常見(jiàn)的用法是,附著在內(nèi)核某些關(guān)鍵流程上,抓取一些關(guān)鍵數(shù)據(jù),用于監(jiān)控、統(tǒng)計(jì)和分析。
1 一個(gè)簡(jiǎn)單的例子
下面是一個(gè)簡(jiǎn)單的例子,我想實(shí)現(xiàn)一個(gè)程序,用來(lái)實(shí)時(shí)監(jiān)控內(nèi)核可執(zhí)行文件(ELF)的加載。這個(gè)程序運(yùn)行如下:
如圖所示,每當(dāng)有一個(gè) ELF 文件被加載時(shí),可以顯示這個(gè) ELF 加載時(shí)的一些內(nèi)核信息,如:加載時(shí)間、加載進(jìn)程名、加載進(jìn)程 PID、以及被加載的 ELF 文件名。
這個(gè)程序就是基于 eBPF 實(shí)現(xiàn)的。接下來(lái),我們就逐步了解一下,如何通過(guò) BCC 框架,成功編寫(xiě)運(yùn)行這個(gè) eBPF 程序。
2 BCC 框架
進(jìn)行 eBPF 編程,有很多種方式。例如:
1)libbpf:使用原生的 C 語(yǔ)言,基于 libbpf 庫(kù),編寫(xiě)用戶態(tài)程序和 BPF 程序的加載;
2)libbpf-bootstrap:使用 libbpf-bootstrap 腳手架,輕而易舉地編寫(xiě) BPF 程序;
3)BCC:使用 BCC 框架,基于 python/Lua 腳本,實(shí)現(xiàn) BPF 和用戶態(tài)程序,上手容易,簡(jiǎn)化了 BPF 的開(kāi)發(fā);
4)Bpftrace:一種用于eBPF的高級(jí)跟蹤語(yǔ)言,使用LLVM作為后端,將腳本編譯為BPF字節(jié)碼;
5)eunomia-bpf:較新的基于 libbpf 的 CO-RE 輕量級(jí)框架,簡(jiǎn)化了 eBPF 程序的開(kāi)發(fā)、構(gòu)建、分發(fā)、運(yùn)行
選擇 BCC 框架作為第一個(gè)學(xué)習(xí)的框架的原因是,BCC 封裝較好,上手容易,用戶態(tài)和內(nèi)核態(tài)的區(qū)分明顯,用戶態(tài)支持 Python,易于理解。
安裝過(guò)程很簡(jiǎn)單,直接通過(guò)對(duì)應(yīng)軟件包管理器安裝即可。
本文的實(shí)驗(yàn)環(huán)境是 REHL 8(x86),因此,執(zhí)行 yum 命令來(lái)安裝。
yum install -y python3-bcc.x86_64
2.1 編寫(xiě) hello world
安裝好 Python BCC 依賴包后,在工作目錄中創(chuàng)建一個(gè) py 腳本文件,輸入以下代碼:
#!/bin/python3
from bcc import BPF
bpf_code = '''
int kprobe__sys_clone(void *ctx) {
bpf_trace_printk("Hello world!\\n");
return 0;
}
'''
b = BPF(text=bpf_code)
b.trace_print()
運(yùn)行這個(gè) py 腳本,當(dāng)有進(jìn)程被創(chuàng)建時(shí),打印一條 Hello world 記錄。
這就是一個(gè)最簡(jiǎn)單的 eBPF 程序。
3 擴(kuò)展這個(gè) Hello world
上面給出的這個(gè)程序結(jié)構(gòu)很清晰,分為兩個(gè)部分:以 C 編寫(xiě)的 eBPF 內(nèi)核態(tài)程序,和以 Python 編寫(xiě)的用戶態(tài)控制程序。eBPF 內(nèi)核態(tài)程序被 BCC 框架編譯到內(nèi)核中,等待預(yù)設(shè)的觸發(fā)條件,——這里是 sys_clone 即進(jìn)程創(chuàng)建的系統(tǒng)調(diào)用,eBPF 被執(zhí)行時(shí),將會(huì)返回?cái)?shù)據(jù)給用戶態(tài)控制程序。
流程可以描述如下:
接下來(lái)我們對(duì)這個(gè)程序進(jìn)行億點(diǎn)點(diǎn)擴(kuò)展,讓它變得規(guī)范一些,代碼如下:
#!/bin/python3
from bcc import BPF
from bcc.utils import printb
# define BPF program
prog = """
int hello(void *ctx) {
bpf_trace_printk("Hello, World!\\n");
return 0;
}
"""
# load BPF program
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")
# header
print("%-18s %-16s %-6s %s" % ("TIME(s)", "COMM", "PID", "MESSAGE"))
# format output
while 1:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
except ValueError:
continue
except KeyboardInterrupt:
exit()
printb(b"%-18.9f %-16s %-6d %s" % (ts, task, pid, msg))
在這段程序中,我們做出了以下幾點(diǎn)變動(dòng):
1)使用 event=b.get_syscall_fnname("clone") 來(lái)綁定內(nèi)核中的系統(tǒng)調(diào)用監(jiān)視點(diǎn),這里綁定了 clone 進(jìn)程創(chuàng)建調(diào)用;使用 fn_name="hello" 綁定了 eBPF 程序中的自定義檢查邏輯;使用 b.attach_kprobe() 函數(shù)將 eBPF 程序加載到內(nèi)核中。
2)使用 b.trace_fields() 函數(shù)按字段的形式,接收內(nèi)核 eBPF 程序傳出的輸出信息;其中,msg 為 bpf_trace_printk() 的打印信息。
3)通過(guò)無(wú)限循環(huán),監(jiān)測(cè) clone 系統(tǒng)調(diào)用的執(zhí)行;增加了異常輸出。
這段程序運(yùn)行后,輸出結(jié)果如下:
4 進(jìn)一步擴(kuò)展,監(jiān)視 do_execve
第 3 節(jié)的代碼,輸出內(nèi)核字段的方式是 bpf_trace_printk() + trace_fields(),比較靈活,但性能較差。實(shí)際上,還有一種比較常見(jiàn)的輸出方式,那就是通過(guò)一段共享內(nèi)存 Ring buffer 來(lái)實(shí)現(xiàn)。
此外,這次我們更換一個(gè)內(nèi)核監(jiān)視點(diǎn),不再關(guān)注進(jìn)程的創(chuàng)建,而關(guān)注進(jìn)程的執(zhí)行。
接下來(lái),對(duì)上面的代碼進(jìn)行大刀闊斧的修改吧。
文件拆分:
// do_execve.c
#include <uapi/linux/limits.h> // #define NAME_MAX 255
#include <linux/fs.h> // struct filename;
#include <linux/sched.h> // #define TASK_COMM_LEN 16
// 定義 Buffer 中的數(shù)據(jù)結(jié)構(gòu),用于內(nèi)核態(tài)和用戶態(tài)的數(shù)據(jù)交換
struct data_t {
u32 pid;
char comm[TASK_COMM_LEN];
char fname[NAME_MAX];
};
BPF_PERF_OUTPUT(events);
// 自定義 hook 函數(shù)
int check_do_execve(struct pt_regs *ctx, struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp) {
truct data_t data = { };
data.pid = bpf_get_current_pid_tgid();
bpf_get_current_comm(&data.comm, sizeof(data.comm));
bpf_probe_read_kernel_str(&data.fname, sizeof(data.fname), (void *)filename->name);
// 提交 buffer 數(shù)據(jù)
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
# do_execve.py
#!/bin/python3
from bcc import BPF
from bcc.utils import printb
# 指定 eBPF 源碼文件
b = BPF(src_file="do_execve.c")
# 以內(nèi)核函數(shù)的方式綁定 eBPF 探針
b.attach_kprobe(event="do_execve", fn_name="check_do_execve")
print("%-6s %-16s %-16s" % ("PID", "COMM", "FILE"))
# 自定義回調(diào)函數(shù)
def print_event(cpu, data, size):
event = b["events"].event(data)
printb(b"%-6d %-16s %-16s" % (event.pid, event.comm, event.fname))
# 指定 buffer 名稱(chēng),為 buffer 的修改添加回調(diào)函數(shù)
b["events"].open_perf_buffer(print_event)
while 1:
try:
# 循環(huán)監(jiān)聽(tīng)
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
這一次,我們又進(jìn)行了億點(diǎn)點(diǎn)修改:
1)首先,對(duì) eBPF BCC 程序的用戶態(tài)和內(nèi)核態(tài)代碼進(jìn)行拆分,并在用戶態(tài)程序中,通過(guò) b = BPF(src_file="do_execve.c") 對(duì)內(nèi)核態(tài)源碼文件進(jìn)行綁定。
2)以內(nèi)核函數(shù)的方式綁定 eBPF 程序,綁定點(diǎn)為 do_execve(),自定義處理函數(shù)為 check_do_execve()。
注意:
可以看到,
check_do_execve()函數(shù)的參數(shù)分為兩部分:① struct pt_regs *ctx; ② struct filename *filename, const char __user *const __user *__argv, const char __user *const __user *__envp這是因?yàn)椋谒淼模莾?nèi)核
do_execve()函數(shù)的參數(shù)。do_execve()函數(shù)簽名如下:// fs/exec.c int do_execve(struct filename *filename, const char __user *const __user *__argv, const char __user *const __user *__envp) {...}是的,通過(guò)這種方式,幾乎可以監(jiān)控任意一個(gè)內(nèi)核中的函數(shù)。
3)內(nèi)核態(tài)程序中,使用了一些 eBPF Helper 函數(shù)來(lái)進(jìn)行一些基礎(chǔ)的操作和數(shù)據(jù)獲取,例如:
bpf_get_current_pid_tgid() // 獲取當(dāng)前進(jìn)程 pid
bpf_get_current_comm(&data.comm, sizeof(data.comm)); // 獲取當(dāng)前進(jìn)程名 comm
bpf_probe_read_kernel_str(&data.fname, sizeof(data.fname), (void *)filename->name); // 將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間
4)內(nèi)核態(tài)程序中,使用 BPF_PERF_OUTPUT(events) 聲明 buffer 中的共享變量;使用 events.perf_submit(ctx, &data, sizeof(data)) 提交數(shù)據(jù)。
用戶態(tài)程序中,使用 b["events"].open_perf_buffer(print_event) 指定 buffer 名稱(chēng),為 buffer 的修改添加回調(diào)函數(shù) print_event。
運(yùn)行這段程序,輸出如下:
可以看到,這段程序可以實(shí)時(shí)監(jiān)控內(nèi)核進(jìn)程執(zhí)行,并輸出執(zhí)行的進(jìn)程和被執(zhí)行的文件名。
5 總結(jié)
本文通過(guò)幾個(gè)程序 demo,簡(jiǎn)單介紹了 eBPF BCC 框架的編程方法,并最終實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的進(jìn)程執(zhí)行的監(jiān)視工具,可以實(shí)時(shí)打印被執(zhí)行的進(jìn)程信息。
本文開(kāi)篇所引出的實(shí)時(shí)監(jiān)控內(nèi)核可執(zhí)行文件(ELF)的加載程序,也就沒(méi)那個(gè)高深莫測(cè)了。
總結(jié)
以上是生活随笔為你收集整理的【eBPF-01】初见:基于 BCC 框架的第一个 eBPF 程序的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 论文怎么文献引用
- 下一篇: 从Redis读取.NET Core配置