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;
}

以上代碼片段的主要處理邏輯:

  1. 前 6 個參數,從寄存器獲取。

  2. 其它參數,則從棧上獲取。

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