此次来填 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;
}
|
以上代码片段的主要处理逻辑:
- 前 6 个参数,从寄存器获取。
- 其它参数,则从栈上获取。
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 个参数的问题了。
内核源代码仓库真是一个大宝藏,看内核源代码比看网络博客的效果强不少;而且不用担心二手解读带来的偏解,毕竟从一手资料里学习正解。