首页 / 科技发展 / ebpf 内核(ebpf for windows)

ebpf 内核(ebpf for windows)

Time:2024-06-08 15:46:39 Read:318 作者:CEO

本文首发于“内核工匠”微信公众号。欢迎关注公众号,获取最新Linux技术分享。

系列目录

ebpf 内核(ebpf for windows)

【资料图】

1. 怀疑

2. vfsstat_bpf__open

2.1 bpf_object__open_骨骼

2.2 bpf_object__open_mem/bpf_object_open

2.3 OPTS_VALID 检查参数的合法性

2.4 bpf_object__new 创建一个新的bpf_object对象

2.5 bpf_object__elf_init初始化elf文件

2.6 bpf_object__elf_collect收集每个段落的信息

2.7 bpf_object__init_maps初始化map

2.8 bpf_object_init_progs初始化程序程序

2.9 打开bpf总结

3. bpf_object__load_骨骼加载bpf

4. bpf_object__attach_sculpture 附加bpf程序

5.触发bpf程序

六、总结

1. 怀疑

在学习bpf的过程中,你是带着问题来学习的:

1、为什么我们写的bpf程序能够在内核上生效?代码是如何注入内核的?

2、libbpf相当于一个框架,那么它是如何设计和构建的呢?

3.elf文件格式是什么? vfsstat.bpf.o的内容包含哪些信息?

在学习bpf之前,我不知道elf文件是什么(也没有关注)。

4.bpf可以用来做什么?它的价值是什么?它能做什么?

有很多问题。我们先看一下代码,先理清一行。

我们以libbpf-tools的vfsstat为例,继续深入探究这个bpf程序是做什么的?

2. vfsstat_bpf__open

vfsstat_bpf__open 打开bpf 程序。这里主要关注libbpf框架和elf文件格式的处理。我们选择以下流程作为讲解内容:

2.1 bpf_object__open_骨骼

1.构建bpf_object_open_opts skel_opts,默认只有sz(bpf_object_open_opts结构体的大小),object_name='vfsstat_bpf'(骨架的名称)

2. bpf_object__open_mem将从vfsstat.bpf.o的内容构建bpf_object(关键过程)

3.给s-maps[0].mmaped赋值

2.2 bpf_object__open_mem/bpf_object_open

=bpf_object__open_mem

检查obj_buf和obj_buf_sz是否合法,同时调用bpf_object_open

=bpf_object_open

1. OPTS_VALID 检查参数是否合法。参数size opts-sz不能小于size_t,附加长度需要为0(任何超过bpf_object_open_opts__last_field的都是附加参数)

2. bpf_object__new 创建一个新的bpf_object对象

3、bpf_object__elf_init初始化elf文件

4. bpf_object__elf_collect收集每个段落的信息

5. bpf_object__init_maps初始化map相关数据

6、bpf_object_init_progs遍历并初始化各个bpf_program

2.3 OPTS_VALID 检查参数的合法性

OPTS_VALID 这是一个宏,用于检查bpf的各个参数是否合法。如果后面传入了额外的参数,则需要注意这一点。

1、OPTS_VALID和offsetofend的宏定义如下

2. OPTS_VALID(opts, bpf_object_open_opts) 扩展后为

3. bpf_object_open_opts__last_field是bpf_object_open_opts中的kernel_log_level元素

4.所以offsetofend(struct bpf_object_open_opts, bpf_object_open_opts__last_field)代表

bpf_object_open_opts从kernel_log_level开始到结束的偏移量(包括kernel_log_level的大小)

5. libbpf_validate_opts 将检查:

bpf_object_open_opts的sz必须大于等于sizeof(size_t),超过元素类型##__last_field的sz必须为0。

2.4 bpf_object__new 创建一个新的bpf_object对象

1.为bpf_object对象分配空间,

2、在初始化结构体elf_state efile中初始化obj_buf(vfsstat.bpf.o文件的实际内容)和obj_buf_sz(vfsstat.bpf.o文件的实际内容)。

2.5 bpf_object__elf_init初始化elf文件

1. efile.elf需要为NULL(未初始化)

2、elf_memory从obj_buf中读取elf文件(efile.elf),其中包括Elf64_Ehdr(elf文件头)、段落详细数据、段落简要数据数组

3. elf-kin 必须是ELF_K_ELF 类型

4、获取字符串表的id(elf_getshdrstrndx)并初始化字符串表的原始数据elf_rawdata

5. ebpf程序必须满足e_type=1(ET_REL可重定向文件),e_machine=247(EM_BPF bpf程序)

2.5.1 elf_memory/__libelf_read_mmaped_file

1. elf_memory已经转移到libelf库中

2.从内存中读取elf文件

1)define_kind判断elf的文件类型

2)file_read_elf读取elf文件

3.查找elf文件类型

1)elf分为ELF_K_ELF(前4个字节以'\177ELF'开头)、ELF_K_AR(前8个字节以'!\n'开头)

2)elf文件(后面提到的elf文件都是ELF_K_ELF类型),文件开头为Elf64_Ehdr,

前4个字节以'\177ELF'开头,第5个字节是eclass(文件类型,这里0x02代表64bit),

第6个字节是data(代表big endian和little endian,这里0x01代表little endian格式),第7个字节version(代表elf的版本号,这里0x01代表版本号)。

elf的具体文件头如图:“ELF文件头”

2.5.2 文件读取elf

1.首先将Elf64_Ehdr贴到vfsstat.bpf.o中看一下。

=例如其二进制数据如下所示:

=转换为Elf64_Ehdr如下

2.file_read_elf函数

1)读取elf文件时,进行32/64位、big endian/little endian检查

2)allocate_elf为elf分配内存并初始化elf对象(额外需要额外分配44 * sizeof(Elf_Scn)的内存,

用于存储struct Elf_Scn data[0]的内容(elf-state.elf32.scns.data))

3)根据map_address(obj_buf)和offset(0)获取Elf64_Ehdr *ehdr(elf文件头)并赋值给elf-state.elf64.ehdr

4)ehdr(elf文件头)+e_shoff(elf文件头结束位置)=得到elf-state.elf64.shdr(elf节文件头)

5)用shdr(elf节文件头)初始化elf-state.elf64.scns.data[](节数据)

3、Elf64_Shdr *shdr 格式如下:

4、shdr段落头的二进制代码如下:

=前四段标题的实际内容如下

2.5.3 elf_rawdata

elf_getscn函数实际上是从file_read_elf中取出的elf-state.elf64.scns.data[cnt]。

其中cnt是elf_getscn传入的idx(对应这里字符串表的id:obj-efile.shstrndx=1)

1. elf_rawdata

如果该段落尚未读取,则调用__libelf_set_rawdata读取原始数据rawdata。

2. __libelf_set_rawdata_wrlock读取段落

以字符串表为例

1)读取原始数据的基地址scn-rawdata_base=scn-rawdata.d.d_buf=obj_buf + 0 + 10600(shdr[1]中的偏移量)

2)设置原始数据的大小Elf_Data scn-rawdata.d.d_size=607,输入scn-rawdata.d.d_type=ELF_T_BYTE(0),

偏移量scn-rawdata.d.d_off=0,版本scn-rawdata.d.d_version=1

3)scn-rawdata.s指向Elf_Scn *scn本身,设置读取的data_read=1,并修改Elf_Scn的flags=ELF_F_FILEDATA=0x100

2.6 bpf_object__elf_collect收集每个段落的信息

1、elf_nextscn首先遍历elf段找到对应的符号表段,如本例中的shdr[43]。

2、通过elf_sec_data获取字符串段落转换后的数据,然后初始化obj-efile的符号(elf_sec_data获取的数据),

symbols_shndx(符号表的段落ID)、strtabidx(符号表中字符串表的段落ID)

3、有了符号表后,再次遍历所有section(ignore_elf_section跳过.strtab、text(section size=0)、debug_、rel.debug_***、'.rel.BTF' '.rel.BTF .ext'),

elf_sec_data读取每个段落数据并根据不同的段落名称(使用elf_sec_str读取)和段落类型sh_type进行处理

4、对于程序段,通过bpf_object__add_programs初始化程序段

2.6.1 elf_sec_data获取段落数据

1、传入的是scn=scns.data[43],读取的是符号表段('.symtab')的信息

大致流程如下

elf_sec_data(libbpf.c) - elf_getdata(elf_getdata.c) - __elf_getdata_rdlock - __libelf_set_rawdata_wrlock/__libelf_set_data_list_rdlock

__libelf_set_data_list_rdlock - 转换数据

2. __elf_getdata_rdlock

1)如果本节未初始化原始数据rawdata,则调用__libelf_set_rawdata_wrlock进行初始化。

2)然后调用设置段落数据函数__libelf_set_data_list_rdlock

3. __libelf_set_data_list_rdlock

如果段落数据存在,则通过convert_data读取段落数据

4.转换数据

根据大小端对齐初始化转换后的数据scn-data_list.data.d

2.6.2 elf_sec_str 通过name的偏移地址获取对应字符串段落中的name

至于段落名从何而来,这里有解释

1、elf_sec_str传入的是sh_name(段落头名称的偏移地址,是一个数字)

其中bpf_object__elf_init的elf_getshdrstrndx获取shstrndx(字符串的段落ID)=e_shstrndx(elf文件头中的字符串段落id元素)=1

2、根据偏移offset从字符串段落中读取对应的字符串地址,如strscn-rawdata_base[offset]

3、关于找到的字符串段落本身的shdr[1]的sh_name=574(字符串段落中的偏移位置),

字符串段落本身的shdr[1]的sh_offset=10600(字符串段落本身在bfp.o中的偏移位置),

那么这个字符串的*.bpf.o对应的位置就是10600 + 574=11174=0x2BA6,

我们看一下*.bpf.o0x2BA6的内容,如下图‘name of string table.strstab’:

=

从上面的内容你就可以知道

sh_name={int} 574代表.strtab,即shdr[1]字符串段落的名称为“.strtab”,

其他段落的名称相同。您可以从此处的偏移量找到该字符串。

2.6.3 bpf_object__add_programs初始化程序部分

1、该函数主要做了以下几件事:

1)遍历所有符号Elf64_Sym数组(这里是32个),找到shdr[3]对应的符号Elf64_Sym d_buf[20]

(st_shndx需要等于sec_idx,st_info的后4位需要是STT_FUNC)

2)根据符号Elf64_Sym d_buf[20]的st_name搜索字符串段落,找到该符号的函数名,打印为:

“libbpf: sec 'kprobe/vfs_read': 在insn 偏移量0(0 字节)处找到程序'kprobe_vfs_read',代码大小6 insns(48 字节)”

上面的意思是在该段中找到程序‘kprobe/vfs_read’,名称为kprobe_vfs_read,指令集偏移地址为0,总共有6条指令。

其中,符号Elf64_Sym中的st_size表示指令集的总大小,st_value表示指令集的偏移量(这里为0,表示shdr[3]段的原始数据以指令数据开始)

3)指令集大致如下,每条指令8字节:

4)所有bpf程序按顺序存储在obj-programs中

5)data(sh_offset of shdr[3]) + sec_off(st_value of d_buf[20])的结果是insn_data(程序指令集的基地址)

6)调用bpf_object__init_prog初始化bpf_program(bpf程序)

2、bpf_object__init_prog初始化bfp程序

=设置段落ID(sec_idx)、指令偏移(sec_insn_off)、指令数量(sec_insn_cnt)、是否加载标签(load)、段落名称(sec_name)、

函数名(name),错误指令集insns的位置

3、我们看一下shdr[3]: sec_name=kprobe/vfs_read的命令数据。

=该命令的原始数据如下所示:

=我们再看一下这个函数(宏定义转换后):

=从sec_data-d_buf=sh_offset=64=0x40开始的48个字节如下,这是函数的指令,

以下是使用llvm-objdump-11 -d /data/vfsstat.bpf.o截获的信息(可以看到一些指令的运行情况):

=转换成指令集形式:

4. 各指令含义

1)第一条指令:

=使用strace -e bpf -v -s 256 /data/vfsstat_bin 2 3 命令查看指令操作码

{code=BPF_ALU64(64 位)|BPF_K(32 位立即数)|BPF_MOV(移动), dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0x1},

意思是:

BPF_ALU64:0x07,64位计算指令(指令详情请查看https://www.kernel.org/doc/html/latest/bpf/instruction-set.html)

BPF_K:0x00,基于32位立即数作为源操作数

BPF_MOV:0xb0,移动指令dst(目的操作寄存器)=src(源操作寄存器/编号)

代码=0xb7=BPF_ALU64(0x07)|BPF_K(0x00)|BPF_MOV(0xb0)

=所以上面的意思就变成了: dst_reg(r1)=imm(1)=r1=1

2.第二、第三条指令

=指令操作码如下:

{代码=BPF_LD(0x00)|BPF_DW(0x18)|BPF_IMM(0x00), dst_reg=BPF_REG_2, src_reg=BPF_REG_2, 关闭=0, imm=0x6},

{代码=BPF_LD(0x00)|BPF_W(0x00)|BPF_IMM(0x00), dst_reg=BPF_REG_0, src_reg=BPF_REG_0, 关闭=0, imm=0},

BPF_LD:0x00 加载操作,代码格式为:模式(3位)+大小(2位)+指令类别(3位)

BPF_LD、BPF_LDX、BPF_ST 和BPF_STX 均采用此格式。

模式必须是BPF_IMM(0x00 立即)、BPF_ABS(0x20 绝对)、BPF_IND(0x40 间接)、BPF_MEM(0x60 常规加载和存储)、BPF_ATOMIC(0xc0 自动操作)之一,

大小必须为BPF_W(0x00一字节)、BPF_H(0x08半字节)、BPF_B(0x10一字节)、BPF_DW(0x18双字节)

=所以上面的意思就变成了

dst_reg(r2)=imm64(该值是运行时生成的立即数)

3. 指令4

=指令操作码如下:

{code=BPF_STX(0x03)|BPF_DW(64 位操作)(0x18)|BPF_XADD(add)(0xc0), dst_reg=BPF_REG_2, src_reg=BPF_REG_1, off=0, imm=0},

BPF_STX:0x03,存储寄存器值

低密度脂蛋白,[

】: load是将源数据地址加载到目的寄存器Destination

STR,[

]: store是将寄存器Destination的内容存储在内存地址中

BPF_XADD:0xc0,类似于内核中的atomic_add(),原子(锁)加法

BPF_DW:0x18,双字节64位操作

=以上意思是:

锁定*(u64 *)(dst_reg(目标寄存器r2) + off(0)) +=src_reg(源寄存器r1)

//如果是BPF_ADD,则没有锁:*(u64 *)(dst_reg + off16) +=src_reg

4. 指令5

=指令操作码如下:

{code=BPF_ALU64|BPF_K|BPF_MOV,dst_reg=BPF_REG_0,src_reg=BPF_REG_0,关闭=0,imm=0},

=以上意思是:

BPF_ALU64|BPF_K(32 位立即数)|BPF_MOV(移动)

dst_reg(r0)=imm(0)

//r0也是保存返回值的寄存器

5. 指令6

=指令操作码如下:

{code=BPF_JMP|BPF_K|BPF_EXIT,dst_reg=BPF_REG_0,src_reg=BPF_REG_0,关闭=0,imm=0}],

BPF_JMP:0x05,64位跳转指令

BPF_K:0x00,32位立即数据操作

BPF_EXIT:0x90,函数或程序返回

=以上意思是:

返回函数返回

6、我们回过头来解释一下上面指令对应的代码。

2.6.4 bpf_object__init_btf初始化btf部分

1. btf段落('.BTF')为shdr[34],btf ext段落('.BTF.ext')为shdr[36]

2.bpf_object__init_btf

根据btf/btf_ext的源数据地址和大小创建新的btf对象(btf__new)和btf_ext对象(btf_ext__new)

3. btf__new 创建一个新的btf对象

1)btf_parse_hdr解析btf的头部

2)btf_parse_str_sec判断btf字符串段落是否合法

3)btf_parse_type_sec解析的type类型

4、btf_parse_hdr解析btf的头部

=btf头的格式如下:

=本例中的btf header如下:

=解析btf数据头看是否合法,如`btf magic header must be0xeB9F`

5. btf_parse_type_sec 解析btf的类型

=每个btf类型的类型为,其中btf类型判断使用info

=btf_parse_type_sec 遍历每个btf类型(btf_type_size)并保存数据

=btf_type_size 根据info中不同内容获取btf类型的大小

例如本例中的前两个btf类型如下:

2.7 bpf_object__init_maps初始化map

在bpf程序中,map非常重要。这是bpf程序传输数据的通道。这里简单提一下。

1)bpf_object__init_user_maps根据符号表初始化bpf_map

2)bpf_object__init_user_btf_maps初始化btf map相关

3)bpf_object__init_global_data_maps初始化SEC_DATA、SEC_RODATA、SEC_BSS段的地图数据(例如全局变量__u64 stats[]在SEC_BSS中),

构建bpf_map,mmaped对象存储map的数据

4)bpf_object__init_kconfig_map初始化EXT_KCFG(.kconfig)相关map

5) bpf_object__init_struct_ops_maps初始化.struct_ops相关maps

2.8 bpf_object_init_progs初始化程序程序

这里会出现SEC('kprobe/vfs_read')的处理流程。

1、bpf_object_init_progs遍历所有程序(bpf_program数组)

find_sec_def主要利用sec_name(段落名)查找对应的程序处理函数和程序类型prog_type

2、find_sec_def从section_defs中查找与段落名称匹配的bpf_sec_def(bfp段落默认处理结构)

3.section_defs数组目前支持以下:

kprobe函数对应attach_kprobe

4. bpf_sec_def(bfp段默认处理结构体)

5.kprobe宏定义扩展

#define SEC_DEF('kprobe/', KPROBE, 0, SEC_NONE, Attach_kprobe)

相当于=

2.9 打开bpf总结

以上就完成了开放部分。开放主要是指libbpf从vfsstat.bpf.o源数据(使用libelf读取)构建bpf程序、bpf映射等。

这部分不涉及与内核的通信,只是准备环境。

Copyright © 2002-2024 讯肆科技网 版权所有 

免责声明: 1、本站部分内容系互联网收集或编辑转载,并不代表本网赞同其观点和对其真实性负责。 2、本页面内容里面包含的图片、视频、音频等文件均为外部引用,本站一律不提供存储。 3、如涉及作品内容、版权和其它问题,请在30日内与本网联系,我们将在第一时间删除或断开链接! 4、本站如遇以版权恶意诈骗,我们必奉陪到底,抵制恶意行为。 ※ 有关作品版权事宜请联系客服邮箱:478923*qq.com(*换成@)

备案号: 沪ICP备2023025279号-31