1. 前言
我們可以使用BPF對Linux內核進行跟蹤,收集我們想要的內核數據,從而對Linux中的程序進行分析和調試。與其它的跟蹤技術相比,使用BPF的主要優點是幾乎可以訪問Linux內核和應用程序的任何信息,同時,BPF對系統性能影響很小,執行效率很高,而且開發人員不需要因為收集數據而修改程序。
本文將介紹保證BPF程序安全的BPF驗證器,然后以BPF程序的工具集BCC為例,分享kprobes和tracepoints類型的BPF程序的使用及程序編寫示例。
2. BPF驗證器
BPF借助跟蹤探針收集信息并進行調試和分析,與其它依賴于重新編譯內核的工具相比,BPF程序的安全性更高。重新編譯內核引入外部模塊的方式,可能會因為程序的錯誤而產生系統奔潰。BPF程序的驗證器會在BPF程序加載到內核之前分析程序,消除這種風險。
BPF驗證器執行的第一項檢查是對BPF虛擬機加載的代碼進行靜態分析,目的是確保程序能夠按照預期結束。驗證器在進行第一項檢查時所做工作為:
程序不包含控制循環;
程序不會執行超過內核允許的最大指令數;
程序不包含任何無法到達的指令;
程序不會超出程序界限。
BPF驗證器執行的第二項檢查是對BPF程序進行預運行,所做工作為:
分析BPF程序執行的每條指令,確保不會執行無效指令;
檢查所有內存指針是否可以正確訪問和引用;
預運行將程序控制流的執行結果通知驗證器,確保BPF程序最終都會執行BPF_EXIT指令。
3. 內核探針 kprobes
內核探針可以跟蹤大多數內核函數,并且系統損耗最小。當跟蹤的內核函數被調用時,附加到探針的BPF代碼將被執行,之后內核將恢復正常模式。
3.1 kprobes類BPF程序的優缺點
優點 動態跟蹤內核,可跟蹤的內核函數眾多,能夠提取內核絕大部分信息。
缺點 沒有穩定的應用程序二進制接口,可能隨著內核版本的演進而更改。
3.2 kprobes
kprobe程序允許在執行內核函數之前插入BPF程序。當內核執行到kprobe掛載的內核函數時,先運行BPF程序,BPF程序運行結束后,返回繼續開始執行內核函數。下面是一個使用kprobe的bcc程序示例,功能是監控內核函數kfree_skb函數,當此函數觸發時,記錄觸發它的進程pid,進程名字和觸發次數,并打印出觸發此函數的進程pid,進程名字和觸發次數:
#!/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,并進行數據處理。c部分會被llvm編譯器編譯為BPF字節碼,經過BPF驗證器驗證安全后,加載到內核中執行。python和c中出現的陌生函數可以查下面這兩個手冊,在此不再贅述:
python部分遇到的陌生函數可以查這個手冊: 點此跳轉
c部分中遇到的陌生函數可以查這個手冊: 點此跳轉
需要說明的是,該BPF程序類型是kprobe,它是在這里進行程序類型定義的:
b.attach_kprobe(event=“kfree_skb”, fn_name=“trace_kfree_skb”)
b.attach_kprobe()指定了該BPF程序類型為kprobe;
event=“kfree_skb”指定了kprobe掛載的內核函數為kfree_skb;
fn_name=“trace_kfree_skb”指定了當檢測到內核函數kfree_skb時,執行程序中的trace_kfree_skb函數;
BPF程序的第一個參數總為ctx,該參數稱為上下文,提供了訪問內核正在處理的信息,依賴于正在運行的BPF程序的類型。CPU將內核正在執行任務的不同信息保存在寄存器中,借助內核提供的宏可以訪問這些寄存器,如PT_REGS_RC。
程序運行結果如下:
3.3 kretprobes
相比于內核探針kprobe程序,kretprobe程序是在內核函數有返回值時插入BPF程序。當內核執行到kretprobe掛載的內核函數時,先執行內核函數,當內核函數返回時執行BPF程序,運行結束后返回。
以上面的BPF程序為例,若要使用kretprobe,可以這樣修改:
b.attach_kretprobe(event=“kfree_skb”, fn_name=“trace_kfree_skb”)
b.attach_kretprobe()指定了該BPF程序類型為kretprobe,kretprobe類型的BPF程序將在跟蹤的內核函數有返回值時執行BPF程序;
event=“kfree_skb”指定了kretprobe掛載的內核函數為kfree_skb;
fn_name=“trace_kfree_skb”指定了當內核函數kfree_skb有返回值時,執行程序中的trace_kfree_skb函數;
4. 內核靜態跟蹤點 tracepoint
tracepoint是內核靜態跟蹤點,它與kprobe類程序的主要區別在于tracepoint由內核開發人員在內核中編寫和修改。
4.1 tracepoint 程序的優缺點
優點 跟蹤點是靜態的,ABI更穩定,不隨內核版本的變化而致不可用。
缺點 跟蹤點是內核人員添加的,不會全面涵蓋內核的所有子系統。
4.2 tracepoint 可用跟蹤點
系統中所有的跟蹤點都定義在/sys/kernel/debug/traceing/events目錄中:
使用命令perf list 也可以列出可使用的tracepoint點:
對于bcc程序來說,以監控kfree_skb為例,tracepoint程序可以這樣寫:
b.attach_tracepoint(tp=“skb:kfree_skb”, fn_name=“trace_kfree_skb”)
bcc遵循tracepoint命名約定,首先是指定要跟蹤的子系統,這里是“skb:”,然后是子系統中的跟蹤點“kfree_skb”:
5. 總結
本文主要介紹了保證BPF程序安全的BPF驗證器,然后以BPF程序的工具集BCC為例,分享了kprobes和tracepoints類型的BPF程序的使用及程序編寫示例。本文分享的是內核跟蹤,那么用戶空間程序該如何跟蹤呢,這將在后面的文章中逐步分享,感謝閱讀。
原文標題:梁金榮:使用eBPF追蹤LINUX內核
文章出處:【微信公眾號:Linuxer】歡迎添加關注!文章轉載請注明出處。
責任編輯:haq
-
內核
+關注
關注
3文章
1372瀏覽量
40282 -
Linux
+關注
關注
87文章
11296瀏覽量
209357
發布評論請先 登錄
相關推薦
評論