使用eBPF追蹤Linux內(nèi)核
簡介
BPF,及伯克利包過濾器Berkeley Packet Filter,最初構(gòu)想提出于 1992 年,其目的是為了提供一種過濾包的方法,并且要避免從內(nèi)核空間到用戶空間的無用的數(shù)據(jù)包復(fù)制行為。它最初是由從用戶空間注入到內(nèi)核的一個簡單的字節(jié)碼構(gòu)成,它在那個位置利用一個校驗(yàn)器進(jìn)行檢查 —— 以避免內(nèi)核崩潰或者安全問題 —— 并附著到一個套接字上,接著在每個接收到的包上運(yùn)行。幾年后它被移植到 Linux 上,并且應(yīng)用于一小部分應(yīng)用程序上(例如,tcpdump)。其簡化的語言以及存在于內(nèi)核中的即時編譯器(JIT),使 BPF 成為一個性能卓越的工具。
然后,在 2013 年,Alexei Starovoitov 對 BPF 進(jìn)行徹底地改造,并增加了新的功能,改善了它的性能。這個新版本被命名為 eBPF (意思是 “extended BPF”),與此同時,將以前的 BPF 變成 cBPF(意思是 “classic” BPF)。新版本出現(xiàn)了如映射和尾調(diào)用tail call這樣的新特性,并且 JIT 編譯器也被重寫了。新的語言比 cBPF 更接近于原生機(jī)器語言。并且,在內(nèi)核中創(chuàng)建了新的附著點(diǎn)。
感謝那些新的鉤子,eBPF 程序才可以被設(shè)計用于各種各樣的情形下,其分為兩個應(yīng)用領(lǐng)域。其中一個應(yīng)用領(lǐng)域是內(nèi)核跟蹤和事件監(jiān)控。BPF 程序可以被附著到探針(kprobe),而且它與其它跟蹤模式相比,有很多的優(yōu)點(diǎn)(有時也有一些缺點(diǎn))。
另外一個應(yīng)用領(lǐng)域是網(wǎng)絡(luò)編程。除了套接字過濾器外,eBPF 程序還可以附加到 tc(Linux 流量控制工具)的入站或者出站接口上,以一種很高效的方式去執(zhí)行各種包處理任務(wù)。這種使用方式在這個領(lǐng)域開創(chuàng)了一個新的天地。
并且 eBPF 通過使用為 IO Visor 項(xiàng)目開發(fā)的技術(shù),使它的性能進(jìn)一步得到提升:也為 XDP(“eXpress Data Path”)添加了新的鉤子,XDP 是不久前添加到內(nèi)核中的一種新式快速路徑。XDP 與 Linux 棧組合,然后使用 BPF ,使包處理的速度更快。
甚至一些項(xiàng)目,如 P4、Open vSwitch,考慮 或者開始去接洽使用 BPF。其它的一些,如 CETH、Cilium,則是完全基于它的。BPF 是如此流行,因此,我們可以預(yù)計,不久之后,將圍繞它有更多工具和項(xiàng)目出現(xiàn) …
什么是BPF程序:
BPF is a highly flexible and efficient virtual machine-like construct in the Linux kernel allowing to execute bytecode at various hook points in a safe manner.
BPF程序 ----LLVM+Clang----> BPF字節(jié)碼 ----JIT----> BPF指令集;
BPF架構(gòu)采用一種新的虛擬機(jī)設(shè)計,包含支持x86_64, arm64, mips64等架構(gòu)的指令集,BPF指令集程序可以高效地工作在基于寄存器架構(gòu)(r0到r10)的CPU上;
Linux內(nèi)核維護(hù)者不斷開發(fā)hook點(diǎn),可以在hook點(diǎn)上掛載BPF程序,當(dāng)hook點(diǎn)對應(yīng)的事件發(fā)生就可以執(zhí)行BPF程序,BPF程序返回hook點(diǎn)預(yù)定義的值,Linux內(nèi)核再根據(jù)返回值執(zhí)行下一步操作(比如XDP類型的BPF程序掛載在指定的網(wǎng)絡(luò)接口上,有數(shù)據(jù)包進(jìn)入該網(wǎng)絡(luò)接口,BPF程序?qū)?shù)據(jù)包進(jìn)行解析然后根據(jù)協(xié)議字段進(jìn)行判斷,如果不符合規(guī)則就返回XDP_DROP,Linux內(nèi)核根據(jù)該返回值就會丟棄該數(shù)據(jù)包)。

BPF工作原理:

【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【865977150】整理了一些個人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。∏?00名進(jìn)群領(lǐng)取,額外贈送一份價值699的內(nèi)核資料包(含視頻教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)

BPF關(guān)鍵組件:
BPF Hooks
BPF映射:
BPF程序和用戶空間程序通過BPF映射通信;
BPF映射以鍵/值保存在內(nèi)核,可以被任何BPF程序訪問,用戶空間的程序可以通過文件描述符訪問BPF映射
BPF映射類型:BPF映射支持多種數(shù)據(jù)結(jié)構(gòu),從而實(shí)現(xiàn)內(nèi)核內(nèi)部數(shù)據(jù)的組織以及用戶態(tài)和內(nèi)核態(tài)的通信,比如哈希表、數(shù)組、隊(duì)列等等

BPF映射用途舉例:
1)在BPF程序不中斷的情況下修改其運(yùn)行方式,修改映射中BPF程序訪問的配置數(shù)據(jù)或應(yīng)用數(shù)據(jù),例如黑名單規(guī)定的IP列表和域名;
2)運(yùn)行在內(nèi)核的BPF程序統(tǒng)計進(jìn)入指定網(wǎng)絡(luò)接口的數(shù)據(jù)包信息,并將統(tǒng)計信息保存到BPF映射,用戶態(tài)程序可以通過BPF映射獲取數(shù)據(jù)包統(tǒng)計信息;

BPF輔助函數(shù)(BPF Helper Function):如其他語言生態(tài)會提供豐富的庫提供大量的API函數(shù),BPF也包含各種常用的輔助函數(shù),提供操作內(nèi)核數(shù)據(jù)和BPF映射的工具類函數(shù);
優(yōu)點(diǎn):通過定義和維護(hù)BPF輔助函數(shù),由BPF輔助函數(shù)維護(hù)者處理Linux內(nèi)核版本的迭代更新,對開發(fā)者透明,形成穩(wěn)定的API接口;
BPF輔助函數(shù)列表:

我們可以使用BPF對Linux內(nèi)核進(jìn)行跟蹤,收集我們想要的內(nèi)核數(shù)據(jù),從而對Linux中的程序進(jìn)行分析和調(diào)試。與其它的跟蹤技術(shù)相比,使用BPF的主要優(yōu)點(diǎn)是幾乎可以訪問Linux內(nèi)核和應(yīng)用程序的任何信息,同時,BPF對系統(tǒng)性能影響很小,執(zhí)行效率很高,而且開發(fā)人員不需要因?yàn)槭占瘮?shù)據(jù)而修改程序。
本文將介紹保證BPF程序安全的BPF驗(yàn)證器,然后以BPF程序的工具集BCC為例,分享kprobes和tracepoints類型的BPF程序的使用及程序編寫示例。
BPF驗(yàn)證器
BPF借助跟蹤探針收集信息并進(jìn)行調(diào)試和分析,與其它依賴于重新編譯內(nèi)核的工具相比,BPF程序的安全性更高。重新編譯內(nèi)核引入外部模塊的方式,可能會因?yàn)槌绦虻腻e誤而產(chǎn)生系統(tǒng)奔潰。BPF程序的驗(yàn)證器會在BPF程序加載到內(nèi)核之前分析程序,消除這種風(fēng)險。
BPF驗(yàn)證器執(zhí)行的第一項(xiàng)檢查是對BPF虛擬機(jī)加載的代碼進(jìn)行靜態(tài)分析,目的是確保程序能夠按照預(yù)期結(jié)束。驗(yàn)證器在進(jìn)行第一項(xiàng)檢查時所做工作為:
程序不包含控制循環(huán);
程序不會執(zhí)行超過內(nèi)核允許的最大指令數(shù);
程序不包含任何無法到達(dá)的指令;
程序不會超出程序界限。
BPF驗(yàn)證器執(zhí)行的第二項(xiàng)檢查是對BPF程序進(jìn)行預(yù)運(yùn)行,所做工作為:
分析BPF程序執(zhí)行的每條指令,確保不會執(zhí)行無效指令;
檢查所有內(nèi)存指針是否可以正確訪問和引用;
預(yù)運(yùn)行將程序控制流的執(zhí)行結(jié)果通知驗(yàn)證器,確保BPF程序最終都會執(zhí)行BPF_EXIT指令。
內(nèi)核探針 kprobes
內(nèi)核探針可以跟蹤大多數(shù)內(nèi)核函數(shù),并且系統(tǒng)損耗最小。當(dāng)跟蹤的內(nèi)核函數(shù)被調(diào)用時,附加到探針的BPF代碼將被執(zhí)行,之后內(nèi)核將恢復(fù)正常模式。
kprobes類BPF程序的優(yōu)缺點(diǎn)
優(yōu)點(diǎn) 動態(tài)跟蹤內(nèi)核,可跟蹤的內(nèi)核函數(shù)眾多,能夠提取內(nèi)核絕大部分信息。
缺點(diǎn) 沒有穩(wěn)定的應(yīng)用程序二進(jìn)制接口,可能隨著內(nèi)核版本的演進(jìn)而更改。
kprobes
kprobe程序允許在執(zhí)行內(nèi)核函數(shù)之前插入BPF程序。當(dāng)內(nèi)核執(zhí)行到kprobe掛載的內(nèi)核函數(shù)時,先運(yùn)行BPF程序,BPF程序運(yùn)行結(jié)束后,返回繼續(xù)開始執(zhí)行內(nèi)核函數(shù)。下面是一個使用kprobe的bcc程序示例,功能是監(jiān)控內(nèi)核函數(shù)kfree_skb函數(shù),當(dāng)此函數(shù)觸發(fā)時,記錄觸發(fā)它的進(jìn)程pid,進(jìn)程名字和觸發(fā)次數(shù),并打印出觸發(fā)此函數(shù)的進(jìn)程pid,進(jìn)程名字和觸發(fā)次數(shù):
#!/usr/bin/python3
# coding=utf-8
from __future__ import print_function
from bcc import BPF
from time import sleep
# define BPF program
bpf_program = """
#include <uapi/linux/ptrace.h>
struct key_t{
u64 pid;
};
BPF_HASH(counts, struct key_t);
int trace_kfree_skb(struct pt_regs *ctx) {
u64 zero = 0, *val, pid;
pid = bpf_get_current_pid_tgid() >> 32;
struct key_t key ?= {};
key.pid = pid;
? ?val = counts.lookup_or_try_init(&key, &zero);
? ?if (val) {
? ? ?(*val)++;
? ?}
? ?return 0;
}
"""
def pid_to_comm(pid):
? ?try:
? ? ? ?comm = open("/proc/%s/comm" % pid, "r").read().rstrip()
? ? ? ?return comm
? ?except IOError:
? ? ? ?return str(pid)
# load BPF
b = BPF(text=bpf_program)
b.attach_kprobe(event="kfree_skb", fn_name="trace_kfree_skb")
# header
print("Tracing kfree_skb... Ctrl-C to end.")
print("%-10s %-12s %-10s" % ("PID", "COMM", "DROP_COUNTS"))
while 1:
sleep(1)
for k, v in sorted(b["counts"].items(),key = lambda counts: counts[1].value):
? print("%-10d %-12s %-10d" % (k.pid, pid_to_comm(k.pid), v.value))
該bcc程序主要包括兩個部分,一部分是python語言,一部分是c語言。python部分主要做的工作是BPF程序的加載和操作BPF程序的map,并進(jìn)行數(shù)據(jù)處理。c部分會被llvm編譯器編譯為BPF字節(jié)碼,經(jīng)過BPF驗(yàn)證器驗(yàn)證安全后,加載到內(nèi)核中執(zhí)行。python和c中出現(xiàn)的陌生函數(shù)可以查下面這兩個手冊,在此不再贅述:
python部分遇到的陌生函數(shù)可以查這個手冊: 點(diǎn)此跳轉(zhuǎn)
c部分中遇到的陌生函數(shù)可以查這個手冊: 點(diǎn)此跳轉(zhuǎn)
需要說明的是,該BPF程序類型是kprobe,它是在這里進(jìn)行程序類型定義的:
b.attach_kprobe(event="kfree_skb", fn_name="trace_kfree_skb")
b.attach_kprobe()指定了該BPF程序類型為kprobe;
event="kfree_skb"指定了kprobe掛載的內(nèi)核函數(shù)為kfree_skb;
fn_name="trace_kfree_skb"指定了當(dāng)檢測到內(nèi)核函數(shù)kfree_skb時,執(zhí)行程序中的trace_kfree_skb函數(shù);
BPF程序的第一個參數(shù)總為ctx,該參數(shù)稱為上下文,提供了訪問內(nèi)核正在處理的信息,依賴于正在運(yùn)行的BPF程序的類型。CPU將內(nèi)核正在執(zhí)行任務(wù)的不同信息保存在寄存器中,借助內(nèi)核提供的宏可以訪問這些寄存器,如PT_REGS_RC。
程序運(yùn)行結(jié)果如下:

kretprobes
相比于內(nèi)核探針kprobe程序,kretprobe程序是在內(nèi)核函數(shù)有返回值時插入BPF程序。當(dāng)內(nèi)核執(zhí)行到kretprobe掛載的內(nèi)核函數(shù)時,先執(zhí)行內(nèi)核函數(shù),當(dāng)內(nèi)核函數(shù)返回時執(zhí)行BPF程序,運(yùn)行結(jié)束后返回。
以上面的BPF程序?yàn)槔?,若要使用kretprobe,可以這樣修改:
b.attach_kretprobe(event="kfree_skb", fn_name="trace_kfree_skb")
b.attach_kretprobe()指定了該BPF程序類型為kretprobe,kretprobe類型的BPF程序?qū)⒃诟櫟膬?nèi)核函數(shù)有返回值時執(zhí)行BPF程序;
event="kfree_skb"指定了kretprobe掛載的內(nèi)核函數(shù)為kfree_skb;
fn_name="trace_kfree_skb"指定了當(dāng)內(nèi)核函數(shù)kfree_skb有返回值時,執(zhí)行程序中的trace_kfree_skb函數(shù);
內(nèi)核靜態(tài)跟蹤點(diǎn) tracepoint
tracepoint是內(nèi)核靜態(tài)跟蹤點(diǎn),它與kprobe類程序的主要區(qū)別在于tracepoint由內(nèi)核開發(fā)人員在內(nèi)核中編寫和修改。
tracepoint 程序的優(yōu)缺點(diǎn)
優(yōu)點(diǎn) 跟蹤點(diǎn)是靜態(tài)的,ABI更穩(wěn)定,不隨內(nèi)核版本的變化而致不可用。
缺點(diǎn) 跟蹤點(diǎn)是內(nèi)核人員添加的,不會全面涵蓋內(nèi)核的所有子系統(tǒng)。
tracepoint 可用跟蹤點(diǎn)
系統(tǒng)中所有的跟蹤點(diǎn)都定義在
/sys/kernel/debug/traceing/events目錄中:

使用命令perf list 也可以列出可使用的tracepoint點(diǎn):

對于bcc程序來說,以監(jiān)控kfree_skb為例,tracepoint程序可以這樣寫:
b.attach_tracepoint(tp="skb:kfree_skb", fn_name="trace_kfree_skb")
bcc遵循tracepoint命名約定,首先是指定要跟蹤的子系統(tǒng),這里是“skb:”,然后是子系統(tǒng)中的跟蹤點(diǎn)“kfree_skb”:

總結(jié)
本文主要介紹了保證BPF程序安全的BPF驗(yàn)證器,然后以BPF程序的工具集BCC為例,分享了kprobes和tracepoints類型的BPF程序的使用及程序編寫示例。本文分享的是內(nèi)核跟蹤,那么用戶空間程序該如何跟蹤呢,這將在后面的文章中逐步分享,感謝閱讀。
原文地址:https://www.toutiao.com/article/6953035895008281124/?channel=&source=search_tab