前言
本片文章并不主要讲解在AOSP平台ebpf程序的整个编写流程,只是一些的map的定义使用问题,如有需要可查看,aosp平台的整个下载流程,以及简单的程序的编译和如何push到手机运行,这位up是我在ebpf领域探索的领路人,本站ID:LiujiaHuan13,如果有需要up本人后面会考虑写一篇aosp程序书写和编译的具体流程。
背景
由于Android本身的安全性要求对于ebpf程序的限制比较高,一些小的问题都会导致内核态程序无法加载(无法加载对应的map和prog),因此本文总结了一些导致map无法产生的原因。本文主要使用tracepoint进行挂载sched/sched_switch(进程切换函数)的例子来进行说明。
1.map无法产生的原因
一般来说map无法产生,就是因为map的类型定义的不对的原因,可能是HASH或者ARRAY类型的问题,也有可能是map的key键和value值的问题等等,因此我们将之分为两类并举出一些可能的例子。这里我们选择使用tracepoint定义了五个map用以说明:
DEFINE_BPF_MAP(cpu_id_map, ARRAY, int, uint64_t, 1024); DEFINE_BPF_MAP(cpu_nextpid_map, ARRAY, int, uint64_t, 1024); DEFINE_BPF_MAP(cpu_prevpid_map, ARRAY, int, uint64_t, 1024); DEFINE_BPF_MAP(rb_test_map, HASH, uint64_t, process_info, 2048); DEFINE_BPF_MAP(comm_name_map, ARRAY, int, names, 1024);
前三个map都是ARRAY,键为int类型,值为uint64_t类型,分别用来传递cpu_id(cpu号),next_pid(上cpu进程id),prev_pid(下cpu进程id)。
第四个map我们定义为HASH类型,键为uint64_t类型,值是一个process_info结构体,process_info的内容如下:
typedef struct process_info { uint32_t tid; uint32_t pid; // 进程的PID uint64_t cpu_id; uint64_t start_t; //进程开始的时间 uint64_t used_t; //已经使用的CPU时间 // uint64_t total_t; //每个cpu占用的总时间 // uint64_t occ; //占用率 // char comm[TASK_COMM_LEN]; uint64_t prev_pid; uint64_t next_pid; }process_info ;
第五个map我们定义为ARRAY类型,键值为int和names结构体(用来传递字符串类型char) ,names内容如下:
typedef struct names{ char name[16]; }names;
这里的定义都是正确的,程序能够正常运行,产生的map和prog如下:
运行结果如下:
下面我们对map进行一些修改来看看可能导致map无法产生的情况。
1.1map的类型定义错误
我们这里将第四个map(rb_test_map)的类型修改为ARRAY类型:
DEFINE_BPF_MAP(rb_test_map, ARRAY, uint64_t, process_info, 2048);
我们重启手机之后发现只产生了三个map,且prog没有产生:
由于map的产生是根据内核态定义的顺序来的,所以就是到第四个map出现了问题,也就是我们刚刚修改的map,这里的原因暂且不清楚,之前猜测可能是因为大小超出了限制的原因,然而实际上修改为1024也不能产生,然而time_in_state中其实用到了ARRAY来传递结构体的:
现在猜测可能是因为array是连续的数组,而hash是一个散列表可能是索引错误的原因,这里后面在做尝试吧。目前测试的结果,只要是使用结构体作value值的话,且map在内核态进行了lookup操作,那么map使用ARRAY类型就会导致map无法产生,但是如果是只进行了update操作没有lookup的话就可以使用ARRAY类型也可以使用HASH,就比如第五个map用来传递comm的结构体类型。
我们定义的第五个map是用来传递comm值的,而他实际上也是在使用一个结构体来传递的,这原因具体可以看这篇文章解决Android-ebpf的MAP保存字符串的问题 - 知乎 (zhihu.com)
但我们并没有使用它来进行lookup而只是进行了update,具体代码如下:
names pidCommon={}; memcpy(pidCommon.name, args->next_comm, 16); bpf_comm_name_map_update_elem(&key, &pidCommon, BPF_ANY);
而他使用HASH或者 ARRAY都可以
前三个单独传递一个int值,或者u64类型的值的map使用ARRAY和HASH类型的map都是可以运行的且运行没有问题,只有结构体这里会出现问题。
1.2key和value使用结构体时的错误
这里的原因,就可能是map的键也使用了结构体的原因,使用AARAY可能无法产生map,而使用HASH后程序无法运行,后面会写一篇将使用结构体传递数据的文章讲到。
2.prog无法产生的原因及Aborted (core dumped)问题
prog无法产生的原因有很多,主要是main函数逻辑错误,或者lookup,update等bpf相关函数使用错误的问题,或是由于程序的加载顺序的原因,map没有成功产生,导致之后的prog肯定也无法产生。
将Aborted (core dumped)和这个放一块将也是因为,出现这个错误的原因主要就是main函数内部逻辑错误或是别的错误的问题,注意这里是指map和prog成功产生,且在用户态正常读取了map和attach了tracepoint的prog之后的情况,如果上面有map和prog报错负值就有可能是用户态的问题了。
2.1没有进行错误处理
这个是最容易出现的问题,因为我们再其他平台编写ebpf程序的安全性要求比较低,可能常常在使用lookup函数查找map时没有加错误处理,而这在Android系统会导致prog直接无法产生,因为Android不允许你在找不到东西的时候程序直接崩掉导致影响到系统本身,所以就需要你手动处理点东西了,也就是说我们只需要在使用lookup函数时加上简单的错误处理就行了。如:
struct process_info *prev_info = bpf_rb_test_map_lookup_elem(&prev_pid); if(!prev_info){ return 0; }
而还有我个人认为比较好用的方式,就是加个初始化,这是从系统自带的程序time_in_state学来的方法,time_in_state的源码如下:
time_key_t key = {.uid = uid, .bucket = freq_idx / FREQS_PER_ENTRY}; tis_val_t* val = bpf_uid_time_in_state_map_lookup_elem(&key); if (!val) { tis_val_t zero_val = {.ar = {0}}; bpf_uid_time_in_state_map_update_elem(&key, &zero_val, BPF_NOEXIST); val = bpf_uid_time_in_state_map_lookup_elem(&key); } if (val) val->ar[freq_idx % FREQS_PER_ENTRY] += delta;
我们可以看到,他在从map找不到东西时,就定义了一个空的对象,然后通过update函数使用相同的键值传入到map中,这样就保证了下一次读值时一定有东西(相当于初始化了一个值),因为我们第一次要的数据本身就是空值在其他平台肯呢个系统本身会处理这块,但在Android就需要我们手动来了,同样的我们的代码就可以修改为:
process_info *prev_info = bpf_rb_test_map_lookup_elem(&prev_pid); if(!prev_info){ process_info zero_info = {}; bpf_rb_test_map_update_elem(&prev_pid, &zero_info, BPF_ANY); prev_info = bpf_rb_test_map_lookup_elem(&prev_pid); }//这里的错误处理必须要加上!!! if(prev_info){ //这里加上你要处理数据的逻辑,读取新的数据,或是更新等 }
2.2Aborted (core dumped)
Aborted: 表示程序执行过程中发生了一个致命错误,导致程序被终止。
(core dumped): 表示在程序终止时,系统生成了一个核心转储文件(core dump file)。核心转储文件包含程序崩溃时的内存映像,它可以帮助开发者在发生错误时分析问题。核心转储文件通常命名为 core,可以用于调试程序。
内存错误: 访问无效的内存地址,使用已释放的内存等。
无限递归: 一个无限递归可能导致栈溢出,最终导致程序终止。
除零错误: 在程序中进行除零操作。
使用未初始化的变量: 试图使用未初始化的变量。
系统限制: 操作系统可能对程序的资源使用施加了一些限制,例如堆栈大小、文件描述符等。
上面这是chat给出的原因,这个是我们经常会遇到的问题,就是map和prog都产生成功但是运行用户态程序时,就是无法运行,这里就是经典的用户态程序主函数内部的问题,这里我们有一个要注意的点可能你之前在编写了一个基础并且push到手机成功加载生成了map和prog,而之后你又在这基础上进行了修改,而push到手机后bpfloader没有报错,且map和prog都还在但就是程序无法运行,可能也会报bad file的错误,这里可能就是map本身可能有错但是因为只是手动热加载(bpfloader)之前的正确产生的map和prog没有删除,这里只需要重启一下手机,错误的map和prog就会被删除,根据问题修改就行了。
下面举例两个可能的原因:
2.2.1函数内部原因
我在使用结构体传递数据时之前是这样写的:
因为up之前是主要在bootstrap框架下编写ebpf程序(偏向于正常的linux的ebpf程序的编写),用的是perf_buffer或者ring_buffer来传递数据,并且在那边对于map的理解是需要使用look_up来给读取的数据赋一个内存,使其能在后面传递数据,而在aosp平台下,我们读取数据是直接从map里来读取的,这里可能是因为lookup在这里根本没有必要的原因,因为我们只需要传递数据,所以只需要定义一个空的结构体,将数值全部赋入然后直接使用update更新进map就行了(up后面会写一篇文章来讲在aosp平台用户态和内核态之间的数据传递以及使用结构体来传递数据),如下:
process_info prev_space = {}; prev_space.cpu_id = cpu_id; prev_space.pid = pid; prev_space.tid = tid; prev_space.used_t = prev_info->used_t; bpf_rb_test_map_update_elem(&zero, &prev_space, BPF_ANY);
这里可能是因为跟上面map一样的问题,AARAY和HASH的使用问题可以上去看看。
2.2.2用户态原因
我们再写用户态读取map的地址时把名字写错了,这个虽然听起来是很简单的错误,但往往是最容易发生的,在刚开始写aosp平台的ebpf程序时极容易发生的问题,up本人就错过好几次,举个例子如我们再sys/fs/bpf下查找我们的map如下:
而我们再写用户态程序时map名字没打对:
多打了一个map,所以运行的结果就是Aborted (core dumped),但是上面如果你写了报错提示就会在map那里得到一个负值,这样我们就很容易发现错误了,所以up建议用户态如下书写。
3.用户态书写建议
const char *tp_prog_path = "/sys/fs/bpf/prog_xintest_tracepoint_sched_sched_switch"; const char *tp_map_path1 = "/sys/fs/bpf/map_xintest_cpu_id_map"; const char *tp_map_path2 = "/sys/fs/bpf/map_xintest_cpu_nextpid_map"; const char *tp_map_path3 = "/sys/fs/bpf/map_xintest_cpu_prevpid_map"; // const char *tp_map_path4 = "/sys/fs/bpf/map_xintest_rb_test_map"; const char *tp_map_path5 = "/sys/fs/bpf/map_xintest_comm_name_map"; // Attach tracepoint and wait for 4 seconds int mProgFd = bpf_obj_get(tp_prog_path); printf("bpf prog fd:%d\n",mProgFd); int mMapFd1 = bpf_obj_get(tp_map_path1); printf("bpf map1 fd:%d\n",mMapFd1); int mMapFd2 = bpf_obj_get(tp_map_path2); printf("bpf map2 fd:%d\n",mMapFd2); int mMapFd3 = bpf_obj_get(tp_map_path3); printf("bpf map3 fd:%d\n",mMapFd3); // int mMapFd4 = bpf_obj_get(tp_map_path4); // printf("bpf map4 fd:%d\n",mMapFd4); int mMapFd5 = bpf_obj_get(tp_map_path5); printf("bpf map5 fd:%d\n",mMapFd5); int ret = bpf_attach_tracepoint(mProgFd, "sched", "sched_switch"); printf("attach tracepoint ret:%d\n",ret);
最好是将每个map和prog先get到一个int值里,然后在下面紧接着打印这个int值,如果为负值就证明这一步出现了错误,我们就能即使进行对应的处理了。
代码
xintest.c
#include#include #include #include #include #include #ifndef memcpy # define memcpy(dest, src, n) __builtin_memcpy((dest), (src), (n)) #endif typedef struct names{ char name[16]; }names; typedef struct process_info { uint32_t tid; uint32_t pid; // 进程的PID uint64_t cpu_id; uint64_t start_t; //进程开始的时间 uint64_t used_t; //已经使用的CPU时间 // uint64_t total_t; //每个cpu占用的总时间 // uint64_t occ; //占用率 // char comm[TASK_COMM_LEN]; uint64_t prev_pid; uint64_t next_pid; }process_info ; struct switch_args { unsigned long long ignore; char prev_comm[16]; int prev_pid; int prev_prio; long long prev_state; char next_comm[16]; int next_pid; int next_prio; }; DEFINE_BPF_MAP(cpu_id_map, ARRAY, int, uint64_t, 1024); DEFINE_BPF_MAP(cpu_nextpid_map, ARRAY, int, uint64_t, 1024); DEFINE_BPF_MAP(cpu_prevpid_map, ARRAY, int, uint64_t, 1024); DEFINE_BPF_MAP(rb_test_map, HASH, uint64_t, process_info, 2048); DEFINE_BPF_MAP(comm_name_map, ARRAY, int, names, 1024); DEFINE_BPF_PROG("tracepoint/sched/sched_switch",AID_ROOT,AID_NET_ADMIN,tp_sched_switch)(struct switch_args* args) { int key; uint64_t used; uint64_t prev_pid, next_pid; next_pid = args->next_pid; prev_pid = args->prev_pid; key = bpf_get_smp_processor_id(); process_info *prev_info = bpf_rb_test_map_lookup_elem(&prev_pid); if(!prev_info){ process_info zero_info = {}; bpf_rb_test_map_update_elem(&prev_pid, &zero_info, BPF_ANY); prev_info = bpf_rb_test_map_lookup_elem(&prev_pid); }//这里的错误处理必须要加上!!! if(prev_info){ used = bpf_ktime_get_ns(); prev_info->used_t = used; prev_info->cpu_id = key; prev_info->next_pid = args->next_pid; prev_info->prev_pid = args->prev_pid; names pidCommon={}; memcpy(pidCommon.name, args->next_comm, 16); bpf_comm_name_map_update_elem(&key, &pidCommon, BPF_ANY); bpf_cpu_id_map_update_elem(&key, &used, BPF_ANY); bpf_cpu_nextpid_map_update_elem(&key, &next_pid, BPF_ANY); bpf_cpu_prevpid_map_update_elem(&key, &prev_pid, BPF_ANY); bpf_rb_test_map_update_elem(&next_pid, prev_info, BPF_ANY); } return 0; } LICENSE("Apache 2.0"); //LICENSE("GPL");
xintest_cli.cpp
#include #include#include #include #include #include #include #include "libbpf.h" #define TASK_COMM_LEN 16 //using namespace android::bpf; typedef struct cpu_info { int cpu_id; uint64_t process_id; }cpu_info ; typedef struct names{ char name[16]; }names; typedef struct process_info { uint32_t tid; uint32_t pid; // 进程的PID uint64_t cpu_id; uint64_t start_t; //进程开始的时间 uint64_t used_t; //已经使用的CPU时间 // uint64_t total_t; //每个cpu占用的总时间 // uint64_t occ; //占用率 // char comm[TASK_COMM_LEN]; uint64_t prev_pid; uint64_t next_pid; }process_info ; int main() { int cpu_no = 0; const char *tp_prog_path = "/sys/fs/bpf/prog_xintest_tracepoint_sched_sched_switch"; const char *tp_map_path1 = "/sys/fs/bpf/map_xintest_cpu_id_map"; const char *tp_map_path2 = "/sys/fs/bpf/map_xintest_cpu_nextpid_map"; const char *tp_map_path3 = "/sys/fs/bpf/map_xintest_cpu_prevpid_map"; // const char *tp_map_path4 = "/sys/fs/bpf/map_xintest_rb_test_map"; const char *tp_map_path5 = "/sys/fs/bpf/map_xintest_comm_name_map"; // Attach tracepoint and wait for 4 seconds int mProgFd = bpf_obj_get(tp_prog_path); printf("bpf prog fd:%d\n",mProgFd); int mMapFd1 = bpf_obj_get(tp_map_path1); printf("bpf map1 fd:%d\n",mMapFd1); int mMapFd2 = bpf_obj_get(tp_map_path2); printf("bpf map2 fd:%d\n",mMapFd2); int mMapFd3 = bpf_obj_get(tp_map_path3); printf("bpf map3 fd:%d\n",mMapFd3); // int mMapFd4 = bpf_obj_get(tp_map_path4); // printf("bpf map4 fd:%d\n",mMapFd4); int mMapFd5 = bpf_obj_get(tp_map_path5); printf("bpf map5 fd:%d\n",mMapFd5); int ret = bpf_attach_tracepoint(mProgFd, "sched", "sched_switch"); printf("attach tracepoint ret:%d\n",ret); sleep(4); android::bpf::BpfMap myMap1(tp_map_path1); android::bpf::BpfMap myMap2(tp_map_path2); android::bpf::BpfMap myMap3(tp_map_path3); // android::bpf::BpfMap< uint64_t, process_info> myMap4(tp_map_path4); android::bpf::BpfMap< int, names> myMap5(tp_map_path5); while(1) { usleep(40000); process_info md = {}; md.cpu_id = *myMap1.readValue(0); md.next_pid = *myMap2.readValue(0); md.prev_pid = *myMap3.readValue(0); // process_info data = myMap4.readValue(0).value(); names pidname = myMap5.readValue(0).value(); printf(" time on CPU %d is %llu,nextpid:%llu,prevpid:%llu,pidname:%s\n", cpu_no, md.cpu_id, md.next_pid, md.prev_pid, pidname.name); // printf(" datacpu: %llu,datanextpid: %llu,dataprevpid: %llu\n", data.cpu_id, data.next_pid, data.prev_pid); } return 0; }
另言
up在后面会写一篇文章来讲在aosp平台用户态和内核态之间的数据传递以及使用结构体来传递数据,里面会大概讲到用户态和内核态文件的基本书写。除此之外运行可能还会遇到badfile的问题,之后遇到了会讲到。