BPF 設計問答

BPF 在 Linux 核心中以及 BPF 虛擬機器的多個使用者空間實現中的可擴充套件性和在網路、跟蹤、安全方面的適用性導致了對 BPF 實際是什麼的許多誤解。這個簡短的問答試圖解決這個問題,並概述 BPF 的長期發展方向。

問題和答案

問:BPF 是一種類似於 x64 和 arm64 的通用指令集嗎?

答:不是。

問:BPF 是一種通用虛擬機器嗎?

答:不是。

BPF 是具有 C 呼叫約定的通用指令集。

問:為什麼選擇 C 呼叫約定?

答:因為 BPF 程式旨在在用 C 編寫的 Linux 核心中執行,因此 BPF 定義了與兩個最常用的架構 x64 和 arm64 相容的指令集(並考慮了其他架構的重要特性),並定義了與這些架構上的 Linux 核心的 C 呼叫約定相容的呼叫約定。

問:未來是否可以支援多個返回值?

答:否。BPF 只允許暫存器 R0 用作返回值。

問:未來是否可以支援超過 5 個函式引數?

答:否。BPF 呼叫約定只允許使用暫存器 R1-R5 作為引數。BPF 不是一個獨立的指令集。(不像 x64 ISA 允許 msft、cdecl 和其他約定)

問:BPF 程式可以訪問指令指標或返回地址嗎?

答:不是。

問:BPF 程式可以訪問堆疊指標嗎?

答:不是。

只有幀指標(暫存器 R10)可以訪問。從編譯器的角度來看,必須有堆疊指標。例如,LLVM 在其 BPF 後端中將暫存器 R11 定義為堆疊指標,但它確保生成的程式碼永遠不會使用它。

問:C 呼叫約定是否會減少可能的用例?

答:是。

BPF 設計迫使以核心輔助函式和核心物件(如 BPF 對映)的形式新增主要功能,並在它們之間實現無縫互操作。它讓核心呼叫 BPF 程式,程式呼叫核心輔助函式,零開銷,因為所有這些都是本機 C 程式碼。對於 JITed BPF 程式尤其如此,這些程式與本機核心 C 程式碼無法區分。

問:這是否意味著不允許對 BPF 程式碼進行“創新”擴充套件?

答:軟是。

至少目前是這樣,直到 BPF 核心支援 bpf 到 bpf 呼叫、間接呼叫、迴圈、全域性變數、跳轉表、只讀部分以及 C 程式碼可以生成的所有其他正常構造。

問:可以安全地支援迴圈嗎?

答:目前尚不清楚。

BPF 開發人員正在努力尋找支援有界迴圈的方法。

問:驗證器的限制是什麼?

答:使用者空間已知的唯一限制是 BPF_MAXINSNS (4096)。這是非特權 bpf 程式可以擁有的最大指令數。驗證器有各種內部限制。比如在程式分析期間可以探索的最大指令數。目前,該限制設定為 100 萬。這實際上意味著最大的程式可以由 100 萬條 NOP 指令組成。對後續分支的最大數量、巢狀 bpf 到 bpf 呼叫的數量、每個指令的驗證器狀態的數量、程式使用的 map 的數量都有一個限制。所有這些限制都可以被足夠複雜的程式命中。還有非數值限制可能會導致程式被拒絕。驗證器曾經只識別指標 + 常量表達式。現在它可以識別指標 + bounded_register。bpf_lookup_map_elem(key) 要求 “key” 必須是指向堆疊的指標。現在,“key” 可以是指向 map 值的指標。驗證器正在穩步變得“更智慧”。限制正在被刪除。要知道程式是否會被驗證器接受的唯一方法是嘗試載入它。bpf 開發過程保證未來的核心版本將接受早期版本接受的所有 bpf 程式。

指令級別問題

問:LD_ABS 和 LD_IND 指令與 C 程式碼

問:為什麼 LD_ABS 和 LD_IND 指令存在於 BPF 中,而 C 程式碼無法表達它們,必須使用內建行內函數?

答:這是與經典 BPF 相容的產物。BPF 中現代網路程式碼在沒有它們的情況下表現更好。請參閱“直接資料包訪問”。

問:BPF 指令對映不是一對一到本機 CPU

問:似乎不是所有 BPF 指令都是一對一到本機 CPU 的。例如,為什麼 BPF_JNE 和其他比較和跳轉不像 CPU 那樣?

答:這是必要的,以避免將標誌引入 ISA,這些標誌無法在 CPU 架構之間實現通用和高效。

問:為什麼 BPF_DIV 指令不對映到 x64 div?

答:因為如果我們選擇與 x64 的一對一關係,那麼在 arm64 和其他 archs 上支援它會更加複雜。還需要除以零的執行時檢查。

問:為什麼 BPF 有隱式序言和尾聲?

答:因為像 sparc 這樣的架構有暫存器視窗,並且通常架構之間存在足夠的細微差異,因此將返回地址天真地儲存到堆疊中是行不通的。另一個原因是 BPF 必須免受零除的影響(以及 LD_ABS insn 的遺留異常路徑)。這些指令需要隱式呼叫尾聲並返回。

問:為什麼 BPF_JLT 和 BPF_JLE 指令最初沒有引入?

答:因為經典的 BPF 沒有它們,BPF 作者認為編譯器解決方法是可以接受的。事實證明,由於缺少這些比較指令,程式會損失效能,因此添加了它們。這兩個指令是完美示例,說明了哪種新的 BPF 指令是可以接受的,並且可以在將來新增。這兩個指令已經在本機 CPU 中具有等效的指令。不具有與 HW 指令一對一對映的新指令將不被接受。

問:BPF 32 位子暫存器要求

問:BPF 32 位子暫存器要求將 BPF 暫存器的較高 32 位清零,這使得 BPF 對於 32 位 CPU 架構和 32 位 HW 加速器來說效率較低的虛擬機器。將來可以在 BPF 中新增真正的 32 位暫存器嗎?

答:不是。

但是,可以使用一些在 BPF 暫存器上將較高 32 位清零的最佳化,並且可以利用這些最佳化來提高 32 位架構的 JITed BPF 程式的效能。

從版本 7 開始,LLVM 能夠生成在 32 位子暫存器上執行的指令,前提是傳遞選項 -mattr=+alu32 來編譯程式。此外,驗證器現在可以標記需要將目標暫存器的較高位清零的指令,並插入顯式的零擴充套件 (zext) 指令(mov32 變體)。這意味著對於沒有 zext 硬體支援的架構,JIT 後端不需要清除 alu32 指令或窄載入寫入的子暫存器的較高位。相反,後端只需要支援該 mov32 變體的程式碼生成,並覆蓋 bpf_jit_needs_zext() 使其返回“true”(以便在驗證器中啟用 zext 插入)。

請注意,JIT 後端可能對 zext 具有部分硬體支援。在這種情況下,如果啟用了驗證器 zext 插入,則可能會導致插入不必要的 zext 指令。可以透過在 JIT 後端中建立一個簡單的窺視孔來刪除此類指令:如果一條指令對 zext 具有硬體支援,並且下一條指令是顯式 zext,則在進行程式碼生成時可以跳過後者。

問:BPF 是否有穩定的 ABI?

答:是。BPF 指令、BPF 程式的引數、輔助函式集及其引數、識別的返回程式碼都是 ABI 的一部分。但是,對於使用 bpf_probe_read() 等輔助函式來遍歷核心內部資料結構並使用核心內部標頭編譯的跟蹤程式,有一個特定的例外。這兩個核心內部結構都可能會發生變化,並且可能會與較新的核心發生中斷,因此需要相應地調整程式。

通常透過使用 kfuncs 而不是新的輔助函式來新增新的 BPF 功能。Kfuncs 不被認為是穩定 API 的一部分,並且具有自己的生命週期期望,如 3. kfunc 生命週期期望 中所述。

問:跟蹤點是穩定 ABI 的一部分嗎?

答:否。跟蹤點與內部實現細節相關聯,因此它們可能會發生變化,並且可能會與較新的核心發生中斷。BPF 程式需要在此發生時進行相應更改。

問:kprobes 可以附加到的位置是穩定 ABI 的一部分嗎?

答:否。kprobes 可以附加到的位置是內部實現細節,這意味著它們可能會發生變化,並且可能會與較新的核心發生中斷。BPF 程式需要在此發生時進行相應更改。

問:BPF 程式使用多少堆疊空間?

答:目前所有程式型別都限制為 512 位元組的堆疊空間,但是驗證器會計算實際使用的堆疊量,並且直譯器和大多數 JITed 程式碼都會消耗必要的量。

問:BPF 可以解除安裝到硬體嗎?

答:是。NFP 驅動程式支援 BPF HW 解除安裝。

問:經典 BPF 直譯器仍然存在嗎?

答:否。經典 BPF 程式已轉換為擴充套件 BPF 指令。

問:BPF 可以呼叫任意核心函式嗎?

答:否。BPF 程式只能呼叫作為 BPF 輔助函式或 kfuncs 公開的特定函式。可用函式集是為每種程式型別定義的。

問:BPF 可以覆蓋任意核心記憶體嗎?

答:不是。

跟蹤 bpf 程式可以使用 bpf_probe_read() 和 bpf_probe_read_str() 輔助函式讀取任意記憶體。網路程式無法讀取任意記憶體,因為它們無權訪問這些輔助函式。程式永遠不能直接讀取或寫入任意記憶體。

問:BPF 可以覆蓋任意使用者記憶體嗎?

答:有點。

跟蹤 BPF 程式可以使用 bpf_probe_write_user() 覆蓋當前任務的使用者記憶體。每次載入此類程式時,核心都會列印警告訊息,因此此輔助函式僅對實驗和原型有用。跟蹤 BPF 程式只能由 root 使用者使用。

問:透過核心模組的新功能?

問:諸如新程式或 map 型別、新輔助函式等 BPF 功能可以從核心模組程式碼中新增嗎?

答:是的,透過 kfuncs 和 kptrs

諸如程式型別、map 和輔助函式之類的核心 BPF 功能不能透過模組新增。但是,模組可以透過匯出 kfuncs(可以將指向模組內部資料結構的指標作為 kptrs 返回)來向 BPF 程式公開功能。

問:直接呼叫核心函式是 ABI 嗎?

問:某些核心函式(例如 tcp_slow_start)可以被 BPF 程式呼叫。這些核心函式是否會成為 ABI?

答:不是。

核心函式原型會發生變化,並且 bpf 程式將被驗證器拒絕。此外,例如,一些可由 bpf 呼叫的核心函式已經被其他核心 tcp cc(擁塞控制)實現使用。如果這些核心函式中的任何一個發生變化,則必須更改樹內和樹外的核心 tcp cc 實現。BPF 程式也是如此,必須相應地進行調整。有關詳細資訊,請參見 3. kfunc 生命週期期望

問:附加到任意核心函式是 ABI 嗎?

問:BPF 程式可以附加到許多核心函式。這些核心函式是否會成為 ABI 的一部分?

答:不是。

核心函式原型將會發生變化,並且附加到它們的 BPF 程式需要進行更改。應該使用 BPF 編譯一次,隨處執行 (CO-RE),以便更輕鬆地使您的 BPF 程式適應不同的核心版本。

問:用 BTF_ID 標記函式會使該函式成為 ABI 嗎?

答:不是。

BTF_ID 宏並不會比 EXPORT_SYMBOL_GPL 宏更會導致函式成為 ABI 的一部分。

問:map 值中特殊 BPF 型別的相容性如何?

問:允許使用者在其 BPF map 值中嵌入 bpf_spin_lock、bpf_timer 欄位(使用 BPF map 的 BTF 支援時)。這允許對 map 值內的這些欄位使用輔助函式。還允許使用者嵌入指向某些核心型別的指標(帶有 __kptr_untrusted 和 __kptr BTF 標記)。核心是否會保留這些功能的向後相容性?

答:視情況而定。對於 bpf_spin_lock、bpf_timer:是,對於 kptr 和其他所有內容:否,但請參見下文。

對於已經新增的結構型別,如 bpf_spin_lock 和 bpf_timer,核心將保留向後相容性,因為它們是 UAPI 的一部分。

對於 kptrs,它們也是 UAPI 的一部分,但僅就 kptr 機制而言。您可以在結構中使用帶有 __kptr_untrusted 和 __kptr 標記指標的型別不是 UAPI 協議的一部分。支援的型別可能會在核心版本之間發生變化。但是,諸如訪問 kptr 欄位和 bpf_kptr_xchg() 輔助函式之類的操作將在支援的型別上繼續在核心版本之間得到支援。

對於任何其他支援的結構型別,除非本文件中明確宣告並新增到 bpf.h UAPI 標頭中,否則此類型別可以並且將在核心版本之間任意更改其大小、型別和對齊方式,或任何其他使用者可見的 API 或 ABI 詳細資訊。使用者必須使其 BPF 程式適應新的更改並更新它們,以確保其程式繼續正常工作。

注意:BPF 子系統專門保留了型別名稱的“bpf_”字首,以便將來引入更多特殊欄位。因此,使用者程式必須避免定義帶有“bpf_”字首的型別,以免在以後的版本中出現中斷。換句話說,如果有人使用 BTF 中帶有“bpf_”字首的型別,則不保證向後相容性。

問:已分配物件中特殊 BPF 型別的相容性如何?

問:與上述相同,但對於已分配的物件(即使用 bpf_obj_new 為使用者定義的型別分配的物件)。核心是否會保留這些功能的向後相容性?

答:不是。

與 map 值型別不同,用於處理已分配物件以及它們內部特殊欄位的任何支援的 API 都透過 kfuncs 公開,因此具有與 kfuncs 相同的生命週期期望。有關詳細資訊,請參見 3. kfunc 生命週期期望