eBPF Talk: kprobe 獲取第 n 個參數
此次來填 eBPF Talk: skbtracer-iptables 中的坑了:在開發一個基於 eBPF 的 iptables TRACE[1] 的替代工具。
不過,遇到的第一個紙老虎是 kprobe
中獲取 nf_log_trace()
的諸多參數。
nf_log_trace()
且看下函數聲明:
// ${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()
時提供了哪些參數:
// ${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 個參數的。
// ${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 個參數的。
// ${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 個參數可以這麼做:
#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 個參數的問題了。
內核源代碼倉庫真是一個大寶藏,看內核源代碼比看網絡博客的效果強不少;而且不用擔心二手解讀帶來的偏解,畢竟從一手資料裏學習正解。
參考資料
[1]
iptables TRACE: https://ipset.netfilter.org/iptables-extensions.man.html#lbDX
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Z9JbUgLfmnlEJb_ht12moA