適合小朋友的 BPF,第一部分:擴展 BPF

一開始有一種技術,叫做BPF。 我們看著她 ,本系列的舊約文章。 2013年,在Alexei Starovoitov和Daniel Borkman的努力下,針對現代64位元機器優化的改進版本被開發出來並包含在Linux核心中。 這項新技術被簡單地稱為“內部 BPF”,然後更名為“擴展 BPF”,現在,幾年後,每個人都簡單地稱之為“BPF”。

粗略地說,BPF 允許您在 Linux 核心空間中運行任意用戶提供的程式碼,並且新架構非常成功,以至於我們還需要十幾篇文章來描述它的所有應用程式。 (開發人員唯一做得不好的事情是創建了一個像樣的徽標,正如您在下面的效能程式碼中看到的那樣。)

本文介紹了 BPF 虛擬機器的結構、使用 BPF 的核心介面、開發工具,以及現有功能的簡要概述,即我們將來深入研究 BPF 的實際應用所需的一切。
適合小朋友的 BPF,第一部分:擴展 BPF

文章摘要

BPF 架構簡介。 首先,我們將鳥瞰 BPF 架構並概述主要元件。

BPF虛擬機器的暫存器和命令系統。 已經了解了整個架構,我們將描述 BPF 虛擬機器的結構。

BPF 物件的生命週期,bpffs 檔案系統。 在本節中,我們將仔細研究 BPF 物件的生命週期 - 程序和映射。

使用 bpf 系統呼叫管理物件。 透過對系統的一些了解,我們最終將了解如何使用特殊的系統呼叫從用戶空間建立和操作物件 - bpf(2).

Пишем программы BPF с помощью libbpf. 當然,您可以使用系統呼叫來編寫程式。 但這很難。 為了更現實的場景,核子程式設計師開發了一個函式庫 libbpf。 我們將建立一個基本的 BPF 應用程式框架,我們將在後續範例中使用它。

內核助手。 在這裡,我們將了解 BPF 程式如何存取核心輔助函數 - 與經典 BPF 相比,該工具與映射一起從根本上擴展了新 BPF 的功能。

從 BPF 程式存取地圖。 至此,我們已經足夠了解如何建立使用地圖的程式。 讓我們快速瀏覽一下偉大而強大的驗證器。

開發工具。 有關如何組裝實驗所需的實用程式和核心的幫助部分。

結論。 在文章的最後,讀到這裡的人會發現激勵人心的話語以及對後續文章中將發生的事情的簡要描述。 我們還會列出一些自學的鏈接,供那些沒有意願或能力等待繼續的人參考。

BPF架構簡介

在我們開始考慮 BPF 架構之前,我們將最後一次(哦)提到 經典帶通濾波器,它是為了響應 RISC 機器的出現而開發的,解決了高效資料包過濾的問題。 這個體系結構非常成功,誕生於伯克利 UNIX 的 XNUMX 年代,後來被移植到大多數現有作業系統,一直延續到瘋狂的 XNUMX 年代,並且仍在尋找新的應用程式。

新的 BPF 是為了應對 64 位元機器、雲端服務的普遍存在以及對創建 SDN 工具日益增長的需求而開發的(S軟體-d精緻的 n網路工作)。 新的BPF 由核心網路工程師開發,作為經典BPF 的改進替代品,六個月後,它在追蹤Linux 系統這一艱鉅的任務中找到了應用,而現在,在它出現六年後,我們需要一篇完整的下一篇文章來了解列出不同類型的程序。

有趣的圖片

從本質上講,BPF 是一個沙箱虛擬機,可讓您在核心空間中執行「任意」程式碼,而不會影響安全性。 BPF 程式在使用者空間中創建,載入到核心中,並連接到某個事件來源。 例如,事件可以是傳送封包至網路介面資料包、啟動某些核心功能等。 在套件的情況下,BPF 程式將有權存取套件的資料和元資料(用於讀取,可能還可以寫入,取決於程式的類型);在運行核心函數的情況下,函數,包括指向內核記憶體的指標等。

讓我們仔細看看這個過程。 首先,我們來談談與經典 BPF 的第一個區別,經典 BPF 的程式是用彙編語言編寫的。 在新版本中,架構得到了擴展,以便可以用高級語言編寫程序,當然主要是用 C 語言。為此,開發了 llvm 的後端,它允許您為 BPF 架構生成字節碼。

適合小朋友的 BPF,第一部分:擴展 BPF

BPF 架構的設計部分是為了在現代機器上有效運作。 為了在實踐中實現這一點,BPF 字節碼一旦載入到核心中,就會使用稱為 JIT 編譯器的元件轉換為本機程式碼(J烏斯 In T我)。 接下來,如果您還記得的話,在經典的 BPF 中,程式被載入到核心中並以原子方式附加到事件來源 - 在單一系統呼叫的上下文中。 在新架構中,這分兩個階段發生 - 首先,使用系統呼叫將程式碼載入到核心中 bpf(2)然後,稍後,透過根據程式類型而變更的其他機制,程式附加到事件來源。

說到這裡,讀者可能會有一個疑問:這可能嗎? 這樣的程式碼執行安全如何保證呢? 我們透過載入BPF程式的階段(稱為verifier)來確保執行安全(這個階段在英文中稱為verifier,我將繼續使用英文單字):

適合小朋友的 BPF,第一部分:擴展 BPF

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 程式都是如此)。

這種能力的存在使得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 (對於 XDP)或結構 struct __sk_buff (針對不同的網路程式)或結構 struct pt_regs (針對不同類型的追蹤程序)等。

因此,我們有一組暫存器、核心助手、堆疊、上下文指標和映射形式的共享記憶體。 並不是說所有這些都是旅行中絕對必要的,但是...

讓我們繼續描述並討論處理這些物件的命令系統。 全部 (差不多所有) BPF 指令具有固定的 64 位元大小。 如果您查看 64 位元 Big Endian 機器上的一條指令,您會看到

適合小朋友的 BPF,第一部分:擴展 BPF

這裡 Code - 這是指令的編碼, Dst/Src 分別是接收器和來源的編碼, Off - 16 位元有符號縮進,以及 Imm 是一些指令中使用的 32 位元有符號整數(類似於 cBPF 常數 K)。 編碼 Code 有兩種類型之一:

適合小朋友的 BPF,第一部分:擴展 BPF

指令類別 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 指令的數字代碼。 當您自己研究架構和/或解析二進位檔案時,您可以在以下來源中找到語義(按複雜程度排序): 非官方 eBPF 規範, BPF 和 XDP 參考指南、指令集, 文檔/網路/filter.txt 當然,還有 Linux 原始碼中的驗證器、JIT、BPF 解譯器。

例:在你的腦中拆解 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

手術 bALU64 - BPF_MOV。 它將一個值指派給目標暫存器。 如果該位元已設定 s (source),那麼該值是從來源暫存器中取得的,如果像我們的例子一樣,它沒有被設置,那麼該值是從欄位中取得的 Imm。 所以在第一條和第三條指令中我們執行操作 r0 = Imm。 此外,JMP 1 類別操作是 BPF_JEQ (如果相等則跳轉)。 在我們的例子中,由於位 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 物件(程式和映射)是使用命令從使用者空間建立的 BPF_PROG_LOAD и BPF_MAP_CREATE 系統調用 bpf(2),我們將在下一節中詳細討論這是如何發生的。 這將創建內核資料結構並為每個結構創建內核資料結構 refcount (引用計數)設定為XNUMX,並且指向該物件的檔案描述符被傳回給使用者。 手柄關閉後 refcount 物件減一,當減到零時,物件被銷毀。

如果程式使用映射,那麼 refcount 這些地圖在載入程式後會加一,即它們的檔案描述符可以從用戶進程中關閉,並且仍然 refcount 不會變成零:

適合小朋友的 BPF,第一部分:擴展 BPF

成功載入程式後,我們通常將其附加到某種事件產生器。 例如,我們可以將其放在網路介面上來處理傳入的封包或將其連接到某些 tracepoint 在核心。 此時,引用計數器也會加一,我們就可以在載入程式中關閉檔案描述子了。

如果我們現在關閉引導程式會發生什麼? 這取決於事件產生器(鉤子)的類型。 所有網路鉤子都會在載入器完成後存在,這些就是所謂的全域鉤子。 並且,例如,追蹤程式將在創建它們的進程終止後被釋放(因此稱為本地,從「本地到進程」)。 從技術上講,本地鉤子在用戶空間中始終有一個相應的檔案描述符,因此當進程關閉時也會關閉,但全域鉤子則不然。 在下圖中,我嘗試使用紅色叉來展示載入程式的終止在本地和全域鉤子的情況下如何影響物件的生命週期。

適合小朋友的 BPF,第一部分:擴展 BPF

為什麼本地鉤子和全局鉤子之間存在區別? 在沒有用戶空間的情況下運行某些類型的網路程式是有意義的,例如,想像 DDoS 保護 - 引導程式編寫規則並將 BPF 程式連接到網路接口,之後引導程式可以自行終止。 另一方面,想像一下您在十分鐘內跪下編寫的調試跟踪程序 - 當它完成時,您希望系統中沒有留下任何垃圾,而本地掛鉤將確保這一點。

另一方面,假設您想要連接到內核中的追蹤點並收集多年的統計資料。 在這種情況下,您可能希望完成使用者部分並不時傳回統計資料。 bpf 檔案系統提供了這個機會。 它是一個僅記憶體中的偽文件系統,允許創建引用 BPF 物件的文件,從而增加 refcount 對象。 此後,載入器可以退出,並且它建立的物件將保持活動狀態。

適合小朋友的 BPF,第一部分:擴展 BPF

在 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。 在這種情況下,舊版本程式的所有活動實例都將完成其工作,並且將從新程式建立新的事件處理程序,這裡的「原子性」意味著不會錯過任何一個事件。

將程式附加到事件來源

在本文中,我們不會單獨描述將程式連接到事件來源,因為在特定類型的程式的上下文中研究這一點是有意義的。 厘米。 例子 下面,我們展示了 XDP 等程式是如何連接的。

使用 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 程序,但首先我們需要決定要載入哪種程式 - 我們必須選擇 類型 並在這種類型的框架內編寫一個將通過驗證者測試的程式。 但是,為了不讓過程複雜化,這裡有一個現成的解決方案:我們將採用以下程序 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 支援進行編譯(我們在 一節中描述如何執行此操作) 開發工具。 您可以透過以下檔案的存在來非常簡單地檢查您的核心是否是使用 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]。 然而,為了滿足核心之外的應用程式的需要,維護了一個單獨的儲存庫 https://github.com/libbpf/libbpf 其中核心庫或多或少被鏡像以進行讀取存取。

在本節中,我們將了解如何建立一個使用 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_opsxdp。 結構 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_xattrlibbpf/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,它動態產生 BPF 二進位代碼。

為了更容易遵循邏輯,我們將為此目的重寫我們的範例 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) 與核心互動的介面: 網路連結套接字, 也可以看看 RFC3549。 最簡單的實作方式 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 由我取自 bpf_devel_QA.)

我們不會安裝剛剛建置的程序,而是將它們新增到 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,我們可以使用通常的 香草 內核或開發內核之一。 從歷史上看,BPF 開發是在 Linux 網路社群內進行的,因此所有變更遲早都要經過 Linux 網路維護者 David Miller。 根據其性質 - 編輯或新功能 - 網路變更屬於兩個核心之一 - netnet-next。 BPF 的變更以相同的方式分佈在 bpf и bpf-next,然後分別匯集到 net 和 net-next 中。 有關更多詳細信息,請參閱 bpf_devel_QA и 網路開發常見問題解答。 因此,根據您的喜好和您正在測試的系統的穩定性需求來選擇核心(*-next 內核是列出的內核中最不穩定的)。

討論如何管理內核設定檔超出了本文的範圍 - 假設您已經知道如何執行此操作,或者 準備學習 靠自己。 但是,以下說明應該或多或少足以為您提供一個支援 BPF 的工作系統。

下載上述核心之一:

$ 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 網路和安全應用程式的範例。

本系列之前的文章

  1. 適合小朋友的 BPF,第 XNUMX 部分:經典 BPF

連結

  1. BPF 和 XDP 參考指南 — 來自 cilium 的 BPF 文檔,或者更準確地說,來自 BPF 的創建者和維護者之一 Daniel Borkman。 這是最早的嚴肅描述之一,與其他描述不同的是,丹尼爾確切地知道他在寫什麼,而且沒有任何錯誤。 特別是,本文檔描述如何使用眾所周知的實用程式來處理 XDP 和 TC 類型的 BPF 程序 ip 從包裝中 iproute2.

  2. 文檔/網路/filter.txt — 原始文件,包含經典 BPF 和擴充 BPF 的文檔。 如果您想深入研究彙編語言和技術架構細節,這是一本很好的讀物。

  3. 來自 Facebook 的有關 BPF 的博客。 它很少更新,但恰如其分,正如 Alexei Starovoitov(eBPF 的作者)和 Andrii Nakryiko(維護者)在那裡寫的那樣 libbpf).

  4. bpftool 的秘密。 Quentin Monnet 的一個有趣的 Twitter 帖子,其中包含使用 bpftool 的範例和秘密。

  5. 深入了解 BPF:閱讀材料列表。 Quentin Monnet 提供的一個龐大(且仍在維護)的 BPF 文件連結清單。

來源: www.habr.com

添加評論