”
系列目录
1. 疑惑
2. vfsstat_bpf__open
2.1 bpf_object__open_skeleton
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初始化maps
2.8 bpf_object_init_progs初始化程序programs
2.9 open bpf总结
3. bpf_object__load_skeleton加载bpf
4. bpf_object__attach_skeleton附着bpf程序
5. 触发bpf程序
6 .总结
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_skeleton
1、构建bpf_object_open_opts skel_opts,默认只有sz(bpf_object_open_opts结构体的大小)、object_name = "vfsstat_bpf"(skeleton的名字)
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检查参数是否合法,参数大小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)、超过元素type##__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) determine_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(代表大端小端,此处是0x01代表小端格式)、第7个字节version(代表elf的版本号,此处是0x01代表版本号)。
elf的文件头具体如图:“ELF file header”
2.5.2 file_read_elf
1、先将vfsstat.bpf.o中的Elf64_Ehdr贴出来看一下,
=>如它的二进制数据如下图:
=>转换成Elf64_Ehdr如下
2、file_read_elf函数
1) 读取elf文件的时候,做32/64位、大端/小端检查
2) allocate_elf给elf分配内存和初始化话elf对象(extra额外需要分配的内存是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的section文件头)
5) 用shdr(elf的section文件头)初始化elf->state.elf64.scns.data[](section data)
3、关于Elf64_Shdr *shdr的格式如下图:
4、关于shdr段落头的二进制如下图:
=> 实际前面4个段落头的内容如下
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]中的offset)
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的section找到对应的符号表的段落,如本例子中的shdr[43]
2、通过elf_sec_data获取字符串段落转换后的数据,然后初始化obj->efile的symbols(elf_sec_data取得的数据)、
symbols_shndx(符号表的段落ID)、strtabidx(符号表中字符串表的段落ID)
3、有了符号表之后,再次遍历所有的段落(ignore_elf_section跳过.strtab、.text(section size = 0)、.debug_、.rel.debug_***、".rel.BTF" ".rel.BTF.ext"),
elf_sec_data读取每个段落数据,根据不同的段落名字name(使用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 -> convert_data
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、convert_data
根据大小端对齐初始化转换后的数据scn->data_list.data.d
2.6.2 elf_sec_str通过名字的偏移地址获得对应字符串段落中的名字
关于段落名字是从哪里来的,这里解释一下
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.o 0x2BA6这个位置的是什么内容,如下图"字符串表的名字.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': found program 'kprobe_vfs_read' at insn offset 0 (0 bytes), code size 6 insns (48 bytes)”
上面的意思是找到段落'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(shdr[3]的sh_offset) + sec_off(d_buf[20]的st_value)得到的就是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 bit)|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(源操作寄存器/数)
code = 0xb7 = BPF_ALU64(0x07)|BPF_K(0x00)|BPF_MOV(0xb0)
=> 于是上面的意思就变成了: dst_reg(r1) = imm(1) => r1 = 1
2、第二、三条指令
=> 指令操作码如下:
{code=BPF_LD(0x00)|BPF_DW(0x18)|BPF_IMM(0x00), dst_reg=BPF_REG_2, src_reg=BPF_REG_2, off=0, imm=0x6},
{code=BPF_LD(0x00)|BPF_W(0x00)|BPF_IMM(0x00), dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0},
BPF_LD: 0x00装载操作,code的格式是:mode(3 bits) + size(2 bits) + instruction class(3 bits)
(
BPF_LD, BPF_LDX, BPF_ST, and BPF_STX这几个存储加载寄存器都是这个格式,
其中mode必须是BPF_IMM(0x00立即数)、BPF_ABS(0x20绝对的)、BPF_IND(0x40间接的)、BPF_MEM(0x60常规加载存储)、BPF_ATOMIC(0xc0自动操作)之一,
其中size必须是BPF_W(0x00一个字节)、BPF_H(0x08半个字节)、BPF_B(0x10一个byte)、BPF_DW(0x18双字节)
)
=> 于是上面的意思就变成了
dst_reg(r2) = imm64(这个值是在运行的时候生成的立即数)
3、第4条指令
=> 指令操作码如下:
{code=BPF_STX(0x03)|BPF_DW(64 bit的操作)(0x18)|BPF_XADD(加)(0xc0), dst_reg=BPF_REG_2, src_reg=BPF_REG_1, off=0, imm=0},
BPF_STX: 0x03,存储寄存器的值
(
LDR, [
]: load是将源数据address装入目的寄存器DestinationSTR, [
]: store是将寄存器Destination的内容,存储在内存里面address)
BPF_XADD:0xc0,在内核类似于atomic_add(),原子(lock)的相加
BPF_DW:0x18,双字节64位操作
=> 上面的意思是:
lock *(u64 *)(dst_reg(目标寄存器r2) + off(0)) += src_reg(源寄存器r1)
//如果是BPF_ADD,则不带lock:*(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, off=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, off=0, imm=0}],
BPF_JMP: 0x05,64位的跳转指令
BPF_K: 0x00,32位立即数操作
BPF_EXIT:0x90,函数或者程序返回
=> 上面的意思是:
return函数返回
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的string段落是否合法
3) btf_parse_type_sec解析的type类型
4、btf_parse_hdr解析btf的头
=> btf头的格式是下面形式:
=> 本例中的btf头如下:
=> 解析btf数据头,查看是否合法,如`btf魔术头必须是0xeB9F`
5、btf_parse_type_sec解析btf的type
=> 每个btf type的类型是,其中btf类型判断使用的就是info
=> btf_parse_type_sec遍历每一个btf type(btf_type_size),并将数据保存起来
=> btf_type_size根据info中的不同内容获取btf type的size的大小
如本例中前面2个btf type如下:
2.7 bpf_object__init_maps初始化maps
在bpf程序中,maps是非常重要的,这个是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段落的map数据(如全局变量__u64 stats[]就在SEC_BSS中),
构建bpf_map,对象mmaped存储的是maps的数据
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初始化程序programs
这里将会出现SEC("kprobe/vfs_read")的处理流程
1、bpf_object_init_progs遍历所有的programs(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 open bpf总结
上面讲完了open部分,open主要是libbpf从vfsstat.bpf.o源数据(读取使用libelf)中构建bpf程序、bpf maps等,
这部分不涉与内核沟通,只是准备环境
下章将在下周五发布,请持续关注~
往
期
推
荐
一文搞定Android VSync机制来龙去脉
一文了解Vulkan在移动端渲染中的带宽与同步
AMD高保真超分算法1.0解密
长按关注内核工匠微信
Linux内核黑科技| 技术文章| 精选教程