BCC会默默地重写你的BPF代码,并将诸如tsk-> parent-> pid之类的字段访问转换为一系列的bpf_probe_read()调用。Libbpf/BPF CO-RE没有此项功能,但bpf_core_read.h提供了一系列普通C代码编写的辅助函数来完成类似的工作。上述的tsk->parent->pid会变成BPF_CORE_READ(tsk, parent, pid)。从Linux 5.5开始使用tp_btf和fentry/fexit BPF程序类型,使用的也是C语法。但对于老版本的内核以及其他BPF程序类型(如Tracepoints和kprobe),最好将其转换为BPF_CORE_READ。
BPF_CORE_READ宏也可以工作在BCC模式下,因此为了避免在#ifdef __BCC__/#else/#endif中重复使用,可以将所有字段的读取转换为BPF_CORE_READ,这样就可以同时给BCC和libbpf模式使用。使用BCC时,需要确保包含 bpf_core_read.h头文件。
BPF mapsBCC 和libbpf对BPF maps的声明是不同的,但转换方式很直接,下面是一些例子:
/* Array */ #ifdef __BCC__ BPF_ARRAY(my_array_map, struct my_value, 128); #else struct { __uint(type, BPF_MAP_TYPE_ARRAY); __uint(max_entries, 128); __type(key, u32); __type(value, struct my_value); } my_array_map SEC(".maps"); #endif /* Hashmap */ #ifdef __BCC__ BPF_HASH(my_hash_map, u32, struct my_value); #else struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 10240); __type(key, u32); __type(value, struct my_value); } my_hash_map SEC(".maps") #endif /* Per-CPU array */ #ifdef __BCC__ BPF_PERCPU_ARRAY(heap, struct my_value, 1); #else struct { __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); __uint(max_entries, 1); __type(key, u32); __type(value, struct my_value); } heap SEC(".maps"); #endif请注意BCC中maps的默认大小,通常为10240。使用libbpf时必须明确指定大小。
PERF_EVENT_ARRAY, STACK_TRACE和其他特殊的maps(DEVMAP, CPUMAP, etc) 尚不支持键/值类型的BTF类型,因此需要直接指定key_size/value_size:
/* Perf event array (for use with perf_buffer API) */ #ifdef __BCC__ BPF_PERF_OUTPUT(events); #else struct { __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); __uint(key_size, sizeof(u32)); __uint(value_size, sizeof(u32)); } events SEC(".maps"); #endif 访问BPF代码中的BPF mapsBCC使用伪C++语言处理maps,在幕后将其重写为实际的BPF辅助调用,通常使用如下模式:
some_map.operation(some, args)将其重写为如下格式:
bpf_map_operation_elem(&some_map, some, args);下面是一些例子:
#ifdef __BCC__ struct event *data = heap.lookup(&zero); #else struct event *data = bpf_map_lookup_elem(&heap, &zero); #endif #ifdef __BCC__ my_hash_map.update(&id, my_val); #else bpf_map_update_elem(&my_hash_map, &id, &my_val, 0 /* flags */); #endif #ifdef __BCC__ events.perf_submit(args, data, data_len); #else bpf_perf_event_output(args, &events, BPF_F_CURRENT_CPU, data, data_len); #endif BPF程序所有BPF程序提供的功能都需要通过SEC()(来自bpf_helpers.h)宏来自定义section名称,如:
#if !defined(__BCC__) SEC("tracepoint/sched/sched_process_exec") #endif int tracepoint__sched__sched_process_exec( #ifdef __BCC__ struct tracepoint__sched__sched_process_exec *args #else struct trace_event_raw_sched_process_exec *args #endif ) { /* ... */ }这只是一个约定,但如果遵循libbpf的section名称,会有更好的开发体验。期望的名称可以参见(原文中给出的代码行可能不准,参见section_defs的定义即可),通常的用法为:
tp/<category>/<name> 用于Tracepoints;
kprobe/<func_name> 用于kprobe ,kretprobe/<func_name> 用于kretprobe;
raw_tp/<name> 用于原始Tracepoint;
cgroup_skb/ingress, cgroup_skb/egress,以及整个cgroup/<subtype> 程序家族。
Tracepoints从上面的例子中可以看到,Tracepoint上下文的类型名称略有不同。BCC允许Tracepoint使用tracepoint__<category>__<name>命名模式。BCC会在编译时自动生成相应的类型。libbpf没有此功能,但幸运的是,内核已经提供了所有Tracepoint数据的类似类型,一般命名为trace_event_raw_<name>,但有时内核中的少量Tracepoints会重用常用的类型,因此如果上述模式不起作用,则需要在内核源码(或 vmlinux.h)中查找具体的类型名称。如必须使用struct trace_event_raw_sched_process_template来代替struct trace_event_raw_sched_process_exit。
在大多数情况下,用于访问tracepoint 上下文数据的代码完全相同,但特殊的可变长度字符串字段除外。对于此类情况,其转换也很直接:data_loc_<some_field>变为__data_loc_<some_field>(注意双下划线)即可。
KprobesBCC有很多种方式声明kprobe。实践中,这类BPF程序会接收一个指向struct pt_regs的指针作为上下文参数,但BCC允许像使用内核函数参数一样给BPF程序传参。使用libbpf的BPF_KPROBE宏可以获得类似的效果,目前其存在于内核selftest的bpf_trace_helpers.h头文件中,但后续应该会作为libbpf的一部分(已经是了):
#ifdef __BCC__ int kprobe__acct_collect(struct pt_regs *ctx, long exit_code, int group_dead) #else SEC("kprobe/acct_collect") int BPF_KPROBE(kprobe__acct_collect, long exit_code, int group_dead) #endif { /* BPF code accessing exit_code and group_dead here */ }对于有返回值的kprobe,也有对应的宏BPF_KRETPROBE。