我是 LEE涌矢,老李扁达,一個在 IT 行業(yè)摸爬滾打 16 年的技術老兵叔汁。
事件背景
在完成了第一章的編寫《ebpf 開發(fā)入門之 helloworld》后叹坦,繼續(xù)往下寫多少怎么寫都是我最近思考的問題。跟周邊的小伙伴一起溝通后扇雕,認為 epbf 還在繼續(xù)發(fā)展拓售,重點應該關注它的核心概念,而不是重點關注它底層的內部實現(xiàn)镶奉,畢竟我的環(huán)境是對 epbf 最大程度的使用础淤,而不是深入開發(fā)崭放。所以經過一段時間的思考,覺得為了使用 cilium 深入 ebpf 研究無可厚非鸽凶,但是過分深入則可能不太合適币砂。
有了上面的觀點,那么我就再寫一個文章輸出 epbf 的核心概念玻侥,后續(xù)深入的小伙伴可以根據(jù)自己實際需要找資料深入决摧。
世界觀
在講解具體概念之前,我們先科普下 epbf 的整體世界觀凑兰。
Hook
中文名
鉤子
大白話
在 epbf 的世界里看 Linux 內核所有核心調用都可以 Hook掌桩,可以理解成為萬物皆可掛鉤子做 Callback。
具體解釋
eBPF 程序都是事件驅動的姑食,它們會在內核或者應用程序經過某個確定的 Hook 點的時候運行波岛,這些 Hook 點都是提前定義的,包括系統(tǒng)調用音半、函數(shù)進入/退出则拷、內核 tracepoints、網絡事件等祟剔。
如果針對某個特定需求的 Hook 點不存在隔躲,可以通過 kprobe 或者 uprobe 來在內核或者用戶程序的幾乎所有地方掛載 eBPF 程序。
Verifier
中文名
驗證器
大白話
生成應用內核層的 bytescode 要想進入到內核中去運行物延,必然要有個“安全檢查員”對這個 bytescode 的安全和合法性進行檢測宣旱。
具體解釋
每一個 eBPF 程序加載到內核都要經過 Verification,用來保證 eBPF 程序的安全性叛薯,主要包括:
-
要保證 加載 eBPF 程序的進程有必要的特權級浑吟,除非節(jié)點開啟了 unpriviledged 特性,只有特權級的程序才能夠加載 eBPF 程序
內核提供了一個配置項 /proc/sys/kernel/unprivileged_bpf_disabled 來禁止非 特權用戶使用 bpf(2) 系統(tǒng)調用耗溜,可以通過 sysctl 命令修改
比較特殊的一點是组力,這個配置項特意設計為一次性開關(one-time kill switch), 這 意味著一旦將它設為 1抖拴,就沒有辦法再改為 0 了燎字,除非重啟內核
-
一旦設置為 1 之后,只有初始命名空間中有 CAP_SYS_ADMIN 特權的進程才可以調用 bpf(2) 系統(tǒng)調用 阿宅。Cilium 啟動后也會將這個配置項設為 1
# echo 1 > /proc/sys/kernel/unprivileged_bpf_disabled
要保證 eBPF 程序不會崩潰或者使得系統(tǒng)出故障
要保證 eBPF 程序不能陷入死循環(huán)候衍,能夠 runs to completion
要保證 eBPF 程序必須滿足系統(tǒng)要求的大小,過大的 eBPF 程序不允許被加載進內核
要保證 eBPF 程序的復雜度有限洒放,Verifier 將會評估 eBPF 程序所有可能的執(zhí)行路徑蛉鹿,必須能夠在有限時間內完成 eBPF 程序復雜度分析
JIT Compiler
中文名
JIT 編譯器
大白話
跟 java 的 JVM 有點類似,就是把 bytescode 編譯成本機能夠運行的二進制代碼往湿。
具體解釋
Just-In-Time(JIT) 編譯用來將通用的 eBPF 字節(jié)碼翻譯成與機器相關的指令集妖异,從而極大加速 BPF 程序的執(zhí)行:
- 與解釋器相比惋戏,它們可以降低每個指令的開銷。通常他膳,指令可以 1:1 映射到底層架構的原生指令
- 這也會減少生成的可執(zhí)行鏡像的大小响逢,因此對 CPU 的指令緩存更友好
- 特別地,對于 CISC 指令集(例如 x86)矩乐,JIT 做了很多特殊優(yōu)化龄句,目的是為給定的指令產生可能的最短操作碼,以降低程序翻譯過程所需的空間
概念講解
有了世界觀的上的認識散罕,同時這篇文章是作為入門,核心概念不應該說的太細太深傀蓉,這樣容易勸退很多小伙伴欧漱。所以這里采用“一句話”說明白的方式解釋和介紹 epbf 的核心概念。
Helper Functions
中文名
輔助函數(shù)
大白話
應用在用戶層不能直接訪問內核層的數(shù)據(jù)葬燎,那么就需要一個代理人幫忙去執(zhí)行误甚,等待執(zhí)行完畢后獲得返回結果。
具體解釋
eBPF 程序不能夠隨意調用內核函數(shù)谱净,如果這么做的話會導致 eBPF 程序與特定的內核版本綁定窑邦,相反它內核定義的一系列 Helper functions。Helper functions 使得 BPF 能夠通過一組內核定義的穩(wěn)定的函數(shù)調用來從內核中查詢數(shù)據(jù)壕探,或者將數(shù)據(jù)推送到內核冈钦。所有的 BPF 輔助函數(shù)都是核心內核的一部分,無法通過內核模塊來擴展或添加李请。
不同類型的 BPF 程序能夠使用的輔助函數(shù)可能是不同的瞧筛,例如:
- 與 attach 到 tc 層的 BPF 程序相比,attach 到 socket 的 BPF 程序只能夠調用前者可以調用的輔助函數(shù)的一個子集
- lightweight tunneling 使用的封裝和解封裝輔助函數(shù)导盅,只能被更低的 tc 層使用较幌;而推送通知到用戶態(tài)所使用的事件輸出輔助函數(shù),既可以被 tc 程序使用也可以被 XDP 程序使用
Maps
中文名
映射存儲
大白話
ebpf Map 是駐留在內核空間中的高效 Key/Value store白翻,包含多種類型的 Map乍炉,由內核實現(xiàn)其功能。用來作為用戶層和內核層之間數(shù)據(jù)交換的媒介滤馍,同時可以在不同程序之間共享數(shù)據(jù)岛琼。
具體解釋
BPF Map 的交互場景有以下幾種:
- BPF 程序和用戶態(tài)程序的交互:BPF 程序運行完,得到的結果存儲到 map 中纪蜒,供用戶態(tài)程序通過文件描述符訪問
- BPF 程序和內核態(tài)程序的交互:和 BPF 程序以外的內核程序交互衷恭,也可以使用 map 作為中介
- BPF 程序間交互:如果 BPF 程序內部需要用全局變量來交互,但是由于安全原因 BPF 程序不允許訪問全局變量纯续,可以使用 map 來充當全局變量
- BPF Tail call:Tail call 是一個 BPF 程序跳轉到另一 BPF 程序随珠,BPF 程序首先通過 BPF_MAP_TYPE_PROG_ARRAY 類型的 map 來知道另一個 BPF 程序的指針灭袁,然后調用 tail_call() 的 helper function 來執(zhí)行 Tail call
- 共享 map 的 BPF 程序不要求是相同的程序類型,例如 tracing 程序可以和網絡程序共享 map窗看,單個 BPF 程序目前最多可直接訪問 64 個不同 map茸歧。
內核中的 通用 map 有:
- BPF_MAP_TYPE_HASH
- BPF_MAP_TYPE_ARRAY
- BPF_MAP_TYPE_PERCPU_HASH
- BPF_MAP_TYPE_PERCPU_ARRAY
- BPF_MAP_TYPE_LRU_HASH
- BPF_MAP_TYPE_LRU_PERCPU_HASH
- BPF_MAP_TYPE_LPM_TRIE
內核中的 非通用 map 有:
- BPF_MAP_TYPE_PROG_ARRAY:一個數(shù)組 map,用于 hold 其他的 BPF 程序
- BPF_MAP_TYPE_PERF_EVENT_ARRAY
- BPF_MAP_TYPE_CGROUP_ARRAY:用于檢查 skb 中的 cgroup2 成員信息
- BPF_MAP_TYPE_STACK_TRACE:用于存儲棧跟蹤的 MAP
- BPF_MAP_TYPE_ARRAY_OF_MAPS:持有(hold) 其他 map 的指針显沈,這樣整個 map 就可以在運行時實現(xiàn)原子替換
- BPF_MAP_TYPE_HASH_OF_MAPS:持有(hold) 其他 map 的指針软瞎,這樣整個 map 就可以在運行時實現(xiàn)原子替換
Object Pinning
中文名
釘住對象 (非常奇怪的翻譯,但是看源代碼拉讯,翻譯 pin 為固定和被釘在那里還是滿合適的)
大白話
ebpf map 和程序作為內核資源只能通過文件描述符訪問(fd)涤浇,這個映射實際就是 fd 到內存對象的屬性路徑的一個映射,這個映射過程叫 pin魔慷。
具體解釋
(★)ebpf map 和程序作為內核資源只能通過文件描述符訪問只锭,其背后是內核中的匿名 inode。 這個觀點很重要院尔,因為 pin 這個行為都是依據(jù)這個概念來的蜻展。
這樣做的優(yōu)點:
- 用戶空間應用程序能夠使用大部分文件描述符相關的 API
- 傳遞給 Unix socket 的文件描述符是透明工作等等
這樣做的缺點:
文件描述符受限于進程的生命周期,使得 map 共享之類的操作非常笨重邀摆,這給某些特定的場景帶來了很多復雜性纵顾。
解法
為了解決這個問題,內核實現(xiàn)了一個最小內核空間 BPF 文件系統(tǒng)栋盹,BPF map 和 BPF 程序 都可以 pin 到這個文件系統(tǒng)內施逾,這個過程稱為 object pinning。BPF 相關的文件系統(tǒng)不是單例模式(singleton)贞盯,它支持多掛載實例音念、硬鏈接、軟連接等等躏敢。
相應的闷愤,BPF 系統(tǒng)調用擴展了兩個新命令,如下圖所示:
- BPF_OBJ_PIN:釘住一個對象
- BPF_OBJ_GET:獲取一個被釘住的對象
Tail Calls
中文名
尾調用
大白話
一個 BPF 程序可以調用另一個 BPF 程序件余,并且調用完成后不用返回到原來的程序讥脐。
具體解釋
尾調用的機制是指:一個 BPF 程序可以調用另一個 BPF 程序,并且調用完成后不用返回到原來的程序啼器。
- 和普通函數(shù)調用相比旬渠,這種調用方式開銷最小,因為它是用長跳轉(long jump)實現(xiàn)的端壳,復用了原來的棧幀 (stack frame)
- BPF 程序都是獨立驗證的告丢,因此要傳遞狀態(tài),要么使用 per-CPU map 作為 scratch 緩沖區(qū) 损谦,要么如果是 tc 程序的話岖免,還可以使用 skb 的某些字段(例如 cb[])
- 相同類型的程序才可以尾調用岳颇,而且它們還要與 JIT 編譯器相匹配,因此要么是 JIT 編譯執(zhí)行颅湘,要么是解釋器執(zhí)行(invoke interpreted programs)话侧,但不能同時使用兩種方式
Hardening
中文名
硬化 (明明說的就是安全,但是用 Hardening 這個單詞闯参,覺得有點奇怪)
大白話
硬化實際是對 epbf 運行狀態(tài)的值和數(shù)據(jù)進行保護瞻鹏,防止以外被篡改和破壞,是一種暗轉防護機制鹿寨。
具體解釋
在程序的生命周期內新博,BPF 將內核中的整個 BPF 解釋器映像(struct bpf_prog)以及 JIT 編譯映像(struct bpf_binary_header)鎖定為只讀,以防止代碼被破壞释移。例如叭披,由于某些內核 bug 而發(fā)生的任何損壞都會導致一般的保護故障,從而導致內核崩潰玩讳,而不是讓損壞靜靜地發(fā)生。
對于 x86_64 JIT 編譯器嚼贡,如果 CONFIG_RETPOLINE 已經設置(大多數(shù) Linux 發(fā)行版在編寫時都是默認設置)熏纯,則通過 retpoline 實現(xiàn)從使用尾部調用的間接跳轉的 JIT。
在/proc/sys/net/core/bpf_jit_harden 設置為 1 的情況下粤策,JIT 編譯的額外加固步驟將對非特權用戶生效樟澜。在不受信任的用戶對系統(tǒng)進行操作的情況下,通過減少(潛在的)攻擊面叮盘,可以有效地略微權衡它們的性能秩贰。與完全切換到解釋器相比,程序執(zhí)行時間的減少仍然會帶來更好的性能柔吼。
通過將實際指令隨機化毒费,這意味著通過將值的實際負載分成兩個步驟來重寫指令,將操作從基于即時的源操作數(shù)轉換為基于寄存器的操作數(shù):
- 加載一個盲化后的(blinded)立即數(shù) rnd ^ imm 到寄存器
- 將寄存器和 rnd 進行異或操作(xor)
Offloads
中文名
卸載
大白話
就是把 eBPF 的網絡程序內核層 bytescode 從 CPU 運行改為由網卡的 MPU 來執(zhí)行愈魏。
具體解釋
eBPF 網絡程序觅玻,尤其是 tc 和 XDP BPF 程序在內核中都有一個 offload 到硬件的接口,這樣就可以直接在網卡上執(zhí)行 BPF 程序培漏。