在使用 bpf 追踪内核函数的时候,通常需要对函数参数进行过滤。
如果使用 bpftrace,可以很方便地在脚本里指定过滤条件;但 bpftrace 依赖 LLVM,很多时候用起来不够方便。
而使用 bpf 进行开发时,过滤条件通常是写死在程序里的,这样就无法在运行的时候动态调整过滤条件了。
有没有办法像 pcap-filter(7) 那样动态地过滤函数参数呢?
TL;DR 在 PR: Add –filter-skb-expr and –filter-xdp-expr 里,pwru 已基本实现了对 skb 参数的动态过滤。
动态过滤函数参数的实现思路
大概的实现思路如下:
- 解析过滤表达式,得到 C 表达式的 AST。
- 校验 AST,确保表达式符合预期。
- 根据 BTF 信息,将 AST 直接转换为 bpf instructions。
- 使用生成的 bpf instructions 替换 bpf 代码里提供的过滤函数。
以下将详细讲解这个过程。
解析过滤表达式
使用一个现成的库来解析表达式即可,比如 rsc.io/c2go/cc。
1
2
3
|
func parse(expr string) (*cc.Expr, error) {
return cc.ParseExpr(expr)
}
|
校验 AST
在 pwru 里,预期只支持一些简单的表达式,如 skb->dev->ifindex == 11
:
- 只支持
==
、!=
、<
、<=
、>
、>=
这几个操作符,=
当作 ==
处理。
- 操作符左侧只能是 skb/xdp 参数的字段访问,不支持 bitfield。
- 操作符右侧只能是常量。
表达式限制得越简单越好。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
func validateOperator(op cc.ExprOp) error {
switch op {
case cc.Eq, cc.EqEq, cc.NotEq, cc.Lt, cc.LtEq, cc.Gt, cc.GtEq:
return nil
default:
return fmt.Errorf("unexpected operator: %s; must be one of =, ==, !=, <, <=, >, >=", op)
}
}
// validateLeftOperand checks if the left operand is struct member access like:
// [[skb] -> dev] -> ifindex
func validateLeftOperand(left *cc.Expr) error {
// ...
return validateLeftOperand(left.Left)
}
func parseNumber(right *cc.Expr) (uint64, error) {
text := right.Text
// ...
return strconv.ParseUint(text, 10, 64)
}
func validateRightOperand(right *cc.Expr) error {
if right.Op != cc.Number {
return fmt.Errorf("unexpected right operand: %s; must be a constant number", right)
}
if _, err := parseNumber(right); err != nil {
return fmt.Errorf("right operand is not a number: %w", err)
}
return nil
}
// validate checks if the expression is expected simple C expression by
// checking:
// 1. The top level operator is one of the following: =, ==, !=, <, <=, >, >=
// 2. The left operand is struct member access
// 3. The right operand is a constant number in hex, octal, or decimal format
func validate(expr *cc.Expr) error {
if err := validateOperator(expr.Op); err != nil {
return err
}
if expr.Left == nil {
return fmt.Errorf("left operand is missing")
}
if err := validateLeftOperand(expr.Left); err != nil {
return err
}
if expr.Right == nil {
return fmt.Errorf("right operand is missing")
}
if err := validateRightOperand(expr.Right); err != nil {
return err
}
return nil
}
|
AST 转换为 BPF 指令
然而,并没有一个现成的库可以直接将 AST 转换为 BPF 指令。
这就需要自己实现一个简单的转换器了:
- 根据 BTF 信息,将操作符左侧的 struct member access 转换为偏移量数组。
- 将偏移量数组转换为一系列的
bpf_probe_read_kernel()
helper。
- 将操作符转换为 BPF 指令。
根据 BTF 信息获取偏移量
为了提升使用体验,不区分用户提供的表达式里的 .
和 ->
,而是根据 BTF 信息来判断。
因此,实现思路如下:
- 从左到右遍历表达式。
- 每次遍历,根据当前的类型,找到对应的 struct/union 成员。
- 计算偏移量:
- 如果是
->
,则新增一个偏移量。
- 如果是
.
,则将当前偏移量加上这个成员的偏移量。
查找 struct/union 成员时,需要处理嵌套的匿名 struct/union。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
type astInfo struct {
offsets []uint32
lastField btf.Type
bigEndian bool // true if the last field is big endian
}
func expr2offset(expr *cc.Expr, ptr *btf.Pointer) (astInfo, error) {
var ast astInfo
var exprStack []*cc.Expr
for left := expr.Left; left != nil; left = left.Left {
exprStack = append(exprStack, left)
}
var offsets []uint32
offsets = append(offsets, 0)
seenArrow := false
var prev btf.Type = ptr
for i, j := len(exprStack)-2, 0; i >= 0; i-- {
var (
prevName string
member *btf.Member
offset uint32
err error
)
ptr, useArrow := prev.(*btf.Pointer)
if useArrow {
prev = underlyingType(ptr.Target)
}
expr := exprStack[i]
switch v := prev.(type) {
case *btf.Struct:
member, err = findStructMember(v, expr.Text)
prevName = v.Name
case *btf.Union:
member, err = findUnionMember(v, expr.Text)
prevName = v.Name
default:
return ast, fmt.Errorf("unexpected type %T of %s(%+v)", v, expr.Text, prev)
}
if err != nil {
return ast, fmt.Errorf("failed to find %s member of %s: %w", expr.Text, prevName, err)
}
switch v := prev.(type) {
case *btf.Struct:
offset, err = structMemberOffset(v, expr.Text, offset)
case *btf.Union:
offset, err = unionMemberOffset(v, expr.Text, offset)
}
if err != nil {
return ast, fmt.Errorf("failed to get offset of %s member of %s: %w", expr.Text, prevName, err)
}
prev = underlyingType(member.Type)
switch expr.Op {
case cc.Arrow, cc.Dot:
if !useArrow {
// access via .
offsets[j] += offset
} else {
// access via ->
if seenArrow {
offsets = append(offsets, offset)
j++
} else {
seenArrow = true
offsets[j] += offset
}
}
if i == 0 {
ast.offsets = offsets
ast.lastField = prev
ast.bigEndian = isBigEndian(member.Type)
return ast, nil
}
default:
// protected by validateLeftOperand()
return ast, fmt.Errorf("unexpected operator: %s", expr.Op)
}
}
return ast, fmt.Errorf("unexpected expression: %s", expr)
}
|
偏移量转换为 BPF 指令
将偏移量数组转换为一系列的 bpf_probe_read_kernel()
helper:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func offset2insns(insns asm.Instructions, offsets []uint32) asm.Instructions {
lastIndex := len(offsets) - 1
for i := 0; i <= lastIndex; i++ {
if offsets[i] != 0 {
insns = append(insns, asm.Add.Imm(asm.R3, int32(offsets[i]))) // r3 += offset
}
insns = append(insns,
asm.Mov.Imm(asm.R2, 8), // r2 = 8; always read 8 bytes
asm.Mov.Reg(asm.R1, asm.R10), // r1 = r10
asm.Add.Imm(asm.R1, -8), // r1 = r10 - 8
asm.FnProbeReadKernel.Call(), // bpf_probe_read_kernel(r1, 8, r3)
asm.LoadMem(asm.R3, asm.R10, -8, asm.DWord), // r3 = *(r10 - 8)
)
if i != lastIndex { // not last member access
insns = append(insns,
asm.JEq.Imm(asm.R3, 0, labelExitFail), // if r3 == 0, goto __exit
)
}
}
return insns
}
|
r3
保存了当前的指针,比如 skb。
- 如果当前偏移量不为 0,将
r3
加上这个偏移量,类似于 r3 = &skb->dev
。
r2
固定为 8,表示读取 8 字节。
r1
保存了栈地址,指从 r3
指针指向的内存地址读取 8 个字节到栈上。
- 如果不是最后一个成员访问,检查读取的值是否为 0,如果是则退出。
操作符转换为 BPF 指令
一一将操作符映射成 BPF 指令即可;不过在这之前,需要处理一下大小端、符号、类型转换等问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
type tgtInfo struct {
constant uint64
typ btf.Type
sizof int
bigEndian bool
}
func op2insns(insns asm.Instructions, op cc.ExprOp, tgt tgtInfo) asm.Instructions {
isSigned := false
intType, isInt := tgt.typ.(*btf.Int)
if isInt {
isSigned = intType.Encoding == btf.Signed
}
const leftOperandReg = asm.R3
tgtConst := tgt.constant
switch tgt.sizof {
case 1:
tgtConst = uint64(uint8(tgtConst))
insns = append(insns,
asm.And.Imm(leftOperandReg, 0xFF), // r3 &= 0xff
)
case 2:
tgtConst = uint64(uint16(tgtConst))
if tgt.bigEndian {
tgtConst = uint64(byteorder.HostToNetwork16(uint16(tgtConst)))
}
insns = append(insns,
asm.And.Imm(leftOperandReg, 0xFFFF), // r3 &= 0xffff
)
case 4:
tgtConst = uint64(uint32(tgtConst))
if tgt.bigEndian {
tgtConst = uint64(byteorder.HostToNetwork32(uint32(tgtConst)))
}
insns = append(insns,
asm.LSh.Imm(leftOperandReg, 32), // r3 <<= 32
asm.RSh.Imm(leftOperandReg, 32), // r3 >>= 32
)
}
var jmpOpCode asm.JumpOp
switch op {
case cc.Eq, cc.EqEq:
// if r3 == tgtConst, goto __return
jmpOpCode = asm.JEq
case cc.NotEq:
// if r3 != tgtConst, goto __return
jmpOpCode = asm.JNE
case cc.Lt:
// if r3 < tgtConst, goto __return
if isSigned {
jmpOpCode = asm.JSLT
} else {
jmpOpCode = asm.JLT
}
case cc.LtEq:
// if r3 <= tgtConst, goto __return
if isSigned {
jmpOpCode = asm.JSLE
} else {
jmpOpCode = asm.JLE
}
case cc.Gt:
// if r3 > tgtConst, goto __return
if isSigned {
jmpOpCode = asm.JSGT
} else {
jmpOpCode = asm.JGT
}
case cc.GtEq:
// if r3 >= tgtConst, goto __return
if isSigned {
jmpOpCode = asm.JSGE
} else {
jmpOpCode = asm.JGE
}
default:
// protected by validateOperator()
log.Fatalf("Unexpected operator: %s", op)
}
insns = append(insns,
asm.Mov.Imm(asm.R0, 1), // r0 = 1
jmpOpCode.Imm(leftOperandReg, int32(tgtConst), labelReturn),
)
return insns
}
|
- 将常量转换为对应大小的无符号整数。
- 如果是大端,将常量转换为网络字节序。
- 将操作符一一映射成 BPF 条件跳转指令。
- 如果是有符号整数,使用有符号比较指令。
替换桩函数
使用生成的 BPF 指令替换原来的桩函数的所有指令即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func injectFilter(spec *btf.Spec, prog *ebpf.ProgramSpec, filterExpr, stubFunc string, compile compileFunc) error {
// ...
insns, err := compile(filterExpr, spec)
if err != nil {
return fmt.Errorf("failed to compile filter expression(%s): %w", filterExpr, err)
}
insns[0] = insns[0].WithMetadata(prog.Instructions[injectIdx].Metadata)
prog.Instructions = append(prog.Instructions[:injectIdx],
append(insns, prog.Instructions[retInsnIdx+1:]...)...) // replace the original insns with the new ones
return nil
}
func InjectSkbFilter(spec *btf.Spec, prog *ebpf.ProgramSpec, filterExpr string) error {
return injectFilter(spec, prog, filterExpr, stubFuncSkb, CompileSkbExpr)
}
|
转换后的结果
通过以上的转换,可以将表达式 skb->dev->ifindex == 11
转换为一系列的 BPF 指令:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
$ sudo bpftool p d x i 100
; ...
bool filter_skb_expr(struct sk_buff * skb):
; filter_skb_expr(struct sk_buff *skb)
997: (bf) r3 = r1
998: (07) r3 += 16
999: (b7) r2 = 8
1000: (bf) r1 = r10
1001: (07) r1 += -8
1002: (85) call bpf_probe_read_kernel#-125280
1003: (79) r3 = *(u64 *)(r10 -8)
1004: (15) if r3 == 0x0 goto pc+10
1005: (07) r3 += 224
1006: (b7) r2 = 8
1007: (bf) r1 = r10
1008: (07) r1 += -8
1009: (85) call bpf_probe_read_kernel#-125280
1010: (79) r3 = *(u64 *)(r10 -8)
1011: (67) r3 <<= 32
1012: (77) r3 >>= 32
1013: (b7) r0 = 1
1014: (15) if r3 == 0xb goto pc+1
1015: (b7) r0 = 0
1016: (95) exit
|
在 x86 上的机器码反汇编结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
$ ./bpflbr -d -p i:100
; ...
; bpf/kprobe_pwru.c:233:0 filter_skb_expr(struct sk_buff *skb)
0xffffffffc03afe8c: 0f 1f 44 00 00 nopl (%rax, %rax)
0xffffffffc03afe91: 66 90 nop
0xffffffffc03afe93: 55 pushq %rbp
0xffffffffc03afe94: 48 89 e5 movq %rsp, %rbp
0xffffffffc03afe97: 48 81 ec 08 00 00 00 subq $8, %rsp
0xffffffffc03afe9e: 48 89 fa movq %rdi, %rdx
0xffffffffc03afea1: 48 83 c2 10 addq $0x10, %rdx
0xffffffffc03afea5: be 08 00 00 00 movl $8, %esi
0xffffffffc03afeaa: 48 89 ef movq %rbp, %rdi
0xffffffffc03afead: 48 83 c7 f8 addq $-8, %rdi
0xffffffffc03afeb1: e8 3a 51 73 c8 callq 0xffffffff88ae4ff0 ; bpf_probe_read_kernel+0x0 kernel/trace/bpf_trace.c:237
0xffffffffc03afeb6: 48 8b 55 f8 movq -8(%rbp), %rdx
0xffffffffc03afeba: 48 85 d2 testq %rdx, %rdx
0xffffffffc03afebd: 74 2f je 0xffffffffc03afeee ; filter_skb_expr+0x62 bpf/kprobe_pwru.c:233 [bpf]
0xffffffffc03afebf: 48 81 c2 e0 00 00 00 addq $0xe0, %rdx
0xffffffffc03afec6: be 08 00 00 00 movl $8, %esi
0xffffffffc03afecb: 48 89 ef movq %rbp, %rdi
0xffffffffc03afece: 48 83 c7 f8 addq $-8, %rdi
0xffffffffc03afed2: e8 19 51 73 c8 callq 0xffffffff88ae4ff0 ; bpf_probe_read_kernel+0x0 kernel/trace/bpf_trace.c:237
0xffffffffc03afed7: 48 8b 55 f8 movq -8(%rbp), %rdx
0xffffffffc03afedb: 48 c1 e2 20 shlq $0x20, %rdx
0xffffffffc03afedf: 48 c1 ea 20 shrq $0x20, %rdx
0xffffffffc03afee3: b8 01 00 00 00 movl $1, %eax
0xffffffffc03afee8: 48 83 fa 0b cmpq $0xb, %rdx
0xffffffffc03afeec: 74 02 je 0xffffffffc03afef0 ; filter_skb_expr+0x64 bpf/kprobe_pwru.c:233 [bpf]
0xffffffffc03afeee: 31 c0 xorl %eax, %eax
0xffffffffc03afef0: c9 leave
0xffffffffc03afef1: c3 retq
0xffffffffc03afef2: cc int3
|
更通用的函数参数动态过滤的库
在 pwru 里,过滤表达式限定了第一个结构体名称必须为 skb
或者 xdp
。
如果解除该限制,动态过滤函数参数的功能就可以用在更多地方了。
该库的源代码将发布在 GitHub bice。
总结
本文介绍了如何实现动态过滤函数参数的功能,以及如何将表达式转换为 BPF 指令。
- 解析过滤表达式,得到 C 表达式的 AST。
- 校验 AST,确保表达式符合预期。
- 根据 BTF 信息,将 AST 直接转换为 bpf instructions。
未来,将在 bice
库中实现更通用的函数参数过滤功能,敬请期待。