一開始有一種技術,叫做BPF。 我們看著她
粗略地說,BPF 允許您在 Linux 核心空間中運行任意用戶提供的程式碼,並且新架構非常成功,以至於我們還需要十幾篇文章來描述它的所有應用程式。 (開發人員唯一做得不好的事情是創建了一個像樣的徽標,正如您在下面的效能程式碼中看到的那樣。)
本文介紹了 BPF 虛擬機器的結構、使用 BPF 的核心介面、開發工具,以及現有功能的簡要概述,即我們將來深入研究 BPF 的實際應用所需的一切。
文章摘要
bpf(2)
.
Пишем программы BPF с помощью libbpf
.libbpf
。 我們將建立一個基本的 BPF 應用程式框架,我們將在後續範例中使用它。
BPF架構簡介
在我們開始考慮 BPF 架構之前,我們將最後一次(哦)提到
新的 BPF 是為了應對 64 位元機器、雲端服務的普遍存在以及對創建 SDN 工具日益增長的需求而開發的(S軟體-d精緻的 n網路工作)。 新的BPF 由核心網路工程師開發,作為經典BPF 的改進替代品,六個月後,它在追蹤Linux 系統這一艱鉅的任務中找到了應用,而現在,在它出現六年後,我們需要一篇完整的下一篇文章來了解列出不同類型的程序。
有趣的圖片
從本質上講,BPF 是一個沙箱虛擬機,可讓您在核心空間中執行「任意」程式碼,而不會影響安全性。 BPF 程式在使用者空間中創建,載入到核心中,並連接到某個事件來源。 例如,事件可以是傳送封包至網路介面資料包、啟動某些核心功能等。 在套件的情況下,BPF 程式將有權存取套件的資料和元資料(用於讀取,可能還可以寫入,取決於程式的類型);在運行核心函數的情況下,函數,包括指向內核記憶體的指標等。
讓我們仔細看看這個過程。 首先,我們來談談與經典 BPF 的第一個區別,經典 BPF 的程式是用彙編語言編寫的。 在新版本中,架構得到了擴展,以便可以用高級語言編寫程序,當然主要是用 C 語言。為此,開發了 llvm 的後端,它允許您為 BPF 架構生成字節碼。
BPF 架構的設計部分是為了在現代機器上有效運作。 為了在實踐中實現這一點,BPF 字節碼一旦載入到核心中,就會使用稱為 JIT 編譯器的元件轉換為本機程式碼(J烏斯 In T我)。 接下來,如果您還記得的話,在經典的 BPF 中,程式被載入到核心中並以原子方式附加到事件來源 - 在單一系統呼叫的上下文中。 在新架構中,這分兩個階段發生 - 首先,使用系統呼叫將程式碼載入到核心中 bpf(2)
然後,稍後,透過根據程式類型而變更的其他機制,程式附加到事件來源。
說到這裡,讀者可能會有一個疑問:這可能嗎? 這樣的程式碼執行安全如何保證呢? 我們透過載入BPF程式的階段(稱為verifier)來確保執行安全(這個階段在英文中稱為verifier,我將繼續使用英文單字):
Verifier 是一個靜態分析器,可確保程式不會破壞核心的正常運作。 順便說一句,這並不意味著程式不能幹擾系統的操作 - BPF 程序,根據類型,可以讀取和重寫內核記憶體的部分、函數的返回值、修剪、追加、重寫甚至轉發網路封包。 驗證器保證運行 BPF 程式不會使核心崩潰,並且根據規則具有寫入存取權限的程式(例如傳出資料包的資料)將無法覆蓋資料包外部的核心記憶體。 在熟悉 BPF 的所有其他組件之後,我們將在相應部分更詳細地了解驗證器。
那麼到目前為止我們學到了什麼? 用戶用C編寫程序,使用系統呼叫將其載入到核心中 bpf(2)
,由驗證者檢查並翻譯為本機字節碼。 然後,同一個或另一個使用者將程式連接到事件來源並開始執行。 由於多種原因,有必要將啟動和連接分開。 首先,執行驗證程式相對昂貴,並且多次下載相同的程式會浪費電腦時間。 其次,程式具體如何連接取決於其類型,一年前開發的「通用」介面可能不適合新類型的程式。 (雖然現在架構越來越成熟,但是有一個想法是在層面上統一這個接口 libbpf
.)
細心的讀者可能會注意到我們的圖片還沒完成。 事實上,以上所有內容並不能解釋為什麼 BPF 與經典 BPF 相比從根本上改變了情況。 顯著擴展適用範圍的兩項創新是使用共享記憶體和核心輔助函數的能力。 在 BPF 中,共享記憶體是使用所謂的映射(具有特定 API 的共享資料結構)來實現的。 他們之所以得到這個名字,可能是因為第一種出現的映射類型是哈希表。 然後出現了數組、本地端(每個 CPU)哈希表和本地數組、搜尋樹、包含 BPF 程式指標的映射等等。 現在我們感興趣的是,BPF 程式現在能夠在呼叫之間保留狀態並與其他程式和用戶空間共享它。
使用系統呼叫從使用者進程存取地圖 bpf(2)
,以及使用輔助函數在核心中執行的 BPF 程式。 此外,助手的存在不僅可以處理映射,還可以存取其他核心功能。 例如,BPF 程式可以使用輔助函數將封包轉送到其他介面、產生效能事件、存取核心結構等。
總之,BPF 提供了將任意(即經過驗證者測試的)使用者程式碼載入到核心空間的能力。 該程式碼可以在呼叫之間保存狀態並與用戶空間交換數據,並且還可以存取此類程式允許的核心子系統。
這已經和核心模組提供的能力類似了,相較之下BPF有一些優勢(當然,你只能比較類似的應用,例如係統追蹤——你不能用BPF寫任意驅動程式)。 你可以注意到較低的入門門檻(一些使用BPF 的實用程式不需要用戶具備內核程式設計技能,或一般的程式設計技能)、運行時安全性(對於那些在編寫時沒有破壞系統的人請在評論中舉手)或測試模組),原子性 - 重新加載模組時存在停機時間,並且 BPF 子系統確保不會錯過任何事件(公平地說,並非所有類型的 BPF 程式都是如此)。
這種能力的存在使得BPF成為擴展核心的通用工具,這在實踐中得到了證實:越來越多的新類型程式被添加到BPF中,越來越多的大公司在作戰伺服器上24×7使用BPF,越來越多新創公司在基於 BPF 的解決方案上建立自己的業務。 BPF 無所不在:防禦 DDoS 攻擊、建立 SDN(例如,為 kubernetes 實作網路)、作為主要係統追蹤工具和統計收集器、入侵偵測系統和沙箱系統等。
我們到這裡就完成了本文的概述部分,並更詳細地了解虛擬機器和 BPF 生態系統。
題外話:公用事業
為了能夠運行以下部分中的範例,您可能需要許多實用程序,至少 llvm
/clang
與 bpf 支援和 bpftool
。 在該部分
BPF虛擬機器暫存器和指令系統
BPF 的架構和命令系統的開發考慮到程式將用 C 語言編寫,並在載入到核心後翻譯為本機程式碼。 因此,在選擇暫存器的數量和命令集時,要著眼於數學意義上的現代機器能力的交集。 此外,對程式也施加了各種限制,例如,直到最近還無法編寫循環和子例程,指令數量限制為 4096 條(現在特權程式最多可以載入一百萬條指令)。
BPF 有 64 個用戶可存取的 XNUMX 位元暫存器 r0
- r10
和一個程式計數器。 登記 r10
包含一個幀指針並且是唯讀的。 程式可以在運行時存取 512 位元組的堆疊以及映射形式的無限量的共享記憶體。
BPF 程式可以執行一組特定的程式類型核心幫助程序,以及最近的常規函數。 每個被呼叫的函數最多可以接受五個參數,並在暫存器中傳遞 r1
- r5
,並將傳回值傳遞給 r0
。 確保從函數返回後,暫存器的內容 r6
- r9
不會改變。
為了高效率的程式翻譯,寄存器 r0
- r11
考慮到目前架構的 ABI 功能,所有受支援的架構都唯一對應到實際暫存器。 例如,對於 x86_64
暫存器 r1
- r5
,用於傳遞函數參數,顯示在 rdi
, rsi
, rdx
, rcx
, r8
,用於將參數傳遞給函數 x86_64
。 例如,左側的程式碼轉換為右側的程式碼,如下所示:
1: (b7) r1 = 1 mov $0x1,%rdi
2: (b7) r2 = 2 mov $0x2,%rsi
3: (b7) r3 = 3 mov $0x3,%rdx
4: (b7) r4 = 4 mov $0x4,%rcx
5: (b7) r5 = 5 mov $0x5,%r8
6: (85) call pc+1 callq 0x0000000000001ee8
註冊 r0
也用於傳回程式執行的結果,並在暫存器中 r1
程式被傳遞一個指向上下文的指標 - 根據程式的類型,這可能是一個結構體 struct xdp_md
struct __sk_buff
struct pt_regs
因此,我們有一組暫存器、核心助手、堆疊、上下文指標和映射形式的共享記憶體。 並不是說所有這些都是旅行中絕對必要的,但是...
讓我們繼續描述並討論處理這些物件的命令系統。 全部 (
這裡 Code
- 這是指令的編碼, Dst
/Src
分別是接收器和來源的編碼, Off
- 16 位元有符號縮進,以及 Imm
是一些指令中使用的 32 位元有符號整數(類似於 cBPF 常數 K)。 編碼 Code
有兩種類型之一:
指令類別 0、1、2、3 定義了使用記憶體的命令。 他們 BPF_LD
, BPF_LDX
, BPF_ST
, BPF_STX
, 分別。 4、7 班(BPF_ALU
, BPF_ALU64
)構成一組ALU指令。 5、6 班(BPF_JMP
, BPF_JMP32
) 包含跳躍指令。
研究BPF指令系統的進一步計劃如下:我們不會詳細列出所有指令及其參數,而是在本節中看幾個示例,從中可以清楚指令實際上是如何工作的以及如何手動反彙編BPF 的任何二進位檔案。 為了鞏固本文後面的內容,我們還將在有關驗證器、JIT 編譯器、經典 BPF 翻譯以及學習映射、呼叫函數等部分中遇到單獨的說明。
當我們談論單一指令時,我們會參考核心文件 bpf.h
bpf_common.h
例:在你的腦中拆解 BPF
讓我們來看一個編譯程式的例子 readelf-example.c
並查看生成的二進位。 我們將揭曉原創內容 readelf-example.c
下面,我們從二進位程式碼恢復其邏輯後:
$ clang -target bpf -c readelf-example.c -o readelf-example.o -O2
$ llvm-readelf -x .text readelf-example.o
Hex dump of section '.text':
0x00000000 b7000000 01000000 15010100 00000000 ................
0x00000010 b7000000 02000000 95000000 00000000 ................
輸出中的第一列 readelf
是一個縮進,因此我們的程式由四個命令組成:
Code Dst Src Off Imm
b7 0 0 0000 01000000
15 0 1 0100 00000000
b7 0 0 0000 02000000
95 0 0 0000 00000000
指令碼相等 b7
, 15
, b7
и 95
。 回想一下,最低有效的三位是指令類別。 在我們的例子中,所有指令的第四位為空,因此指令類別分別等於 7, 5, 7, 5。類別 7 是 BPF_ALU64
,5 是 BPF_JMP
。 對於這兩個類,指令格式是相同的(見上文),我們可以像這樣重寫我們的程式(同時我們將以人類形式重寫其餘的列):
Op S Class Dst Src Off Imm
b 0 ALU64 0 0 0 1
1 0 JMP 0 1 1 0
b 0 ALU64 0 0 0 2
9 0 JMP 0 0 0 0
手術 b
類 ALU64
- s
(source),那麼該值是從來源暫存器中取得的,如果像我們的例子一樣,它沒有被設置,那麼該值是從欄位中取得的 Imm
。 所以在第一條和第三條指令中我們執行操作 r0 = Imm
。 此外,JMP 1 類別操作是 S
為零,它將來源暫存器的值與欄位進行比較 Imm
。 如果值一致,則發生轉變 PC + Off
哪裡 PC
與往常一樣,包含下一指令的位址。 最後,JMP 9 類別操作是 BPF_EXIT
r0
。 讓我們在表中新增一個欄位:
Op S Class Dst Src Off Imm Disassm
MOV 0 ALU64 0 0 0 1 r0 = 1
JEQ 0 JMP 0 1 1 0 if (r1 == 0) goto pc+1
MOV 0 ALU64 0 0 0 2 r0 = 2
EXIT 0 JMP 0 0 0 0 exit
我們可以用更方便的形式重寫它:
r0 = 1
if (r1 == 0) goto END
r0 = 2
END:
exit
如果我們記得寄存器中的內容 r1
該程式從內核傳遞一個指向上下文的指針,並在寄存器中 r0
該值返回給內核,然後我們可以看到,如果指向上下文的指標為零,則返回 1,否則返回 - 2。讓我們透過查看原始程式碼來檢查我們是否正確:
$ cat readelf-example.c
int foo(void *ctx)
{
return ctx ? 2 : 1;
}
是的,這是一個毫無意義的程序,但它只是翻譯成四個簡單的指令。
異常範例:16 位元組指令
我們前面提到有些指令佔用的空間超過 64 位元。 例如,這適用於指令 lddw
(代碼= 0x18
= BPF_LD
BPF_DW
BPF_IMM
Imm
。 事實是這樣的 Imm
大小為 32,而一個雙字為 64 位,因此在一條 64 位元指令中將 64 位元立即數載入到暫存器中是行不通的。 為此,使用兩個相鄰指令將 64 位元值的第二部分儲存在欄位中 Imm
. 例子:
$ cat x64.c
long foo(void *ctx)
{
return 0x11223344aabbccdd;
}
$ clang -target bpf -c x64.c -o x64.o -O2
$ llvm-readelf -x .text x64.o
Hex dump of section '.text':
0x00000000 18000000 ddccbbaa 00000000 44332211 ............D3".
0x00000010 95000000 00000000 ........
二進位程式中只有兩條指令:
Binary Disassm
18000000 ddccbbaa 00000000 44332211 r0 = Imm[0]|Imm[1]
95000000 00000000 exit
我們將根據指示再次見面 lddw
,當我們談論搬遷和使用地圖時。
範例:使用標準工具拆卸 BPF
因此,我們已經學會了讀取 BPF 二進位程式碼,並準備好在必要時解析任何指令。 不過,值得一提的是,在實務上使用標準工具反組譯程式更加方便快捷,例如:
$ llvm-objdump -d x64.o
Disassembly of section .text:
0000000000000000 <foo>:
0: 18 00 00 00 dd cc bb aa 00 00 00 00 44 33 22 11 r0 = 1234605617868164317 ll
2: 95 00 00 00 00 00 00 00 exit
BPF物件的生命週期,bpffs檔案系統
(我首先從這裡了解到本小節中描述的一些細節
BPF 物件(程式和映射)是使用命令從使用者空間建立的 BPF_PROG_LOAD
и BPF_MAP_CREATE
系統調用 bpf(2)
,我們將在下一節中詳細討論這是如何發生的。 這將創建內核資料結構並為每個結構創建內核資料結構 refcount
(引用計數)設定為XNUMX,並且指向該物件的檔案描述符被傳回給使用者。 手柄關閉後 refcount
物件減一,當減到零時,物件被銷毀。
如果程式使用映射,那麼 refcount
這些地圖在載入程式後會加一,即它們的檔案描述符可以從用戶進程中關閉,並且仍然 refcount
不會變成零:
成功載入程式後,我們通常將其附加到某種事件產生器。 例如,我們可以將其放在網路介面上來處理傳入的封包或將其連接到某些 tracepoint
在核心。 此時,引用計數器也會加一,我們就可以在載入程式中關閉檔案描述子了。
如果我們現在關閉引導程式會發生什麼? 這取決於事件產生器(鉤子)的類型。 所有網路鉤子都會在載入器完成後存在,這些就是所謂的全域鉤子。 並且,例如,追蹤程式將在創建它們的進程終止後被釋放(因此稱為本地,從「本地到進程」)。 從技術上講,本地鉤子在用戶空間中始終有一個相應的檔案描述符,因此當進程關閉時也會關閉,但全域鉤子則不然。 在下圖中,我嘗試使用紅色叉來展示載入程式的終止在本地和全域鉤子的情況下如何影響物件的生命週期。
為什麼本地鉤子和全局鉤子之間存在區別? 在沒有用戶空間的情況下運行某些類型的網路程式是有意義的,例如,想像 DDoS 保護 - 引導程式編寫規則並將 BPF 程式連接到網路接口,之後引導程式可以自行終止。 另一方面,想像一下您在十分鐘內跪下編寫的調試跟踪程序 - 當它完成時,您希望系統中沒有留下任何垃圾,而本地掛鉤將確保這一點。
另一方面,假設您想要連接到內核中的追蹤點並收集多年的統計資料。 在這種情況下,您可能希望完成使用者部分並不時傳回統計資料。 bpf 檔案系統提供了這個機會。 它是一個僅記憶體中的偽文件系統,允許創建引用 BPF 物件的文件,從而增加 refcount
對象。 此後,載入器可以退出,並且它建立的物件將保持活動狀態。
在 bpffs 中建立引用 BPF 物件的檔案稱為「固定」(如以下短語所示:「進程可以固定 BPF 程式或映射」)。 為 BPF 物件建立文件物件不僅對於延長本地物件的壽命有意義,而且對於全域物件的可用性也有意義 - 回到全域 DDoS 防護程式的範例,我們希望能夠來看看統計資料時。
BPF檔案系統通常掛載在 /sys/fs/bpf
,但也可以本地安裝,例如,如下所示:
$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpoint
檔案系統名稱是使用指令建立的 BPF_OBJ_PIN
BPF 系統呼叫。 為了說明這一點,我們來看一個程序,編譯它,上傳它,然後將其固定到 bpffs
。 我們的程式沒有做任何有用的事情,我們只是提供程式碼,以便您可以重現該範例:
$ cat test.c
__attribute__((section("xdp"), used))
int test(void *ctx)
{
return 0;
}
char _license[] __attribute__((section("license"), used)) = "GPL";
讓我們編譯這個程式並創建檔案系統的本機副本 bpffs
:
$ clang -target bpf -c test.c -o test.o
$ mkdir bpf-mountpoint
$ sudo mount -t bpf none bpf-mountpoint
現在讓我們使用該實用程式下載我們的程序 bpftool
並查看附帶的系統調用 bpf(2)
(從 strace 輸出中刪除了一些不相關的行):
$ sudo strace -e bpf bpftool prog load ./test.o bpf-mountpoint/test
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, prog_name="test", ...}, 120) = 3
bpf(BPF_OBJ_PIN, {pathname="bpf-mountpoint/test", bpf_fd=3}, 120) = 0
這裡我們使用載入程序 BPF_PROG_LOAD
,從核心接收到一個檔案描述符 3
並使用命令 BPF_OBJ_PIN
將此文件描述符固定為文件 "bpf-mountpoint/test"
。 之後是引導程式 bpftool
工作完成了,但我們的程式仍然保留在核心中,儘管我們沒有將其附加到任何網路介面:
$ sudo bpftool prog | tail -3
783: xdp name test tag 5c8ba0cf164cb46c gpl
loaded_at 2020-05-05T13:27:08+0000 uid 0
xlated 24B jited 41B memlock 4096B
我們可以正常刪除文件對象 unlink(2)
之後相應的程序將被刪除:
$ sudo rm ./bpf-mountpoint/test
$ sudo bpftool prog show id 783
Error: get by id (783): No such file or directory
刪除對象
說到刪除對象,需要澄清的是,當我們將程式與鉤子(事件產生器)斷開連接後,不會有任何新事件觸發其啟動,但是程式的所有當前實例都將按正常順序完成。
某些類型的 BPF 程式可讓您即時替換程序,即提供序列原子性 replace = detach old program, attach new program
。 在這種情況下,舊版本程式的所有活動實例都將完成其工作,並且將從新程式建立新的事件處理程序,這裡的「原子性」意味著不會錯過任何一個事件。
將程式附加到事件來源
在本文中,我們不會單獨描述將程式連接到事件來源,因為在特定類型的程式的上下文中研究這一點是有意義的。 厘米。
使用 bpf 系統呼叫操作對象
BPF 計劃
所有 BPF 物件都是使用系統呼叫從使用者空間建立和管理的 bpf
,具有以下原型:
#include <linux/bpf.h>
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
這是團隊 cmd
是 type 的值之一 enum bpf_cmd
attr
— 指向特定程式參數的指標以及 size
— 根據指標的物件大小,即通常是這個 sizeof(*attr)
。 5.8核心中的系統調用 bpf
支援 34 種不同的指令,並且 union bpf_attr
佔用200行。 但我們不應該被這個嚇倒,因為我們將在幾篇文章的過程中熟悉命令和參數。
讓我們從團隊開始 BPF_PROG_LOAD
,它創建 BPF 程式 - 獲取一組 BPF 指令並將其載入到核心中。 載入的瞬間,驗證器啟動,然後JIT編譯器,成功執行後,將程式檔案描述子回傳給使用者。 我們在上一節中看到了他接下來會發生什麼
我們現在將編寫一個自訂程式來載入一個簡單的 BPF 程序,但首先我們需要決定要載入哪種程式 - 我們必須選擇 BPF_PROG_TYPE_XDP
,這將傳回值 XDP_PASS
(跳過所有包)。 在 BPF 彙編器中,它看起來非常簡單:
r0 = 2
exit
當我們決定之後 該 我們將上傳,我們可以告訴您我們將如何做到:
#define _GNU_SOURCE
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/bpf.h>
static inline __u64 ptr_to_u64(const void *ptr)
{
return (__u64) (unsigned long) ptr;
}
int main(void)
{
struct bpf_insn insns[] = {
{
.code = BPF_ALU64 | BPF_MOV | BPF_K,
.dst_reg = BPF_REG_0,
.imm = XDP_PASS
},
{
.code = BPF_JMP | BPF_EXIT
},
};
union bpf_attr attr = {
.prog_type = BPF_PROG_TYPE_XDP,
.insns = ptr_to_u64(insns),
.insn_cnt = sizeof(insns)/sizeof(insns[0]),
.license = ptr_to_u64("GPL"),
};
strncpy(attr.prog_name, "woo", sizeof(attr.prog_name));
syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr));
for ( ;; )
pause();
}
程式中有趣的事件從陣列的定義開始 insns
- 我們的 BPF 機器碼程式。 在這種情況下,BPF程式的每個指令都被打包到結構體中 bpf_insn
insns
符合指示 r0 = 2
, 第二 - exit
.
撤退。 核心定義了更方便的巨集來編寫機器碼,並使用核心頭文件 tools/include/linux/filter.h
我們可以寫
struct bpf_insn insns[] = {
BPF_MOV64_IMM(BPF_REG_0, XDP_PASS),
BPF_EXIT_INSN()
};
但由於用本機程式碼編寫 BPF 程式僅是在核心中編寫測試和有關 BPF 的文章所必需的,因此缺少這些巨集並不會真正使開發人員的生活變得複雜。
定義 BPF 程式後,我們繼續將其載入到核心中。 我們的極簡參數集 attr
包括程式類型、指令集和數量、所需的許可證和名稱 "woo"
,我們用它來在下載後在系統上找到我們的程式。 正如所承諾的,該程式使用系統呼叫載入到系統中 bpf
.
在程式結束時,我們最終陷入了模擬有效負載的無限循環。 沒有它,當系統呼叫返回給我們的檔案描述符關閉時,程式將被核心殺死 bpf
,並且我們不會在系統中看到它。
好了,我們已經準備好進行測試了。 讓我們在下面編譯並運行程序 strace
檢查一切是否正常運作:
$ clang -g -O2 simple-prog.c -o simple-prog
$ sudo strace ./simple-prog
execve("./simple-prog", ["./simple-prog"], 0x7ffc7b553480 /* 13 vars */) = 0
...
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, insn_cnt=2, insns=0x7ffe03c4ed50, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_V
ERSION(0, 0, 0), prog_flags=0, prog_name="woo", prog_ifindex=0, expected_attach_type=BPF_CGROUP_INET_INGRESS}, 72) = 3
pause(
一切都好, bpf(2)
返回句柄 3 給我們,我們進入了無限循環 pause()
。 讓我們嘗試在系統中找到我們的程式。 為此,我們將轉到另一個終端並使用該實用程序 bpftool
:
# bpftool prog | grep -A3 woo
390: xdp name woo tag 3b185187f1855c4c gpl
loaded_at 2020-08-31T24:66:44+0000 uid 0
xlated 16B jited 40B memlock 4096B
pids simple-prog(10381)
我們看到系統上已經載入了一個程序 woo
其全域 ID 為 390,目前正在進行中 simple-prog
有一個打開的文件描述符指向該程式(並且如果 simple-prog
將完成工作,然後 woo
會消失)。 正如預期的那樣,該計劃 woo
BPF 架構中的二進位程式碼佔用 16 個位元組(兩個指令),但在其本機形式 (x86_64) 中,它已經是 40 個位元組。 讓我們看看我們的程式的原始形式:
# bpftool prog dump xlated id 390
0: (b7) r0 = 2
1: (95) exit
沒有什麼驚喜。 現在讓我們來看看 JIT 編譯器產生的程式碼:
# bpftool prog dump jited id 390
bpf_prog_3b185187f1855c4c_woo:
0: nopl 0x0(%rax,%rax,1)
5: push %rbp
6: mov %rsp,%rbp
9: sub $0x0,%rsp
10: push %rbx
11: push %r13
13: push %r14
15: push %r15
17: pushq $0x0
19: mov $0x2,%eax
1e: pop %rbx
1f: pop %r15
21: pop %r14
23: pop %r13
25: pop %rbx
26: leaveq
27: retq
不太有效 exit(2)
,但平心而論,我們的程式太簡單了,對於不平凡的程式來說,JIT編譯器添加的序言和尾聲當然是需要的。
地圖
BPF 程式可以使用其他 BPF 程式和使用者空間中的程式都可以存取的結構化記憶體區域。 這些物件稱為映射,在本節中我們將展示如何使用系統呼叫來操作它們 bpf
.
讓我們立即說,映射的功能不僅限於存取共享記憶體。 有一些特殊用途的映射,例如包含指向 BPF 程式的指標或指向網路介面的指標、用於處理效能事件的映射等。 這裡我們不再談論它們,以免讀者感到困惑。 除此之外,我們忽略同步問題,因為這對我們的範例並不重要。 可用地圖類型的完整清單可以在以下位置找到: <linux/bpf.h>
BPF_MAP_TYPE_HASH
.
如果你用 C++ 建立一個哈希表,你會說 unordered_map<int,long> woo
,俄語中的意思是「我需要一張桌子 woo
大小不受限制,其鍵為類型 int
,值是類型 long
」 為了建立 BPF 雜湊表,我們需要做很多相同的事情,除了我們必須指定表的最大大小,並且我們需要指定它們的大小(以位元組為單位),而不是指定鍵和值的類型。 若要建立地圖,請使用命令 BPF_MAP_CREATE
系統調用 bpf
。 讓我們來看看創建地圖的或多或少的最小程式。 在上一個載入 BPF 程式的程式之後,這個程式對您來說應該很簡單:
$ cat simple-map.c
#define _GNU_SOURCE
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/bpf.h>
int main(void)
{
union bpf_attr attr = {
.map_type = BPF_MAP_TYPE_HASH,
.key_size = sizeof(int),
.value_size = sizeof(int),
.max_entries = 4,
};
strncpy(attr.map_name, "woo", sizeof(attr.map_name));
syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));
for ( ;; )
pause();
}
這裡我們定義一組參數 attr
,其中我們說「我需要一個包含鍵和大小值的哈希表 sizeof(int)
,其中我最多可以放入四個元素。” 建立BPF映射時,可以指定其他參數,例如與程式範例中相同的方式,我們將物件的名稱指定為 "woo"
.
讓我們編譯並運行該程式:
$ clang -g -O2 simple-map.c -o simple-map
$ sudo strace ./simple-map
execve("./simple-map", ["./simple-map"], 0x7ffd40a27070 /* 14 vars */) = 0
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_HASH, key_size=4, value_size=4, max_entries=4, map_name="woo", ...}, 72) = 3
pause(
這是系統調用 bpf(2)
傳回給我們描述符映射編號 3
然後程式按照預期等待系統呼叫中的進一步指令 pause(2)
.
現在讓我們將程式發送到後台或打開另一個終端機並使用實用程式查看我們的對象 bpftool
(我們可以透過名稱將我們的地圖與其他地圖區分開來):
$ sudo bpftool map
...
114: hash name woo flags 0x0
key 4B value 4B max_entries 4 memlock 4096B
...
數字 114 是我們物件的全域 ID。 系統上的任何程式都可以使用此 ID 使用以下命令開啟現有地圖 BPF_MAP_GET_FD_BY_ID
系統調用 bpf
.
現在我們可以使用我們的哈希表了。 讓我們來看看它的內容:
$ sudo bpftool map dump id 114
Found 0 elements
空的。 讓我們給它賦值 hash[1] = 1
:
$ sudo bpftool map update id 114 key 1 0 0 0 value 1 0 0 0
我們再看一下表格:
$ sudo bpftool map dump id 114
key: 01 00 00 00 value: 01 00 00 00
Found 1 element
萬歲! 我們設法添加了一個元素。 請注意,我們必須在位元組層級上工作才能做到這一點,因為 bptftool
不知道哈希表中的值是什麼類型。 (這些知識可以使用 BTF 轉移給她,但現在更多。)
bpftool到底是如何讀取、加入元素的? 讓我們看看幕後:
$ sudo strace -e bpf bpftool map dump id 114
bpf(BPF_MAP_GET_FD_BY_ID, {map_id=114, next_id=0, open_flags=0}, 120) = 3
bpf(BPF_MAP_GET_NEXT_KEY, {map_fd=3, key=NULL, next_key=0x55856ab65280}, 120) = 0
bpf(BPF_MAP_LOOKUP_ELEM, {map_fd=3, key=0x55856ab65280, value=0x55856ab652a0}, 120) = 0
key: 01 00 00 00 value: 01 00 00 00
bpf(BPF_MAP_GET_NEXT_KEY, {map_fd=3, key=0x55856ab65280, next_key=0x55856ab65280}, 120) = -1 ENOENT
首先,我們使用指令透過全域 ID 開啟地圖 BPF_MAP_GET_FD_BY_ID
и bpf(2)
返回描述符 3 給我們。進一步使用命令 BPF_MAP_GET_NEXT_KEY
我們透過傳遞找到了表中的第一個鍵 NULL
作為指向“上一個”鍵的指標。 如果我們有鑰匙我們可以做 BPF_MAP_LOOKUP_ELEM
它傳回一個值給指針 value
。 下一步是我們嘗試透過傳遞指向當前鍵的指標來尋找下一個元素,但我們的表僅包含一個元素和命令 BPF_MAP_GET_NEXT_KEY
回報 ENOENT
.
好吧,我們透過鍵1來改變值,假設我們的業務邏輯需要註冊 hash[1] = 2
:
$ sudo strace -e bpf bpftool map update id 114 key 1 0 0 0 value 2 0 0 0
bpf(BPF_MAP_GET_FD_BY_ID, {map_id=114, next_id=0, open_flags=0}, 120) = 3
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=3, key=0x55dcd72be260, value=0x55dcd72be280, flags=BPF_ANY}, 120) = 0
正如預期的那樣,非常簡單:命令 BPF_MAP_GET_FD_BY_ID
透過 ID 打開我們的地圖,然後命令 BPF_MAP_UPDATE_ELEM
覆蓋該元素。
因此,在從一個程式建立哈希表後,我們可以從另一個程式讀取和寫入其內容。 請注意,如果我們能夠從命令列執行此操作,那麼系統上的任何其他程式都可以執行此操作。 除了上述命令之外,為了處理用戶空間中的映射,
BPF_MAP_LOOKUP_ELEM
:透過鍵查找值BPF_MAP_UPDATE_ELEM
:更新/創造價值BPF_MAP_DELETE_ELEM
:刪除鑰匙BPF_MAP_GET_NEXT_KEY
:找到下一個(或第一個)鍵BPF_MAP_GET_NEXT_ID
:允許您瀏覽所有現有地圖,這就是它的工作原理bpftool map
BPF_MAP_GET_FD_BY_ID
:透過全域ID開啟現有地圖BPF_MAP_LOOKUP_AND_DELETE_ELEM
:原子地更新物件的值並傳回舊的值BPF_MAP_FREEZE
:使地圖在使用者空間中不可變(此操作無法撤銷)BPF_MAP_LOOKUP_BATCH
,BPF_MAP_LOOKUP_AND_DELETE_BATCH
,BPF_MAP_UPDATE_BATCH
,BPF_MAP_DELETE_BATCH
: 大規模行動。 例如,BPF_MAP_LOOKUP_AND_DELETE_BATCH
- 這是從地圖中讀取和重置所有值的唯一可靠方法
並非所有這些命令都適用於所有映射類型,但一般來說,從使用者空間處理其他類型的映射看起來與使用雜湊表完全相同。
為了順序起見,讓我們完成我們的哈希表實驗。 還記得我們創建了一個最多可以包含四個鍵的表嗎? 讓我們加入更多元素:
$ sudo bpftool map update id 114 key 2 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 3 0 0 0 value 1 0 0 0
$ sudo bpftool map update id 114 key 4 0 0 0 value 1 0 0 0
到目前為止,一切都很好:
$ sudo bpftool map dump id 114
key: 01 00 00 00 value: 01 00 00 00
key: 02 00 00 00 value: 01 00 00 00
key: 04 00 00 00 value: 01 00 00 00
key: 03 00 00 00 value: 01 00 00 00
Found 4 elements
讓我們嘗試再增加一個:
$ sudo bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
Error: update failed: Argument list too long
正如所料,我們沒有成功。 讓我們更詳細地看看該錯誤:
$ sudo strace -e bpf bpftool map update id 114 key 5 0 0 0 value 1 0 0 0
bpf(BPF_MAP_GET_FD_BY_ID, {map_id=114, next_id=0, open_flags=0}, 120) = 3
bpf(BPF_OBJ_GET_INFO_BY_FD, {info={bpf_fd=3, info_len=80, info=0x7ffe6c626da0}}, 120) = 0
bpf(BPF_MAP_UPDATE_ELEM, {map_fd=3, key=0x56049ded5260, value=0x56049ded5280, flags=BPF_ANY}, 120) = -1 E2BIG (Argument list too long)
Error: update failed: Argument list too long
+++ exited with 255 +++
一切都很好:正如預期的那樣,團隊 BPF_MAP_UPDATE_ELEM
嘗試創建新的第五個密鑰,但崩潰了 E2BIG
.
因此,我們可以建立和載入 BPF 程序,以及從使用者空間建立和管理對應。 現在我們應該看看如何使用 BPF 程式本身的對應。 我們可以用機器宏程式碼中難以閱讀的程式語言來討論這一點,但實際上現在是時候展示 BPF 程式實際上是如何編寫和維護的了 - 使用 libbpf
.
(對於對缺乏低階範例不滿意的讀者:我們將詳細分析使用使用建立的映射和輔助函數的程序 libbpf
並告訴您在指令層面會發生什麼。 致不滿意的讀者 非常,我們添加了
使用 libbpf 編寫 BPF 程式
使用機器碼編寫 BPF 程式只有第一次會很有趣,然後就會產生滿足感。 此時你需要將注意力轉向 llvm
,它有一個用於為 BPF 架構生成程式碼的後端,以及一個庫 libbpf
,它允許您編寫 BPF 應用程式的用戶端並載入使用生成的 BPF 程式的程式碼 llvm
/clang
.
事實上,正如我們將在本文和後續文章中看到的那樣, libbpf
沒有它(或類似的工具)可以做很多工作 - iproute2
, libbcc
, libbpf-go
等)不可能生存。 該專案的殺手級功能之一 libbpf
BPF CO-RE(編譯一次,到處運行) - 一個項目,允許您編寫可從一個內核移植到另一個內核的 BPF 程序,並且能夠在不同的 API 上運行(例如,當內核結構從版本更改時)到版本)。 為了能夠使用 CO-RE,您的核心必須使用 BTF 支援進行編譯(我們在 一節中描述如何執行此操作)
$ ls -lh /sys/kernel/btf/vmlinux
-r--r--r-- 1 root root 2.6M Jul 29 15:30 /sys/kernel/btf/vmlinux
該文件存儲有關內核中使用的所有數據類型的信息,並在我們所有的示例中使用 libbpf
。 我們將在下一篇文章中詳細討論 CO-RE,但在這篇文章中 - 只需使用以下命令為自己建立一個內核 CONFIG_DEBUG_INFO_BTF
.
文庫 libbpf
就在目錄中 tools/lib/bpf
核心及其開發是透過郵件列表進行的 [email protected]
。 然而,為了滿足核心之外的應用程式的需要,維護了一個單獨的儲存庫
在本節中,我們將了解如何建立一個使用 libbpf
,讓我們編寫幾個(或多或少沒有意義的)測試程式並詳細分析它是如何工作的。 這將使我們能夠在接下來的章節中更輕鬆地解釋 BPF 程式如何與映射、核心助手、BTF 等互動。
通常項目使用 libbpf
新增 GitHub 儲存庫作為 git 子模組,我們將執行相同的操作:
$ mkdir /tmp/libbpf-example
$ cd /tmp/libbpf-example/
$ git init-db
Initialized empty Git repository in /tmp/libbpf-example/.git/
$ git submodule add https://github.com/libbpf/libbpf.git
Cloning into '/tmp/libbpf-example/libbpf'...
remote: Enumerating objects: 200, done.
remote: Counting objects: 100% (200/200), done.
remote: Compressing objects: 100% (103/103), done.
remote: Total 3354 (delta 101), reused 118 (delta 79), pack-reused 3154
Receiving objects: 100% (3354/3354), 2.05 MiB | 10.22 MiB/s, done.
Resolving deltas: 100% (2176/2176), done.
即將 libbpf
很簡單:
$ cd libbpf/src
$ mkdir build
$ OBJDIR=build DESTDIR=root make -s install
$ find root
root
root/usr
root/usr/include
root/usr/include/bpf
root/usr/include/bpf/bpf_tracing.h
root/usr/include/bpf/xsk.h
root/usr/include/bpf/libbpf_common.h
root/usr/include/bpf/bpf_endian.h
root/usr/include/bpf/bpf_helpers.h
root/usr/include/bpf/btf.h
root/usr/include/bpf/bpf_helper_defs.h
root/usr/include/bpf/bpf.h
root/usr/include/bpf/libbpf_util.h
root/usr/include/bpf/libbpf.h
root/usr/include/bpf/bpf_core_read.h
root/usr/lib64
root/usr/lib64/libbpf.so.0.1.0
root/usr/lib64/libbpf.so.0
root/usr/lib64/libbpf.a
root/usr/lib64/libbpf.so
root/usr/lib64/pkgconfig
root/usr/lib64/pkgconfig/libbpf.pc
我們本節的下一個計劃如下:我們將編寫一個 BPF 程序,例如 BPF_PROG_TYPE_XDP
,與前面的範例相同,但在 C 中,我們使用以下命令編譯它 clang
,並編寫一個幫助程式將其載入到核心中。 在下面的部分中,我們將擴展 BPF 程式和助手程式的功能。
範例:使用 libbpf 建立成熟的應用程式
首先,我們使用該文件 /sys/kernel/btf/vmlinux
,上面提到過,並以頭文件的形式創建其等效項:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
該檔案將儲存我們核心中可用的所有資料結構,例如,這是核心中定義 IPv4 標頭的方式:
$ grep -A 12 'struct iphdr {' vmlinux.h
struct iphdr {
__u8 ihl: 4;
__u8 version: 4;
__u8 tos;
__be16 tot_len;
__be16 id;
__be16 frag_off;
__u8 ttl;
__u8 protocol;
__sum16 check;
__be32 saddr;
__be32 daddr;
};
現在我們將用 C 語言寫 BPF 程式:
$ cat xdp-simple.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
SEC("xdp/simple")
int simple(void *ctx)
{
return XDP_PASS;
}
char LICENSE[] SEC("license") = "GPL";
雖然我們的程式結果很簡單,但是我們仍然需要注意很多細節。 首先,我們包含的第一個頭檔是 vmlinux.h
,我們剛剛使用生成 bpftool btf dump
- 現在我們不需要安裝 kernel-headers 套件來了解核心結構。 以下頭檔來自庫 libbpf
。 現在我們只需要它來定義宏 SEC
,它將字元傳送到 ELF 目標檔案的相應部分。 我們的程式包含在該部分中 xdp/simple
,在斜線之前我們定義了程式類型 BPF - 這是使用的約定 libbpf
,根據節名稱,它將在啟動時替換正確的類型 bpf(2)
。 BPF 程式本身就是 C
- 非常簡單,由一行組成 return XDP_PASS
。 最後,單獨的一個部分 "license"
包含許可證的名稱。
我們可以使用 llvm/clang 編譯我們的程序,版本 >= 10.0.0,或者更好,更高(請參閱部分
$ clang --version
clang version 11.0.0 (https://github.com/llvm/llvm-project.git afc287e0abec710398465ee1f86237513f2b5091)
...
$ clang -O2 -g -c -target bpf -I libbpf/src/root/usr/include xdp-simple.bpf.c -o xdp-simple.bpf.o
有趣的功能包括:我們指出了目標架構 -target bpf
以及標題的路徑 libbpf
,我們最近安裝的。 另外,不要忘記 -O2
,如果沒有這個選項,您將來可能會遇到意外。 讓我們看看我們的程式碼,我們是否成功編寫了我們想要的程式?
$ llvm-objdump --section=xdp/simple --no-show-raw-insn -D xdp-simple.bpf.o
xdp-simple.bpf.o: file format elf64-bpf
Disassembly of section xdp/simple:
0000000000000000 <simple>:
0: r0 = 2
1: exit
是的,它起作用了! 現在,我們有了一個包含該程式的二進位文件,我們想要建立一個應用程式將其載入到核心中。 為此,圖書館 libbpf
為我們提供了兩種選擇 - 使用較低層級的 API 或較高層級的 API。 我們將採用第二種方式,因為我們希望以最少的努力學習如何編寫、載入和連接 BPF 程序,以便後續學習。
首先,我們需要使用相同的實用程式從程式的二進位檔案產生程式的“骨架” bpftool
——BPF 世界的瑞士刀(可以從字面上理解,因為 BPF 的創建者和維護者之一 Daniel Borkman 是瑞士人):
$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
在文件中 xdp-simple.skel.h
包含我們程式的二進位程式碼和用於管理的函數 - 載入、附加、刪除我們的物件。 在我們的簡單情況下,這看起來有點矯枉過正,但它也適用於目標文件包含許多BPF 程序和映射的情況,並且要加載這個巨大的ELF,我們只需要生成骨架並從我們的自定義應用程序中呼叫一兩個函數正在寫 現在我們繼續。
嚴格來說,我們的加載程序很簡單:
#include <err.h>
#include <unistd.h>
#include "xdp-simple.skel.h"
int main(int argc, char **argv)
{
struct xdp_simple_bpf *obj;
obj = xdp_simple_bpf__open_and_load();
if (!obj)
err(1, "failed to open and/or load BPF objectn");
pause();
xdp_simple_bpf__destroy(obj);
}
這裡 struct xdp_simple_bpf
文件中定義的 xdp-simple.skel.h
並描述我們的目標檔案:
struct xdp_simple_bpf {
struct bpf_object_skeleton *skeleton;
struct bpf_object *obj;
struct {
struct bpf_program *simple;
} progs;
struct {
struct bpf_link *simple;
} links;
};
我們可以在這裡看到低階 API 的痕跡:結構 struct bpf_program *simple
и struct bpf_link *simple
。 第一個結構具體描述了我們的程序,寫在 xdp/simple
,第二個描述了程式如何連接到事件來源。
功能 xdp_simple_bpf__open_and_load
,開啟一個ELF對象,解析它,創建所有結構和子結構(除了程式之外,ELF還包含其他部分——資料、只讀資料、偵錯資訊、許可證等),然後使用系統將其載入到核心中稱呼 bpf
,我們可以透過編譯並執行程式來檢查:
$ clang -O2 -I ./libbpf/src/root/usr/include/ xdp-simple.c -o xdp-simple ./libbpf/src/root/usr/lib64/libbpf.a -lelf -lz
$ sudo strace -e bpf ./xdp-simple
...
bpf(BPF_BTF_LOAD, 0x7ffdb8fd9670, 120) = 3
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, insn_cnt=2, insns=0xdfd580, license="GPL", log_level=0, log_size=0, log_buf=NULL, kern_version=KERNEL_VERSION(5, 8, 0), prog_flags=0, prog_name="simple", prog_ifindex=0, expected_attach_type=0x25 /* BPF_??? */, ...}, 120) = 4
現在讓我們看看我們的程式使用 bpftool
。 讓我們找到她的ID:
# bpftool p | grep -A4 simple
463: xdp name simple tag 3b185187f1855c4c gpl
loaded_at 2020-08-01T01:59:49+0000 uid 0
xlated 16B jited 40B memlock 4096B
btf_id 185
pids xdp-simple(16498)
和轉儲(我們使用命令的縮寫形式 bpftool prog dump xlated
):
# bpftool p d x id 463
int simple(void *ctx):
; return XDP_PASS;
0: (b7) r0 = 2
1: (95) exit
新鮮玩意! 程式列印了我們的 C 原始檔的區塊。這是由庫完成的 libbpf
,它在二進位檔案中找到了調試部分,將其編譯成 BTF 對象,然後使用將其載入到核心中 BPF_BTF_LOAD
,然後在使用命令載入程式時指定生成的檔案描述符 BPG_PROG_LOAD
.
核心助手
BPF 程式可以執行「外部」函數—核心助手。 這些輔助函數允許 BPF 程式存取核心結構、管理映射以及與「現實世界」通訊 - 創建效能事件、控制硬體(例如,重定向資料包)等。
範例:bpf_get_smp_processor_id
在「透過範例學習」範式的框架內,讓我們考慮其中一個輔助函數, bpf_get_smp_processor_id()
, kernel/bpf/helpers.c
。 它會傳回呼叫它的 BPF 程式正在執行的處理器的編號。 但我們對它的語義並不感興趣,而是對它的實現只需要一行這一事實感興趣:
BPF_CALL_0(bpf_get_smp_processor_id)
{
return smp_processor_id();
}
BPF 輔助函數定義與 Linux 系統呼叫定義類似。 例如,這裡定義了一個沒有參數的函數。 (一個有三個參數的函數是使用巨集定義的 BPF_CALL_3
。 參數的最大數量為五個。)但是,這只是定義的第一部分。 第二部分是定義類型結構 struct bpf_func_proto
,其中包含驗證者理解的輔助函數的描述:
const struct bpf_func_proto bpf_get_smp_processor_id_proto = {
.func = bpf_get_smp_processor_id,
.gpl_only = false,
.ret_type = RET_INTEGER,
};
註冊輔助函數
為了讓特定類型的 BPF 程式使用此函數,它們必須註冊它,例如類型 BPF_PROG_TYPE_XDP
內核中定義了一個函數 xdp_func_proto
,它根據輔助函數 ID 確定 XDP 是否支援該函數。 我們的函數是
static const struct bpf_func_proto *
xdp_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
{
switch (func_id) {
...
case BPF_FUNC_get_smp_processor_id:
return &bpf_get_smp_processor_id_proto;
...
}
}
新的 BPF 程式類型在文件中“定義” include/linux/bpf_types.h
BPF_PROG_TYPE
。 用引號定義是因為它是邏輯定義,而在 C 語言術語中,一整套具體結構的定義出現在其他地方。 特別是在文件中 kernel/bpf/verifier.c
文件中的所有定義 bpf_types.h
用於建立結構體數組 bpf_verifier_ops[]
:
static const struct bpf_verifier_ops *const bpf_verifier_ops[] = {
#define BPF_PROG_TYPE(_id, _name, prog_ctx_type, kern_ctx_type)
[_id] = & _name ## _verifier_ops,
#include <linux/bpf_types.h>
#undef BPF_PROG_TYPE
};
即對於每種類型的BPF程序,都定義了一個指向該類型的資料結構的指針 struct bpf_verifier_ops
,它是用值初始化的 _name ## _verifier_ops
, IE。, xdp_verifier_ops
為 xdp
。 結構 xdp_verifier_ops
net/core/filter.c
如下所示:
const struct bpf_verifier_ops xdp_verifier_ops = {
.get_func_proto = xdp_func_proto,
.is_valid_access = xdp_is_valid_access,
.convert_ctx_access = xdp_convert_ctx_access,
.gen_prologue = bpf_noop_prologue,
};
這裡我們看到我們熟悉的函數 xdp_func_proto
,每次遇到挑戰時都會執行驗證程序 一些 BPF 程式中的函數,請參閱 verifier.c
讓我們看看假設的 BPF 程式如何使用函數 bpf_get_smp_processor_id
。 為此,我們重寫了上一節的程序,如下所示:
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
SEC("xdp/simple")
int simple(void *ctx)
{
if (bpf_get_smp_processor_id() != 0)
return XDP_DROP;
return XDP_PASS;
}
char LICENSE[] SEC("license") = "GPL";
符號 bpf_get_smp_processor_id
<bpf/bpf_helper_defs.h>
圖書館 libbpf
作為
static u32 (*bpf_get_smp_processor_id)(void) = (void *) 8;
那是, bpf_get_smp_processor_id
是一個函數指針,其值為8,其中8是值 BPF_FUNC_get_smp_processor_id
類型 enum bpf_fun_id
,它是在文件中為我們定義的 vmlinux.h
(文件 bpf_helper_defs.h
核心中的數字是由腳本產生的,因此“魔術”數字是可以的)。 此函數不帶參數並傳回類型的值 __u32
。 當我們在程式中運行它時, clang
產生一條指令 BPF_CALL
“正確的種類” 讓我們編譯一下程式並查看該部分 xdp/simple
:
$ clang -O2 -g -c -target bpf -I libbpf/src/root/usr/include xdp-simple.bpf.c -o xdp-simple.bpf.o
$ llvm-objdump -D --section=xdp/simple xdp-simple.bpf.o
xdp-simple.bpf.o: file format elf64-bpf
Disassembly of section xdp/simple:
0000000000000000 <simple>:
0: 85 00 00 00 08 00 00 00 call 8
1: bf 01 00 00 00 00 00 00 r1 = r0
2: 67 01 00 00 20 00 00 00 r1 <<= 32
3: 77 01 00 00 20 00 00 00 r1 >>= 32
4: b7 00 00 00 02 00 00 00 r0 = 2
5: 15 01 01 00 00 00 00 00 if r1 == 0 goto +1 <LBB0_2>
6: b7 00 00 00 01 00 00 00 r0 = 1
0000000000000038 <LBB0_2>:
7: 95 00 00 00 00 00 00 00 exit
在第一行我們看到說明 call
, 範圍 IMM
等於 8,並且 SRC_REG
- 零。 根據驗證者使用的 ABI 協議,這是對第八個輔助函數的呼叫。 一旦啟動,邏輯就很簡單。 從暫存器回傳值 r0
複製到 r1
在第 2,3 行,它被轉換為類型 u32
— 高 32 位元被清除。 在第 4,5,6,7 行我們回傳 2 (XDP_PASS
) 或 1 (XDP_DROP
) 取決於第 0 行的輔助函數是否傳回零值或非零值。
讓我們測試一下自己:加載程式並查看輸出 bpftool prog dump xlated
:
$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
$ clang -O2 -g -I ./libbpf/src/root/usr/include/ -o xdp-simple xdp-simple.c ./libbpf/src/root/usr/lib64/libbpf.a -lelf -lz
$ sudo ./xdp-simple &
[2] 10914
$ sudo bpftool p | grep simple
523: xdp name simple tag 44c38a10c657e1b0 gpl
pids xdp-simple(10915)
$ sudo bpftool p d x id 523
int simple(void *ctx):
; if (bpf_get_smp_processor_id() != 0)
0: (85) call bpf_get_smp_processor_id#114128
1: (bf) r1 = r0
2: (67) r1 <<= 32
3: (77) r1 >>= 32
4: (b7) r0 = 2
; }
5: (15) if r1 == 0x0 goto pc+1
6: (b7) r0 = 1
7: (95) exit
好的,驗證程式找到了正確的核心幫助程式。
範例:傳遞參數並最終運行程式!
所有運行級輔助函數都有一個原型
u64 fn(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5)
輔助函數的參數在暫存器中傳遞 r1
- r5
,並且該值被返回到暫存器中 r0
。 沒有任何函數需要超過五個參數,並且預計將來不會添加對它們的支援。
讓我們來看看新的核心助手以及 BPF 如何傳遞參數。 讓我們重寫一下 xdp-simple.bpf.c
如下(其餘行沒有改變):
SEC("xdp/simple")
int simple(void *ctx)
{
bpf_printk("running on CPU%un", bpf_get_smp_processor_id());
return XDP_PASS;
}
我們的程式列印它正在運行的CPU的編號。 我們來編譯一下,看看程式碼:
$ llvm-objdump -D --section=xdp/simple --no-show-raw-insn xdp-simple.bpf.o
0000000000000000 <simple>:
0: r1 = 10
1: *(u16 *)(r10 - 8) = r1
2: r1 = 8441246879787806319 ll
4: *(u64 *)(r10 - 16) = r1
5: r1 = 2334956330918245746 ll
7: *(u64 *)(r10 - 24) = r1
8: call 8
9: r1 = r10
10: r1 += -24
11: r2 = 18
12: r3 = r0
13: call 6
14: r0 = 2
15: exit
在第 0-7 行我們寫入字串 running on CPU%un
,然後在第 8 行我們運行熟悉的一個 bpf_get_smp_processor_id
。 在第 9-12 行,我們準備輔助參數 bpf_printk
- 暫存器 r1
, r2
, r3
。 為什麼是三個而不是兩個? 因為 bpf_printk
- bpf_trace_printk
,需要傳遞格式字串的大小。
現在讓我們來新增幾行 xdp-simple.c
這樣我們的程式就可以連接到該介面了 lo
並真正開始了!
$ cat xdp-simple.c
#include <linux/if_link.h>
#include <err.h>
#include <unistd.h>
#include "xdp-simple.skel.h"
int main(int argc, char **argv)
{
__u32 flags = XDP_FLAGS_SKB_MODE;
struct xdp_simple_bpf *obj;
obj = xdp_simple_bpf__open_and_load();
if (!obj)
err(1, "failed to open and/or load BPF objectn");
bpf_set_link_xdp_fd(1, -1, flags);
bpf_set_link_xdp_fd(1, bpf_program__fd(obj->progs.simple), flags);
cleanup:
xdp_simple_bpf__destroy(obj);
}
這裡我們使用函數 bpf_set_link_xdp_fd
,它將 XDP 類型的 BPF 程式連接到網路介面。 我們硬編碼了介面編號 lo
,始終為 1。我們運行該函數兩次,首先將舊程式分離(如果已附加)。 請注意,現在我們不需要挑戰 pause
或無限循環:我們的載入程式將退出,但 BPF 程式不會被終止,因為它已連接到事件來源。 下載並連接成功後,每個到達的網路資料包都會啟動該程序 lo
.
我們下載程式看看介面 lo
:
$ sudo ./xdp-simple
$ sudo bpftool p | grep simple
669: xdp name simple tag 4fca62e77ccb43d6 gpl
$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
prog/xdp id 669
我們下載的程式的ID為669,我們在介面上看到相同的ID lo
。 我們將寄幾個包裹到 127.0.0.1
(請求+回覆):
$ ping -c1 localhost
現在讓我們來看看調試虛擬文件的內容 /sys/kernel/debug/tracing/trace_pipe
,其中 bpf_printk
寫下他的留言:
# cat /sys/kernel/debug/tracing/trace_pipe
ping-13937 [000] d.s1 442015.377014: bpf_trace_printk: running on CPU0
ping-13937 [000] d.s1 442015.377027: bpf_trace_printk: running on CPU0
發現兩個包裹 lo
並在 CPU0 上進行處理——我們的第一個成熟的無意義 BPF 程式成功了!
值得一提的是 bpf_printk
它寫入調試文件並非毫無意義:這不是在生產中使用的最成功的幫助程序,但我們的目標是展示一些簡單的東西。
從 BPF 程式存取地圖
範例:使用 BPF 程式中的映射
在前面的部分中,我們學習瞭如何從用戶空間建立和使用映射,現在讓我們來看看核心部分。 像往常一樣,讓我們從一個例子開始。 讓我們重寫我們的程式 xdp-simple.bpf.c
如下所示:
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, 8);
__type(key, u32);
__type(value, u64);
} woo SEC(".maps");
SEC("xdp/simple")
int simple(void *ctx)
{
u32 key = bpf_get_smp_processor_id();
u32 *val;
val = bpf_map_lookup_elem(&woo, &key);
if (!val)
return XDP_ABORTED;
*val += 1;
return XDP_PASS;
}
char LICENSE[] SEC("license") = "GPL";
在程式的開頭我們加入了一個地圖定義 woo
:這是一個 8 元素數組,儲存如下值 u64
(在 C 中,我們將這樣的陣列定義為 u64 woo[8]
)。 在一個節目中 "xdp/simple"
我們將目前處理器編號放入變數中 key
然後使用輔助函數 bpf_map_lookup_element
我們得到一個指向數組中相應條目的指針,我們將其加一。 翻譯成俄語:我們計算有關 CPU 處理傳入資料包的統計資訊。 讓我們嘗試運行該程式:
$ clang -O2 -g -c -target bpf -I libbpf/src/root/usr/include xdp-simple.bpf.c -o xdp-simple.bpf.o
$ bpftool gen skeleton xdp-simple.bpf.o > xdp-simple.skel.h
$ clang -O2 -g -I ./libbpf/src/root/usr/include/ -o xdp-simple xdp-simple.c ./libbpf/src/root/usr/lib64/libbpf.a -lelf -lz
$ sudo ./xdp-simple
讓我們檢查一下她是否已連接 lo
並發送一些資料包:
$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
prog/xdp id 108
$ for s in `seq 234`; do sudo ping -f -c 100 127.0.0.1 >/dev/null 2>&1; done
現在讓我們來看看數組的內容:
$ sudo bpftool map dump name woo
[
{ "key": 0, "value": 0 },
{ "key": 1, "value": 400 },
{ "key": 2, "value": 0 },
{ "key": 3, "value": 0 },
{ "key": 4, "value": 0 },
{ "key": 5, "value": 0 },
{ "key": 6, "value": 0 },
{ "key": 7, "value": 46400 }
]
幾乎所有進程都在 CPU7 上處理。 這對我們來說並不重要,最主要的是程式可以運行,我們了解如何從 BPF 程式存取地圖 - 使用 хелперов bpf_mp_*
神秘指數
因此,我們可以使用以下呼叫從 BPF 程式存取地圖
val = bpf_map_lookup_elem(&woo, &key);
輔助函數的樣子
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)
但我們正在傳遞一個指針 &woo
到一個未命名的結構 struct { ... }
...
如果我們查看程式彙編器,我們會看到該值 &woo
實際上並未定義(第 4 行):
llvm-objdump -D --section xdp/simple xdp-simple.bpf.o
xdp-simple.bpf.o: file format elf64-bpf
Disassembly of section xdp/simple:
0000000000000000 <simple>:
0: 85 00 00 00 08 00 00 00 call 8
1: 63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0
2: bf a2 00 00 00 00 00 00 r2 = r10
3: 07 02 00 00 fc ff ff ff r2 += -4
4: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
6: 85 00 00 00 01 00 00 00 call 1
...
並包含在搬遷中:
$ llvm-readelf -r xdp-simple.bpf.o | head -4
Relocation section '.relxdp/simple' at offset 0xe18 contains 1 entries:
Offset Info Type Symbol's Value Symbol's Name
0000000000000020 0000002700000001 R_BPF_64_64 0000000000000000 woo
但是如果我們查看已經載入的程序,我們會看到一個指向正確映射的指標(第 4 行):
$ sudo bpftool prog dump x name simple
int simple(void *ctx):
0: (85) call bpf_get_smp_processor_id#114128
1: (63) *(u32 *)(r10 -4) = r0
2: (bf) r2 = r10
3: (07) r2 += -4
4: (18) r1 = map[id:64]
...
因此,我們可以得出結論,在啟動載入程式時,連結到 &woo
被帶有圖書館的東西所取代 libbpf
。 首先我們來看看輸出 strace
:
$ sudo strace -e bpf ./xdp-simple
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_ARRAY, key_size=4, value_size=8, max_entries=8, map_name="woo", ...}, 120) = 4
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, prog_name="simple", ...}, 120) = 5
我們看到 libbpf
創建了一張地圖 woo
然後下載我們的程式 simple
。 讓我們仔細看看如何載入程式:
- 稱呼
xdp_simple_bpf__open_and_load
從文件xdp-simple.skel.h
- 什麼導致
xdp_simple_bpf__load
從文件xdp-simple.skel.h
- 什麼導致
bpf_object__load_skeleton
從文件libbpf/src/libbpf.c
- 什麼導致
bpf_object__load_xattr
的libbpf/src/libbpf.c
除此之外,最後一個函數將調用 bpf_object__create_maps
,它會建立或開啟現有的映射,將它們轉換為檔案描述符。 (這是我們看到的 BPF_MAP_CREATE
在輸出中 strace
.) 接下來呼叫該函數 bpf_object__relocate
我們對她感興趣,因為我們記得我們所看到的 woo
在重定位表中。 探索一下,我們最終發現自己身處函數之中 bpf_program__relocate
, 哪個
case RELO_LD64:
insn[0].src_reg = BPF_PSEUDO_MAP_FD;
insn[0].imm = obj->maps[relo->map_idx].fd;
break;
所以我們接受我們的指示
18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
並將其中的來源暫存器替換為 BPF_PSEUDO_MAP_FD
,以及映射的檔案描述符的第一個 IMM,如果它等於,例如, 0xdeadbeef
,那麼結果我們會收到指令
18 11 00 00 ef eb ad de 00 00 00 00 00 00 00 00 r1 = 0 ll
這就是映射資訊如何傳輸到特定載入的 BPF 程式的方式。 在這種情況下,可以使用以下命令建立地圖 BPF_MAP_CREATE
,並使用 ID 打開 BPF_MAP_GET_FD_BY_ID
.
使用時總計 libbpf
演算法如下:
- 在編譯期間,會在重定位表中建立記錄以連結到地圖
libbpf
開啟 ELF 物件簿,尋找所有使用的對應並為它們建立檔案描述符- 文件描述符作為指令的一部分載入到核心中
LD64
正如你可以想像的那樣,還會有更多的事情發生,我們將不得不研究核心。 幸運的是,我們有線索——我們已經寫下了它的意思 BPF_PSEUDO_MAP_FD
進入源登記冊,我們可以將其埋葬,這將引導我們前往萬聖之聖地— kernel/bpf/verifier.c
,其中具有獨特名稱的函數將文件描述符替換為類型結構的地址 struct bpf_map
:
static int replace_map_fd_with_map_ptr(struct bpf_verifier_env *env) {
...
f = fdget(insn[0].imm);
map = __bpf_map_get(f);
if (insn->src_reg == BPF_PSEUDO_MAP_FD) {
addr = (unsigned long)map;
}
insn[0].imm = (u32)addr;
insn[1].imm = addr >> 32;
(完整程式碼可以找到
- 載入程式時,驗證器檢查映射的正確使用並寫入對應結構的位址
struct bpf_map
使用下載 ELF 二進位檔案時 libbpf
還有更多內容,但我們將在其他文章中討論。
不使用 libbpf 載入程式和地圖
正如所承諾的,這裡是一個示例,供那些想知道如何創建和加載使用地圖的程式而無需幫助的讀者使用 libbpf
。 當您在無法建立依賴項的環境中工作、無法保存每一點或編寫類似這樣的程式時,這會很有用 ply
為了更容易遵循邏輯,我們將為此目的重寫我們的範例 xdp-simple
。 本例中討論的程式的完整且稍微擴展的程式碼可以在以下位置找到
我們的應用程式的邏輯如下:
- 建立類型映射
BPF_MAP_TYPE_ARRAY
使用命令BPF_MAP_CREATE
, - 建立一個使用該地圖的程序,
- 將程式連接到接口
lo
,
這翻譯成人類
int main(void)
{
int map_fd, prog_fd;
map_fd = map_create();
if (map_fd < 0)
err(1, "bpf: BPF_MAP_CREATE");
prog_fd = prog_load(map_fd);
if (prog_fd < 0)
err(1, "bpf: BPF_PROG_LOAD");
xdp_attach(1, prog_fd);
}
這裡 map_create
以與我們在第一個關於系統呼叫的範例中相同的方式建立映射 bpf
- 「內核,請為我製作一個由 8 個元素組成的陣列形式的新地圖,例如 __u64
並將文件描述符還給我」:
static int map_create()
{
union bpf_attr attr;
memset(&attr, 0, sizeof(attr));
attr.map_type = BPF_MAP_TYPE_ARRAY,
attr.key_size = sizeof(__u32),
attr.value_size = sizeof(__u64),
attr.max_entries = 8,
strncpy(attr.map_name, "woo", sizeof(attr.map_name));
return syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));
}
該程式也很容易載入:
static int prog_load(int map_fd)
{
union bpf_attr attr;
struct bpf_insn insns[] = {
...
};
memset(&attr, 0, sizeof(attr));
attr.prog_type = BPF_PROG_TYPE_XDP;
attr.insns = ptr_to_u64(insns);
attr.insn_cnt = sizeof(insns)/sizeof(insns[0]);
attr.license = ptr_to_u64("GPL");
strncpy(attr.prog_name, "woo", sizeof(attr.prog_name));
return syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr));
}
棘手的部分 prog_load
是我們的 BPF 程式作為結構體數組的定義 struct bpf_insn insns[]
。 但由於我們使用的是 C 語言的程序,所以我們可以做一些欺騙:
$ llvm-objdump -D --section xdp/simple xdp-simple.bpf.o
0000000000000000 <simple>:
0: 85 00 00 00 08 00 00 00 call 8
1: 63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0
2: bf a2 00 00 00 00 00 00 r2 = r10
3: 07 02 00 00 fc ff ff ff r2 += -4
4: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
6: 85 00 00 00 01 00 00 00 call 1
7: b7 01 00 00 00 00 00 00 r1 = 0
8: 15 00 04 00 00 00 00 00 if r0 == 0 goto +4 <LBB0_2>
9: 61 01 00 00 00 00 00 00 r1 = *(u32 *)(r0 + 0)
10: 07 01 00 00 01 00 00 00 r1 += 1
11: 63 10 00 00 00 00 00 00 *(u32 *)(r0 + 0) = r1
12: b7 01 00 00 02 00 00 00 r1 = 2
0000000000000068 <LBB0_2>:
13: bf 10 00 00 00 00 00 00 r0 = r1
14: 95 00 00 00 00 00 00 00 exit
總共,我們需要以結構形式編寫 14 條指令,例如 struct bpf_insn
(建議: 從上面取出轉儲,重新閱讀說明部分,打開 linux/bpf.h
linux/bpf_common.h
struct bpf_insn insns[]
靠自己):
struct bpf_insn insns[] = {
/* 85 00 00 00 08 00 00 00 call 8 */
{
.code = BPF_JMP | BPF_CALL,
.imm = 8,
},
/* 63 0a fc ff 00 00 00 00 *(u32 *)(r10 - 4) = r0 */
{
.code = BPF_MEM | BPF_STX,
.off = -4,
.src_reg = BPF_REG_0,
.dst_reg = BPF_REG_10,
},
/* bf a2 00 00 00 00 00 00 r2 = r10 */
{
.code = BPF_ALU64 | BPF_MOV | BPF_X,
.src_reg = BPF_REG_10,
.dst_reg = BPF_REG_2,
},
/* 07 02 00 00 fc ff ff ff r2 += -4 */
{
.code = BPF_ALU64 | BPF_ADD | BPF_K,
.dst_reg = BPF_REG_2,
.imm = -4,
},
/* 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll */
{
.code = BPF_LD | BPF_DW | BPF_IMM,
.src_reg = BPF_PSEUDO_MAP_FD,
.dst_reg = BPF_REG_1,
.imm = map_fd,
},
{ }, /* placeholder */
/* 85 00 00 00 01 00 00 00 call 1 */
{
.code = BPF_JMP | BPF_CALL,
.imm = 1,
},
/* b7 01 00 00 00 00 00 00 r1 = 0 */
{
.code = BPF_ALU64 | BPF_MOV | BPF_K,
.dst_reg = BPF_REG_1,
.imm = 0,
},
/* 15 00 04 00 00 00 00 00 if r0 == 0 goto +4 <LBB0_2> */
{
.code = BPF_JMP | BPF_JEQ | BPF_K,
.off = 4,
.src_reg = BPF_REG_0,
.imm = 0,
},
/* 61 01 00 00 00 00 00 00 r1 = *(u32 *)(r0 + 0) */
{
.code = BPF_MEM | BPF_LDX,
.off = 0,
.src_reg = BPF_REG_0,
.dst_reg = BPF_REG_1,
},
/* 07 01 00 00 01 00 00 00 r1 += 1 */
{
.code = BPF_ALU64 | BPF_ADD | BPF_K,
.dst_reg = BPF_REG_1,
.imm = 1,
},
/* 63 10 00 00 00 00 00 00 *(u32 *)(r0 + 0) = r1 */
{
.code = BPF_MEM | BPF_STX,
.src_reg = BPF_REG_1,
.dst_reg = BPF_REG_0,
},
/* b7 01 00 00 02 00 00 00 r1 = 2 */
{
.code = BPF_ALU64 | BPF_MOV | BPF_K,
.dst_reg = BPF_REG_1,
.imm = 2,
},
/* <LBB0_2>: bf 10 00 00 00 00 00 00 r0 = r1 */
{
.code = BPF_ALU64 | BPF_MOV | BPF_X,
.src_reg = BPF_REG_1,
.dst_reg = BPF_REG_0,
},
/* 95 00 00 00 00 00 00 00 exit */
{
.code = BPF_JMP | BPF_EXIT
},
};
為那些不是自己寫這個的人提供的練習 - 查找 map_fd
.
我們的計劃中還剩下一個未公開的部分—— xdp_attach
。 不幸的是,像 XDP 這樣的程式無法使用系統呼叫來連接 bpf
。 創建 BPF 和 XDP 的人來自線上 Linux 社區,這意味著他們使用了他們最熟悉的社區(但不是為了 普通的 people) 與核心互動的介面: xdp_attach
正在複製程式碼 libbpf
,即從文件 netlink.c
歡迎來到 netlink 套接字的世界
開啟netlink套接字類型 NETLINK_ROUTE
:
int netlink_open(__u32 *nl_pid)
{
struct sockaddr_nl sa;
socklen_t addrlen;
int one = 1, ret;
int sock;
memset(&sa, 0, sizeof(sa));
sa.nl_family = AF_NETLINK;
sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (sock < 0)
err(1, "socket");
if (setsockopt(sock, SOL_NETLINK, NETLINK_EXT_ACK, &one, sizeof(one)) < 0)
warnx("netlink error reporting not supported");
if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0)
err(1, "bind");
addrlen = sizeof(sa);
if (getsockname(sock, (struct sockaddr *)&sa, &addrlen) < 0)
err(1, "getsockname");
*nl_pid = sa.nl_pid;
return sock;
}
我們從這個套接字讀取:
static int bpf_netlink_recv(int sock, __u32 nl_pid, int seq)
{
bool multipart = true;
struct nlmsgerr *errm;
struct nlmsghdr *nh;
char buf[4096];
int len, ret;
while (multipart) {
multipart = false;
len = recv(sock, buf, sizeof(buf), 0);
if (len < 0)
err(1, "recv");
if (len == 0)
break;
for (nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, len);
nh = NLMSG_NEXT(nh, len)) {
if (nh->nlmsg_pid != nl_pid)
errx(1, "wrong pid");
if (nh->nlmsg_seq != seq)
errx(1, "INVSEQ");
if (nh->nlmsg_flags & NLM_F_MULTI)
multipart = true;
switch (nh->nlmsg_type) {
case NLMSG_ERROR:
errm = (struct nlmsgerr *)NLMSG_DATA(nh);
if (!errm->error)
continue;
ret = errm->error;
// libbpf_nla_dump_errormsg(nh); too many code to copy...
goto done;
case NLMSG_DONE:
return 0;
default:
break;
}
}
}
ret = 0;
done:
return ret;
}
最後,這是我們打開套接字並向其發送包含文件描述符的特殊訊息的函數:
static int xdp_attach(int ifindex, int prog_fd)
{
int sock, seq = 0, ret;
struct nlattr *nla, *nla_xdp;
struct {
struct nlmsghdr nh;
struct ifinfomsg ifinfo;
char attrbuf[64];
} req;
__u32 nl_pid = 0;
sock = netlink_open(&nl_pid);
if (sock < 0)
return sock;
memset(&req, 0, sizeof(req));
req.nh.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
req.nh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
req.nh.nlmsg_type = RTM_SETLINK;
req.nh.nlmsg_pid = 0;
req.nh.nlmsg_seq = ++seq;
req.ifinfo.ifi_family = AF_UNSPEC;
req.ifinfo.ifi_index = ifindex;
/* started nested attribute for XDP */
nla = (struct nlattr *)(((char *)&req)
+ NLMSG_ALIGN(req.nh.nlmsg_len));
nla->nla_type = NLA_F_NESTED | IFLA_XDP;
nla->nla_len = NLA_HDRLEN;
/* add XDP fd */
nla_xdp = (struct nlattr *)((char *)nla + nla->nla_len);
nla_xdp->nla_type = IFLA_XDP_FD;
nla_xdp->nla_len = NLA_HDRLEN + sizeof(int);
memcpy((char *)nla_xdp + NLA_HDRLEN, &prog_fd, sizeof(prog_fd));
nla->nla_len += nla_xdp->nla_len;
/* if user passed in any flags, add those too */
__u32 flags = XDP_FLAGS_SKB_MODE;
nla_xdp = (struct nlattr *)((char *)nla + nla->nla_len);
nla_xdp->nla_type = IFLA_XDP_FLAGS;
nla_xdp->nla_len = NLA_HDRLEN + sizeof(flags);
memcpy((char *)nla_xdp + NLA_HDRLEN, &flags, sizeof(flags));
nla->nla_len += nla_xdp->nla_len;
req.nh.nlmsg_len += NLA_ALIGN(nla->nla_len);
if (send(sock, &req, req.nh.nlmsg_len, 0) < 0)
err(1, "send");
ret = bpf_netlink_recv(sock, nl_pid, seq);
cleanup:
close(sock);
return ret;
}
所以,一切準備就緒,可以進行測試了:
$ cc nolibbpf.c -o nolibbpf
$ sudo strace -e bpf ./nolibbpf
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_ARRAY, map_name="woo", ...}, 72) = 3
bpf(BPF_PROG_LOAD, {prog_type=BPF_PROG_TYPE_XDP, insn_cnt=15, prog_name="woo", ...}, 72) = 4
+++ exited with 0 +++
讓我們看看我們的程式是否已連接到 lo
:
$ ip l show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
prog/xdp id 160
讓我們發送 ping 並查看地圖:
$ for s in `seq 234`; do sudo ping -f -c 100 127.0.0.1 >/dev/null 2>&1; done
$ sudo bpftool m dump name woo
key: 00 00 00 00 value: 90 01 00 00 00 00 00 00
key: 01 00 00 00 value: 00 00 00 00 00 00 00 00
key: 02 00 00 00 value: 00 00 00 00 00 00 00 00
key: 03 00 00 00 value: 00 00 00 00 00 00 00 00
key: 04 00 00 00 value: 00 00 00 00 00 00 00 00
key: 05 00 00 00 value: 00 00 00 00 00 00 00 00
key: 06 00 00 00 value: 40 b5 00 00 00 00 00 00
key: 07 00 00 00 value: 00 00 00 00 00 00 00 00
Found 8 elements
萬歲,一切正常。 順便請注意,我們的地圖再次以位元組的形式顯示。 這是因為,與 libbpf
我們沒有載入類型資訊(BTF)。 但我們下次會詳細討論這個問題。
開發工具
在本節中,我們將了解最小的 BPF 開發人員工具包。
一般來說,您不需要任何特殊的東西來開發 BPF 程式 - BPF 可以在任何像樣的發行版核心上運行,並且程式是使用 clang
,可以從包裝中提供。 但是,由於 BPF 正在開發中,內核和工具都在不斷變化,如果你不想用 2019 年以來的老式方法編寫 BPF 程序,那麼你將不得不編譯
llvm
/clang
pahole
- 其核心
bpftool
(作為參考,本節和本文中的所有範例均在 Debian 10 上運行。)
llvm/clang
BPF 與 LLVM 很友好,雖然最近 BPF 的程式可以使用 gcc 編譯,但目前的所有開發都是針對 LLVM 進行的。 因此,首先我們將建立當前版本 clang
來自 git:
$ sudo apt install ninja-build
$ git clone --depth 1 https://github.com/llvm/llvm-project.git
$ mkdir -p llvm-project/llvm/build/install
$ cd llvm-project/llvm/build
$ cmake .. -G "Ninja" -DLLVM_TARGETS_TO_BUILD="BPF;X86"
-DLLVM_ENABLE_PROJECTS="clang"
-DBUILD_SHARED_LIBS=OFF
-DCMAKE_BUILD_TYPE=Release
-DLLVM_BUILD_RUNTIME=OFF
$ time ninja
... много времени спустя
$
現在我們可以檢查一切是否正確組合在一起:
$ ./bin/llc --version
LLVM (http://llvm.org/):
LLVM version 11.0.0git
Optimized build.
Default target: x86_64-unknown-linux-gnu
Host CPU: znver1
Registered Targets:
bpf - BPF (host endian)
bpfeb - BPF (big endian)
bpfel - BPF (little endian)
x86 - 32-bit X86: Pentium-Pro and above
x86-64 - 64-bit X86: EM64T and AMD64
(組裝說明 clang
由我取自
我們不會安裝剛剛建置的程序,而是將它們新增到 PATH
例如:
export PATH="`pwd`/bin:$PATH"
(這可以添加到 .bashrc
或到一個單獨的文件。 就我個人而言,我將這樣的內容添加到 ~/bin/activate-llvm.sh
必要時我會這樣做 . activate-llvm.sh
.)
帕霍爾和 BTF
效用 pahole
建構核心時用於建立 BTF 格式的偵錯資訊。 我們在本文中不會詳細討論 BTF 技術的細節,除了它很方便並且我們想使用它之外。 因此,如果您要構建內核,請先構建 pahole
(不 pahole
您將無法使用該選項建立內核 CONFIG_DEBUG_INFO_BTF
:
$ git clone https://git.kernel.org/pub/scm/devel/pahole/pahole.git
$ cd pahole/
$ sudo apt install cmake
$ mkdir build
$ cd build/
$ cmake -D__LIB=lib ..
$ make
$ sudo make install
$ which pahole
/usr/local/bin/pahole
用於試驗 BPF 的內核
在探索BPF的可能性時,我想組裝自己的核心。 一般來說,這不是必需的,因為您將能夠在發行版內核上編譯和加載BPF 程序,但是,擁有自己的內核允許您使用最新的BPF 功能,這些功能最多會在幾個月內出現在您的發行版中,或者,就像某些調試工具在可預見的將來根本不會被打包一樣。 此外,它自己的核心使得嘗試程式碼變得非常重要。
為了建立內核,您首先需要內核本身,其次需要內核設定檔。 為了試驗 BPF,我們可以使用通常的 net
net-next
bpf
bpf-next
*-next
內核是列出的內核中最不穩定的)。
討論如何管理內核設定檔超出了本文的範圍 - 假設您已經知道如何執行此操作,或者
下載上述核心之一:
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/bpf/bpf-next.git
$ cd bpf-next
建構最小的工作內核配置:
$ cp /boot/config-`uname -r` .config
$ make localmodconfig
在檔案中啟用 BPF 選項 .config
您自己選擇的(最有可能的是 CONFIG_BPF
由於 systemd 使用它,因此已經啟用)。 以下是本文使用的核心選項清單:
CONFIG_CGROUP_BPF=y
CONFIG_BPF=y
CONFIG_BPF_LSM=y
CONFIG_BPF_SYSCALL=y
CONFIG_ARCH_WANT_DEFAULT_BPF_JIT=y
CONFIG_BPF_JIT_ALWAYS_ON=y
CONFIG_BPF_JIT_DEFAULT_ON=y
CONFIG_IPV6_SEG6_BPF=y
# CONFIG_NETFILTER_XT_MATCH_BPF is not set
# CONFIG_BPFILTER is not set
CONFIG_NET_CLS_BPF=y
CONFIG_NET_ACT_BPF=y
CONFIG_BPF_JIT=y
CONFIG_BPF_STREAM_PARSER=y
CONFIG_LWTUNNEL_BPF=y
CONFIG_HAVE_EBPF_JIT=y
CONFIG_BPF_EVENTS=y
CONFIG_BPF_KPROBE_OVERRIDE=y
CONFIG_DEBUG_INFO_BTF=y
然後我們就可以輕鬆地組裝和安裝模組和內核了(順便說一句,您可以使用新組裝的內核來組裝內核) clang
透過增加 CC=clang
):
$ make -s -j $(getconf _NPROCESSORS_ONLN)
$ sudo make modules_install
$ sudo make install
並使用新核心重新啟動(我為此使用 kexec
從包裝中 kexec-tools
):
v=5.8.0-rc6+ # если вы пересобираете текущее ядро, то можно делать v=`uname -r`
sudo kexec -l -t bzImage /boot/vmlinuz-$v --initrd=/boot/initrd.img-$v --reuse-cmdline &&
sudo kexec -e
bpf工具
本文最常用的實用程式是 bpftool
,作為 Linux 核心的一部分提供。 它由 BPF 開發人員為 BPF 開發人員編寫和維護,可用於管理所有類型的 BPF 物件 - 載入程式、建立和編輯地圖、探索 BPF 生態系統的生命週期等。 可以找到手冊頁原始碼形式的文檔
在撰寫本文時 bpftool
僅針對 RHEL、Fedora 和 Ubuntu 提供現成的版本(例如,請參閱 bpftool
在 Debian 中)。 但是如果你已經構建了內核,那麼構建 bpftool
就像派一樣簡單:
$ cd ${linux}/tools/bpf/bpftool
# ... пропишите пути к последнему clang, как рассказано выше
$ make -s
Auto-detecting system features:
... libbfd: [ on ]
... disassembler-four-args: [ on ]
... zlib: [ on ]
... libcap: [ on ]
... clang-bpf-co-re: [ on ]
Auto-detecting system features:
... libelf: [ on ]
... zlib: [ on ]
... bpf: [ on ]
$
(這裡 ${linux}
- 這是你的核心目錄。)執行這些命令後 bpftool
將會被收集在一個目錄中 ${linux}/tools/bpf/bpftool
並且可以將其添加到路徑中(首先添加到用戶 root
)或只是複製到 /usr/local/sbin
.
蒐集 bpftool
最好使用後者 clang
,如上所述進行組裝,並檢查其是否正確組裝 - 例如使用以下命令
$ sudo bpftool feature probe kernel
Scanning system configuration...
bpf() syscall for unprivileged users is enabled
JIT compiler is enabled
JIT compiler hardening is disabled
JIT compiler kallsyms exports are enabled for root
...
這將顯示您的核心中啟用了哪些 BPF 功能。
順便說一句,前面的命令可以運行為
# bpftool f p k
這是透過類比套件中的實用程式來完成的 iproute2
,例如,我們可以說 ip a s eth0
而不是 ip addr show dev eth0
.
結論
BPF 讓您能夠有效地測量和即時更改核心的功能。 事實證明,該系統非常成功,繼承了 UNIX 的最佳傳統:允許對核心進行(重新)編程的簡單機制允許大量人員和組織進行實驗。 而且,儘管實驗以及 BPF 基礎架構本身的開發還遠未完成,但該系統已經擁有穩定的 ABI,可讓您建立可靠且最重要的是有效的業務邏輯。
我想指出的是,在我看來,這項技術之所以如此受歡迎,一方面是因為它可以 玩 (一台機器的架構在一晚上就可以大致了解),另一方面解決它出現之前無法(漂亮地)解決的問題。 這兩個組成部分共同迫使人們進行實驗和夢想,從而導致越來越多的創新解決方案的出現。
這篇文章雖然不是特別短,但只是對 BPF 世界的介紹,並沒有描述該架構的「高級」功能和重要部分。 未來的計劃是這樣的:下一篇文章將概述 BPF 程式類型(5.8 核心支援 30 種程式類型),然後我們最後看看如何使用內核追蹤程式編寫真正的 BPF 應用程式作為一個例子,那麼是時候學習關於BPF 架構的更深入的課程了,然後是BPF 網路和安全應用程式的範例。
本系列之前的文章
連結
-
BPF 和 XDP 參考指南 — 來自 cilium 的 BPF 文檔,或者更準確地說,來自 BPF 的創建者和維護者之一 Daniel Borkman。 這是最早的嚴肅描述之一,與其他描述不同的是,丹尼爾確切地知道他在寫什麼,而且沒有任何錯誤。 特別是,本文檔描述如何使用眾所周知的實用程式來處理 XDP 和 TC 類型的 BPF 程序ip
從包裝中iproute2
. -
文檔/網路/filter.txt — 原始文件,包含經典 BPF 和擴充 BPF 的文檔。 如果您想深入研究彙編語言和技術架構細節,這是一本很好的讀物。 -
來自 Facebook 的有關 BPF 的博客 。 它很少更新,但恰如其分,正如 Alexei Starovoitov(eBPF 的作者)和 Andrii Nakryiko(維護者)在那裡寫的那樣libbpf
). -
bpftool 的秘密 。 Quentin Monnet 的一個有趣的 Twitter 帖子,其中包含使用 bpftool 的範例和秘密。 -
深入了解 BPF:閱讀材料列表 。 Quentin Monnet 提供的一個龐大(且仍在維護)的 BPF 文件連結清單。
來源: www.habr.com