暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

bpf工具链 - 使用bpf的c和通用c比较

囧囧妹 2022-08-05
353

点击上方蓝字【囧囧妹】一起学习,一起成长!

在为 BPF 编写 C 程序时,与使用 C 进行通常的应用程序开发相比,有几个陷阱需要注意。以下描述了与 BPF 模型的一些差异:


一,一切都需要内联,没有函数调用(在旧 LLVM 版本上)或共享库调用可用。

共享库等不能与 BPF 一起使用。但是,BPF 程序中使用的通用库代码可以放在头文件中并包含在主程序中。例如,Cilium 大量使用它(请参阅 参考资料bpf/lib/
)。但是,这仍然允许包含例如来自内核或其他库的头文件,并重用它们的静态内联函数或宏/定义。
除非在支持 BPF 到 BPF 函数调用的情况下使用最近的内核 (4.16+) 和 LLVM (6.0+),否则 LLVM 需要将整个代码编译并内联为给定程序部分的 BPF 指令的平面序列。在这种情况下,最佳做法是为每个库函数使用注释__inline
 ,如下所示。建议使用always_inline
 ,因为编译器仍然可以决定取消内联仅注释为inline
.
如果发生后者,LLVM 将在 ELF 文件中生成一个重定位条目,而 BPF ELF 加载器(例如 iproute2)无法解析,因此会产生错误,因为只有 BPF 映射是加载器可以处理的有效重定位条目。
    #include <linux/bpf.h>


    #ifndef __section
    # define __section(NAME) \
    __attribute__((section(NAME), used))
    #endif


    #ifndef __inline
    # define __inline \
    inline __attribute__((always_inline))
    #endif


    static __inline int foo(void)
    {
    return XDP_DROP;
    }


    __section("prog")
    int xdp_drop(struct xdp_md *ctx)
    {
    return foo();
    }


    char __license[] __section("license") = "GPL";



    二,多个程序可以驻留在不同部分的单个 C 文件中。

    BPF 的 C 程序大量使用节注释。AC 文件通常分为 3 个或更多部分。BPF ELF 加载器使用这些名称来提取和准备相关信息,以便通过 bpf 系统调用加载程序和映射。例如,iproute2 使用maps
    license
    作为默认部分名称来分别查找创建地图所需的元数据和 BPF 程序的许可证。在程序创建时,后者也被推送到内核中,并启用一些作为 GPL 公开的辅助函数,仅在程序还持有 GPL 兼容许可证的情况下,例如 bpf_ktime_get_ns()
    bpf_probe_read()
    等等。
    其余部分名称是特定于 BPF 程序代码的,例如,下面的代码已被修改为包含两个程序部分,ingress
     并且egress
    玩具示例代码演示了两者都可以共享一个地图和常见的静态内联帮助器,例如account_data()
    函数。
    xdp-example.c
    示例已修改为tc-example.c
     可以使用 tc 加载并附加到网络设备的入口和出口挂钩的示例。它将传输的字节记入一个名为 的映射 acc_map
    中,该映射有两个映射槽,一个用于入口钩子上的流量,一个用于出口钩子。
      #include <linux/bpf.h>
      #include <linux/pkt_cls.h>
      #include <stdint.h>
      #include <iproute2/bpf_elf.h>


      #ifndef __section
      # define __section(NAME) \
      __attribute__((section(NAME), used))
      #endif


      #ifndef __inline
      # define __inline \
      inline __attribute__((always_inline))
      #endif


      #ifndef lock_xadd
      # define lock_xadd(ptr, val) \
      ((void)__sync_fetch_and_add(ptr, val))
      #endif


      #ifndef BPF_FUNC
      # define BPF_FUNC(NAME, ...) \
      (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME
      #endif


      static void *BPF_FUNC(map_lookup_elem, void *map, const void *key);


      struct bpf_elf_map acc_map __section("maps") = {
      .type = BPF_MAP_TYPE_ARRAY,
      .size_key = sizeof(uint32_t),
      .size_value = sizeof(uint32_t),
      .pinning = PIN_GLOBAL_NS,
      .max_elem = 2,
      };


      static __inline int account_data(struct __sk_buff *skb, uint32_t dir)
      {
      uint32_t *bytes;


      bytes = map_lookup_elem(&acc_map, &dir);
      if (bytes)
      lock_xadd(bytes, skb->len);


      return TC_ACT_OK;
      }


      __section("ingress")
      int tc_ingress(struct __sk_buff *skb)
      {
      return account_data(skb, 0);
      }


      __section("egress")
      int tc_egress(struct __sk_buff *skb)
      {
      return account_data(skb, 1);
      }


      char __license[] __section("license") = "GPL";


      该示例还演示了在开发程序时需要注意的一些其他事项。代码包括内核头、标准C头和一个特定于iproute2的头,其中包含struct bpf_elf_映射的定义。iproute2有一个通用的BPF ELF加载程序,因此,对于XDP和tc类型的程序,结构struct bpf_elf_map映射的定义是一样的。

      一个struct bpf_elf_map在程序中定义了一个映射,并包含生成映射所需的所有相关信息(例如键/值大小等),该映射从两个 BPF 程序中使用。必须将结构放入maps部分中,以便加载程序可以找到它。这种类型的映射声明可以有多个具有不同变量名的声明,但都必须用.__section("maps")

      结构struct bpf_elf_map被指定到 iproute2 不同的 BPF ELF 加载器可以有不同的格式,例如内核源代码树中的 libbpf,该库主要被perf使用。 iproute2 向后兼容为了struct bpf_elf_mapCilium 遵循 iproute2 模型。

      该示例还演示了 BPF 辅助函数如何映射到 C 代码中并被使用。在这里,map_lookup_elem()
      通过将此函数映射到BPF_FUNC_map_lookup_elem
      作为助手公开的枚举值来定义uapi/linux/bpf.h
      当程序稍后被加载到内核中时,验证器检查传递的参数是否是预期的类型,并将帮助程序调用重新指向真正的函数调用。此外,map_lookup_elem()
      还演示了如何将映射传递给 BPF 辅助函数。在这里,&acc_map
      来自 maps
      部分作为第一个参数传递给map_lookup_elem()
      .

      由于定义的数组映射是全局的,需要使用原子操作,定义为lock_xadd()
      __sync_fetch_and_add()
      作为内置函数映射到 BPF 原子添加指令。

      三,不允许使用全局变量

      由于第 1 点中已经提到的原因,BPF 不能像普通 C 程序中经常使用的那样具有全局变量。

      但是,有一种变通方法,程序可以简单地使用类型为 BPF 的映射,BPF_MAP_TYPE_PERCPU_ARRAY
      其中只有一个任意值大小的槽。这是可行的,因为在执行期间,BPF 程序保证永远不会被内核抢占,因此可以使用单个映射条目作为临时数据的暂存缓冲区,例如,扩展堆栈限制。这也适用于尾调用,因为它在抢占方面具有相同的保证。

      否则,为了在多个 BPF 程序运行中保持状态,可以使用普通的 BPF 映射。

      四,不允许使用 const 字符串或数组。

      在 BPF C 程序中定义const
      字符串或其他数组不起作用,原因与第 1 节和第 3 节中指出的相同,即,将在 ELF 文件中生成重定位条目,由于不是一部分,加载程序将拒绝这些条目加载器的 ABI(加载器也无法修复此类条目,因为它需要对已编译的 BPF 序列进行大量重写)。

      将来,LLVM 可能会检测到这些事件并提前向用户抛出错误。

      trace_printk()
      可以按如下方式解决辅助功能:

        static void BPF_FUNC(trace_printk, const char *fmt, int fmt_size, ...);


        #ifndef printk
        # define printk(fmt, ...) \
        ({ \
        char ____fmt[] = fmt; \
        trace_printk(____fmt, sizeof(____fmt), ##__VA_ARGS__); \
        })
        #endif


        五,将 LLVM 内置函数用于 memset()/memcpy()/memmove()/memcmp()

        由于 BPF 程序除了对 BPF 助手的调用之外,不能执行任何函数调用,因此需要将公共库代码实现为内联函数。此外,LLVM 还提供了一些内置程序可用于固定大小(此处n
        :),然后将始终内联:

          #ifndef memset
          # define memset(dest, chr, n) __builtin_memset((dest), (chr), (n))
          #endif


          #ifndef memcpy
          # define memcpy(dest, src, n) __builtin_memcpy((dest), (src), (n))
          #endif


          #ifndef memmove
          # define memmove(dest, src, n) __builtin_memmove((dest), (src), (n))
          #endif


          memcmp()
          内置有一些极端情况,由于后端的 LLVM 问题而没有发生内联,因此在问题得到解决之前不建议使用


          六,没有可用的循环

          内核中的 BPF 验证器通过对除其他控制流图验证之外的所有可能的程序路径执行深度优先搜索来检查 BPF 程序是否包含循环。目的是确保程序始终保证终止。
          通过 使用#pragma unroll
          指令,一种非常有限的循环形式可用于恒定的循环上限。编译为 BPF 的示例代码:
            #pragma unroll
            for (i = 0; i < IPV6_MAX_HEADERS; i++) {
            switch (nh) {
            case NEXTHDR_NONE:
            return DROP_INVALID_EXTHDR;
            case NEXTHDR_FRAGMENT:
            return DROP_FRAG_NOSUPPORT;
            case NEXTHDR_HOP:
            case NEXTHDR_ROUTING:
            case NEXTHDR_AUTH:
            case NEXTHDR_DEST:
            if (skb_load_bytes(skb, l3_off + len, &opthdr, sizeof(opthdr)) < 0)
            return DROP_INVALID;


            nh = opthdr.nexthdr;
            if (nh == NEXTHDR_AUTH)
            len += ipv6_authlen(&opthdr);
            else
            len += ipv6_optlen(&opthdr);
            break;
            default:
            *nexthdr = nh;
            return len;
            }
            }


            七,使用尾调用对程序进行分区

            尾调用通过从一个 BPF 程序跳转到另一个 BPF 程序,提供了在运行期间以原子方式改变程序行为的灵活性。为了选择下一个程序,尾调用利用程序数组映射(BPF_MAP_TYPE_PROG_ARRAY
            ),并将映射以及索引传递给下一个程序以跳转。执行跳转后不会返回旧程序,如果在给定的映射索引处不存在程序,则继续执行原始程序。
            例如,这可用于实现解析器的各个阶段,这些阶段可以在运行时使用新的解析功能进行更新。
            另一个用例是事件通知,例如,Cilium 可以在运行时选择丢包通知,其中skb_event_output()
            调用位于被调用程序的尾部。因此,在正常操作期间,除非将程序添加到相关地图索引中,否则将始终执行直通路径,然后该程序准备元数据并触发事件通知给用户空间守护进程。
            程序数组映射非常灵活,还可以为位于每个映射索引中的程序实现单独的操作。例如,附加到 XDP 或 tc 的根程序可以对程序数组映射的索引 0 执行初始尾调用,执行流量采样,然后跳转到程序数组映射的索引 1,其中应用了防火墙策略并且数据包要么在程序数组映射的索引 2 中删除或进一步处理,在那里它被破坏并再次从接口发送出去。当然,程序数组映射中的跳转可以是任意的。当达到最大尾调用限制时,内核最终将执行失败路径。
            使用尾调用的最小示例摘录:
              [...]


              #ifndef __stringify
              # define __stringify(X) #X
              #endif


              #ifndef __section
              # define __section(NAME) \
              __attribute__((section(NAME), used))
              #endif


              #ifndef __section_tail
              # define __section_tail(ID, KEY) \
              __section(__stringify(ID) "/" __stringify(KEY))
              #endif


              #ifndef BPF_FUNC
              # define BPF_FUNC(NAME, ...) \
              (*NAME)(__VA_ARGS__) = (void *)BPF_FUNC_##NAME
              #endif


              #define BPF_JMP_MAP_ID 1


              static void BPF_FUNC(tail_call, struct __sk_buff *skb, void *map,
              uint32_t index);


              struct bpf_elf_map jmp_map __section("maps") = {
              .type = BPF_MAP_TYPE_PROG_ARRAY,
              .id = BPF_JMP_MAP_ID,
              .size_key = sizeof(uint32_t),
              .size_value = sizeof(uint32_t),
              .pinning = PIN_GLOBAL_NS,
              .max_elem = 1,
              };


              __section_tail(BPF_JMP_MAP_ID, 0)
              int looper(struct __sk_buff *skb)
              {
              printk("skb cb: %u\n", skb->cb[0]++);
              tail_call(skb, &jmp_map, 0);
              return TC_ACT_OK;
              }


              __section("prog")
              int entry(struct __sk_buff *skb)
              {
              skb->cb[0] = 0;
              tail_call(skb, &jmp_map, 0);
              return TC_ACT_OK;
              }


              char __license[] __section("license") = "GPL";


              八,最大 512 字节的有限堆栈空间。

              BPF 程序中的堆栈空间仅限于 512 字节,在 C 中实现 BPF 程序时需要仔细考虑。但是,如前面第 3 点所述,BPF_MAP_TYPE_PERCPU_ARRAY
              可以使用具有单个条目的映射来扩大暂存空间缓冲空间。


              九,可以使用 BPF 内联汇编。

              LLVM 6.0 或更高版本允许在可能需要的极少数情况下使用 BPF 的内联汇编。下面的(废话)玩具示例显示了一个 64 位原子加法。由于缺乏文档,LLVM 源代码lib/Target/BPF/BPFInstrInfo.td
               以及test/CodeGen/BPF/
              可能有助于提供一些额外的示例。测试代码:

                #include <linux/bpf.h>


                #ifndef __section
                # define __section(NAME) \
                __attribute__((section(NAME), used))
                #endif


                __section("prog")
                int xdp_test(struct xdp_md *ctx)
                {
                __u64 a = 2, b = 3, *c = &a;
                /* just a toy xadd example to show the syntax */
                asm volatile("lock *(u64 *)(%0+0) += %1" : "=r"(c) : "r"(b), "0"(c));
                return a;
                }


                char __license[] __section("license") = "GPL";


                上面的程序被编译成如下的BPF指令序列:

                  Verifier analysis:


                  0: (b7) r1 = 2
                  1: (7b) *(u64 *)(r10 -8) = r1
                  2: (b7) r1 = 3
                  3: (bf) r2 = r10
                  4: (07) r2 += -8
                  5: (db) lock *(u64 *)(r2 +0) += r1
                  6: (79) r0 = *(u64 *)(r10 -8)
                  7: (95) exit
                  processed 8 insns (limit 131072), stack depth 8


                  十,使用#pragma pack 移除带有对齐成员的结构填充。

                  在现代编译器中,数据结构默认对齐以有效地访问内存。结构成员被打包到内存地址并添加填充以与处理器字大小正确对齐(例如,64 位处理器为 8 字节,32 位处理器为 4 字节)。正因为如此,struct 的大小可能经常比预期的要大。

                    struct called_info {
                    u64 start; // 8-byte
                    u64 end; // 8-byte
                    u32 sector; // 4-byte
                    }; // size of 20-byte ?


                    printf("size of %d-byte\n", sizeof(struct called_info)); // size of 24-byte


                    // Actual compiled composition of struct called_info
                    // 0x0(0) 0x8(8)
                    // ↓________________________↓
                    // | start (8) |
                    // |________________________|
                    // | end (8) |
                    // |________________________|
                    // | sector(4) | PADDING | <= address aligned to 8
                    // |____________|___________| with 4-byte PADDING.


                    内核中的 BPF 验证器检查 BPF 程序不会访问边界或未初始化堆栈区域之外的堆栈边界。

                      struct called_info {
                      u64 start;
                      u64 end;
                      u32 sector;
                      };


                      struct bpf_map_def SEC("maps") called_info_map = {
                      .type = BPF_MAP_TYPE_HASH,
                      .key_size = sizeof(long),
                      .value_size = sizeof(struct called_info),
                      .max_entries = 4096,
                      };


                      SEC("kprobe/submit_bio")
                      int submit_bio_entry(struct pt_regs *ctx)
                      {
                      char fmt[] = "submit_bio(bio=0x%lx) called: %llu\n";
                      u64 start_time = bpf_ktime_get_ns();
                      long bio_ptr = PT_REGS_PARM1(ctx);
                      struct called_info called_info = {
                      .start = start_time,
                      .end = 0,
                      .sector = 0
                      };


                      bpf_map_update_elem(&called_info_map, &bio_ptr, &called_info, BPF_ANY);
                      bpf_trace_printk(fmt, sizeof(fmt), bio_ptr, start_time);
                      return 0;
                      }


                      对应的输出bpf_load_program()

                        bpf_load_program() err=13
                        0: (bf) r6 = r1
                        ...
                        19: (b7) r1 = 0
                        20: (7b) *(u64 *)(r10 -72) = r1
                        21: (7b) *(u64 *)(r10 -80) = r7
                        22: (63) *(u32 *)(r10 -64) = r1
                        ...
                        30: (85) call bpf_map_update_elem#2
                        invalid indirect read from stack off -80+20 size 24


                        十一,通过无效引用访问数据包数据

                        一些网络 BPF 辅助函数bpf_skb_store_bytes
                        可能会改变数据包数据的大小。由于验证者无法跟踪此类更改,因此对数据的任何先验引用都将被验证者无效。因此,在访问数据之前需要更新引用,以避免验证者拒绝程序。

                        为了说明这一点,请考虑以下代码段:

                          struct iphdr *ip4 = (struct iphdr *) skb->data + ETH_HLEN;


                          skb_store_bytes(skb, l3_off + offsetof(struct iphdr, saddr), &new_saddr, 4, 0);


                          if (ip4->protocol == IPPROTO_TCP) {
                          // do something
                          }


                          由于取消引用了 invalidated ,验证程序将拒绝该片段 ip4->protocol

                            R1=pkt_end(id=0,off=0,imm=0) R2=pkt(id=0,off=34,r=34,imm=0) R3=inv0
                            R6=ctx(id=0,off=0,imm=0) R7=inv(id=0,umax_value=4294967295,var_off=(0x0; 0xffffffff))
                            R8=inv4294967162 R9=pkt(id=0,off=0,r=34,imm=0) R10=fp0,call_-1
                            ...
                            18: (85) call bpf_skb_store_bytes#9
                            19: (7b) *(u64 *)(r10 -56) = r7
                            R0=inv(id=0) R6=ctx(id=0,off=0,imm=0) R7=inv(id=0,umax_value=2,var_off=(0x0; 0x3))
                            R8=inv4294967162 R9=inv(id=0) R10=fp0,call_-1 fp-48=mmmm???? fp-56=mmmmmmmm
                            21: (61) r1 = *(u32 *)(r9 +23)
                            R9 invalid mem access 'inv'


                            要解决此问题,ip4
                            必须更新对的引用:

                              struct iphdr *ip4 = (struct iphdr *) skb->data + ETH_HLEN;


                              skb_store_bytes(skb, l3_off + offsetof(struct iphdr, saddr), &new_saddr, 4, 0);


                              ip4 = (struct iphdr *) skb->data + ETH_HLEN;


                              if (ip4->protocol == IPPROTO_TCP) {
                              // do something
                              }




                              觉得不错,点击“分享”,“赞”,“在看”传播给更多热爱嵌入式的小伙伴吧!

                              文章转载自囧囧妹,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                              评论