本文首发于“内核工匠”微信公众号。欢迎关注公众号,获取最新Linux技术分享。
系列目录
【资料图】
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映射等。
这部分不涉及与内核的通信,只是准备环境。