LLVM eBPF 匯編編程教程
1 引言
1.1 主流開發(fā)方式:從 C 代碼直接生成 eBPF 字節(jié)碼
eBPF 相比于 cBPF(經(jīng)典 BPF)的優(yōu)勢之一是:Clang/LLVM 為它提供了一個(gè)編譯后端, 能從 C 源碼直接生成 eBPF 字節(jié)碼(bytecode)。(寫作本文時(shí),GCC 也提供了一個(gè)類似 的后端,但各方面都沒有 Clang/LLVM 完善,因此后者仍然是生成 eBPF 字節(jié)碼 的最佳參考工具)。
將 C 代碼編譯成 eBPF 目標(biāo)文件非常有用,因?yàn)?直接用字節(jié)碼編寫高級程序是非常耗時(shí)的。此外,截至本文寫作時(shí), 還無法直接編寫字節(jié)碼程序來使用 CO-RE 等復(fù)雜特性。
因此,Clang 和 LLVM 仍然是 eBPF 工作流不可或缺的部分。
1.2 特殊場景需求:eBPF 匯編編程更合適
但是,C 方式不適用于某些特殊的場景,例如:
只是想測試特定的 eBPF 指令流 對程序的某個(gè)特定部分進(jìn)行深度調(diào)優(yōu)
在這些情況下,就需要直接編寫或修改 eBFP 匯編程序。
1.3 幾種 eBPF 匯編編程方式
直接編寫 eBPF 字節(jié)碼程序。也就是編寫可直接加載運(yùn)行的 二進(jìn)制 eBPF 程序,
這肯定是可行的,但過程非常冗長無聊,對開發(fā)者極其不友好。 此外,為保證與 tc 等工具的兼容,還要將寫好的程序轉(zhuǎn)換成目標(biāo)文件(object file),因此工作量又多了一些。 直接用 eBPF 匯編語言編寫,然后用專門的匯編器 (例如 ebpf_asm)將其匯編(assemble)成字節(jié)碼。
相比字節(jié)碼(二進(jìn)制),匯編語言(文本)至少可讀性還是好很多的。 用 LLVM 將 C 編譯成 eBPF 匯編,然后手動修改生成的匯編程序, 最后再將其匯編(assemble)成字節(jié)碼放到對象文件。
在 C 中插入內(nèi)聯(lián)匯編,然后統(tǒng)一用 clang/llvm 編譯。
以上幾種方式 Clang/LLVM 都支持!先用可讀性比較好的方式寫, 然后再將其匯編(assembling)成另字節(jié)碼程序。此外,甚至能 dump 對象文件中包含的程序。
本文將會展示第三種和第四種方式,第二種可以認(rèn)為是第三種的更加徹底版,開發(fā)的流程 、步驟等已經(jīng)包括在第三種了。
2 Clang/LLVM 編譯 eBPF 基礎(chǔ)
在開始匯編編程之前,先來熟悉一下 clang/llvm 將 C 程序編譯成 eBPF 程序的過程。
2.1 將 C 程序編譯成 BPF 目標(biāo)文件
下面是個(gè) eBPF 程序:沒做任何事情,直接返回零,
//?bpf.c
int?func()?{
????return?0;
}
如下命令可以將其編譯成對象文件(目標(biāo)文件):
#?注意?target?類型指定為?`bpf`
$?clang?-target?bpf?-Wall?-O2?-c?bpf.c?-o?bpf.o
某些復(fù)雜的程序可能需要用下面的命令來編譯:
$?clang?-O2?-emit-llvm?-c?bpf.c?-o?-?|?\
?llc?-march=bpf?-mcpu=probe?-filetype=obj?-o?bpf.o
以上命令會將 C 源碼編譯成字節(jié)碼,然后生成一個(gè) ELF 格式的目標(biāo)文件。
1.2 查看 ELF 文件中的 eBPF 字節(jié)碼
默認(rèn)情況下,代碼位于 ELF 的 .text 區(qū)域(section):
$?readelf?-x?.text?bpf.o
Hex?dump?of?section?'.text':
??0x00000000?b7000000?00000000?95000000?00000000?................
這就是編譯生成的字節(jié)碼!
以上字節(jié)碼包含了兩條 eBPF 指令:
b7 0 0 0000 00000000 # r0 = 0
95 0 0 0000 00000000 # exit and return r0
如果對 eBPF 匯編語法不熟悉,可參考:
簡潔文檔: (https://github.com/iovisor/bpf-docs/blob/master/eBPF.md) 詳細(xì)文檔: (https://www.kernel.org/doc/Documentation/networking/filter.txt)
有了以上基礎(chǔ),接下來看如何開發(fā) eBPF 匯編程序。
3 方式一:C 生成 eBPF 匯編 + 手工修改匯編
本節(jié)需要 Clang/LLVM 6.0+ 版本(clang -v)。
譯文基于 10.0,結(jié)果與原文略有差異。
C 源碼:
//?bpf.c
int?func()?{
?return?0;
}
3.1 將 C 編譯成 eBPF 匯編(clang)
其實(shí)前面已經(jīng)看到了,與將普通 C 程序編譯成匯編類似,只是這里指定 target 類型是 bpf (bpf target 與默認(rèn) target 的不同,見 Cilium 文檔 BPF 和 XDP 參考指南):
Cilium:BPF 和 XDP 參考指南:
http://docs.cilium.io/en/latest/bpf/#llvm
$?clang?-target?bpf?-S?-o?bpf.s?bpf.c
查看生成的匯編代碼:
$?cat?bpf.s
????.text
????.file???"bpf.c"
????.globl??func?????????????????#?--?Begin?function?func
????.p2align????????3
????.type???func,@function
func:???????????????????????????#?@func
#?%bb.0:
????r0?=?0
????exit
.Lfunc_end0:
????.size???func,?.Lfunc_end0-func
???????????????????????????????#?--?End?function
????.addrsig
接下來就可以修改這段匯編代碼了。
3.2 手工修改匯編程序
因?yàn)閰R編程序是文本文件,因此編輯起來很容易。作為練手,我們在程序最后加上一行匯編指令 r0 = 3:
$?cat?bpf.s
????.text
????.file???"bpf.c"
????.globl??func????????????????????#?--?Begin?function?func
????.p2align????????3
????.type???func,@function
func:???????????????????????????????#?@func
#?%bb.0:
????r0?=?0
????exit
????r0?=?3??????????????????????????#?--?這行是我們手動加的
.Lfunc_end0:
????.size???func,?.Lfunc_end0-func
????????????????????????????????????#?--?End?function
????.addrsig
這行放在了 exit 之后,因此實(shí)際上沒任何作用。
3.3 將匯編程序 assemble 成 ELF 對象文件(llvm-mc)
接下來將 bpf.s 匯編(assemble)成包含字節(jié)碼的 ELF 對象文件。這 里需要用到 LLVM 自帶的與機(jī)器碼(machine code,mc)打交道的工具 llvm-mc:
$?llvm-mc?-triple?bpf?-filetype=obj?-o?bpf.o?bpf.s
bpf.o 就是生成的 ELF 文件!
3.4 查看對象文件中的 eBPF 字節(jié)碼(readelf)
查看 bpf.o 中的字節(jié)碼:
$?readelf?-x?.text?bpf.o
Hex?dump?of?section?'.text':
??0x00000000?b7000000?00000000?95000000?00000000?................
??0x00000010?b7000000?03000000???????????????????........
看到和之前相比,
第一行(包含前兩條指令)一樣, 第二行是新多出來的(對應(yīng)的正是我們新加的一行匯編指令),作用:將常量 3 load 到寄存器 r0 中。
至此,我們已經(jīng)成功地修改了指令流。接下來就可以用 bpftool 之 類的工具將這個(gè)程序加載到內(nèi)核,任務(wù)完成!
3.5 以更加人類可讀的方式查看 eBPF 字節(jié)碼(llvm-objdump -d)
LLVM 還能以人類可讀的方式 dump eBPF 對象文件中的指令,這里就要用到 llvm-objdump:
#?-d???????????:?alias?for?--disassemble
#?--disassemble:?display?assembler?mnemonics?for?the?machine?instructions
$?llvm-objdump?-d?bpf.o
bpf.o:??file?format?ELF64-BPF
Disassembly?of?section?.text:
0000000000000000?func:
???????0:???????b7?00?00?00?00?00?00?00?r0?=?0
???????1:???????95?00?00?00?00?00?00?00?exit
???????2:???????b7?00?00?00?03?00?00?00?r0?=?3
最后一列顯示了對應(yīng)的 LLVM 使用的匯編指令(也是前面我們手工編輯時(shí)使用的 eBPF 指令)。
3.6 編譯時(shí)嵌入調(diào)試符號或 C 源碼(clang -g + llvm-objdump -S)
除了字節(jié)碼和匯編指令,LLVM 還能將調(diào)試信息(debug symbols)嵌入到對象文件, 更具體說就是能在字節(jié)碼旁邊同時(shí)顯示對應(yīng)的 C 源碼,對調(diào)試非常有用,也是 觀察 C 指令如何映射到 eBPF 指令的好機(jī)會。
在 clang 編譯時(shí)加上 -g 參數(shù):
#?-g:?generate?debug?information.
$?clang?-target?bpf?-g?-S?-o?bpf.s?bpf.c
$?llvm-mc?-triple?bpf?-filetype=obj?-o?bpf.o?bpf.s
#?-S??????:?alias?for?--source
#?--source:?display?source?inlined?with?disassembly.?Implies?disassemble?object
$?llvm-objdump?-S?bpf.o
Disassembly?of?section?.text:
0000000000000000?func:
;?int?func()?{
???????0:???????b7?00?00?00?00?00?00?00?r0?=?0
;?????return?0;
???????1:???????95?00?00?00?00?00?00?00?exit
注意這里用的是 -S(顯示源碼),不是 -d(反匯編)。
4 方式二:內(nèi)聯(lián)匯編(inline assembly)
接下來看另一種生成和編譯 eBPF 匯編的方式:直接在 C 程序中嵌入 eBPF 匯編。
4.1 C 內(nèi)聯(lián)匯編示例
下面是個(gè)非常簡單的例子,受 Cilium 文檔 BPF 和 XDP 參考指南的啟發(fā):
//?inline_asm.c
int?func()?{
????unsigned?long?long?foobar?=?2,?r3?=?3,?*foobar_addr?=?&foobar;
????asm?volatile("lock?*(u64?*)(%0+0)?+=?%1"?:?//?等價(jià)于:foobar += r3
?????????"=r"(foobar_addr)?:
?????????"r"(r3),?"0"(foobar_addr));
????return?foobar;
}
關(guān)鍵字 asm 用于插入?yún)R編代碼。
4.2 編譯及查看生成的字節(jié)碼
$?clang?-target?bpf?-Wall?-O2?-c?inline_asm.c?-o?inline_asm.o
反匯編:
$?llvm-objdump?-d?inline_asm.o
Disassembly?of?section?.text:
0000000000000000?func:
???????0:???????b7?01?00?00?02?00?00?00?r1?=?2
???????1:???????7b?1a?f8?ff?00?00?00?00?*(u64?*)(r10?-?8)?=?r1
???????2:???????b7?01?00?00?03?00?00?00?r1?=?3
???????3:???????bf?a2?00?00?00?00?00?00?r2?=?r10
???????4:???????07?02?00?00?f8?ff?ff?ff?r2?+=?-8
???????5:???????db?12?00?00?00?00?00?00?lock?*(u64?*)(r2?+?0)?+=?r1
???????6:???????79?a0?f8?ff?00?00?00?00?r0?=?*(u64?*)(r10?-?8)
???????7:???????95?00?00?00?00?00?00?00?exit
對應(yīng)到最后一列的匯編,大家應(yīng)該大致能看懂。
4.3 小結(jié)
這種方式的好處是:源碼仍然是 C,因此無需像前一種方式那樣必須手動執(zhí)行編譯( compile)和匯編(assemble)兩個(gè)分開的過程。
5 結(jié)束語
本文通過兩個(gè)極簡的例子展示了兩種 eBPF 匯編編程方式:
手動生成并修改一段特定的指令流 在 C 中插入內(nèi)聯(lián)匯編
這兩種方式我認(rèn)為都是有用的,比如在 Netronome,我們經(jīng)常用前一種方式做單元測試, 檢查 nfp 驅(qū)動中的 eBPF hw offload 特性。
LLVM 支持編寫任意的 eBPF 匯編程序(但提醒一下:編譯能通過是一回事,能不能通過校驗(yàn)器是另一回事)。有興趣自己試試吧!
原文:?https://arthurchiao.art/blog/ebpf-assembly-with-llvm-zh/
