在之前的bcc代码中我们知道其程序是分为两部分的,一部分是C语言,另一部分是基于Python的。本篇是关于C语言部分的。
使用kprobe的语法是:
kprobe__kernel_function_name
其中kprobe__是前缀,用于给内核函数创建一个kprobe(内核函数调用的动态跟踪)。也可通过C语言函数定义一个C函数,然后使用python的BPF.attach_kprobe()来关联到内核函数。
例如:
int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk)
其中参数struct pt_regs *ctx是寄存器和BPF文件
sock *sk是tcp_v4_connect的第一个参数。
kretprobes动态跟踪内核函数的返回,语法如下:
kretprobe__kernel_function_name,前缀是kretprobe__。也可以使用python的BPF.attach_kretprobe()来关联C函数到内核函数。
例如:
int kretprobe__tcp_v4_connect(struct pt_regs *ctx)
{ int ret = PT_REGS_RC(ctx); [...]
}
返回值保存在ret中。
语法如下:TRACEPOINT_PROBE(category,event)
TRACEPOINT_PROBE是一个宏, tracepiont定义的方式是categroy:event,
可以的参数是通过结构体args获取的,获取参数相关格式的方法是cat文件:
/sys/kernel/debug/tracing/events/category/event/format
结构体args可以在函数中使用例如:
TRACEPOINT_PROBE(random, urandom_read) {
// args is from /sys/kernel/debug/tracing/events/random/urandom_read/format bpf_trace_printk("%d\\n", args->got_bits); return 0;
}
通过python的BPF.attach_uprobe()可以将普通C函数关联到uprobe探针。
参数可以通过PT_REGS_PARM宏来检测。
程序本身名字使用宏PT_REGS_PARM1,第一个参数使用宏PT_REGS_PARM2。
同uprobes,只不过该探针是在函数返回时候触发。
返回的值通过PT_REGS_RC(ctx)获取,例如:
BPF_HISTOGRAM(dist); int count(struct pt_regs *ctx) { dist.increment(PT_REGS_RC(ctx)); return 0;
}
在直方图dist中,根据增加返回键对应的值。
User Statically-Defined Tracing用户静态定义的探针,会在应用和库中定义用来提供用户级别追踪。BPF中提供对USDT的支持方法是enable_probe。
使用方法同其他probes,定义C函数,然后通过USDT.enable_probe()来关联USDT probe。
参数可以通过bpf_usdt_readarg(index,ctx,&addr)来读取。
例如:
int do_trace(struct pt_regs *ctx) {
uint64_t addr;
char path[128];
bpf_usdt_readarg(6, ctx, &addr);
bpf_probe_read(&path, sizeof(path), (void *)addr); bpf_trace_printk("path:%s\\n", path);
return 0;
};
事件 |
C语法 |
Python方式 |
参数获取 |
kprobes |
kprobe__ |
BPF.attach_kprobe() |
struct pt_regs *ctx |
kretprobes |
kretprobe__ |
BPF.attach_kretprobe() |
PT_REGS_RC |
tracepoints |
TRACEPOINT_PROBE |
BPF.attach_tracepoint() |
struct args |
uprobes |
|
BPF.attach_uprobe() |
PT_REGS_PARM |
uretprobes |
|
BPF.attach_uretprobe() |
PT_REGS_RC |
USDT probes |
|
USDT.enable_probe |
bpf_usdt_readarg |
上节是关于事件和参数的,这边是看下如何获取所监控函数中数据。
语法:int bpf_probe_read(void *dst, int size, const void *src)
如果成功返回0.
该函数将内容复制到BPF栈中,用于后续使用。为了安全起见,所有内存读取都通过bpf_probe_read函数。
语法:int bpf_probe_read_str(void *dst, int size, const void *src)
复制NULL结尾的字符串到BPF栈,用于后续使用。
语法:u64 bpf_ktime_get_ns(void)
获取当前时间,以纳秒方式。
语法:u64 bpf_get_current_pid_tgid(void)
低32位为当前进程ID,高32位是组ID。
语法:u64 bpf_get_current_uid_gid(void)
返回用户ID和组ID.
语法:bpf_get_current_comm(char *buf, int size_of_buf)
用当前进程名字填充第一个参数地址。
返回指向当前task_struct对象的指针。可用于计算CPU在线时间,内核线程,运行队列和其他相关信息。
返回提供值的log-2结果,经常用于创建直方图索引构建直方图。
返回一个无符号32位伪随机值。
语法:int bpf_override_return(struct pt_regs *, unsigned long rc)
用于关联到函数入口的kprobe,忽略要执行的函数,返回rc,用于目标的故障注入。
这个函数在允许故障注入的kprobe白名单中才能工作。白名单在内核代码树中会标记BPF_ALOW_ERROR_INJECTION()。
语法:int bpf_trace_printk(const char *fmt, int fmt_size, ...)
简单的内核printf函数,输出到/sys/kernel/debug/tracing/trace_pipe。这个在之前有描述过,其最多3个参数,智能输出%s,而且trace_pipe是全局共享的并发输出会有问题,生产中工具使用BPF_PERF_OUTPUT()函数。
通过perf ring buffer创建BPF表,将定义的事件数据输出。这个是将数据推送到用户态的建议方法。
例如:
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
};
BPF_PERF_OUTPUT(events);
int hello(struct pt_regs *ctx) {
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
bpf_get_current_comm(&data.comm, sizeof(data.comm));
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
代码中的输出表是events,数据通过events.perf_submit来推送。
语法:int perf_submit((void *)ctx, (void *)data, u32 data_size)
该函数是BPF_PERF_OUTPUT表的方法,将定义的事件数据推到用户态。
映射的Maps是BPF数据保存,是高级对象表、哈希表和直方图的基础。
语法:BPF_TABLE(_table_type, _key_type, _leaf_type, _name, _max_entries)
创建一个映射名字为_name。大多数时候会被高层宏使用,例如BPF_HASH,BPF_HIST等。
还有map.lookup(), map.lookup_or_init(), map.delete(), map.update(), map.insert(), map.increment().
语法BPF_HASH(name [, key_type [, leaf_type [, size]]])
创建一个name哈希表,中括号中是可选参数。
默认:BPF_HASH(name, key_type=u64, leaf_type=u64, size=10240)
相关函数:map.lookup(), map.lookup_or_init(), map.delete(), map.update(), map.insert(), map.increment().
语法:BPF_ARRAY(name [, leaf_type [, size]])
创建一个整数索引整列,用于快速查询和更新。
默认参数如下:BPF_ARRAY(name, leaf_type=u64, size=10240)
相关函数:map.lookup(), map.update(), map.increment().
整列中数据是预分配的,不能删除,所有没有删除操作了。
语法:BPF_HISTOGRAM(name [, key_type [, size ]])
创建一个直方图,默认是由64整型桶索引。
相关函数:map.increment().
语法:BPF_STACK_TRACE(name, max_entries)
创建一个栈跟踪映射,叫做stack_traces。相关函数:map.get_stackid().
语法:BPF_PERF_ARRAY(name, max_entries)
创建perf array,参数中max_entries保持和系统中CPU数量一致。这个映射用于获取硬件性能计数器.
例如:
text="""
BPF_PERF_ARRAY(cpu_cycles, NUM_CPUS);
"""
b = bcc.BPF(text=text, cflags=["-DNUM_CPUS=%d" % multiprocessing.cpu_count()])
b["cpu_cycles"].open_perf_event(b["cpu_cycles"].HW_CPU_CYCLES)
创建一个名字为cpu_cycles的perf array,入口和cpu数量一致。
Array配置计数器HW_CPU_CYCLES,后续可以通过map.perf_read函数读取到。每个表一次只能配置一种硬件计数器。
语法:BPF_PERCPU_ARRAY(name [, leaf_type [, size]])
创建NUM_CPU个整数索引数组,优化了快速查找和更新。每个CPU的数组相互之间不用同步。
语法:BPF_LPM_TRIE(name [, key_type [, leaf_type [, size]]])
创建LPM trie映射。
语法:BPF_PROG_ARRAY(name, size)
创建映射程序的数组,每个值要么是一个文件描述符指向bpf程序要么是NULL.
这个数组可以作为跳转表,可以同跳转到其他的bpf程序。
相关函数:map.call()
语法:*val map.lookup(&key)
寻找map中键为key的值,如果存在则返回指向该健值的指针。
语法:*val map.lookup_or_init(&key, &zero)
在map中寻找键,找到返回健值的指针,找不到则初始化为第二个参数。
从map中删除某个健值。
语法:map.update(&key, &val)
更新健值。
插入健值。
增加指定键的值,用于直方图。
该函数从堆栈中寻找指定参数,返回栈跟踪的唯一ID。
从BPF_PERF_ARRAY中定义的数组返回硬件性能计数。
语法:void map.call(void *ctx, int index)
调用bpf_tail_call来执行BPF_PROG_ARRAY数值中索引指向的程序,调用方式为tail-call表示不会返回到原先调用的函数。
例如:
BPF_PROG_ARRAY(prog_array, 10);
int tail_call(void *ctx) {
bpf_trace_printk("Tail-call\n");
return 0;
}
int do_tail_call(void *ctx) {
bpf_trace_printk("Original program\n");
prog_array.call(ctx, 2);
return 0;
}
b = BPF(src_file="example.c")
tail_fn = b.load_func("tail_call", BPF.KPROBE)
prog_array = b.get_table("prog_array")
prog_array[c_int(2)] = c_int(tail_fn.fd)
b.attach_kprobe(event="some_kprobe_event", fn_name="do_tail_call")
tail_call()赋给了prog_array[2],在函数do_tail_call()中调用prog_array.call(ctx,2)来调用tail_call()并执行。