使用 EBPF 追蹤 LINUX 內核

  1. 前言

我們可以使用 BPF 對 Linux 內核進行跟蹤,收集我們想要的內核數據,從而對 Linux 中的程序進行分析和調試。與其它的跟蹤技術相比,使用 BPF 的主要優點是幾乎可以訪問 Linux 內核和應用程序的任何信息,同時,BPF 對系統性能影響很小,執行效率很高,而且開發人員不需要因爲收集數據而修改程序。

本文將介紹保證 BPF 程序安全的 BPF 驗證器,然後以 BPF 程序的工具集 BCC 爲例,分享 kprobes 和 tracepoints 類型的 BPF 程序的使用及程序編寫示例。

  1. BPF 驗證器

BPF 藉助跟蹤探針收集信息並進行調試和分析,與其它依賴於重新編譯內核的工具相比,BPF 程序的安全性更高。重新編譯內核引入外部模塊的方式,可能會因爲程序的錯誤而產生系統奔潰。BPF 程序的驗證器會在 BPF 程序加載到內核之前分析程序,消除這種風險。

BPF 驗證器執行的第一項檢查是對 BPF 虛擬機加載的代碼進行靜態分析,目的是確保程序能夠按照預期結束。驗證器在進行第一項檢查時所做工作爲:

BPF 驗證器執行的第二項檢查是對 BPF 程序進行預運行,所做工作爲:

  1. 內核探針 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_)

# 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_)

BPF 程序的第一個參數總爲 ctx,該參數稱爲上下文,提供了訪問內核正在處理的信息,依賴於正在運行的 BPF 程序的類型。CPU 將內核正在執行任務的不同信息保存在寄存器中,藉助內核提供的宏可以訪問這些寄存器,如 PT_REGS_RC。

程序運行結果如下:

3.3 kretprobes

相比於內核探針 kprobe 程序,kretprobe 程序是在內核函數有返回值時插入 BPF 程序。當內核執行到 kretprobe 掛載的內核函數時,先執行內核函數,當內核函數返回時執行 BPF 程序,運行結束後返回。

以上面的 BPF 程序爲例,若要使用 kretprobe,可以這樣修改:

b.attach_kretprobe(event="kfree_skb", fn_)
  1. 內核靜態跟蹤點 tracepoint

tracepoint 是內核靜態跟蹤點,它與 kprobe 類程序的主要區別在於 tracepoint 由內核開發人員在內核中編寫和修改。

4.1 tracepoint 程序的優缺點

4.2 tracepoint 可用跟蹤點

系統中所有的跟蹤點都定義在/sys/kernel/debug/traceing/events目錄中:

使用命令perf list 也可以列出可使用的 tracepoint 點:

對於 bcc 程序來說,以監控kfree_skb爲例,tracepoint 程序可以這樣寫:

b.attach_tracepoint(tp="skb:kfree_skb", fn_)

bcc 遵循 tracepoint 命名約定,首先是指定要跟蹤的子系統,這裏是 “skb:”,然後是子系統中的跟蹤點 “kfree_skb”:

  1. 總結

本文主要介紹了保證 BPF 程序安全的 BPF 驗證器,然後以 BPF 程序的工具集 BCC 爲例,分享了 kprobes 和 tracepoints 類型的 BPF 程序的使用及程序編寫示例。本文分享的是內核跟蹤,那麼用戶空間程序該如何跟蹤呢,這將在後面的文章中逐步分享,感謝閱讀。

參考資料:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/aVfaL0_WB2viJa4BTIrEGA