分类
ebpf

ebpf map 用户态源码分析

最近 facebook 开源了一份 L4LB katran 基于 xdp做的。 里面大量使用了 ebpf maps 进行用户态和内核态进行数据交互。今天dig了一下代码大概了解一下原理。 使用的代码时 kernel/sample/bpf/中的代码。
ebpf 代码部分:
在ebpf代码中需要申明map

struct bpf_map_def SEC("maps") port_a = {           //SEC("maps") 这个是llvm生成代码的section标记,表示为这段代码单独生成一个maps code setion。 在load_bpf代码时,会根据这个标记找到这个section 然后在内核创建相应的内存。
        .type = BPF_MAP_TYPE_ARRAY,                 //map的类型  有很多当前为array类型
        .key_size = sizeof(u32),                    //key 的长度
        .value_size = sizeof(int),                  //value 的长度
        .max_entries = MAX_NR_PORTS,                //这个array的最大长度。
};

编译完成后 会生成一个 elf的.o文件,然后在进程上下文中解析elf文件获取maps信息,代码如下

        fd = open(path, O_RDONLY, 0);               //打开llvm 编译后的test_map_in_kernel.o文件
        if (fd < 0)
                return 1;

        elf = elf_begin(fd, ELF_C_READ, NULL);

        if (!elf)
                return 1;

        if (gelf_getehdr(elf, &ehdr) != &ehdr)      //获取所有section head信息
                return 1;

        /* clear all kprobes */
        i = system("echo \"\" > /sys/kernel/debug/tracing/kprobe_events");

        /* scan over all elf sections to get license and map info */
        for (i = 1; i < ehdr.e_shnum; i++) {

                if (get_sec(elf, i, &ehdr, &shname, &shdr, &data))
                        continue;

                if (0) /* helpful for llvm debugging */
                        printf("section %d:%s data %p size %zd link %d flags %d\n",
                               i, shname, data->d_buf, data->d_size,
                               shdr.sh_link, (int) shdr.sh_flags);

                if (strcmp(shname, "license") == 0) {
                        processed_sec[i] = true;
                        memcpy(license, data->d_buf, data->d_size);
                } else if (strcmp(shname, "version") == 0) {
                        processed_sec[i] = true;
                        if (data->d_size != sizeof(int)) {
                                printf("invalid size of version section %zd\n",
                                       data->d_size);
                                return 1;
                        }
                        memcpy(&kern_version, data->d_buf, sizeof(int));
                } else if (strcmp(shname, "maps") == 0) {           //如果section 头部名字是maps,则说明 test_map_in_kernel中有map
                        int j;

                        maps_shndx = i;
                        data_maps = data;
                        for (j = 0; j < MAX_MAPS; j++)
                                map_data[j].fd = -1;
                } else if (shdr.sh_type == SHT_SYMTAB) {
                        strtabidx = shdr.sh_link;
                        symbols = data;
                }
        }

分析 maps section 并在内核创建map 对象,对外表现为文件类型

        if (data_maps) {
                nr_maps = load_elf_maps_section(map_data, maps_shndx,
                                                elf, symbols, strtabidx);           //解析maps section 获取每个定义map的对象
                if (nr_maps < 0) {
                        printf("Error: Failed loading ELF maps (errno:%d):%s\n",
                               nr_maps, strerror(-nr_maps));
                        ret = 1;
                        goto done;
                }
                if (load_maps(map_data, nr_maps, fixup_map))                //加载map并在内核创建相应的对象。返回文件描述符
                        goto done;
                map_data_count = nr_maps;

                processed_sec[maps_shndx] = true;
        }

load_elf_maps_section 主要是讲maps 对象的定义读取出来,到load_maps中使用,关键代码如下

        for (i = 0; i < nr_maps; i++) {
                unsigned char *addr, *end;
                struct bpf_map_def *def;
                const char *map_name;
                size_t offset;

                map_name = elf_strptr(elf, strtabidx, sym[i].st_name);
                maps[i].name = strdup(map_name);
                if (!maps[i].name) {
                        printf("strdup(%s): %s(%d)\n", map_name,
                               strerror(errno), errno);
                        free(sym);
                        return -errno;
                }

                /* Symbol value is offset into ELF maps section data area */
                offset = sym[i].st_value;                               //获取变量的偏移位置
                def = (struct bpf_map_def *)(data_maps->d_buf + offset);
                maps[i].elf_offset = offset;
                memset(&maps[i].def, 0, sizeof(struct bpf_map_def));
                memcpy(&maps[i].def, def, map_sz_copy);             //拷贝变量的定义到maps[i].def中后面会使用

                /* Verify no newer features were requested */
                if (validate_zero) {
                        addr = (unsigned char*) def + map_sz_copy;
                        end  = (unsigned char*) def + map_sz_elf;
                        for (; addr < end; addr++) {
                                if (*addr != 0) {
                                        free(sym);
                                        return -EFBIG;
                                }
                        }
                }
        }

load_maps函数,根据上一个函数获取的信息将信息下发到内核并创建文件描述符,关键代码如下

                if (maps[i].def.type == BPF_MAP_TYPE_ARRAY_OF_MAPS ||
                    maps[i].def.type == BPF_MAP_TYPE_HASH_OF_MAPS) {
                        int inner_map_fd = map_fd[maps[i].def.inner_map_idx];

                        map_fd[i] = bpf_create_map_in_map_node(maps[i].def.type,        //元素的值是map的
                                                        maps[i].name,
                                                        maps[i].def.key_size,
                                                        inner_map_fd,
                                                        maps[i].def.max_entries,
                                                        maps[i].def.map_flags,
                                                        numa_node);
                } else {
                        map_fd[i] = bpf_create_map_node(maps[i].def.type,           //元素的值是普通类型的
                                                        maps[i].name,
                                                        maps[i].def.key_size,
                                                        maps[i].def.value_size,
                                                        maps[i].def.max_entries,
                                                        maps[i].def.map_flags,
                                                        numa_node);
                }

bpf_create_map_node函数就是将这些信息组织成attr 结构体 并调用 bpf系统调用,将信息下发到内核,并返回map的fd信息给用户态,在加载程序时需要用到。
接下来是将代码段中的 maps 与相应的fd 关联起来,内核中会根据fd,给代码中使用符号赋地址

        for (i = 1; i < ehdr.e_shnum; i++) {
                if (processed_sec[i])
                        continue;

                if (get_sec(elf, i, &ehdr, &shname, &shdr, &data))
                        continue;

                if (shdr.sh_type == SHT_REL) {
                        struct bpf_insn *insns;

                        /* locate prog sec that need map fixup (relocations) */
                        if (get_sec(elf, shdr.sh_info, &ehdr, &shname_prog,
                                    &shdr_prog, &data_prog))
                                continue;

                        if (shdr_prog.sh_type != SHT_PROGBITS ||
                            !(shdr_prog.sh_flags & SHF_EXECINSTR))
                                continue;

                        insns = (struct bpf_insn *) data_prog->d_buf;
                        processed_sec[i] = true; /* relo section */

                        if (parse_relo_and_apply(data, symbols, &shdr, insns,           //这个函数是将fd与insns关联起来。内核中根据fd将map地址赋值到程序中
                                                 map_data, nr_maps))
                                continue;
                }
        }

最后是通过加载EBPF程序到内核中

        for (i = 1; i < ehdr.e_shnum; i++) {

                if (processed_sec[i])
                        continue;

                if (get_sec(elf, i, &ehdr, &shname, &shdr, &data))
                        continue;

                if (memcmp(shname, "kprobe/", 7) == 0 ||
                    memcmp(shname, "kretprobe/", 10) == 0 ||
                    memcmp(shname, "tracepoint/", 11) == 0 ||
                    memcmp(shname, "raw_tracepoint/", 15) == 0 ||
                    memcmp(shname, "xdp", 3) == 0 ||
                    memcmp(shname, "perf_event", 10) == 0 ||
                    memcmp(shname, "socket", 6) == 0 ||
                    memcmp(shname, "cgroup/", 7) == 0 ||
                    memcmp(shname, "sockops", 7) == 0 ||
                    memcmp(shname, "sk_skb", 6) == 0 ||
                    memcmp(shname, "sk_msg", 6) == 0) {
                        ret = load_and_attach(shname, data->d_buf,
                                              data->d_size);                    //这个函数根据不同的ebpf类型讲代码加载到内核中。
                        if (ret != 0)
                                goto done;
                }
        }

发表评论

电子邮件地址不会被公开。 必填项已用*标注