在使用 bpf 追踪内核函数的时候,通常需要对函数参数进行过滤。

如果使用 bpftrace,可以很方便地在脚本里指定过滤条件;但 bpftrace 依赖 LLVM,很多时候用起来不够方便。

而使用 bpf 进行开发时,过滤条件通常是写死在程序里的,这样就无法在运行的时候动态调整过滤条件了。

有没有办法像 pcap-filter(7) 那样动态地过滤函数参数呢?

TL;DR 在 PR: Add –filter-skb-expr and –filter-xdp-expr 里,pwru 已基本实现了对 skb 参数的动态过滤。

动态过滤函数参数的实现思路

大概的实现思路如下:

  1. 解析过滤表达式,得到 C 表达式的 AST。
  2. 校验 AST,确保表达式符合预期。
  3. 根据 BTF 信息,将 AST 直接转换为 bpf instructions。
  4. 使用生成的 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

  1. 只支持 ==!=<<=>>= 这几个操作符,= 当作 == 处理。
  2. 操作符左侧只能是 skb/xdp 参数的字段访问,不支持 bitfield。
  3. 操作符右侧只能是常量。

表达式限制得越简单越好。

 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 指令。

这就需要自己实现一个简单的转换器了:

  1. 根据 BTF 信息,将操作符左侧的 struct member access 转换为偏移量数组。
  2. 将偏移量数组转换为一系列的 bpf_probe_read_kernel() helper。
  3. 将操作符转换为 BPF 指令。

根据 BTF 信息获取偏移量

为了提升使用体验,不区分用户提供的表达式里的 .->,而是根据 BTF 信息来判断。

因此,实现思路如下:

  1. 从左到右遍历表达式。
  2. 每次遍历,根据当前的类型,找到对应的 struct/union 成员。
  3. 计算偏移量:
    1. 如果是 ->,则新增一个偏移量。
    2. 如果是 .,则将当前偏移量加上这个成员的偏移量。

查找 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
}
  1. r3 保存了当前的指针,比如 skb。
  2. 如果当前偏移量不为 0,将 r3 加上这个偏移量,类似于 r3 = &skb->dev
  3. r2 固定为 8,表示读取 8 字节。
  4. r1 保存了栈地址,指从 r3 指针指向的内存地址读取 8 个字节到栈上。
  5. 如果不是最后一个成员访问,检查读取的值是否为 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
}
  1. 将常量转换为对应大小的无符号整数。
  2. 如果是大端,将常量转换为网络字节序。
  3. 将操作符一一映射成 BPF 条件跳转指令。
  4. 如果是有符号整数,使用有符号比较指令。

替换桩函数

使用生成的 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 指令。

  1. 解析过滤表达式,得到 C 表达式的 AST。
  2. 校验 AST,确保表达式符合预期。
  3. 根据 BTF 信息,将 AST 直接转换为 bpf instructions。

未来,将在 bice 库中实现更通用的函数参数过滤功能,敬请期待。