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