宏是 C 语言中最强大的语言特性,能够用来简化 eBPF 的 C 代码;毕竟 eBPF 的 C 代码是一种语法、语义都受限的 C 代码,不能像普通 C 代码那样“肆意妄为”。

写法一:带有返回值的多行宏

常见的宏定义是常量定义,如 #define VXLAN_PORT 4789。可是如果在宏定义中包含有 if 条件判断,该如何编写该宏定义呢?不使用函数来实现,是因为函数需要带上类型,而宏定义则不需要参数的类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#define GET_TOS(sip, pod)                  \
    ({                                     \
        __u8 tos;                          \
        if (USE_TOS)                       \
            tos = POD_TOS;                 \
        else                               \
            tos = __get_pod_tos(sip, pod); \
                                           \
        tos;                               \
    })

如上代码片段,使用 ({}) 包裹多行代码,且在第一行声明返回值、在最后一行提供返回值。

参考 BPF_CORE_READ 的宏定义:

1
2
3
4
5
#define BPF_CORE_READ(src, a, ...) ({                  \
    ___type((src), a, ##__VA_ARGS__) __r;              \
    BPF_CORE_READ_INTO(&__r, (src), a, ##__VA_ARGS__); \
    __r;                                               \
})

写法二:多行宏的推荐写法

对于多行宏,常见写法如下:

1
2
3
4
5
#define __load_bytes(dst, size)                                                             \
    bpf_skb_load_bytes_relative(skb, var_off, ev->buff + cpy_off, size, BPF_HDR_START_MAC); \
    cpy_off += size;                                                                        \
    ev->total_len = cpy_off;                                                                \
    var_off += size;

直接写成多行。可是如果需要在宏里定义一个临时变量,这种写法就行不通了。此时,可以采取下面这种写法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#define print_ip_version(iph)                  \
    do {                                       \
        __u8 b;                                \
        bpf_probe_read(&b, 1, iph);            \
        b >>= 4;                               \
        if (b == 4)                            \
            bpf_printk("IPv4\n");              \
        else if (b == 6)                       \
            bpf_printk("IPv6\n");              \
        else                                   \
            bpf_printk("Unkown IP version\n"); \
    } while (0)

使用 do {} while(0) 包裹着多行代码。此时,do-while 里的代码块只会执行一遍。而在使用的时候,像函数调用一样直接使用就可以了,print_ip_version(iph);

小结

在 Linux 内核的源代码中,写法二更加常见。而写法一,则是从其它 eBPF 项目中学习到的。

多看源代码,多从源代码中学习。