BPF LLVM 重定位¶
本文件描述了 LLVM BPF 後端重定位型別。
重定位記錄¶
LLVM BPF 後端使用以下 16 位元組 ELF 結構記錄每個重定位
typedef struct
{
Elf64_Addr r_offset; // Offset from the beginning of section.
Elf64_Xword r_info; // Relocation type and symbol index.
} Elf64_Rel;
例如,對於以下程式碼
int g1 __attribute__((section("sec")));
int g2 __attribute__((section("sec")));
static volatile int l1 __attribute__((section("sec")));
static volatile int l2 __attribute__((section("sec")));
int test() {
return g1 + g2 + l1 + l2;
}
使用 clang --target=bpf -O2 -c test.c 編譯後,使用 llvm-objdump -dr test.o 檢視的程式碼如下
0: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
0000000000000000: R_BPF_64_64 g1
2: 61 11 00 00 00 00 00 00 r1 = *(u32 *)(r1 + 0)
3: 18 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r2 = 0 ll
0000000000000018: R_BPF_64_64 g2
5: 61 20 00 00 00 00 00 00 r0 = *(u32 *)(r2 + 0)
6: 0f 10 00 00 00 00 00 00 r0 += r1
7: 18 01 00 00 08 00 00 00 00 00 00 00 00 00 00 00 r1 = 8 ll
0000000000000038: R_BPF_64_64 sec
9: 61 11 00 00 00 00 00 00 r1 = *(u32 *)(r1 + 0)
10: 0f 10 00 00 00 00 00 00 r0 += r1
11: 18 01 00 00 0c 00 00 00 00 00 00 00 00 00 00 00 r1 = 12 ll
0000000000000058: R_BPF_64_64 sec
13: 61 11 00 00 00 00 00 00 r1 = *(u32 *)(r1 + 0)
14: 0f 10 00 00 00 00 00 00 r0 += r1
15: 95 00 00 00 00 00 00 00 exit
上述程式碼中有四個 LD_imm64 指令的四個重定位。以下 llvm-readelf -r test.o 顯示了這四個重定位的二進位制值
Relocation section '.rel.text' at offset 0x190 contains 4 entries:
Offset Info Type Symbol's Value Symbol's Name
0000000000000000 0000000600000001 R_BPF_64_64 0000000000000000 g1
0000000000000018 0000000700000001 R_BPF_64_64 0000000000000004 g2
0000000000000038 0000000400000001 R_BPF_64_64 0000000000000000 sec
0000000000000058 0000000400000001 R_BPF_64_64 0000000000000000 sec
每個重定位由 Offset(8 位元組)和 Info(8 位元組)表示。例如,第一個重定位對應於第一個指令(偏移 0x0),並且相應的 Info 指示重定位型別為 R_BPF_64_64(型別 1)以及符號表中的條目(條目 6)。以下是使用 llvm-readelf -s test.o 檢視的符號表
Symbol table '.symtab' contains 8 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000008 4 OBJECT LOCAL DEFAULT 4 l1
3: 000000000000000c 4 OBJECT LOCAL DEFAULT 4 l2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 sec
5: 0000000000000000 128 FUNC GLOBAL DEFAULT 2 test
6: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 g1
7: 0000000000000004 4 OBJECT GLOBAL DEFAULT 4 g2
第 6 個條目是全域性變數 g1,其值為 0。
同樣,第二個重定位位於 .text 偏移 0x18,指令 3,型別為 R_BPF_64_64 並引用符號表中的條目 7。第二個重定位解析為全域性變數 g2,其符號值為 4。符號值表示從 .data 節開頭到儲存全域性變數 g2 初始值的位置的偏移量。
第三和第四個重定位引用靜態變數 l1 和 l2。從上面的 .rel.text 節中,不清楚它們實際引用的是哪個符號,因為它們都引用符號表條目 4,即符號 sec,該符號具有 STT_SECTION 型別並表示一個節。因此,對於靜態變數或函式,節偏移量會寫入原始指令緩衝區,這被稱為 A(加數)。檢視上面的指令 7 和 11,它們的節偏移量分別為 8 和 12。從符號表中,我們可以發現它們分別對應 l1 和 l2 的條目 2 和 3。
通常,對於全域性變數和函式,A 為 0;對於靜態變數/函式,A 是節偏移量或基於節偏移量的某些計算結果。非節偏移量的情況指的是函式呼叫。詳見下文。
不同的重定位型別¶
支援六種重定位型別。以下是概述,其中 S 表示符號表中符號的值
Enum ELF Reloc Type Description BitSize Offset Calculation
0 R_BPF_NONE None
1 R_BPF_64_64 ld_imm64 insn 32 r_offset + 4 S + A
2 R_BPF_64_ABS64 normal data 64 r_offset S + A
3 R_BPF_64_ABS32 normal data 32 r_offset S + A
4 R_BPF_64_NODYLD32 .BTF[.ext] data 32 r_offset S + A
10 R_BPF_64_32 call insn 32 r_offset + 4 (S + A) / 8 - 1
例如,R_BPF_64_64 重定位型別用於 ld_imm64 指令。實際要重定位的資料(0 或節偏移量)儲存在 r_offset + 4 處,讀/寫資料位大小為 32(4 位元組)。重定位可以透過符號值加上隱式加數來解析。請注意,BitSize 為 32,這意味著節偏移量必須小於或等於 UINT32_MAX,這是由 LLVM BPF 後端強制執行的。
在另一種情況下,R_BPF_64_ABS64 重定位型別用於正常的 64 位資料。實際要重定位的資料儲存在 r_offset 處,讀/寫資料位大小為 64(8 位元組)。重定位可以透過符號值加上隱式加數來解析。
`R_BPF_64_ABS32` 和 `R_BPF_64_NODYLD32` 型別都用於 32 位資料。但 `R_BPF_64_NODYLD32` 特別指的是 `.BTF` 和 `.BTF.ext` 節中的重定位。對於涉及 LLVM `ExecutionEngine RuntimeDyld` 的情況(例如 bcc),`R_BPF_64_NODYLD32` 型別的重定位不應解析為實際的函式/變數地址。否則,bcc 和核心將無法使用 `.BTF` 和 `.BTF.ext`。
型別 R_BPF_64_32 用於呼叫指令。呼叫目標節偏移量儲存在 r_offset + 4 處(32 位),並計算為 (S + A) / 8 - 1。
示例¶
型別 R_BPF_64_64 和 R_BPF_64_32 用於解析 ld_imm64 和 call 指令。例如
__attribute__((noinline)) __attribute__((section("sec1")))
int gfunc(int a, int b) {
return a * b;
}
static __attribute__((noinline)) __attribute__((section("sec1")))
int lfunc(int a, int b) {
return a + b;
}
int global __attribute__((section("sec2")));
int test(int a, int b) {
return gfunc(a, b) + lfunc(a, b) + global;
}
使用 clang --target=bpf -O2 -c test.c 編譯後,我們將得到以下程式碼,可以使用 `llvm-objdump -dr test.o` 檢視
Disassembly of section .text:
0000000000000000 <test>:
0: bf 26 00 00 00 00 00 00 r6 = r2
1: bf 17 00 00 00 00 00 00 r7 = r1
2: 85 10 00 00 ff ff ff ff call -1
0000000000000010: R_BPF_64_32 gfunc
3: bf 08 00 00 00 00 00 00 r8 = r0
4: bf 71 00 00 00 00 00 00 r1 = r7
5: bf 62 00 00 00 00 00 00 r2 = r6
6: 85 10 00 00 02 00 00 00 call 2
0000000000000030: R_BPF_64_32 sec1
7: 0f 80 00 00 00 00 00 00 r0 += r8
8: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
0000000000000040: R_BPF_64_64 global
10: 61 11 00 00 00 00 00 00 r1 = *(u32 *)(r1 + 0)
11: 0f 10 00 00 00 00 00 00 r0 += r1
12: 95 00 00 00 00 00 00 00 exit
Disassembly of section sec1:
0000000000000000 <gfunc>:
0: bf 20 00 00 00 00 00 00 r0 = r2
1: 2f 10 00 00 00 00 00 00 r0 *= r1
2: 95 00 00 00 00 00 00 00 exit
0000000000000018 <lfunc>:
3: bf 20 00 00 00 00 00 00 r0 = r2
4: 0f 10 00 00 00 00 00 00 r0 += r1
5: 95 00 00 00 00 00 00 00 exit
第一個重定位對應於 gfunc(a, b),其中 gfunc 的值為 0,因此 call 指令的偏移量是 (0 + 0)/8 - 1 = -1。第二個重定位對應於 lfunc(a, b),其中 lfunc 的節偏移量為 0x18,因此 call 指令的偏移量是 (0 + 0x18)/8 - 1 = 2。第三個重定位對應於 global 的 ld_imm64,其節偏移量為 0。
以下是一個示例,展示瞭如何生成 R_BPF_64_ABS64
int global() { return 0; }
struct t { void *g; } gbl = { global };
使用 clang --target=bpf -O2 -g -c test.c 編譯後,我們將在 .data 節中看到一個重定位,使用命令 llvm-readelf -r test.o
Relocation section '.rel.data' at offset 0x458 contains 1 entries:
Offset Info Type Symbol's Value Symbol's Name
0000000000000000 0000000700000002 R_BPF_64_ABS64 0000000000000000 global
該重定位表示 .data 節的前 8 位元組應填充 global 變數的地址。
透過 llvm-readelf 的輸出,我們可以看到 DWARF 節有大量的 R_BPF_64_ABS32 和 R_BPF_64_ABS64 重定位
Relocation section '.rel.debug_info' at offset 0x468 contains 13 entries:
Offset Info Type Symbol's Value Symbol's Name
0000000000000006 0000000300000003 R_BPF_64_ABS32 0000000000000000 .debug_abbrev
000000000000000c 0000000400000003 R_BPF_64_ABS32 0000000000000000 .debug_str
0000000000000012 0000000400000003 R_BPF_64_ABS32 0000000000000000 .debug_str
0000000000000016 0000000600000003 R_BPF_64_ABS32 0000000000000000 .debug_line
000000000000001a 0000000400000003 R_BPF_64_ABS32 0000000000000000 .debug_str
000000000000001e 0000000200000002 R_BPF_64_ABS64 0000000000000000 .text
000000000000002b 0000000400000003 R_BPF_64_ABS32 0000000000000000 .debug_str
0000000000000037 0000000800000002 R_BPF_64_ABS64 0000000000000000 gbl
0000000000000040 0000000400000003 R_BPF_64_ABS32 0000000000000000 .debug_str
......
`.BTF/.BTF.ext` 節包含 `R_BPF_64_NODYLD32` 重定位
Relocation section '.rel.BTF' at offset 0x538 contains 1 entries:
Offset Info Type Symbol's Value Symbol's Name
0000000000000084 0000000800000004 R_BPF_64_NODYLD32 0000000000000000 gbl
Relocation section '.rel.BTF.ext' at offset 0x548 contains 2 entries:
Offset Info Type Symbol's Value Symbol's Name
000000000000002c 0000000200000004 R_BPF_64_NODYLD32 0000000000000000 .text
0000000000000040 0000000200000004 R_BPF_64_NODYLD32 0000000000000000 .text
CO-RE 重定位¶
從目標檔案的角度來看,CO-RE 機制被實現為一組 CO-RE 特定的重定位記錄。這些重定位記錄與 ELF 重定位無關,並編碼在 .BTF.ext 節中。有關 .BTF.ext 結構的更多資訊,請參閱 Documentation/bpf/btf.rst。
CO-RE 重定位應用於 BPF 指令,以便在載入時使用與目標核心相關的資訊更新指令的即時欄位或偏移欄位。
根據指令類別選擇要修補的欄位
對於 BPF_ALU, BPF_ALU64, BPF_LD,`immediate` 欄位被修補;
對於 BPF_LDX, BPF_STX, BPF_ST,`offset` 欄位被修補;
BPF_JMP, BPF_JMP32 指令不應被修補。
重定位型別¶
有幾種 CO-RE 重定位型別,可以分為三組
基於欄位 - 使用欄位相關資訊修補指令,例如,更改 BPF_LDX 指令的偏移欄位以反映目標核心中特定結構欄位的偏移量。
基於型別 - 使用型別相關資訊修補指令,例如,將 BPF_ALU 移動指令的即時欄位更改為 0 或 1,以反映目標核心中是否存在特定型別。
基於列舉 - 使用列舉相關資訊修補指令,例如,更改 BPF_LD_IMM64 指令的即時欄位以反映目標核心中特定列舉字面量的值。
重定位型別的完整列表由以下列舉表示
enum bpf_core_relo_kind {
BPF_CORE_FIELD_BYTE_OFFSET = 0, /* field byte offset */
BPF_CORE_FIELD_BYTE_SIZE = 1, /* field size in bytes */
BPF_CORE_FIELD_EXISTS = 2, /* field existence in target kernel */
BPF_CORE_FIELD_SIGNED = 3, /* field signedness (0 - unsigned, 1 - signed) */
BPF_CORE_FIELD_LSHIFT_U64 = 4, /* bitfield-specific left bitshift */
BPF_CORE_FIELD_RSHIFT_U64 = 5, /* bitfield-specific right bitshift */
BPF_CORE_TYPE_ID_LOCAL = 6, /* type ID in local BPF object */
BPF_CORE_TYPE_ID_TARGET = 7, /* type ID in target kernel */
BPF_CORE_TYPE_EXISTS = 8, /* type existence in target kernel */
BPF_CORE_TYPE_SIZE = 9, /* type size in bytes */
BPF_CORE_ENUMVAL_EXISTS = 10, /* enum value existence in target kernel */
BPF_CORE_ENUMVAL_VALUE = 11, /* enum value integer value */
BPF_CORE_TYPE_MATCHES = 12, /* type match in target kernel */
};
注意
BPF_CORE_FIELD_LSHIFT_U64和BPF_CORE_FIELD_RSHIFT_U64旨在用於使用以下演算法讀取位欄位值// To read bitfield ``f`` from ``struct s`` is_signed = relo(s->f, BPF_CORE_FIELD_SIGNED) off = relo(s->f, BPF_CORE_FIELD_BYTE_OFFSET) sz = relo(s->f, BPF_CORE_FIELD_BYTE_SIZE) l = relo(s->f, BPF_CORE_FIELD_LSHIFT_U64) r = relo(s->f, BPF_CORE_FIELD_RSHIFT_U64) // define ``v`` as signed or unsigned integer of size ``sz`` v = *({s|u}<sz> *)((void *)s + off) v <<= l v >>= r
BPF_CORE_TYPE_MATCHES查詢匹配關係,定義如下對於整數:如果大小和有符號性匹配,則型別匹配;
對於陣列和指標:目標型別遞迴匹配;
對於結構體和聯合體
區域性成員需要在目標中以相同的名稱存在;
對於每個成員,我們遞迴檢查匹配,除非它已經在一個指標後面,在這種情況下我們只檢查匹配的名稱和相容的型別;
對於列舉
區域性變體必須在目標中透過符號名稱(而非數值)進行匹配;
大小必須匹配(但 enum 可以匹配 enum64,反之亦然);
對於函式指標
區域性型別中引數的數量和位置必須與目標匹配;
對於每個引數和返回值,我們遞迴檢查匹配。
CO-RE 重定位記錄¶
重定位記錄編碼為以下結構
struct bpf_core_relo {
__u32 insn_off;
__u32 type_id;
__u32 access_str_off;
enum bpf_core_relo_kind kind;
};
insn_off- 與此重定位關聯的程式碼節中的指令偏移量(位元組);type_id- 可重定位型別或欄位的“根”(包含)實體的 BTF 型別 ID;access_str_off- 對應 .BTF 字串節中的偏移量。字串解釋取決於特定的重定位型別對於基於欄位的重定位,字串使用一系列欄位和陣列索引(由冒號 (:) 分隔)編碼所訪問的欄位。它在概念上與 LLVM 的 getelementptr 指令的引數非常接近,用於識別字段的偏移量。例如,考慮以下 C 程式碼
struct sample { int a; int b; struct { int c[10]; }; } __attribute__((preserve_access_index)); struct sample *s;
對
s[0].a的訪問將被編碼為0:00:s的第一個元素(如同s是一個數組);0:struct sample中欄位a的索引。
對
s->a的訪問也將被編碼為0:0。對
s->b的訪問將被編碼為0:10:s的第一個元素;1:struct sample中欄位b的索引。
對
s[1].c[5]的訪問將被編碼為1:2:0:51:s的第二個元素;2:struct sample中匿名結構體欄位的索引;0:匿名結構體中欄位c的索引;5:訪問陣列元素 #5。
對於基於型別的重定位,字串預計僅為“0”;
- 對於基於列舉值的重定位,字串包含列舉的索引
其列舉型別中的值;
kind-enum bpf_core_relo_kind中的一個。
CO-RE 重定位示例¶
對於以下 C 程式碼
struct foo {
int a;
int b;
unsigned c:15;
} __attribute__((preserve_access_index));
enum bar { U, V };
結合以下 BTF 定義
...
[2] STRUCT 'foo' size=8 vlen=2
'a' type_id=3 bits_offset=0
'b' type_id=3 bits_offset=32
'c' type_id=4 bits_offset=64 bitfield_size=15
[3] INT 'int' size=4 bits_offset=0 nr_bits=32 encoding=SIGNED
[4] INT 'unsigned int' size=4 bits_offset=0 nr_bits=32 encoding=(none)
...
[16] ENUM 'bar' encoding=UNSIGNED size=4 vlen=2
'U' val=0
'V' val=1
當使用 __attribute__((preserve_access_index)) 時,欄位偏移重定位會自動生成,例如
void alpha(struct foo *s, volatile unsigned long *g) {
*g = s->a;
s->a = 1;
}
00 <alpha>:
0: r3 = *(s32 *)(r1 + 0x0)
00: CO-RE <byte_off> [2] struct foo::a (0:0)
1: *(u64 *)(r2 + 0x0) = r3
2: *(u32 *)(r1 + 0x0) = 0x1
10: CO-RE <byte_off> [2] struct foo::a (0:0)
3: exit
所有重定位型別都可以透過內建函式請求。例如,基於欄位的重定位
void bravo(struct foo *s, volatile unsigned long *g) {
*g = __builtin_preserve_field_info(s->b, 0 /* field byte offset */);
*g = __builtin_preserve_field_info(s->b, 1 /* field byte size */);
*g = __builtin_preserve_field_info(s->b, 2 /* field existence */);
*g = __builtin_preserve_field_info(s->b, 3 /* field signedness */);
*g = __builtin_preserve_field_info(s->c, 4 /* bitfield left shift */);
*g = __builtin_preserve_field_info(s->c, 5 /* bitfield right shift */);
}
20 <bravo>:
4: r1 = 0x4
20: CO-RE <byte_off> [2] struct foo::b (0:1)
5: *(u64 *)(r2 + 0x0) = r1
6: r1 = 0x4
30: CO-RE <byte_sz> [2] struct foo::b (0:1)
7: *(u64 *)(r2 + 0x0) = r1
8: r1 = 0x1
40: CO-RE <field_exists> [2] struct foo::b (0:1)
9: *(u64 *)(r2 + 0x0) = r1
10: r1 = 0x1
50: CO-RE <signed> [2] struct foo::b (0:1)
11: *(u64 *)(r2 + 0x0) = r1
12: r1 = 0x31
60: CO-RE <lshift_u64> [2] struct foo::c (0:2)
13: *(u64 *)(r2 + 0x0) = r1
14: r1 = 0x31
70: CO-RE <rshift_u64> [2] struct foo::c (0:2)
15: *(u64 *)(r2 + 0x0) = r1
16: exit
基於型別的重定位
void charlie(struct foo *s, volatile unsigned long *g) {
*g = __builtin_preserve_type_info(*s, 0 /* type existence */);
*g = __builtin_preserve_type_info(*s, 1 /* type size */);
*g = __builtin_preserve_type_info(*s, 2 /* type matches */);
*g = __builtin_btf_type_id(*s, 0 /* type id in this object file */);
*g = __builtin_btf_type_id(*s, 1 /* type id in target kernel */);
}
88 <charlie>:
17: r1 = 0x1
88: CO-RE <type_exists> [2] struct foo
18: *(u64 *)(r2 + 0x0) = r1
19: r1 = 0xc
98: CO-RE <type_size> [2] struct foo
20: *(u64 *)(r2 + 0x0) = r1
21: r1 = 0x1
a8: CO-RE <type_matches> [2] struct foo
22: *(u64 *)(r2 + 0x0) = r1
23: r1 = 0x2 ll
b8: CO-RE <local_type_id> [2] struct foo
25: *(u64 *)(r2 + 0x0) = r1
26: r1 = 0x2 ll
d0: CO-RE <target_type_id> [2] struct foo
28: *(u64 *)(r2 + 0x0) = r1
29: exit
基於列舉的重定位
void delta(struct foo *s, volatile unsigned long *g) {
*g = __builtin_preserve_enum_value(*(enum bar *)U, 0 /* enum literal existence */);
*g = __builtin_preserve_enum_value(*(enum bar *)V, 1 /* enum literal value */);
}
f0 <delta>:
30: r1 = 0x1 ll
f0: CO-RE <enumval_exists> [16] enum bar::U = 0
32: *(u64 *)(r2 + 0x0) = r1
33: r1 = 0x1 ll
108: CO-RE <enumval_value> [16] enum bar::V = 1
35: *(u64 *)(r2 + 0x0) = r1
36: exit