[譯] LLVM eBPF 彙編程式設計(2020)

語言: CN / TW / HK

[譯] LLVM eBPF 彙編程式設計(2020) Published at 2021-08-15 | Last Update 2021-08-15

譯者序

本文翻譯自 2020 年 Quentin Monnet 的一篇英文部落格: eBPF assembly with LLVM。 Quentin Monnet 是 Cilium 開發者之一,此前也在從事網路、eBPF 相關的開發。

文章介紹瞭如何直接基於 LLVM eBPF 彙編開發 BPF 程式,雖然給出的 兩個例子極其簡單,但開發更大的程式,流程也是類似的。為什麼不用 C,而用匯編 這麼不友好的程式設計方式呢?至少有兩個特殊場景:

測試特定的 eBPF 指令流 對程式的某個特定部分進行深度調優

原文歷時(開頭之後拖延)了好幾年,因此文中存在一些(檔名等)前後不一致之處,翻譯時已經改正; 另外,譯文基於 clang/llvm 10.0 驗證了其中的每個步驟,因此程式碼、輸出等與原文不完全一致。

由於譯者水平有限,本文不免存在遺漏或錯誤之處。如有疑問,請查閱原文。

以下是譯文。

譯者序 1 引言

1.1 主流開發方式:從 C 程式碼直接生成 eBPF 位元組碼 1.2 特殊場景需求:eBPF 彙編程式設計更合適 1.3 幾種 eBPF 彙編程式設計方式

2 Clang/LLVM 編譯 eBPF 基礎

2.1 將 C 程式編譯成 BPF 目標檔案 1.2 檢視 ELF 檔案中的 eBPF 位元組碼

3 方式一:C 生成 eBPF 彙編 + 手工修改彙編

3.1 將 C 編譯成 eBPF 彙編(clang) 3.2 手工修改彙編程式 3.3 將彙編程式 assemble 成 ELF 物件檔案(llvm-mc) 3.4 檢視物件檔案中的 eBPF 位元組碼(readelf) 3.5 以更加人類可讀的方式檢視 eBPF 位元組碼(llvm-objdump -d) 3.6 編譯時嵌入除錯符號或 C 原始碼(clang -g + llvm-objdump -S)

4 方式二:內聯彙編(inline assembly)

4.1 C 內聯彙編示例 4.2 編譯及檢視生成的位元組碼 4.3 小結

5 結束語

1 引言

1.1 主流開發方式:從 C 程式碼直接生成 eBPF 位元組碼

eBPF 相比於 cBPF(經典 BPF)的優勢之一是:Clang/LLVM 為它提供了一個編譯後端, 能從 C 原始碼直接生成 eBPF 位元組碼(bytecode)。(寫作本文時,GCC 也提供了一個類似 的後端,但各方面都沒有 Clang/LLVM 完善,因此後者仍然是生成 eBPF 位元組碼 的最佳參考工具)。

將 C 程式碼編譯成 eBPF 目標檔案非常有用,因為 直接用位元組碼編寫高階程式是非常耗時的。此外,截至本文寫作時, 還無法直接編寫位元組碼程式來使用 CO-RE 等複雜特性。

(譯) BPF 可移植性和 CO-RE(一次編譯,到處執行)(Facebook,2020)。 譯註。

因此,Clang 和 LLVM 仍然是 eBPF 工作流不可或缺的部分。

1.2 特殊場景需求:eBPF 彙編程式設計更合適

但是,C 方式不適用於某些特殊的場景,例如:

只是想測試特定的 eBPF 指令流 對程式的某個特定部分進行深度調優

在這些情況下,就需要直接編寫或修改 eBFP 彙編程式。

1.3 幾種 eBPF 彙編程式設計方式

直接編寫 eBPF 位元組碼程式。也就是編寫可直接載入執行的

二進位制 eBPF 程式,

這肯定是可行的,但過程非常冗長無聊,對開發者極其不友好。
  此外,為保證與 tc 等工具的相容,還要將寫好的程式轉換成目標檔案(object file),因此工作量又多了一些。



直接用 eBPF 組合語言編寫,然後用專門的彙編器

(例如 ebpf_asm)將其彙編(assemble)成位元組碼。

相比位元組碼(二進位制),組合語言(文字)至少可讀性還是好很多的。



用 LLVM 將 C 編譯成 eBPF 彙編,然後手動修改生成的彙編程式,

最後再將其彙編(assemble)成位元組碼放到物件檔案。

在 C 中插入內聯彙編,然後統一用 clang/llvm 編譯。

以上幾種方式 Clang/LLVM 都支援!先用可讀性比較好的方式寫, 然後再將其彙編(assembling)成另位元組碼程式。此外,甚至能 dump 物件檔案中包含的程式。

本文將會展示第三種和第四種方式,第二種可以認為是第三種的更加徹底版,開發的流程 、步驟等已經包括在第三種了。

2 Clang/LLVM 編譯 eBPF 基礎

在開始彙編程式設計之前,先來熟悉一下 clang/llvm 將 C 程式編譯成 eBPF 程式的過程。

2.1 將 C 程式編譯成 BPF 目標檔案

下面是個 eBPF 程式:沒做任何事情,直接返回零,

$ cat bpf.c int func() { return 0; }

如下命令可以將其編譯成物件檔案(目標檔案):

注意 target 型別指定為 bpf

$ clang -target bpf -Wall -O2 -c bpf.c -o bpf.o

某些複雜的程式可能需要用下面的命令來編譯:

$ clang -O2 -emit-llvm -c bpf.c -o - |

llc -march=bpf -mcpu=probe -filetype=obj -o bpf.o

以上命令會將 C 原始碼編譯成位元組碼,然後生成一個 ELF 格式的目標檔案。

1.2 檢視 ELF 檔案中的 eBPF 位元組碼

預設情況下,程式碼位於 ELF 的 .text 區域(section):

$ readelf -x .text bpf.o Hex dump of section '.text': 0x00000000 b7000000 00000000 95000000 00000000 ................

這就是編譯生成的位元組碼!

以上位元組碼包含了兩條 eBPF 指令:

b7 0 0 0000 00000000 # r0 = 0 95 0 0 0000 00000000 # exit and return r0

如果對 eBPF 彙編語法不熟悉,可參考:

iovisor/bpf-docs 中的簡潔文件 更詳細的核心文件 networking/filter.txt。

有了以上基礎,接下來看如何開發 eBPF 彙編程式。

3 方式一:C 生成 eBPF 彙編 + 手工修改彙編

本節需要 Clang/LLVM 6.0+ 版本(clang -v)。

譯文基於 10.0,結果與原文略有差異。

C 原始碼:

$ cat bpf.c int func() { return 0; }

3.1 將 C 編譯成 eBPF 彙編(clang)

其實前面已經看到了,與將普通 C 程式編譯成彙編類似,只是這裡指定 target 型別是 bpf (bpf target 與預設 target 的不同,見 Cilium 文件 BPF and XDP Reference Guide ):

(譯) Cilium:BPF 和 XDP 參考指南(2021)。 譯註。

$ 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 手工修改彙編程式

因為彙編程式是文字檔案,因此編輯起來很容易。 作為練手,我們在程式最後加上一行彙編指令 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 之後,因此實際上沒任何作用。

3.3 將彙編程式 assemble 成 ELF 物件檔案(llvm-mc)

接下來將 bpf.s 彙編(assemble)成包含位元組碼的 ELF 物件檔案。這 裡需要用到 LLVM 自帶的與機器碼(machine code,mc)打交道的工具 llvm-mc:

$ llvm-mc -triple bpf -filetype=obj -o bpf.o bpf.s

bpf.o 就是生成的 ELF 檔案!

3.4 檢視物件檔案中的 eBPF 位元組碼(readelf)

檢視 bpf.o 中的位元組碼:

$ readelf -x .text bpf.o

Hex dump of section '.text': 0x00000000 b7000000 00000000 95000000 00000000 ................ 0x00000010 b7000000 03000000 ........

看到和之前相比,

第一行(包含前兩條指令)一樣, 第二行是新多出來的(對應的正是我們新加的一行彙編指令),作用:將常量 3 load 到暫存器 r0 中。

至此,我們已經成功地修改了指令流。接下來就可以用 bpftool 之 類的工具將這個程式載入到核心,任務完成!

3.5 以更加人類可讀的方式檢視 eBPF 位元組碼(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

最後一列顯示了對應的 LLVM 使用的彙編指令(也是前面我們手工編輯時使用的 eBPF 指令)。

3.6 編譯時嵌入除錯符號或 C 原始碼(clang -g + llvm-objdump -S)

除了位元組碼和彙編指令,LLVM 還能將除錯資訊(debug symbols)嵌入到物件檔案, 更具體說就是能在位元組碼旁邊同時顯示對應的 C 原始碼,對除錯非常有用,也是 觀察 C 指令如何對映到 eBPF 指令的好機會。

在 clang 編譯時加上 -g 引數:

-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 方式二:內聯彙編(inline assembly)

接下來看另一種生成和編譯 eBPF 彙編的方式:直接在 C 程式中嵌入 eBPF 彙編。

4.1 C 內聯彙編示例

下面是個非常簡單的例子,受 Cilium 文件 BPF and XDP Reference Guide 的啟發:

(譯) Cilium:BPF 和 XDP 參考指南(2021)。 譯註。

$ cat inline_asm.c int func() { unsigned long long foobar = 2, r3 = 3, *foobar_addr = &foobar;

asm volatile("lock *(u64 *)(%0+0) += %1" : // 等價於:foobar += r3
     "=r"(foobar_addr) :
     "r"(r3), "0"(foobar_addr));

return foobar;

}

關鍵字 asm 用於插入彙編程式碼。

4.2 編譯及檢視生成的位元組碼

$ 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

對應到最後一列的彙編,大家應該大致能看懂。

4.3 小結

這種方式的好處是:原始碼仍然是 C,因此無需像前一種方式那樣必須手動執行編譯( compile)和彙編(assemble)兩個分開的過程。

5 結束語

本文通過兩個極簡的例子展示了兩種 eBPF 彙編程式設計方式:

手動生成並修改一段特定的指令流 在 C 中插入內聯彙編

這兩種方式我認為都是有用的,比如在 Netronome,我們經常用前一種方式做單元測試, 檢查 nfp 驅動中的 eBPF hw offload 特性。

LLVM 支援編寫任意的 eBPF 彙編程式(但提醒一下:編譯能通過是一回事,能不能通過校驗器是另一回事)。 有興趣自己試試吧!

« [譯] [論文] XDP (eXpress Data Path):在作業系統核心中實現快速、可程式設計包處理(ACM,2018)