概述eBPF 中的kprobe是一种基于 Linux 内核函数的动态探测机制可以在不修改内核或应用程序源代码的情况下在目标内核函数执行到指定位置时触发 eBPF 程序从而获取函数参数、调用上下文等运行信息。如果需要获取函数返回值通常需要配合kretprobe使用。例如当我们使用 Socket 编写一个 TCP Server 时服务端通常通过recv()函数接收客户端发送的数据。在常见的 Linux 实现中recv()会通过系统调用进入内核并经过__sys_recvfrom()等内核函数完成数据接收。因此我们可以使用 kprobe 探测__sys_recvfrom()的入口获取接收缓冲区地址等参数再结合 kretprobe 在函数返回时获取实际接收长度从而读取服务端接收到的业务数据。这里提到的 kretprobe 一般与 kprobe 结合使用。以recv()场景为例kprobe 在__sys_recvfrom()入口处可以拿到用户态缓冲区地址ubuf和请求接收长度size但此时数据通常还没有被拷贝到该缓冲区。kretprobe 在函数返回时触发此时可以通过返回值判断实际接收了多少字节再根据之前保存的ubuf地址读取对应长度的数据。二者区别如下kprobe主要用于探测内核函数入口或指定位置获取函数参数等信息。kretprobe用于探测内核函数返回获取函数返回值。kprobe 基本原理kprobe 的核心思想是在内核函数的入口或指定指令位置设置一个动态探测点。当目标位置被执行时内核会暂停当前执行路径转而执行挂载在该探测点上的 eBPF 程序。eBPF 程序运行在 Linux 内核中因此可以读取当前执行上下文中的信息例如函数参数、当前进程 ID、线程名称等。在使用 libbpf 编写 eBPF 程序时通常会通过SEC(kprobe/函数名)或BPF_KPROBE()这类方式声明探测点。例如挂载到__sys_recvfrom()时eBPF 程序会在该内核函数刚被调用时执行此时可以读取函数入参中的fd、ubuf、size等信息。需要注意的是kprobe 触发时函数通常还没有真正完成执行所以它更适合获取“函数入口时已经存在的信息”例如参数、当前进程信息和调用上下文。如果要获取函数执行后的结果例如返回值、实际读取的字节数或者根据入口参数在函数返回后读取缓冲区内容就需要配合 kretprobe。一个常见的使用流程是在 kprobe 中读取并保存函数入口参数。使用 BPF Map 以pid_tgid等值作为 key把入口参数临时保存起来。在 kretprobe 中读取函数返回值并通过相同的 key 找回入口参数。根据返回值判断函数是否成功执行再读取或上报需要的数据。这种方式可以把“函数进入时能拿到的参数”和“函数返回时才能知道的执行结果”关联起来因此非常适合分析系统调用、网络收发、文件读写等场景简单示例探测函数的选择本次案例以一个 TCP Server 为例通过探测内核接收数据的函数__sys_recvfrom()直接获取发送给 TCP Server 的数据这里为什么选择这个函数呢在 Linux TCP 网络通信中客户端发送的数据首先由服务器网卡接收再经过网卡驱动和 Linux TCP/IP 协议栈处理。对于已经建立的 TCP 连接内核会将接收到的有效数据组织到对应 Socket 的接收队列中等待应用程序读取。当用户态 TCP Server 调用recv()接收数据时在常见的 Linux 实现中接收请求会通过系统调用进入内核并经过__sys_recvfrom()、sock_recvmsg()、inet_recvmsg()和tcp_recvmsg()等函数完成接收处理最终将数据复制到用户态接收缓冲区。数据接收流程如下。需要注意的是图中如果按返回阶段理解应从底层函数逐级返回到上层系统调用入口这里选择__sys_recvfrom()作为 kprobe 探测点主要是因为该函数的参数中包含 Socket 文件描述符、用户态接收缓冲区地址ubuf以及请求接收长度size。通过 kprobe 可以在函数入口获取并保存这些参数再结合 kretprobe 在函数返回时获取实际接收长度。__sys_recvfrom函数原型/* * Receive a frame from the socket and optionally record the address of the * sender. We verify the buffers are writable and if needed move the * sender address from kernel to user space. */int__sys_recvfrom(intfd,void__user*ubuf,size_tsize,unsignedintflags,structsockaddr__user*addr,int__user*addr_len){structsockaddr_storageaddress;structmsghdrmsg{/* Save some cycles and dont copy the address if not needed */.msg_nameaddr?(structsockaddr*)address:NULL,};structsocket*sock;interr,err2;errimport_ubuf(ITER_DEST,ubuf,size,msg.msg_iter);if(unlikely(err))returnerr;CLASS(fd,f)(fd);if(fd_empty(f))return-EBADF;socksock_from_file(fd_file(f));if(unlikely(!sock))return-ENOTSOCK;if(sock-file-f_flagsO_NONBLOCK)flags|MSG_DONTWAIT;errsock_recvmsg(sock,msg,flags);if(err0addr!NULL){err2move_addr_to_user(address,msg.msg_namelen,addr,addr_len);if(err20)errerr2;}returnerr;}__sys_recvfrom函数参数解释:判断内核函数是否可以挂载在 Linux 中可以通过grep命令查看目标内核符号是否存在。以__sys_recvfrom为例执行sudogrep-w__sys_recvfrom /proc/kallsyms这里的/proc/kallsyms是 Linux 内核提供的符号表接口其中包含内核函数、变量等符号信息。如果输出类似如下信息ffffffff81a12340 T __sys_recvfrom#ffffffff81a12340: 内核符号地址示例#T: 该符号位于代码段中的全局符号#__sys_recvfrom: 内核函数名称则说明当前运行的 Linux 内核中存在这个函数符号具备作为 kprobe 探测目标的基础条件。如果输出为空说明当前符号表没有找到这个精确名称可能需要根据当前内核版本选择其他函数作为探测点。需要注意/proc/kallsyms能说明符号是否存在但实际是否能成功挂载还会受到内核配置、权限和符号可见性等因素影响。BPF 方法简单介绍Linux 内核提供了一些 Helper 方法来读取当前上下文信息同时也可以通过 BPF Map、Ring Buffer 等机制向用户态提交数据。常使用以下头文件头文件作用vmlinux.h提供 Linux 内核结构体和类型定义例如task_struct、sockbpf/bpf_helpers.h声明 eBPF Helper例如bpf_get_current_pid_tgid()、bpf_ringbuf_reserve()bpf/bpf_tracing.h提供BPF_KPROBE()、BPF_KRETPROBE()等宏bpf/bpf_core_read.h提供 CO-RE 相关读取宏例如BPF_CORE_READ()用于更安全地读取内核结构体字段bpf/libbpf.h用户态 libbpf API例如ring_buffer__new()、ring_buffer__poll()获取当前线程名称staticlong(*bpf_get_current_comm)(void*buf,__u32 size_of_buf)bpf_get_current_comm(void *buf, __u32 size_of_buf)Linux 内核提供的 eBPF Helper用于获取当前触发探针的线程名称可用于识别和过滤目标线程。在单线程程序中通常与进程名称一致但多线程程序中不一定相同。获取当前进程ID/* * bpf_get_current_pid_tgid * * Get the current pid and tgid. * * Returns * A 64-bit integer containing the current tgid and pid, and * created as such: * *current_task*\ **-tgid 32 \|** * *current_task*\ **-pid**. */static__u64(*bpf_get_current_pid_tgid)(void)(void*)14;该方法会同时返回进程 ID 和线程 ID其中高 32 位是 TGID通常可理解为进程 ID低 32 位是 PID通常可理解为线程 ID如图所示 如果需要单独获取进程 ID需要将返回值右移 32 bits代码如下/*bpf_get_current_pid_tgid: get bpf current process id*/// [32bits] [32bits] 32 | [32bits] [32bits]// process_id thread_id | process_id__u64 pid_tgidbpf_get_current_pid_tgid();__u32 pidpid_tgid32;不同的探测点之间共享数据有时候我们一个 eBPF 程序中需要探测多个函数也有时候同一个函数需要在不同的地方探测比如需要在进入函数的时候探测一次然后在函数返回的时候探测一次。这个时候就需要使用bpf_map_update_elem()方法中保存需要的数据然后在其他的探测点通过bpf_map_lookup_elem()拿到对应数据的地址。注意这两个方法的key参数和BPF Map自行定义的共享数据结构必须一致才可以共享数据函数原型如下/*Return 0 on success, or a negative error in case of failure. */staticlong(*bpf_map_update_elem)(void*map,constvoid*key,constvoid*value,__u64 flags)(void*)2;/* * Returns * Map value associated to *key*, or **NULL** if no entry was * found. */staticvoid*(*bpf_map_lookup_elem)(void*map,constvoid*key)(void*)1;伪代码简单示例/*自定义需要共享的数据的结构体*/structrecv_args{__u64 ubuff;__u64 size;intfd;};/* BPF Hash Map */struct{__uint(type,BPF_MAP_TYPE_HASH);//哈希表__uint(max_entries,1024);//最多保存 1024 条 Key-Value 记录__type(key,__u64);//key是一个__u64类型__type(value,structrecv_args);//共享的value是一个struct recv_args类型}recv_mapSEC(.maps);SEC(探测点)//进入的时候intBPF_KPROBE(){/*bpf_get_current_pid_tgid: get bpf current process id*/// [32bits] [32bits] 32 | [32bits] [32bits]__u64 pid_tgidbpf_get_current_pid_tgid();// process_id thread_id | process_id__u32 pidpid_tgid32;/*给共享数据赋值*/structrecv_argsr_args{0};r_args.sizesize;r_args.fdfd;r_args.ubuff(__u64)(unsignedlong)ubuf;/*store kvstore pid and r_args*/bpf_map_update_elem(recv_map,pid_tgid,r_args,BPF_ANY);return0;}SEC(探测点)//返回的时候intBPF_KRETPROBE(){__u64 pid_tgidbpf_get_current_pid_tgid();__u32 pidpid_tgid32;/*获得共享数据的地址*/structrecv_args*argsbpf_map_lookup_elem(recv_map,pid_tgid);if(!args)return0;/*Other code*/bpf_map_delete_elem(recv_map,pid_tgid);return0;}在真实程序中kretprobe 获取到入口阶段保存的数据后通常需要调用bpf_map_delete_elem()删除对应的 Map 记录避免长时间运行后残留无用数据。分配 Ring Buffer 空间并提交数据 Linux 内核提供了分配 Ring Buffer 空间的方法bpf_ringbuf_reserve()/* * bpf_ringbuf_reserve * * Reserve *size* bytes of payload in a ring buffer *ringbuf*. * *flags* must be 0. * * Returns * Valid pointer with *size* bytes of memory available; NULL, * otherwise. */staticvoid*(*bpf_ringbuf_reserve)(void*ringbuf,__u64 size,__u64 flags)(void*)131;在使用这个方法之前首先需要定义好 Ring Buffer 类型的 Map用于暂存准备发送到用户态的数据。/*Ring buffer: kernel send data -- user program*/struct{__uint(type,BPF_MAP_TYPE_RINGBUF);// 指定 Map 类型为 Ring Buffer__uint(max_entries,32*1024*1024);// 设置 Ring Buffer 容量为 32 MiB,可以存放数据的字节大小}ring_buffSEC(.maps);当我们对 Ring Buffer 中预留的数据区域完成赋值后就可以使用bpf_ringbuf_submit()方法把这条数据提交给用户态程序。/* * bpf_ringbuf_submit * * Submit reserved ring buffer sample, pointed to by *data*. * If **BPF_RB_NO_WAKEUP** is specified in *flags*, no notification * of new data availability is sent. * If **BPF_RB_FORCE_WAKEUP** is specified in *flags*, notification * of new data availability is sent unconditionally. * If **0** is specified in *flags*, an adaptive notification * of new data availability is sent. * * See bpf_ringbuf_output() for the definition of adaptive notification. * * Returns * Nothing. Always succeeds. */staticvoid(*bpf_ringbuf_submit)(void*data,__u64 flags)(void*)132;eBPF程序这里我将ebpf的探测程序定义为kprobe.bpf.c这程序是需要载入到内核进行探测的这里通过SEC()主要探测两个点一个是__sys_recvfrom函数触发的时候一个是__sys_recvfrom函数返回的时候探测的进程名称是kvstorekprobe.bpf.c代码如下// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause/* Copyright (c) 2021 Sartura */#includevmlinux.h#includebpf/bpf_helpers.h#includebpf/bpf_tracing.h#includebpf/bpf_core_read.hcharLICENSE[]SEC(license)Dual BSD/GPL;/*kv_kprobe.c, kv_kprobe.bpf.c*//*define kprobe recv event */typedefstruct{__u32 pid;__u32 tid;__u32 fd;__u32 data_len;// max data lenbuffer max size__u32 recv_len;// actual recv data len// int recv_type;chardata[BUFFER_MAXLEN];// resp protocal message}Recv_event;/*Ring buffer: kernel send data -- user program*/struct{__uint(type,BPF_MAP_TYPE_RINGBUF);// 指定 Map 类型为 Ring Buffer__uint(max_entries,32*1024*1024);// 设置 Ring Buffer 容量为 32 MiB,可以存放数据的字节大小}ring_buffSEC(.maps);/*store __sys_recvfrom parameters*/structrecv_args{__u64 ubuff;__u64 size;intfd;};/* BPF Hash Map */struct{__uint(type,BPF_MAP_TYPE_HASH);__uint(max_entries,1024);__type(key,__u64);__type(value,structrecv_args);}recv_mapSEC(.maps);staticintjudge_kvstore_process(void){charcomm[TASK_COMM_LEN]{0};if(bpf_get_current_comm(comm,sizeof(comm))!0)return0;if(comm[0]!k||comm[1]!v||comm[2]!s||comm[3]!t||comm[4]!o||comm[5]!r||comm[6]!e||comm[7]!\0){return0;}return1;}SEC(kprobe/__sys_recvfrom)intBPF_KPROBE(trace_recvfrom_enter,intfd,void*ubuf,size_tsize,unsignedintflags,structsockaddr*addr,int*addr_len){/*judge_kvstore_process: filter the process name ! kvsotre*/if(!judge_kvstore_process())return0;/*bpf_get_current_pid_tgid: get bpf current process id*/// [32bits] [32bits] 32 | [32bits] [32bits]__u64 pid_tgidbpf_get_current_pid_tgid();// process_id thread_id | process_id__u32 pidpid_tgid32;structrecv_argsr_args{0};r_args.sizesize;r_args.fdfd;r_args.ubuff(__u64)(unsignedlong)ubuf;/*store kvstore pid and r_args*/bpf_map_update_elem(recv_map,pid_tgid,r_args,BPF_ANY);return0;}SEC(kretprobe/__sys_recvfrom)intBPF_KRETPROBE(trace_recvfrom_exit,longret){if(!judge_kvstore_process())return0;__u64 pid_tgidbpf_get_current_pid_tgid();__u32 pidpid_tgid32;structrecv_args*argsbpf_map_lookup_elem(recv_map,pid_tgid);if(!args)return0;if(ret0)gotocleanup;/*alloc ring buffer space*/Recv_event*ring_evbpf_ringbuf_reserve(ring_buff,sizeof(Recv_event),0);if(!ring_ev)gotocleanup;// copy recv data to need to send event__u32 copy_len0;if(retBUFFER_MAXLEN)copy_lenBUFFER_MAXLEN;elsecopy_lenret;/*wirte recv message to structure ev*/// Recv_event recv_msg {0};ring_ev-fdargs-fd;ring_ev-pidpid;ring_ev-data_lencopy_len;ring_ev-recv_len(__u32)ret;/* bpf_probe_read_user 从KVstore用户态缓冲区读取数据 *//*这里不能直接使用memcpy因为eBPF程序受到 Verifier 的内存访问限制*/if(bpf_probe_read_user(ring_ev-data,copy_len,(void*)(unsignedlong)args-ubuff)!0){bpf_ringbuf_discard(ring_ev,0);gotocleanup;};/*send event to ring buffer*/bpf_ringbuf_submit(ring_ev,0);cleanup:bpf_map_delete_elem(recv_map,pid_tgid);return0;}用户态程序这里在用户态定义kprobe.c程序接收内核ebpf程序探测到的发送给kvstore程序的数据然后将接受到的数据进行打印kprobe.c代码如下#includeerrno.h#includesignal.h#includestdarg.h#includestdio.h#includestring.h#includeunistd.h#includebpf/libbpf.h#includekv_kprobe.skel.h/*kv_kprobe.c, kv_kprobe.bpf.c*//*define kprobe recv event */typedefstruct{__u32 pid;__u32 tid;__u32 fd;__u32 data_len;// max data lenbuffer max size__u32 recv_len;// actual recv data len// int recv_type;chardata[BUFFER_MAXLEN];// resp protocal message}Recv_event;/* kprobe_handle_ev负责将kprobe探测的数据加入到task queue即可 void *ctx:用户自定义上下文 void *data:指向本次 Ring Buffer 事件的数据 size_t size:本次事件数据的实际字节数 */staticintkprobe_handle_ev(void*ctx,void*data,size_tsize){// printf(enter handler\n);Recv_event*recv_ev(Recv_event*)data;char*recv_datarecv_ev-data;__u32 pidrecv_ev-pid;__u32 tidrecv_ev-tid;__u32 fdrecv_ev-fd;__u32 recv_lenrecv_ev-recv_len;printf(PID: %u, TID: %u, fd: %u\n,pid,tid,fd);printf(Receive %u bytes from kprobe, data\n%s\n,recv_len,recv_data);return0;}intmain(intargc,char*argv[]){structkv_kprobe_bpf*skelNULL;interr0;/* Open -- load -- verify BPF application */skelkv_kprobe_bpf__open_and_load();if(!skel){fprintf(stderr,Failed to open BPF skeleton\n);return1;}/* Attach tracepoint handler into kernel */errkv_kprobe_bpf__attach(skel);if(err){fprintf(stderr,Failed to attach BPF skeleton\n);gotocleanup;}/*define the ebpf data process hanlder*/structring_buffer*r_buffring_buffer__new(bpf_map__fd(skel-maps.ring_buff),kprobe_handle_ev,NULL,NULL);if(!r_buff){fprintf(stderr,perf_buffer__new failed\n);gotocleanup;}while(1){errring_buffer__poll(r_buff,100);if(err0){fprintf(stderr,ring_buffer__poll failed: %d\n,err);break;}}kv_kprobe_bpf__destroy(skel);return0;cleanup:kv_kprobe_bpf__destroy(skel);return-err;}用例执行结果我们通过libbpf-bootstrap的Makefile编译kprobe.bpf.c和kprobe.c之后生成了可执行程序kprobe执行kprobe然后在kvstore启动的情况下向kvstore发送一个数据hello, world结果如下
阅读完成 · 觉得有帮助?