当前位置:科学 > 正文

eBPF程序注入到内核中的流程,现在就带你研究(上)

2023-08-30 17:13:06  来源:内核工匠

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

系列目录



【资料图】

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装入目的寄存器Destination

STR, [

]: 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等,

这部分不涉与内核沟通,只是准备环境

关键词:

推荐阅读

月壤形成的主要原因 月壤与土壤有什么区别

月壤形成的主要原因月壤形成过程没有生物活动参与,没有有机质,还极度缺水干燥;组成月壤的矿物粉末基本是由陨石撞击破砰形成,因此,粉末 【详细】

域名抢注是是什么意思?投资角度来看什么域名好?

域名抢注是是什么意思域名抢注是通过抢先注册的方式获得互联网删除的域名的使用权。域名是由点分隔的一串数字,用于标记一台计算机或一组计 【详细】

捷达保养费用是多少?捷达是哪个国家的品牌?

捷达保养费用是多少?全新捷达的保修期为2年或6万公里,以先到者为准,新车可享受一次免费保养,首次免费保养在5000-7500km或1年内进行。如 【详细】

天然气泄露会造成爆炸吗?天然气泄漏怎么办?

天然气泄露会造成爆炸吗?家里用的天然气如果泄露是会发生爆炸的。当空气中含有混合天然气时,在与火源接触的一系列爆炸危险中,就会发生爆 【详细】

四部门明确App收集个人信息范围 个人信息保护范围判断标准

四部门明确App收集个人信息范围近日,国家互联网信息办公室、工业和信息化部、公安部、国家市场监督管理总局联合印发《常见类型移动互联网 【详细】

关于我们  |  联系方式  |  免责条款  |  招聘信息  |  广告服务  |  帮助中心

联系我们:85 572 98@qq.com备案号:粤ICP备18023326号-40

科技资讯网 版权所有