此次来填 eBPF Talk: skbtracer-iptables 中的坑了:在开发一个基于 eBPF 的 iptables TRACE 的替代工具。

不过,遇到的第一个纸老虎是 kprobe 中获取 nf_log_trace() 的诸多参数。

nf_log_trace()

且看下函数声明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// ${KERNEL}/net/netfilter/nf_log.c

void nf_log_trace(struct net *net,
          u_int8_t pf,
          unsigned int hooknum,
          const struct sk_buff *skb,
          const struct net_device *in,
          const struct net_device *out,
          const struct nf_loginfo *loginfo, const char *fmt, ...)
{
    // ...
}

可以看到,nf_log_trace() 有 8 个固定参数,和 N 个不定参数。

看下调用 nf_log_trace() 时提供了哪些参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// ${KERNEL}/net/ipv4/netfilter/ip_tables.c

static void trace_packet(struct net *net,
             const struct sk_buff *skb,
             unsigned int hook,
             const struct net_device *in,
             const struct net_device *out,
             const char *tablename,
             const struct xt_table_info *private,
             const struct ipt_entry *e)
{
    // ...

    nf_log_trace(net, AF_INET, hook, skb, in, out, &trace_loginfo,
             "TRACE: %s:%s:%s:%u ",
             tablename, chainname, comment, rulenum);
}

除了 8 个固定参数,还提供了 4 个不定参数。

该怎么获取这些参数呢?

pt_regs on x86

其实,网络上有不少讲解 kprobe 的第 n 个参数的文章。

不过,这些文章的质量参差不齐,尽管有所帮助。

不如,看下内核里是怎么从 pt_regs 获取第 n 个参数的。

 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
// ${KERNEL}/arch/x86/include/asm/ptrace.h

static inline unsigned long regs_get_kernel_argument(struct pt_regs *regs,
                             unsigned int n)
{
    static const unsigned int argument_offs[] = {
#ifdef __i386__
        offsetof(struct pt_regs, ax),
        offsetof(struct pt_regs, dx),
        offsetof(struct pt_regs, cx),
#define NR_REG_ARGUMENTS 3
#else
        offsetof(struct pt_regs, di),
        offsetof(struct pt_regs, si),
        offsetof(struct pt_regs, dx),
        offsetof(struct pt_regs, cx),
        offsetof(struct pt_regs, r8),
        offsetof(struct pt_regs, r9),
#define NR_REG_ARGUMENTS 6
#endif
    };

    if (n >= NR_REG_ARGUMENTS) {
        n -= NR_REG_ARGUMENTS - 1;
        return regs_get_kernel_stack_nth(regs, n);
    } else
        return regs_get_register(regs, argument_offs[n]);
}

static inline unsigned long regs_get_register(struct pt_regs *regs,
                          unsigned int offset)
{
    // ...
    return *(unsigned long *)((unsigned long)regs + offset);
}

static inline unsigned long *regs_get_kernel_stack_nth_addr(struct pt_regs *regs, unsigned int n)
{
    unsigned long *addr = (unsigned long *)regs->sp;

    addr += n;
    if (regs_within_kernel_stack(regs, (unsigned long)addr))
        return addr;
    else
        return NULL;
}

/* To avoid include hell, we can't include uaccess.h */
extern long copy_from_kernel_nofault(void *dst, const void *src, size_t size);

static inline unsigned long regs_get_kernel_stack_nth(struct pt_regs *regs,
                              unsigned int n)
{
    unsigned long *addr;
    unsigned long val;
    long ret;

    addr = regs_get_kernel_stack_nth_addr(regs, n);
    if (addr) {
        ret = copy_from_kernel_nofault(&val, addr, sizeof(val));
        if (!ret)
            return val;
    }
    return 0;
}

以上代码片段的主要处理逻辑:

  1. 前 6 个参数,从寄存器获取。
  2. 其它参数,则从栈上获取。

pt_regs on arm64

顺便看下 arm64 是怎么从 pt_regs 获取第 n 个参数的。

 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
// ${KERNEL}/arch/arm64/include/asm/ptrace.h

static inline unsigned long regs_get_kernel_argument(struct pt_regs *regs,
                             unsigned int n)
{
#define NR_REG_ARGUMENTS 8
    if (n < NR_REG_ARGUMENTS)
        return pt_regs_read_reg(regs, n);
    return 0;
}

static inline unsigned long pt_regs_read_reg(const struct pt_regs *regs, int r)
{
    return (r == 31) ? 0 : regs->regs[r];
}

struct pt_regs {
    union {
        struct user_pt_regs user_regs;
        struct {
            u64 regs[31];
            u64 sp;
            u64 pc;
            u64 pstate;
        };
    };
    // ...
};

arm64 的处理逻辑就比较简单了,因为 arm64 本身就支持多达 31 个寄存器,可以将参数都通过寄存器传递。

kprobe 获取第 n 个参数

参考、整理内核的源代码,在 eBPF 中 kprobe 获取第 n 个参数可以这么做:

 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
#define NR_REG_ARGUMENTS 6
#define NR_ARM64_MAX_REG_ARGUMENTS 31

static __inline unsigned long
regs_get_kernel_stack_nth_addr(struct pt_regs *regs, unsigned int n)
{
    unsigned long *addr = (unsigned long *)regs->sp, retval = 0;

    addr += n;
    return 0 != bpf_probe_read_kernel(&retval, sizeof(retval), addr) ? 0 : retval;
}

static __inline unsigned long
regs_get_nth_argument(struct pt_regs *regs,
    unsigned int n)
{
    switch (n) {
    case 0:
        return PT_REGS_PARM1_CORE(regs);
    case 1:
        return PT_REGS_PARM2_CORE(regs);
    case 2:
        return PT_REGS_PARM3_CORE(regs);
    case 3:
        return PT_REGS_PARM4_CORE(regs);
    case 4:
        return PT_REGS_PARM5_CORE(regs);
    case 5:
        return PT_REGS_PARM6_CORE(regs);
    default:
#ifdef __TARGET_ARCH_arm64
        if (n < NR_ARM64_MAX_REG_ARGUMENTS)
            return regs->regs[n];
        else
            return 0;
#elifdef __TARGET_ARCH_x86
        n -= NR_REG_ARGUMENTS - 1;
        return regs_get_kernel_stack_nth_addr(regs, n);
#else
        return 0;
#endif
    }
}

// 用法如下:

SEC("kprobe/nf_log_trace")
int BPF_KPROBE(k_nf_log_trace, struct net *net, u_int8_t pf, unsigned int hooknum,
    struct sk_buff *skb, struct net_device *in)
{
    struct net_device *out;
    char *tablename;
    char *chainname;
    unsigned int rulenum;

    out = (typeof(out))(void *)regs_get_nth_argument(ctx, 5);
    tablename = (typeof(tablename))(void *)regs_get_nth_argument(ctx, 8);
    chainname = (typeof(chainname))(void *)regs_get_nth_argument(ctx, 9);
    rulenum = (typeof(rulenum))regs_get_nth_argument(ctx, 11);

    return __ipt_do_table_trace(ctx, pf, hooknum, skb, in, out, tablename,
        chainname, rulenum);
}

从栈上获取参数时,需要使用 bpf_probe_read_kernel() 帮助函数读取内核内存内容。

kprobe nf_log_trace() on x86

单纯看代码,是比较难理解 x86 上从 pt_regs 获取第 n 个参数是怎么实现的;使用图片来解释一下吧。

下图是 kprobe nf_log_trace() 时的 pt_regs 和栈的状态。

总结

内核源代码结合图片,总算解决了在 eBPF kprobe 中从 pt_regs 获取第 n 个参数的问题了。

内核源代码仓库真是一个大宝藏,看内核源代码比看网络博客的效果强不少;而且不用担心二手解读带来的偏解,毕竟从一手资料里学习正解。