0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

內(nèi)核觀測技術(shù)BPF詳解

科技綠洲 ? 來源:Linux開發(fā)架構(gòu)之路 ? 作者:Linux開發(fā)架構(gòu)之路 ? 2023-11-10 10:34 ? 次閱讀

BPF簡介

BPF,全稱是Berkeley Packet Filter(伯克利數(shù)據(jù)包過濾器)的縮寫。其誕生于1992年,最初的目的是提升網(wǎng)絡(luò)包過濾工具的性能。后面,隨著這個工具重新實現(xiàn)BPF的內(nèi)核補丁和不斷完善代碼,BPF程序變成了一個更通用的執(zhí)行引擎,可以完成多種任務(wù)。簡單來說,BPF提供了一種在各種內(nèi)核時間和應(yīng)用程序事件發(fā)生時運行一小段程序的機制。其允許內(nèi)核在系統(tǒng)和應(yīng)用程序事件發(fā)生時運行一小段程序,這樣就將內(nèi)核變得完全可編程,允許用戶定制和控制他們的系統(tǒng)。

BPF其有指令集、存儲對象和輔助函數(shù)等幾部分組成。由于它采取了虛擬指令集規(guī)范,因此也可將其視為一種虛擬機實現(xiàn)。當Linux指定的時候,其會提供兩種執(zhí)行機制:一個解釋器和一個將BPF指令動態(tài)轉(zhuǎn)換為本地化指令的即時編程器。在實際執(zhí)行之前,BPF指令必須先通過驗證器的安全性檢查,以確保BPF程序自身不會崩潰或者損壞內(nèi)核。

注:擴展后的BPF通??s寫為eBPF,但官方縮寫仍然是BPF。在內(nèi)核之中只有一個執(zhí)行引擎,其同時支持eBPF和經(jīng)典BPF程序。

BPF驗證器

BPF允許任何人在Linux內(nèi)核之中執(zhí)行任意的代碼,這聽起來的十分危險,但是由于有著BPF驗證器使得這一過程變的相當?shù)陌踩?。BPF時內(nèi)核的一個模塊,所有的BPF程序都必須經(jīng)過它的審查才能夠被加載到內(nèi)核之中去運行。

驗證器執(zhí)行的第一項檢查就是對BPF虛擬機加載的代碼進行靜態(tài)分析。這一步的目的是保證程序可以按照預(yù)期去結(jié)束,而不會產(chǎn)生死循環(huán)拜拜浪費系統(tǒng)資源。驗證器會創(chuàng)建一個DAG(有向無環(huán)圖),將BPF程序的每個執(zhí)行首位相連之后去執(zhí)行DFS(深度優(yōu)先遍歷),當且僅當每個路徑都能達到DAG的底部才會通過驗證。

之后其會執(zhí)行第二項檢查,也就是對BPF程序執(zhí)行預(yù)執(zhí)行處理。這個時候驗證器會去分析程序執(zhí)行的每條指令,確保不會執(zhí)行無效的指令。同時也會檢查所有內(nèi)存指針是否可以正確訪問和解引用。

尾部調(diào)用

BPF程序可以使用尾部調(diào)用來調(diào)用其他BPF程序,這是個強大的功能。其允許通過組合比較小的BPF功能來實現(xiàn)更為復(fù)雜的程序。當從一個BPF程序調(diào)用另外一個BPF程序的時候,內(nèi)核會完全重置程序上下文。這意味著如果想要在多個BPF程序之中共享信息這是做不到的。為了解決程序間共享信息的問題,BPF引入了BPF映射的機制來解決這個問題,我們會在后面詳細的介紹BPF映射機制。

注:內(nèi)核5.2 版本之前BPF只允許執(zhí)行4096條指令,所以才有了尾部調(diào)用這個特性。從5.2開始,指令限制擴展到了100w條,尾部調(diào)用的遞歸層次也有了32次的限制。

BPF 環(huán)境配置

內(nèi)核升級

BPF程序在4系內(nèi)核之后就已經(jīng)成為了內(nèi)核的頂級子系統(tǒng),但是為了讓我們的系統(tǒng)能夠穩(wěn)定運行BPF程序,還是推薦安裝5系內(nèi)核。首先,我們可以使用如下的命令獲取當前系統(tǒng)的版本:

uname -a

Linux localhost 5.0.9 #2 SMP PREEMPT Mon Feb 27 00:00:23 CST 2023 x86_64 x86_64 x86_64 GNU/Linux

筆者這里的系統(tǒng)已經(jīng)經(jīng)過升級了,如果沒有經(jīng)歷過升級,可以按照如下的命令獲取系統(tǒng)的源碼:

# 獲取相應(yīng)版本的內(nèi)核源碼
cd /tmp
wget -c https://mirrors.aliyun.com/linux-kernel//v5.x/linux-5.0.9.tar.gz -O - | tar -xz

之后的過程,同學(xué)們可以百度相應(yīng)的教程獲取安裝,本文章將專注于BPF技術(shù)的使用。

安裝好相應(yīng)內(nèi)核之后,為了讓我們在開發(fā)的時候更為容易,推薦這里將內(nèi)核源碼單獨編譯一下,方便我們鏈接:

tar -xvf linux-5.0.9.tar.gz
sudo mv linux-5.0.9 /kernel-src
cd /kernel-src/tools/lib/bpf
sudo make && sudo make install prefix=/

依賴環(huán)境安裝

升級好內(nèi)核環(huán)境之后,我們還需要安裝BPF程序的依賴環(huán)境,主要可以分為三個部分:

  • BCC 工具包:通過github 獲取相應(yīng)的源碼進行安裝
  • LLVM 編譯器:訪問官網(wǎng)可獲取安裝教程
  • 其他依賴程序:
sudo dnf install make glibc-devel.i686 elfutils-libelf-devel wget tar clang bcc strace kernel-devel -y

運行第一個BPF程序

在安裝好上述程序之后,我們使用如下的代碼可以來測試我們的環(huán)境是否配置完成。BPF程序可以由C語言來編寫,之后由LLVM編譯,其可以將C語言寫的程序編譯成能夠加載到內(nèi)核執(zhí)行的匯編代碼。

# 指定編譯器為clang
CLANG = clang
# 編譯完后的程序名稱
EXECABLE = monitor-exec
# 源碼名稱
BPFCODE = bpf_program
# BPF依賴地址
BPFTOOLS = /kernel-src/samples/bpf
BPFLOADER = $(BPFTOOLS)/bpf_load.c
# 指定頭文件
CCINCLUDE += -I/kernel-src/tools/testing/selftests/bpf
LOADINCLUDE += -I/kernel-src/samples/bpf
LOADINCLUDE += -I/kernel-src/tools/lib
LOADINCLUDE += -I/kernel-src/tools/perf
LOADINCLUDE += -I/kernel-src/tools/include
LIBRARY_PATH = -L/usr/local/lib64
BPFSO = -lbpf

CFLAGS += $(shell grep -q "define HAVE_ATTR_TEST 1" /kernel-src/tools/perf/perf-sys.h 
                  && echo "-DHAVE_ATTR_TEST=0")

.PHONY: clean $(CLANG) bpfload build

clean:
	rm -f *.o *.so $(EXECABLE)

build: ${BPFCODE.c} ${BPFLOADER}
	$(CLANG) -O2 -target bpf -c $(BPFCODE:=.c) $(CCINCLUDE) -o ${BPFCODE:=.o}

bpfload: build
	# 編譯程序
	clang $(CFLAGS) -o $(EXECABLE) -lelf $(LOADINCLUDE) $(LIBRARY_PATH) $(BPFSO) 
        $(BPFLOADER) loader.c

$(EXECABLE): bpfload

.DEFAULT_GOAL := $(EXECABLE)

程序源碼有兩個,一個是bpf_program.c這里面存放的是要執(zhí)行的BPF源碼,其會被編譯成為一個.o文件。

在這里我們使用BPF提供的SEC屬性告知BPF虛擬機在何時運行此程序。下面的代碼會在execve系統(tǒng)調(diào)用跟蹤點被執(zhí)行的時候運行BPF程序。當內(nèi)核檢測到execve的時候,BPF程序被執(zhí)行時,我們會看到輸出消息"Hello, World, BPF!"

#include < linux/bpf.h >
#define SEC(NAME) __attribute__((section(NAME), used))

static int (*bpf_trace_printk)(const char *fmt, int fmt_size,
                               ...) = (void *)BPF_FUNC_trace_printk;

SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx) {
  char msg[] = "Hello, World, BPF!";
  bpf_trace_printk(msg, sizeof(msg));
  return 0;
}

// 程序許可證,linux內(nèi)核只允許加載GPL許可的程序
char _license[] SEC("license") = "GPL";

上面的.o文件會被下面的這個由loader.c編譯成為的moniter-exec程序去執(zhí)行。其會把BPF程序加載到內(nèi)核之中去運行,這里依賴的就是我們使用的load_bpf_file,其將會獲取一個二進制文件并把它加載到內(nèi)核之中。

#include "bpf_load.h"
#include < stdio.h >

int main(int argc, char **argv) {
  if (load_bpf_file("bpf_program.o") != 0) {
    printf("The kernel didn't load the BPF programn");
    return -1;
  }

  read_trace_pipe();

  return 0;
}

之后我們執(zhí)行如下的命令去編譯上述的代碼:

make

# 運行以下程序
sudo ./loader

BPF映射

BPF映射以的形式會被保存到內(nèi)核之中,其可以被任何其他的BPF程序訪問。用戶空間的程序也可以通過文件描述符訪問BPF映射。BPF映射之中可以保存事先指定大小的任何類型的數(shù)據(jù)。內(nèi)核會將數(shù)據(jù)看作二進制塊,這意味著內(nèi)核并不關(guān)系BPF映射保存的具體內(nèi)容。

此內(nèi)容會存在較多的代碼,這里會將相關(guān)所需要的MakeFile文件內(nèi)容展示出來:

CLANG = clang

INCLUDE_PATH += -I/kernel-src/tools/lib/bpf
INCLUDE_PATH += -I/kernel-src/tools/**
LIBRARY_PATH = -L/usr/local/lib64
BPFSO = -lbpf
.PHONY: clean 

clean:
	rm -f # 要刪除的BPF模塊
	
build: # 填寫要編譯的 BPF程序模塊

.DEFAULT_GOAL := build

創(chuàng)建BPF映射

創(chuàng)建BPF映射的最值方式就是使用bpf_create_map系統(tǒng)調(diào)用。這個函數(shù)需要傳入五個參數(shù)

  • map_type:map的類型,如果設(shè)置為BPF_MAP_CREATE,則表示創(chuàng)建一個新的映射。
  • key_size: key的字節(jié)數(shù)
  • value_size:value的字節(jié)數(shù)
  • max_entries:最大的鍵值對數(shù)量
  • map_flags:map創(chuàng)建行為的參數(shù),0表示不預(yù)先分配內(nèi)存
int bpf_create_map(bpf_map_type map_type, int key_size, int value_size, int max_entries, int map_flags);

如果創(chuàng)建成功,這個接口會返回一個指向這個map的文件描述符。如果創(chuàng)建失敗,將返回-1。失敗會有三種原因,我們可以通過errno來進行區(qū)分。

  1. 如果屬性無效,內(nèi)核將errnor變量設(shè)置為EINVAL;
  2. 如果用戶權(quán)限不夠,內(nèi)核將errno變量設(shè)置為EPERM
  3. 如果沒有足夠的內(nèi)存來保存映射的話,內(nèi)核將errno變量設(shè)置為ENOMEM;

Demo

#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < stdlib.h >
#include < unistd.h >

int main(int argc, char **argv) {
  //# create
  int fd = bpf_create_map(BPF_MAP_TYPE_HASH, sizeof(int), sizeof(int), 100, 0);
  if (fd < 0) {
    printf("Failed to create map: %d (%s)n", fd, strerror(errno));
    return -1;
  }
  printf("Create BPF map success!n");
}

我們在一開始提到的MakeFile文件之中添加如下信息即可編譯上述代碼:

create: map_create.c 
	clang -o create -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: create

最后運行編譯后的程序:

sudo ./create 
Create BPF map success!

BPF映射類型

在Demo之中我們使用到了BPF_MAP_TYPE_HASH這個map類型,其表示在內(nèi)核空間之中創(chuàng)建一個哈希表映射。除此之外,BPF還支持如下的Map類型:

  • BPF_MAP_TYPE_HASH: 哈希表映射,和我們熟知的哈希表是類似的。該映射可以使用任意大小的Key和Value,內(nèi)核會按照需求分配和釋放他們。當在哈希表映射上使用更新操作的時候,內(nèi)核會自動的更新元素。
  • BPF_MAP_TYPE_ARRAY:數(shù)據(jù)映射,在對數(shù)據(jù)初始化的時候,所有元素在內(nèi)存之中將預(yù)分配空間并且設(shè)置為0。數(shù)據(jù)映射的Key必須是4字節(jié)的,而且使用數(shù)組映射的一個缺點是映射之中的元素不能夠被刪除,這使得無法使數(shù)據(jù)變小。如果在數(shù)組上執(zhí)行刪除操作,那么用戶將得到一個EINVAL錯誤。
  • BPF_MAP_TYPE_PROG_ARRAY:程序數(shù)組映射,這種類型保存對BPF程序的引用(其他BPF程序的文件描述符),程序數(shù)據(jù)映射類型可以使用bpf_tail_call來執(zhí)行剛剛提到的尾部調(diào)用。
  • BPF_MAP_TYPE_PERF_EVENT_AYYAY:Perf事件數(shù)組映射,該映射將perf_events數(shù)據(jù)存儲在環(huán)形緩存區(qū),用于BPF程序和用戶空間程序進行實時通信。其可以將內(nèi)核跟蹤工具發(fā)出的事件轉(zhuǎn)發(fā)給用戶空間程序,使很多可觀測工具的基礎(chǔ)。
  • BPF_MAP_TYPE_PERCUP_HASH:哈希表映射的改進版本,我們可以將此哈希表分配給單個獨立的CPU(每個CPU都有自己獨立的哈希表),而不是多個CPU共享一個哈希表。
  • BPF_MAP_TYPE_PRECPU_ARRAY:數(shù)據(jù)映射的改進版本,也是每個CPU擁有自己獨立的數(shù)組。
  • BPF_MAP_TYPE_STACK_TRACE:棧跟蹤信息,可以結(jié)合內(nèi)核開發(fā)人員添加的幫助函數(shù)bpf_get_stackid將棧跟蹤信息寫入到該映射。

持久化BPF MAP

BPF映射的基本特征使基于文件描述符的,這意味著關(guān)閉文件描述符后,映射及其所保存的所有信息都會消失。這意味著我們無法獲取已經(jīng)結(jié)束的BPF程序保存在映射之中的信息,在Linux 內(nèi)核4.4 版本之后,引入了兩個新的系統(tǒng)調(diào)用,bpf_obj_pin用來固定(固定后不可更改)和bpf_obj_get獲取來自BPF虛擬文件系統(tǒng)的映射和BPF程序。

BPF虛擬文件系統(tǒng)的默認目錄使/sys/fs/bpf,如果Linux系統(tǒng)內(nèi)核不支持BPF,可以使用mount命令掛載此文件系統(tǒng):

mount -t bpf /sys/fs/bpf /sys/fs/bpf

BPF固定的系統(tǒng)調(diào)用為bpf_obj_pin,其函數(shù)原型如下:

  • file_fd:表示map的文件描述符
  • file_path:要固定到的文件路徑
int bpf_obj_pin(int file_fd, const char* file_path)

Demo

#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < string.h >
#include < unistd.h >
#include < stdlib.h >
#include 

static const char *file_path = "/sys/fs/bpf/my_hash";

int main(int argc, char **argv) {
  //# create
  int fd = bpf_create_map(BPF_MAP_TYPE_HASH, sizeof(int), sizeof(int), 100, 0);
  if (fd < 0) {
    printf("Failed to create map: %d (%s)n", fd, strerror(errno));
    return -1;
  }

  int pinned = bpf_obj_pin(fd, file_path);
  if (pinned < 0) {
    printf("Failed to pin map to the file system: %d (%s)n", pinned,
           strerror(errno));
    return -1;
  }

  return 0;
}

我們在一開始提到的MakeFile文件之中添加如下信息即可編譯上述代碼:

save: map_save.c 
	clang -o save -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: save

之后,我們可以查看這個目錄查看是否固定成功了:

sudo ls  /sys/fs/bpf/
my_hash

對BPF 元素進行CRUD

Update

我們可以使用bpf_map_update_elem系統(tǒng)調(diào)用去插入元素到剛創(chuàng)建的map之中。內(nèi)核程序需要從bpf/bpf_helpers.h文件加載此函數(shù),而用戶空間程序則需要從tools/lib/bpf/bpf.h文件加載,所以內(nèi)核程序訪問的函數(shù)簽名和用戶空間之不同的。當然,訪問的行為也是不同的:內(nèi)核程序可以原子的執(zhí)行更新操作,用戶空間則需要發(fā)送消息到內(nèi)核,之后先復(fù)制值,然后再進行更新映射。這意味著更新操作不是原子性的。

下面使這個函數(shù)的函數(shù)原型,如果執(zhí)行成功,該函數(shù)返回0;如果失敗,則將返回復(fù)數(shù)并且把失敗的原因?qū)懭肴肿兞縠rrno之中。

  • file_fd:map的文件描述符表示
  • key:指向key的指針
  • value:指向value的指針
  • type:表示更新映射的方式。
  1. 如果傳入0,表示元素存在則更新,不存在則創(chuàng)建;
  2. 如果傳入1,表示在元素不存在的時候,內(nèi)核創(chuàng)建元素
  3. 如果傳入2,表示元素存在的時候,內(nèi)核更新元素
int bpf_map_update_elem(int file_fd, void* key, void* value, int type);

Demo

#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < stdlib.h >
#include < string.h >
#include < unistd.h >

#include "bpf.h"

extern char *optarg;

extern int optind;

extern int opterr;

extern int optopt;

static const char *file_path = "/sys/fs/bpf/my_hash";

int main(int argc, char **argv) {
  char ch;
  int key;
  int value;
  while ((ch = getopt(argc, argv, "k:v:")) != -1) {
    switch (ch) {
      case 'k':
        printf("set key: %sn", optarg);
        key = atoi(optarg);
        break;
      case 'v':
        printf("set value: %sn", optarg);
        value = atoi(optarg);
        break;
    }
  }

  int fd, added, pinned;

  //# open
  fd = bpf_obj_get(file_path);
  if (fd < 0) {
    printf("Failed to fetch the map: %d (%s)n", fd, strerror(errno));
    return -1;
  }

  added = bpf_map_update_elem(fd, &key, &value, BPF_ANY);
  if (added < 0) {
    printf("Failed to update map: %d (%s)n", added, strerror(errno));
    return -1;
  }

  return 0;
}

我們在一開始提到的MakeFile文件之中添加如下信息即可編譯上述代碼:

update: map_update.c 
	clang -o update -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: update

最后運行編譯后的程序:

sudo ./update -k 1 -v 9
set key: 1
set value: 9

Fetch

當新元素寫入到map之后,我們可以使用bpf_map_lookup_elem系統(tǒng)調(diào)用來讀取map之中的元素,其函數(shù)原型如下:

下面使這個函數(shù)的函數(shù)原型,如果執(zhí)行成功,該函數(shù)返回0;如果失敗,則將返回復(fù)數(shù)并且把失敗的原因?qū)懭肴肿兞縠rrno之中。

  • file_fd:map的文件描述符表示
  • key:指向key的指針
  • value:指向value的指針
int bpf_map_lookp_elem(int file_fd, void* key, void* value);

Demo

#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < string.h >
#include "bpf.h"
#include < unistd.h >
#include < stdlib.h >
#include "bpf.h"

extern char* optarg;

extern int optind;

extern int opterr;

extern int optopt;

static const char *file_path = "/sys/fs/bpf/my_hash";

int main(int argc, char **argv) {
    char ch;
  int key;
  int value;
  while ((ch = getopt(argc, argv, "k:v:")) != -1)
  {
    switch (ch)
    {
    case 'k':
      key = atoi(optarg);
      break;
    }
  }

  int fd, result;
  fd = bpf_obj_get(file_path);
  if (fd < 0) {
    printf("Failed to fetch the map: %d (%s)n", fd, strerror(errno));
    return -1;
  }

  result = bpf_map_lookup_elem(fd, &key, &value);
  if (result < 0) {
    printf("Failed to read value from the map: %d (%s)n", result,
           strerror(errno));
    return -1;
  }

  printf("Value read from the key %d: '%d'n", key,value);
  return 0;
}

我們在一開始提到的MakeFile文件之中添加如下信息即可編譯上述代碼:

fetch: map_fetch.c 
	clang -o fetch -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: fetch

最后運行編譯后的程序:

sudo ./update -k 1 -v 9
set key: 1
set value: 9

Delete

當新元素寫入到map之后,我們可以使用bpf_map_delete_elem系統(tǒng)調(diào)用來刪除map之中的元素,其函數(shù)原型如下:

下面使這個函數(shù)的函數(shù)原型,如果執(zhí)行成功,該函數(shù)返回0;如果失敗,則將返回復(fù)數(shù)并且把失敗的原因?qū)懭肴肿兞縠rrno之中。

  • file_fd:map的文件描述符表示
  • key:指向key的指針
int bpf_map_delete_elem(int file_fd, void* key);

Demo

#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < string.h >
#include "bpf.h"
#include < unistd.h >
#include < stdlib.h >
#include "bpf.h"

extern char* optarg;

extern int optind;

extern int opterr;

extern int optopt;

static const char *file_path = "/sys/fs/bpf/my_hash";

int main(int argc, char **argv) {
    char ch;
  int key;
  int value;
  while ((ch = getopt(argc, argv, "k:v:")) != -1)
  {
    switch (ch)
    {
    case 'k':
      key = atoi(optarg);
      break;
    }
  }

  int fd,result;

  fd = bpf_obj_get(file_path);
  if (fd < 0) {
    printf("Failed to fetch the map: %d (%s)n", fd, strerror(errno));
    return -1;
  }

  key = 1;
  result = bpf_map_delete_elem(fd, &key);
  if (result < 0) {
    printf("Failed to delete value from the map: %d (%s)n", fd,
           strerror(errno));
    return -1;
  }

  printf("delte key:%d success!n", key);
  return 0;
}

我們在一開始提到的MakeFile文件之中添加如下信息即可編譯上述代碼:

delete: map_delete.c 
	clang -o delete -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: delete

最后運行編譯后的程序:

sudo ./delete -k 1
delte key:1 success!

Iter

假設(shè)我們寫入了很多元素到map之后,我們可以使用bpf_map_get_next_key系統(tǒng)調(diào)用來遍歷map之中的元素,其函數(shù)原型如下:

下面使這個函數(shù)的函數(shù)原型,如果執(zhí)行成功,該函數(shù)返回0;如果失敗,則將返回復(fù)數(shù)并且把失敗的原因?qū)懭肴肿兞縠rrno之中。

  • file_fd:map的文件描述符表示
  • key:指向key的指針
  • next_key:指向下個key的指針
int bpf_map_get_next_key(int file_fd, void* key, void* next_key);

Demo

#include < errno.h >
#include < linux/bpf.h >
#include < stdio.h >
#include < stdlib.h >
#include < string.h >
#include < unistd.h >

#include "bpf.h"

extern char *optarg;

extern int optind;

extern int opterr;

extern int optopt;

static const char *file_path = "/sys/fs/bpf/my_hash";

int main(int argc, char **argv) {
  int fd, value, result;

  fd = bpf_obj_get(file_path);
  if (fd < 0) {
    printf("Failed to fetch the map: %d (%s)n", fd, strerror(errno));
    return -1;
  }

  int start_key = -1;
  int next_key;
  while (bpf_map_get_next_key(fd, &start_key, &next_key) == 0) {
    start_key = next_key;
    printf("Key read from the map: '%d'n", next_key);
  }

  return 0;
}

Demo

iter: map_iter.c 
	clang -o iter -lelf $(INCLUDE_PATH) $(LIBRARY_PATH) $(BPFSO) $?
...
build: iter

最后運行編譯后的程序:

[ik@localhost chapter-3]$ sudo ./iter 
Key read from the map: '2'
Key read from the map: '8'
Key read from the map: '10'
Key read from the map: '5'
Key read from the map: '6'
Key read from the map: '3'
Key read from the map: '4'
Key read from the map: '9'
Key read from the map: '7'
Key read from the map: '11'

BPF跟蹤

跟蹤使一種為了進行分析和調(diào)試工作的數(shù)據(jù)收集行為,通過有效的利用BPF來使得我們可以以盡可能小的代價來訪問Linux內(nèi)核和應(yīng)用程序的任何信息。

探針

探針使一種探測程序,其會傳遞程序執(zhí)行時環(huán)境的相關(guān)信息,我們通過BPF探針收集系統(tǒng)之中的數(shù)據(jù)以方便我們后續(xù)進行探索分析。在BPF之中,主要會提供以下四種探針:

  1. 內(nèi)核探針:提供對內(nèi)核中內(nèi)部組件的動態(tài)訪問能力;
  2. 跟蹤點:提供對內(nèi)核中內(nèi)部組件的靜態(tài)訪問能力;
  3. 用戶空間探針:提供對用戶空間運行的程序的動態(tài)訪問能力;
  4. 用戶靜態(tài)定義跟蹤點:提供對用戶空間運行的程序的靜態(tài)訪問能力;

內(nèi)核探針

內(nèi)核探針提供了對幾乎任何內(nèi)核指令設(shè)置動態(tài)標記和中斷的能力。當內(nèi)核到達這些標志的時候,附加到探針的代碼就會被執(zhí)行,之后內(nèi)核將恢復(fù)到正常運行的模式。

注:這里指的注意的是,內(nèi)核探針沒有穩(wěn)定的應(yīng)用程序二進制接口(ABI),其會隨著內(nèi)核版本的演進而更改。

內(nèi)核探針可以分為兩類:

  • kprobes:kprobes允許在執(zhí)行任何內(nèi)核指令之前插入BPF程序。我們首先可以指定一個要探測的程序,之后當內(nèi)核執(zhí)行到設(shè)置探針的指令的時候,它將會從代碼處開始執(zhí)行我們編寫的BPF程序,在BPF程序執(zhí)行完之后繼續(xù)執(zhí)行原有的程序。

下面的例子是個簡單的Demo:

我們首先在python之中插入C代碼,其主要工作就是獲取當前內(nèi)核正在運行的命令名稱。之后使用python 的BPF加載此C代碼,并將此代碼和execve系統(tǒng)調(diào)用相關(guān)聯(lián)起來,也就是當execve系統(tǒng)調(diào)用被觸發(fā)之后,會先去執(zhí)行我們指定的用戶代碼。

from bcc import BPF

bpf_source = """
#include < uapi/linux/ptrace.h >

int do_sys_execve(struct pt_regs *ctx) {
  char comm[16];
  //獲得當前內(nèi)核正在運行的命令名
  bpf_get_current_comm(&comm, sizeof(comm));
  bpf_trace_printk("executing program: %s
", comm);
  return 0;
}
"""

# 加載BPF程序到內(nèi)核
bpf = BPF(text=bpf_source)
# 將BPF程序和execve系統(tǒng)調(diào)用關(guān)聯(lián)
execve_function = bpf.get_syscall_fnname("execve")
# 由于不同內(nèi)核版本提供的ABI不同,bcc工具包提供了獲得函數(shù)簽名的接口
bpf.attach_kprobe(event=execve_function, fn_name="do_sys_execve")
# 輸出跟蹤日志
bpf.trace_print()

上面的代碼最終執(zhí)行效果如下:

sudo python3 example.py 
b'            node-35560   [005] d..31 26011.217315: bpf_trace_printk: executing program: node'
b''
b'              sh-35562   [007] d..31 26011.219055: bpf_trace_printk: executing program: sh'
b''
b'            node-35563   [006] d..31 26011.221001: bpf_trace_printk: executing program: node'
b''
b'              sh-35563   [007] d..31 26011.222363: bpf_trace_printk: executing program: sh'
b''
b'            node-35564   [007] d..31 26011.233929: bpf_trace_printk: executing program: node'
b''
b'              sh-35564   [007] d..31 26011.235267: bpf_trace_printk: executing program: sh'
b''
b'     cpuUsage.sh-35565   [002] d..31 26011.236663: bpf_trace_printk: executing program: cpuUsage.sh'

kretprobes:kretprobes是在內(nèi)核指令有返回值時插入BPF程序
下面是一個使用kretprobs的例子,其會在execve系統(tǒng)調(diào)用之后開始執(zhí)行我們的指定的BPF程序。

from bcc import BPF

bpf_source = """
#include < uapi/linux/ptrace.h >

int ret_sys_execve(struct pt_regs *ctx) {
  int return_value;
  char comm[16];
  bpf_get_current_comm(&comm, sizeof(comm));
  //獲取返回值 PT_REGS_RC 獲取上下文之中寄存器的返回值
  return_value = PT_REGS_RC(ctx);

  bpf_trace_printk("program: %s, return: %d
", comm, return_value);
  return 0;
}
"""

bpf = BPF(text=bpf_source)
execve_function = bpf.get_syscall_fnname("execve")
bpf.attach_kretprobe(event=execve_function, fn_name="ret_sys_execve")
bpf.trace_print()

上面的程序執(zhí)行效果如下:

sudo python3 example.py 
b'              sh-35856   [000] d..31 26366.112370: bpf_trace_printk: program: sh, return: 0'
b''
b'           which-35858   [007] d..31 26366.114034: bpf_trace_printk: program: which, return: 0'
b''
b'              sh-35859   [007] d..31 26366.116329: bpf_trace_printk: program: sh, return: 0'
b''
b'              ps-35859   [007] d..31 26366.117328: bpf_trace_printk: program: ps, return: 0'
b''
b'              sh-35860   [007] d..31 26366.129422: bpf_trace_printk: program: sh, return: 0'
b''
b'     cpuUsage.sh-35860   [007] d..31 26366.130579: bpf_trace_printk: program: cpuUsage.sh, return: 0'

跟蹤點

跟蹤點時內(nèi)核代碼的靜態(tài)標記,可用于將代碼附加在運行的內(nèi)核中。跟蹤點和kprobes的主要區(qū)別在于跟蹤點由內(nèi)核開發(fā)人員在內(nèi)核中編寫和修改。由于其是靜態(tài)存在的,所以跟蹤點的ABI會更加的穩(wěn)定。我們可以查看/sys/kernel/debug/tracing/events目錄下的內(nèi)容,這里是系統(tǒng)之中所有可用的跟蹤點,在筆者的電腦上,跟蹤點如下:

[ik@localhost kretprobes]$ sudo ls /sys/kernel/debug/tracing/events
alarmtimer        devlink       gvt             iomap        mdio       nmi             rcu      sunrpc    workqueue
avc               dma_fence     hda             iommu        mei        oom             regmap   swiotlb   writeback
block             drm           hda_controller  io_uring     migrate    page_isolation  resctrl  syscalls  x86_fpu
bpf_test_run      enable        hda_intel       irq          mmap       pagemap         rpm      task      xdp
bpf_trace         error_report  header_event    irq_matrix   mmap_lock  page_pool       rseq     tcp       xen
bridge            exceptions    header_page     irq_vectors  mmc        percpu          rtc      thermal   xfs
cfg80211          fib           huge_memory     kmem         module     power           sched    timer     xhci-hcd
cgroup            fib6          hwmon           kvm          mptcp      printk          scsi     tlb
clk               filelock      hyperv          kvmmmu       msr        pwm             signal   ucsi
compaction        filemap       i2c             kyber        napi       qdisc           skb      udp
context_tracking  fs_dax        i915            libata       neigh      random          smbus    vmscan
cpuhp             ftrace        initcall        mac80211     net        ras             sock     vsyscall
dev               gpio          intel_iommu     mce          netlink    raw_syscalls    spi      wbt

這里我們可以看到由兩個額外的文件:

  • enable:表示允許啟用和禁用BPF子系統(tǒng)的所有跟蹤點。如果該文件的內(nèi)容為0,表示禁用跟蹤點;如果該文件的內(nèi)容為1,表示跟蹤點已啟用

我們可以用以下命令去啟用跟蹤點:

  • filter:用來編寫表達式,定義內(nèi)核跟蹤子系統(tǒng)過濾事件。

下面是一個使用BPF程序跟蹤系統(tǒng)加載其他BPF程序的Demo。我們定義我們的BPF程序,其會在執(zhí)行到跟蹤點的時候,執(zhí)行我們的BPF程序,這里我們指定了跟蹤點為net_dev_xmit,其會在執(zhí)行這個跟蹤點的之后,執(zhí)行我們的BPF程序trace_net_dev_xmit

from bcc import BPF

bpf_source = """
int trace_net_dev_xmit(struct pt_regs *ctx) {
  char comm[16];
  bpf_get_current_comm(&comm, sizeof(comm));
  bpf_trace_printk("%s is loading a BPF program", comm);
  return 0;
}
"""

bpf = BPF(text = bpf_source)
bpf.attach_tracepoint(tp = "net:net_dev_xmit", fn_name = "trace_net_dev_xmit")
bpf.trace_print()

注:這里的net表示跟蹤子系統(tǒng),net_dev_xmit 才是具體的跟蹤點

上面的函數(shù)執(zhí)行結(jié)果如下:

sudo python3 example.py 
b'            node-34494   [005] d..31 27609.874798: bpf_trace_printk: node is loading a BPF program'
b'            sshd-34382   [007] d..31 27609.874937: bpf_trace_printk: sshd is loading a BPF program'
b'            node-34494   [005] d..31 27609.876698: bpf_trace_printk: node is loading a BPF program'
b'            sshd-34382   [007] d..31 27609.876769: bpf_trace_printk: sshd is loading a BPF program'
b' irq/129-iwlwifi-847     [006] d.s61 27609.877073: bpf_trace_printk: irq/129-iwlwifi is loading a BPF program'
b' irq/129-iwlwifi-847     [006] d.s61 27609.877078: bpf_trace_printk: irq/129-iwlwifi is loading a BPF program'
b' irq/129-iwlwifi-847     [006] d.s61 27609.877079: bpf_trace_printk: irq/129-iwlwifi is loading a BPF program'

用戶空間探針

用戶空間探針允許也在用戶空間運行的程序中設(shè)置動態(tài)標志。它們等同于內(nèi)核探針,用戶空間探針是運行在用戶空間的監(jiān)測程序。當我們定義uprobe的時候,內(nèi)核會在附加的指令上創(chuàng)建陷阱。當程序執(zhí)行到該指令的時候,內(nèi)核將觸發(fā)事件以回調(diào)函數(shù)的方式調(diào)用探針函數(shù)。

跟內(nèi)核探針類似,用戶探針也分為兩類:

  • uprobes:其是內(nèi)核在程序特定指令執(zhí)行之前插入該指令集的鉤子。下面是個示例代碼:
package main

import "fmt"

func main()  {
    fmt.Println("Hello, BPF")
}
from bcc import BPF

bpf_source = """
int trace_go_main(struct pt_regs *ctx) {
  u64 pid = bpf_get_current_pid_tgid();
  bpf_trace_printk("New main process running with PID: %d
", pid);
  return 0;
}
"""

bpf = BPF(text = bpf_source)
bpf.attach_uprobe(name = "./main", sym = "main.main", fn_name = "trace_go_main")
bpf.trace_print()

在這里我們用go語言寫了個程序用于打印"Hello, BPF",之后我們指定BPF程序,其會在執(zhí)行main函數(shù)的時候打印一個提示信息。下面是這個程序執(zhí)行的示例:

sudo python3 example.py 
b'            main-38680   [004] d..31 31093.647465: bpf_trace_printk: New main process running with PID: 38680'
b''
  • uretprobes:uretprobes是kretprobes并行探針,用于用戶空間程序,其會將BPF程序附加到指令返回值上,允許通過BPF代碼從寄存器中訪問返回值,下面是這個程序示例:
from bcc import BPF

bpf_source = """
BPF_HASH(cache, u64, u64);

int trace_start_time(struct pt_regs *ctx) {
  u64 pid = bpf_get_current_pid_tgid();
  u64 start_time_ns = bpf_ktime_get_ns();
  cache.update(&pid, &start_time_ns);
  return 0;
}
"""

bpf_source += """
int print_duration(struct pt_regs *ctx) {
  u64 pid = bpf_get_current_pid_tgid();
  u64 *start_time_ns = cache.lookup(&pid);
  if (start_time_ns == 0) {
    return 0;
  }
  u64 duration_ns = bpf_ktime_get_ns() - *start_time_ns;
  bpf_trace_printk("Function call duration: %d
", duration_ns);
  return 0;
}
"""

bpf = BPF(text = bpf_source)
bpf.attach_uprobe(name = "./main", sym = "main.main", fn_name = "trace_start_time")
bpf.attach_uretprobe(name = "./main", sym = "main.main", fn_name = "print_duration")
bpf.trace_print()

上面的程序會統(tǒng)計man函數(shù)開始和結(jié)束的時間,其會將開始時間放到BPF映射之中,然后再結(jié)束的時候從映射之中讀取這個一開始的值,得到程序的執(zhí)行時間:

sudo python3 example.py 
b'            main-39066   [005] d..31 31384.927590: bpf_trace_printk: Function call duration: 52049'
b''

FQA

Q:使用python作為bcc前端的時候遇到報錯:“ Option ‘openmp-ir-builder-optimistic-attributes’ registered more than once!”

A: 重新編譯一遍BCC,使用如下命令:

# 編譯bcc模塊
git clone https://github.com/iovisor/bcc.git
mkdir bcc/build; cd bcc/build
sudo cmake ..
sudo make
sudo make install

# 解決上述報錯
sudo cmake -DENABLE_LLVM_SHARED=1 ..
sudo make
sudo make install

# 編譯python3依賴
sudo cmake -DPYTHON_CMD=python3 .. # build python3 binding
pushd src/python/
sudo make
sudo make install
popd
聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 應(yīng)用程序
    +關(guān)注

    關(guān)注

    37

    文章

    3268

    瀏覽量

    57715
  • BPF
    BPF
    +關(guān)注

    關(guān)注

    0

    文章

    25

    瀏覽量

    4006
  • 解釋器
    +關(guān)注

    關(guān)注

    0

    文章

    103

    瀏覽量

    6513
收藏 人收藏

    評論

    相關(guān)推薦

    ucosII內(nèi)核詳解

    ucosII內(nèi)核詳解
    發(fā)表于 08-16 20:11

    ucosII內(nèi)核詳解

    [url=]ucosII內(nèi)核詳解[/url]
    發(fā)表于 01-29 14:06

    關(guān)于 eBPF 安全可觀測性,你需要知道的那些事兒

    非常復(fù)雜的話題,牽一發(fā)而動全身,防御機制、加固配置、漏洞利用等等挑戰(zhàn)性的技術(shù)。在進行加固防御的過程中,又會產(chǎn)生性能或者系統(tǒng)穩(wěn)定性相關(guān)的影響。從 eBPF + LSM 的角度可以更加可視化、數(shù)據(jù)豐富的觀測內(nèi)核
    發(fā)表于 09-08 15:31

    TCP-IP詳解卷2_BPF:BSD 分組過濾程序

    TCP-IP詳解卷2 BPF:BSD 分組過濾程序,學(xué)習TCP很好的資料。歡迎下載。
    發(fā)表于 05-09 14:13 ?0次下載

    Linux內(nèi)核GPIO操作函數(shù)的詳解分析

    本文檔的主要內(nèi)容詳細介紹的是Linux內(nèi)核GPIO操作函數(shù)的詳解分析免費下載。
    發(fā)表于 01-22 16:58 ?28次下載

    保證BPF程序安全的BPF驗證器介紹

    1. 前言 我們可以使用BPF對Linux內(nèi)核進行跟蹤,收集我們想要的內(nèi)核數(shù)據(jù),從而對Linux中的程序進行分析和調(diào)試。與其它的跟蹤技術(shù)相比,使用B
    的頭像 發(fā)表于 05-03 11:27 ?1888次閱讀
    保證<b class='flag-5'>BPF</b>程序安全的<b class='flag-5'>BPF</b>驗證器介紹

    教你們?nèi)绾问褂胑BPF追蹤LINUX內(nèi)核

    1. 前言 我們可以使用BPF對Linux內(nèi)核進行跟蹤,收集我們想要的內(nèi)核數(shù)據(jù),從而對Linux中的程序進行分析和調(diào)試。與其它的跟蹤技術(shù)相比,使用B
    的頭像 發(fā)表于 04-20 11:26 ?2371次閱讀
    教你們?nèi)绾问褂胑BPF追蹤LINUX<b class='flag-5'>內(nèi)核</b>

    如何使用BPF對Linux內(nèi)核進行實時跟蹤

    我們可以使用BPF對Linux內(nèi)核進行跟蹤,收集我們想要的內(nèi)核數(shù)據(jù),從而對Linux中的程序進行分析和調(diào)試。與其它的跟蹤技術(shù)相比,使用BPF
    的頭像 發(fā)表于 06-30 17:28 ?2304次閱讀
    如何使用<b class='flag-5'>BPF</b>對Linux<b class='flag-5'>內(nèi)核</b>進行實時跟蹤

    BPF系統(tǒng)調(diào)用與Tracing類型的BPF程序

    既然是提供向內(nèi)核注入代碼的技術(shù),那么安全問題肯定是重中之重。平時防范他人通過漏洞向內(nèi)核中注入代碼,這下子專門開了一個口子不是大開方便之門。所以內(nèi)核指定了很多的規(guī)則來限制
    的頭像 發(fā)表于 03-14 16:42 ?3545次閱讀

    BPF ring buffer解決的問題及背后的設(shè)計

    文章介紹了 BPF ring buffer 解決的問題及背后的設(shè)計,并給出了一些代碼示例和內(nèi)核 patch 鏈接,深度和廣度兼?zhèn)?,是學(xué)習 ring buffer 的極佳參考。
    的頭像 發(fā)表于 05-17 09:37 ?2295次閱讀

    BPF編程的環(huán)境搭建方法

    本來想寫一篇“BPF 深度分析、環(huán)境搭建與案例分析”的文章,但是篇幅過長,于是先把BPF編程的環(huán)境搭建先放出來。接下來的文章將對BPF深度分析(包括BPF虛擬機、
    的頭像 發(fā)表于 10-14 17:02 ?2009次閱讀
    <b class='flag-5'>BPF</b>編程的環(huán)境搭建方法

    BPF內(nèi)核編程提供了一個新的參考模型

    這個新的編程環(huán)境混合使用了 C語言擴展以及運行時環(huán)境的組合實現(xiàn)的,這個運行時環(huán)境包含了 Clang、用戶空間的 BPF 加載器庫(libbpf)和內(nèi)核中的 BPF 子系統(tǒng)。
    的頭像 發(fā)表于 10-19 11:27 ?1147次閱讀

    Linux內(nèi)核觀測技術(shù)eBPF中文入門指南

    eBPF(extened Berkeley Packet Filter)是一種內(nèi)核技術(shù),它允許開發(fā)人員在不修改內(nèi)核代碼的情況下運行特定的功能。eBPF 的概念源自于 Berkeley Packet Filter(BPF),后者是
    的頭像 發(fā)表于 02-08 09:45 ?2205次閱讀

    BPF如何在Unix內(nèi)核實現(xiàn)網(wǎng)絡(luò)數(shù)據(jù)包過濾

    BPF發(fā)展到現(xiàn)在名稱升級為eBPF:「extended Berkeley Packet Filter」。它演進成為了一套通用執(zhí)行引擎,提供可基于系統(tǒng)或程序事件高效安全執(zhí)行特定代碼的通用能力,通用能力的使用者不再局限于內(nèi)核開發(fā)者。
    發(fā)表于 06-11 15:24 ?1133次閱讀
    <b class='flag-5'>BPF</b>如何在Unix<b class='flag-5'>內(nèi)核</b>實現(xiàn)網(wǎng)絡(luò)數(shù)據(jù)包過濾

    Linux內(nèi)核革命性技術(shù)BPF的前世今生

    從指令集角度,BPF 起初的架構(gòu)比較簡單,只有一個32位寬度累加器A,一個32位寬度寄存器X,以及16x32bit 數(shù)組內(nèi)存空間。但BPF 實現(xiàn)了加載、存儲、跳轉(zhuǎn)、運算四類指令。
    發(fā)表于 07-26 12:28 ?2012次閱讀
    Linux<b class='flag-5'>內(nèi)核</b>革命性<b class='flag-5'>技術(shù)</b>之<b class='flag-5'>BPF</b>的前世今生