关于bpf:eBPF-HashMap-与-padding-的坑

4次阅读

共计 3486 个字符,预计需要花费 9 分钟才能阅读完成。

前言

上一篇文章《ebpf-go 初体验》中,咱们提到了一个小插曲,就是当 map 的 key 这样写的时候 struct tuple key = {ip, bpf_ntohs(sport)},map 的 key 看起来会反复,有些令人惊讶,于是我用另外一台机器 B 测了下(内核 6.6,clang 14.0.0)。发现了报错:"invalid indirect read from stack R2 off",顺藤摸瓜找到了这篇文档 1,才反馈过去:咱们的 struct tuple 是不规整的,须要 padding,而不同的架构 / 编译器对 padding 的解决又是一样的,从而导致了不同的后果。

那么这个 padding 到底是怎么导致看起来反复的 key 的呢?这就得看看 ebpf 的 hashmap 的实现原理了。

ebpf hashmap 外围原理

bpf 的 map 的操作都在 syscall2 中,从其中的 map_update_elem 下手,找到 bpf_map_update_value,而后是 map->ops->map_update_elem,找到 hashmap 对应的实现就在此 3(per cpu4 的咱们先不看),外围如下:

static long htab_map_update_elem(struct bpf_map *map, void *key, void *value,
                 u64 map_flags)
{hash = htab_map_hash(key, key_size, htab->hashrnd);
    b = __select_bucket(htab, hash);
    head = &b->head;

    l_old = lookup_elem_raw(head, hash, key, key_size);
    l_new = alloc_htab_elem(htab, key, value, key_size, hash, false, false,
                l_old);
    /* add new element to the head of the list, so that
     * concurrent search will find it before old elem
     */
    hlist_nulls_add_head_rcu(&l_new->hash_node, head);
}

如果你学过 Java 就晓得:一个 Objec 要能成为 hashmap 的 key,必须得有 hashcodeequals 办法。这也是 hashmap 的外围,与语言无关。那么下面的代码如何体现的呢?

首先,htab_map_hash 计算 key 的哈希值,次要实现是 jhash25,这里就不开展了。
而后 equasls 体现在 lookup_elem_raw 中,用的是 memcmp 也就是:二进制相等。

所以我猜想:key 尽管看起来雷同,然而二进制是不同的。接下来天然是验证一番。

验证

c 代码能够这样写

struct tuple key = {ip,r_sport};

char serialized[sizeof(struct tuple)];
__builtin_memcpy(serialized, &key, sizeof(struct tuple));
for (int i = 0; i < sizeof(struct tuple); i++) {bpf_printk("0x%x",serialized[i]);
}

go 代码能够这样写

iter := objs.PktCountMap.Iterate()
for iter.Next(&key, &val) {const sz = int(unsafe.Sizeof(counterTuple{}))
  var asByteSlice []byte = (*(*[sz]byte)(unsafe.Pointer(&key)))[:]

  var sb strings.Builder 
  for _, b := range asByteSlice {sb.WriteString(fmt.Sprintf("0x%x", b))
  }
 
  sourceIP := key.Addr
  sourcePort := key.Port
  packetCount := val
  log.Printf("%d/%s:%d(%s) => %d\n", sourceIP, int2ip(sourceIP), sourcePort, sb.String(), packetCount)
}

采纳上文的环境进行测试,go 代码输入

tuple num: 5
16777343/127.0.0.1:4000(0x7f 0x0 0x0 0x1 0xa0 0xf 0x0 0x0) => 4
16777343/127.0.0.1:4000(0x7f 0x0 0x0 0x1 0xa0 0xf 0xff 0xff) => 3
16777343/127.0.0.1:4002(0x7f 0x0 0x0 0x1 0xa2 0xf 0x0 0x0) => 4
16777343/127.0.0.1:4002(0x7f 0x0 0x0 0x1 0xa2 0xf 0xff 0xff) => 3
16777343/127.0.0.1:4001(0x7f 0x0 0x0 0x1 0xa1 0xf 0xff 0xff) => 3

bpf 输入

bpf_trace_printk: Process a packet of tuple from 16777343|127.0.0.1:41487|4002
bpf_trace_printk: 0x7f 
bpf_trace_printk: 0x0 
bpf_trace_printk: 0x0 
bpf_trace_printk: 0x1 
bpf_trace_printk: 0xffffffa2 
bpf_trace_printk: 0xf 
bpf_trace_printk: 0x0 
bpf_trace_printk: 0x0 

bpf_trace_printk: Process a packet of tuple from 16777343|127.0.0.1:41487|4002
bpf_trace_printk: 0x7f 
bpf_trace_printk: 0x0 
bpf_trace_printk: 0x0 
bpf_trace_printk: 0x1 
bpf_trace_printk: 0xffffffa2 
bpf_trace_printk: 0xf 
bpf_trace_printk: 0xffffffff 
bpf_trace_printk: 0xffffffff 

可见尽管 tuple 外部的两个字段一样,然而 padding 的两个字节却不一样,导致在 hashmap 中存了两个看起来一样的 key。
机器 B 应该是间接没有初始化 padding,导致了报错。当我把机器 B 的 clang 降级到 15.0.7 后,它的 padding 又稳固雷同了,没有再呈现看起来雷同的 key。可见这个 padding 不具备可移植性。

解法

padding 的值不能依赖编译器去解决,最举荐的做法是这样的:

struct tuple key;
__builtin_memset(&key,0,sizeof(key));
key.addr = ip;
key.port = r_sport;

让 padding 被显示地初始化,确保不会呈现各种奇奇怪怪的谬误(无论是失去谬误的后果还是间接运行不起来)。

PS:观测 padding 能够用上面这个工具:

pahole counter_bpfel.o -C tuple

struct tuple {
        __u32                      addr;                 /*     0     4 */
        __u16                      port;                 /*     4     2 */

        /* size: 8, cachelines: 1, members: 2 */
        /* padding: 2 */
        /* last cacheline: 8 bytes */
};

参考


  1. https://docs.kernel.org/bpf/verifier.html ↩
  2. https://github.com/torvalds/linux/blob/ab27740f76654ed58dd32ac0ba0031c18a6dea3b/kernel/bpf/syscall.c#L1483 ↩
  3. https://github.com/torvalds/linux/blob/ab27740f76654ed58dd32ac0ba0031c18a6dea3b/kernel/bpf/hashtab.c#L2261 ↩
  4. https://github.com/torvalds/linux/blob/ab27740f76654ed58dd32ac0ba0031c18a6dea3b/kernel/bpf/hashtab.c#L2379 ↩
  5. https://github.com/torvalds/linux/blob/ab27740f76654ed58dd32ac0ba0031c18a6dea3b/include/linux/jhash.h#L117 ↩
正文完
 0