bcc Reference Guide
文章目录
BPF C
这一部分是 bcc 程序的 c 部分
Events & Arguments
1、kprobes
语法格式:kprobe__*kernel_function_name*
kprobe_
是一个用来建立一个kprobe的前缀,kprobe就是内核函数调用的动态追踪。后半部分是要追踪的内核函数的名称。这种方式是第一种使用kprobe的方式,还有另外一种方式来使用kprobe,就是使用BPF.attach_kprobe()
把bcc程序和内核函数相关联,这个后面会介绍。
例如说:
int kprobe__tcp_v4_connect(struct pt_regs * ctx,struct sock * sk)
[...]
}
在tcp_v4_connect函数上使用kprobe,参数如下:
struct pt_regs *ctx
:struct sock *sk
:这是tcp_v4_connect 内核函数的第一个参数。
tcp_v4_connect在内核中的定义如下:
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len);
第一个参数总是 struct pt_regs *
,其余的参数就是这个内核函数的参数(如果不使用的话,就不用写了)。
注:该用例出自:bcc/examples/tracing/tcpv4connect.py
2、kretprobes
语法格式:kretprobe__*kernel_function_name*
这里需要关注一下kprobe、kretprobes,可以参考一下文档:
http://adam8157.info/blog/2013/06/debugging-kernel-with-kprobes/
https://www.kernel.org/doc/Documentation/kprobes.txt
kretprobe__
是一个用来创建kretprobe的前缀,kretprobe就是对内核函数return的动态追踪。后面的是内核函数名称,和前面的一样,也可以声明一个普通的c函数,然后使用BPF.attach_kretprobe()
和内核函数相关联。
返回的参数可以在c程序中使用PT_REGS_RC()
来获得,给出一个例子:
int kretprobe__tcp_v4_connect(struct pt_regs *ctx)
{
int ret = PT_REGS_RC(ctx);
[...]
}
这个就对tcp_v4_connect的返回值使用了kretprobe,并且把该函数的返回值保存到了ret中。
3、Tracepoint
语法格式:TRACEPOINT_PROBE(*category*, *event*)
这是一个宏,用于使用instruments由category:event定义的tracepoint
参数可以通过args结构获得,也就是tracepoint的参数。
一种查看这些格式的方法是查看:/sys/kernel/debug/tracing/events/category/event/format.
注意这里在系统中进入/sys/kernel/debug/tracing/events/,ls一下看到的就是category,再进去一个文件夹就是event,进入对应的event文件夹,再cat format,就可以看到tracepoint的参数了
在需要上下文作为参数的每个函数中,可以使用args结构代替ctx。这主要包括perf_submit()。
例子:
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;
}
这个例子是对random:urandom_read跟踪点进行检测,并输出tracepoint的参数got_bits。
注:在Linux中打开:/sys/kernel/debug/tracing/events/random/urandom_read,之后cat format:
name: urandom_read
ID: 1123
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:int got_bits; offset:8; size:4; signed:1;
field:int pool_left; offset:12; size:4; signed:1;
field:int input_left; offset:16; size:4; signed:1;
print fmt: "got_bits %d nonblocking_pool_entropy_left %d input_entropy_left %d", REC->got_bits, REC->pool_left, REC->input_left
信息是很全的,最后连打印的格式都写好了,但是还需要看一下tracepoint的用法。
4、uprobes
使用uprobe的方式只有先声明一个c函数,然后通过BPF.at tach_uprobe()(稍后讨论)将其关联为Python中的uprobe探针。
参数可以使用 PT_REGS_PARMn
宏进行检查。
一个例子:
int count(struct pt_regs *ctx) {
char buf[64];
bpf_probe_read(&buf, sizeof(buf), (void *)PT_REGS_PARM1(ctx));
bpf_trace_printk("%s %d", buf, PT_REGS_PARM2(ctx));
return(0);
}
把第一个参数读取成字符串,把第二个参数打印成整数。
5、uretprobes
使用uretprobe的方式只有先声明一个c函数,然后通过BPF.at tach_uretprobe()(稍后讨论)将其关联为Python中的uretprobe探针。
给定函数声明为:*function_name*(struct pt_regs *ctx)
,返回值可就是:PT_REGS_RC(ctx)
。
一个例子:
BPF_HISTOGRAM(dist);
int count(struct pt_regs *ctx) {
dist.increment(PT_REGS_RC(ctx));
return 0;
}
这将增加由返回值索引为bucket的 dist 直方图。
6、USDT probes
这些是用户静态定义的跟踪(USDT)探针,可以将其放置在某些应用程序或库中,以提供等效于用户级别的跟踪点。为USDT支持方法提供的主要BPF方法是Enable_probe()。使用USDT探针首先要声明一个c函数,然后通过USDT.enable_probe()将其关联为Python中的USDT探针。
参数可以这样获取: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;
};
这个在读取第六个USDT参数,之后将它转换成字符串存储在path中
通过BPF::init
C API中的第三个参数初始化USDT时,如果任何USDT失败init
,则整个操作BPF::init
都会失败。如果某些USDT init
失败,但无关紧要的话,就可以在使用BPF::init_usdt
之前使用BPF::init
。
7、Raw Tracepoints
语法格式:RAW_TRACEPOINT_PROBE(*event*)
这是一个宏,用于使用event定义的raw tracepoint
参数是一个指向bpf_raw_tracepoint_args
结构的指针,这个参数在bcc/src/cc/compat/linux/virtual_bpf.h中定义,结构字段args包含raw tracepoint的所有参数,可以在Linux树Include/trace/events目录中找到这些参数。(linux源码)
一个例子:
RAW_TRACEPOINT_PROBE(sched_switch)
{
// TP_PROTO(bool preempt, struct task_struct *prev, struct task_struct *next)
struct task_struct *prev = (struct task_struct *)ctx->args[1];
struct task_struct *next= (struct task_struct *)ctx->args[2];
s32 prev_tgid, next_tgid;
bpf_probe_read(&prev_tgid, sizeof(prev->tgid), &prev->tgid);
bpf_probe_read(&next_tgid, sizeof(next->tgid), &next->tgid);
bpf_trace_printk("%d -> %d\\n", prev_tgid, next_tgid);
}
这个例子使用sched:sched_switch tracepoint,并且打印prev和next进程的tgid号。
注:在Linux树Include/trace/events/sched.h中可以找到关于该tracepoint的定义:
TRACE_EVENT(sched_switch,
TP_PROTO(bool preempt,
struct task_struct *prev,
struct task_struct *next),
TP_ARGS(preempt, prev, next),
TP_STRUCT__entry(
__array( char, prev_comm, TASK_COMM_LEN )
__field( pid_t, prev_pid )
__field( int, prev_prio )
__field( long, prev_state )
__array( char, next_comm, TASK_COMM_LEN )
__field( pid_t, next_pid )
__field( int, next_prio )
),
···
8、system call tracepoint
语法格式:syscall__SYSCALLNAME
syscall__
是一个特殊的前缀,它为前缀后的系统调用名称创建一个kprobe。也可以通过声明一个普通的C函数,然后使用Python的 BPF.get_syscall_name(SYSCALLNAME)
和BPF.attach_kprobe()
将其关联来使用它。
参数在函数定义中声明:syscall__SYSCALLNAME(struct pt_regs *ctx, [, argument1 ...])
一个例子:
int syscall__execve(struct pt_regs *ctx,
const char __user *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
[...]
}
这个例子使用execve系统调用
第一个参数永远是struct pt_regs *
,剩余的参数就是函数的参数,(不一定非要声明,如果不用的话就不用声明了)
对应的python代码:
b = BPF(text=bpf_text)
execve_fnname = b.get_syscall_name("execve")
b.attach_kprobe(event=execve_fnname, fn_name="syscall__execve")
9、kfuncs
语法格式:KFUNC_PROBE(*function*, typeof(arg1) arg1, typeof(arg2) arge ...)
这是一个宏,它在执行函数之前通过trampoline来instruments内核函数。它由function和定义为argn的函数参数定义。
一个例子:
KFUNC_PROBE(do_sys_open, int dfd, const char *filename, int flags, int mode)
{
...
这里注入/装备do_sys_open函数,并使它的参数作为标准参数值可访问。
10、kretfuncs
语法格式:KRETFUNC_PROBE(*function*, typeof(arg1) arg1, typeof(arg2) arge ..., int ret)
这是一个宏,这个宏通过trampoline在函数执行执行之后注入/装备内核函数,它由function定义,函数的参数定义为srgn。
最后一个参数是注入/装备函数的返回值。
一个例子:
KFUNC_PROBE(do_sys_open, int dfd, const char *filename, int flags, int mode, int ret)
{
...
这里注入do_sys_open函数,使其参数和返回值作为标准参数值可访问。
Data
1、bpf_probe_read()
语法格式:int bpf_probe_read(void *dst, int size, const void *src)
返回值:成功的话返回0
这将内存位置复制到BPF堆栈,以便BPF以后可以对其进行操作。为了安全起见,所有内存读取必须通过bpf_probe_read()。这在某些情况下是自动发生的,例如去引用内核变量,因为bcc将重写bpf程序,以包括必要的bpf_probe_reads()。
2、bpf_probe_read_str()
语法格式:int bpf_probe_read_str(void *dst, int size, const void *src)
返回值:> 0 成功,包括全程跟踪的成功,< 0 错误
把一个NULL终止的字符串从内存位置复制到BPF堆栈,以便BPF以后可以对其进行操作。如果字符串长度小于size,则目标不会填充进一步的空字节。如果字符串长度大于大小,则只复制大小-1字节,并将最后一个字节设置为NULL。
3、bpf_ktime_get_ns()
语法格式:u64 bpf_ktime_get_ns(void)
返回值:当前时间(纳秒)
4、bpf_get_current_pid_tgid()
语法格式:u64 bpf_get_current_pid_tgid(void)
返回值:current->tgid << 32 | current->pid
返回较低32位中的进程ID(内核角度的PID,在用户空间中通常表示为线程ID)和上32位中的线程组ID(用户空间通常认为是PID)。通过直接将此设置为u32,我们丢弃了上面的32位。
5、bpf_get_current_uid_gid()
语法格式:u64 bpf_get_current_uid_gid(void)
返回值:current_gid << 32 | current_uid
返回用户ID和组IDs
6、bpf_get_current_comm()
语法格式:bpf_get_current_comm(char *buf, int size_of_buf)
返回值:成功返回0
用当前进程名称填充第一个参数地址。它应该是指向至少大小为TASK_COMM_LEN的字符数组的指针,该数组在linux/sched.h中定义。
一个例子:
#include <linux/sched.h>
int do_trace(struct pt_regs *ctx) {
char comm[TASK_COMM_LEN];
bpf_get_current_comm(&comm, sizeof(comm));
[...]
7、bpf_get_current_task()
语法格式:bpf_get_current_task()
返回值:返回一个指向当前进程的task_struct结构
返回指向当前任务的task_struct对象的指针。这个helper可以用来计算进程的CPU时间、识别内核线程、获取当前CPU的运行队列或检索许多其他信息。
使用Linux4.13,由于字段随机化的问题,可能需要两个#定义指令,然后才包括:
#define randomized_struct_fields_start struct {
#define randomized_struct_fields_end };
#include <linux/sched.h>
int do_trace(void *ctx) {
struct task_struct *t = (struct task_struct *)bpf_get_current_task();
[...]
8、bpf_log2l()
语法格式:unsigned int bpf_log2l(unsigned long v)
返回值:返回所提供值的log-2。这经常被用来创建直方图的索引,构造幂-2直方图.
9、bpf_get_prandom_u32()
语法格式:u32 bpf_get_prandom_u32()
返回值:返回伪随机u32。
Output
1、bpf_trace_printk()
语法格式:int bpf_trace_printk(const char *fmt, ...)
返回值:成功返回0
一个用于printf()到公共trace_pipe(/sys/kernel/debug/tracing/trace_pipe)的简单内核设施。对于一些快速示例来说,这是可以的,但有局限性:最多三个参数,只能有一个字符串,以及trace_pipe是全局共享的,因此并发程序将有冲突的输出。更好的接口是通过BPF_PER F_OUTP UT()。请注意,调用此助手比原始内核版本简单,后者以fmt_size作为第二个参数。
2、BPF_PERF_OUTPUT
语法格式:BPF_PERF_OUTPUT(name)
创建BPF table,用于通过PE RF环缓冲区将自定义事件数据推送到用户空间。这是将每个事件数据推送到用户空间的首选方法。
一个例子:
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;
}
返回的table是events,数据是通过events.perf_submit()方法传入进去的。下面一个就进行解释。
3、perf_submit()
语法格式:int perf_submit((void *)ctx, (void *)data, u32 data_size)
返回值:成功返回0
这是一个BPF_PERF_OUTPUT的方法,用于提交特定数据到用户空间。
ctx参数是由kprobes和kretprobes提供的,对于SCHED_CLS
或者 SOCKET_FILTER
程序, 必须使用 struct __sk_buff *skb
替代。
MAPS
maps是BPF数据的存储方式,是更高级对象类型的基础,包括table表、hashes哈希和histograms直方图。
1、BPF_TABLE
语法格式:BPF_TABLE(_table_type, _key_type, _leaf_type, _name, _max_entries)
创建一个map,名称是_name
,大多数时候会通过更高级别的宏(例如BPF_HASH,BPF_HIST等)使用。
它的方法有:map.lookup(), map.lookup_or_try_init(), map.delete(), map.update(), map.insert(), map.increment().
一些例子:
search /examples,
search /tools
Pinned Maps
绑定到BPF 文件系统的map可以通过这样的扩展语法访问:BPF_TABLE_PINNED(_table_type, _key_type, _leaf_type, _name, _max_entries, “/sys/fs/bpf/xyz”)
类型信息不强制要求,实际的地图类型取决于map被绑定在位置。
一个例子:
BPF_TABLE_PINNED("hash", u64, u64, ids, 1024, "/sys/fs/bpf/ids");
2、BPF_HASH
语法格式:BPF_HASH(name [, key_type [, leaf_type [, size]]])
创建一个名为 name
的散列映射(关联数组),具有可选参数。
默认是:BPF_HASH(name, key_type=u64, leaf_type=u64, size=10240)
一个例子:
BPF_HASH(start, struct request *);
创建了一个叫start的哈希,key值是struct request *,value是默认的u64类型,这个哈希在disksnoop.py中使用,用于保存每个IO访问的时间戳,key是一个指向struct request的指针,value是时间戳。
它的方法有:map.lookup(), map.lookup_or_try_init(), map.delete(), map.update(), map.insert(), map.increment().(和BPF_TABLE一样)
一些例子:
search /examples,
search /tools
3、BPF_ARRAY
语法格式:BPF_ARRAY(name [, leaf_type [, size]])
建立一个int-索引的array,便于快速查找和更新,建立的array名为name,还有一些其余的参数。
默认是:BPF_ARRAY(name, leaf_type=u64, size=10240)
一个例子:
BPF_ARRAY(counts, u64, 32);
建立了一个名为counts的array,共有32个空间,存储的值的类型是u64。这个数组用于funccount.py中,用于存储每个函数调用的次数
它的方法:map.lookup(), map.update(), map.increment().
文档注:所有数组元素都是用零值预先分配的,不能删除。
一些例子:
search /examples,
search /tools
4、BPF_HISTOGRAM
语法格式: BPF_HISTOGRAM(name [, key_type [, size ]])
创建一个直方图的map类型,名称是name,和一些其他的参数。
默认是:BPF_HISTOGRAM(name, key_type=int, size=64)
一个例子:
BPF_HISTOGRAM(dist);
创建一个直方图名称为dist,默认有64个空间,默认是通过int类型的key值来进行索引。
它的方法:map.increment().
一些例子:
search /examples,
search /tools
5、BPF_STACK_TRACE
语法格式:BPF_STACK_TRACE(name, max_entries)
创建名为name的堆栈跟踪映射,并提供max_entries个计数。这些映射用于存储堆栈跟踪。
一个例子:
BPF_STACK_TRACE(stack_traces, 1024);
创建了一个堆栈跟踪映射名为stack_traces,堆栈跟踪条目的最大数量为1024个。
它的方法:map.get_stackid().
一些例子:
search /examples,
search /tools
6、BPF_PERF_ARRAY
语法格式:BPF_PERF_ARRAY(name, max_entries)
创建一个perf数组名为name,提供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)
创建了一个perf数组名为cpu_cycle,条目数等于cpu的个数。配置后的数组以后调用map.perf_read() ,会返回一个硬件计算的计数器,该计数器的周期数从过去的某个点经过。每个table一次只能配置一种类型的硬件计数器。
它的方法:map.perf_read().
一些例子:
search /tests
7、BPF_PERCPU_ARRAY
语法格式:BPF_PERCPU_ARRAY(name [, leaf_type [, size]])
创建一个int-索引的NUM_CPU数组,这些数组是为最快查找和更新而优化的,名为name,还有一些其他参数,每个CPU都会有一份独立的这个数组的拷贝,并且这些拷贝不以任何方式同步。
文档注:由于内核(在linux/mm/percpu.c)中定义的限制,leaf_type的大小不能超过32KB。
换言之,BPF_PER_CPU_ARRAY 元素的大小不能大于32KB。
默认是:BPF_PERCPU_ARRAY(name, leaf_type=u64, size=10240)
一个例子:
BPF_PERCPU_ARRAY(counts, u64, 32);
创建一个名为counts,有32个空间,存储64位int类型数据的NUM_CPU数组。
它的方法:map.lookup(), map.update(), map.increment().
Note that all array elements are pre-allocated with zero values and can not be deleted.
文档注:所有数组元素都是用零值预先分配的,不能删除。
一些例子:
search /examples,
search /tools
8、BPF_LPM_TRIE
语法格式:BPF_LPM_TRIE(name [, key_type [, leaf_type [, size]]])
创建一个最长的前缀匹配trie映射,名为name,具有可选参数。
默认是:BPF_LPM_TRIE(name, key_type=u64, leaf_type=u64, size=10240)
一个例子:
BPF_LPM_TRIE(trie, struct key_v6);
创建一个名为trie,key值是struct key_v6,value默认是u64的LPM trie map
它的方法:map.lookup(), map.lookup_or_try_init(), map.delete(), map.update(), map.insert(), map.increment().
一些例子:
search /examples,
search /tools
9、BPF_PROG_ARRAY
语法格式:BPF_PROG_ARRAY(name, size)
创建一个名为name的带有size项的程序数组prog_array,数组的每个条目要么是bpf程序的文件描述符,要么是NULL,数组充当跳转表,以便bpf程序可以“tail-call”其他bpf程序(尾调用)。
它的方法:map.call().
一些例子:
search /examples,
search /tests,
assign fd
10、BPF_DEVMAP
语法格式:BPF_DEVMAP(name, size)
创建一个名为name的设备映射,其中包含size项。map的每个条目都是一个ifindex到一个网络接口。这个map仅在XDP中使用。
一个例子:
BPF_DEVMAP(devmap, 10);
它的方法:map.redirect_map().
一些例子:
search /examples
11、BPF_CPUMAP
bcc python
INIALIZATION
构造器Constructors
1、BPF
语法格式: BPF({text=BPF_program | src_file=filename} [, usdt_contexts=[USDT_object, ...]] [, cflags=[arg1, ...]] [, debug=int])
创建一个BPF对象,这是定义一个BPF程序的主要对象,之后利用它进行输出交互
text
和src_file
二者之一必须提供,不是两个都必须提供。
cflags
制定要传给编译器的其他参数,例如: -DMACRO_NAME=value
或 -I/include/path
。参数作为数组传递,每个元素都是一个附加参数。请注意,字符串不会在空格上拆分,因此每个参数必须是数组的不同元素,例如["-include", "header.h"]
。
debug
标志控制调试输出,并且可以一起进行:
-
DEBUG_LLVM_IR = 0x1
编译 LLVM IR -
DEBUG_B PF=0x2
加载的BPF字节码和分支上的寄存器状态 -
DEBUG_PREP ROCESS OR=0x4
预处理器结果 -
DEBUG_SOURCE = 0x8
嵌入源程序的ASM指令 -
DEBUG_BPF_REGISTER_STATE = 0x10
除了DEBUG_B PF之外,所有指令上的注册状态
一个例子:
# 在一行中定义整个BPF程序:
BPF(text='int do_trace(void *ctx) { bpf_trace_printk("hit!\\n"); return 0; }');
# 将程序定义为一个变量:
prog = """
int hello(void *ctx) {
bpf_trace_printk("Hello, World!\\n");
return 0;
}
"""
b = BPF(text=prog)
# 源文件:
b = BPF(src_file = "vfsreadlat.c")
# 包括一个USDT对象:
u = USDT(pid=int(pid))
[...]
b = BPF(text=bpf_text, usdt_contexts=[u])
# 添加包括路径:
u = BPF(text=prog, cflags=["-I/path/to/include"])
一些例子:
search /examples,
search /tools
2、USDT
语法格式:USDT({pid=pid | path=path})
创建一个对象使用User Statically-Defined Tracing(USDT)探针。其主要方法是enable_probe()
。
参数:
- pid:attach到这个PID进程上
- path:从这个二进制路径探测来使用USDT探针
一个例子:
# include a USDT object:
u = USDT(pid=int(pid))
[...]
b = BPF(text=bpf_text, usdt_contexts=[u])
一些例子:
search /examples,
search /tools
EVENTS
1、attach_kprobe()
语法格式:BPF.attach_kprobe(event="event", fn_name="name")
使用内核动态跟踪来检测内核函数Event()
,并附加我们的C语言函数Name()
,以便在调用内核函数时调用。
一个例子:
b.attach_kprobe(event="sys_clone", fn_name="do_trace")
这里会注入到内核的sys_clone()函数,之后每次执行时都会调用我们定义的do_trace()函数
可以不止一次调用attach_kprobe(),并将BPF函数附加到多个内核函数。
请参阅前面的kprobe部分,了解如何从BPF中检测参数。
一些例子:
search /examples,
search /tools
2、attach_kretprobe()
语法格式:BPF.attach_kretprobe(event="event", fn_name="name" [, maxactive=int])
使用函数返回的内核动态追踪注入到内核event()函数的返回处,之后吧我们的C语言定义的函数name()附加到这里,当内核函数返回时就会触发执行。
一个例子:
b.attach_kretprobe(event="vfs_read", fn_name="do_return")
这里会注入到内核vfs_read函数,之后该函数每次执行的时候都会执行我们定义的BPF函数do_return()。
可以不止一次调用attach_kretprobe(),并将BPF函数附加到多个内核函数返回处。
当一个kretprobe安装在内核函数上时,它可以捕捉到多少个并行调用是有限制的。可以用maxactive
更改该限制。有关其默认值,请参阅kprobe文档。
请参阅前面的kretprobe部分,了解如何从BPF中检测返回值。
一些例子:
search /examples,
search /tools
3、attach_tracepoint()
语法格式:BPF.attach_tracepoint(tp="tracepoint", fn_name="name")
注入tracepoint
描述的内核跟踪点,并在击中时运行BPF函数name()
。
这是使用追踪点的一种明确的方法。前面的tracepoint部分所涵盖的TRACEPINT_PROBE
语法是一种替代方法,其优点是自动声明包含跟踪点参数的args
结构。使用attach_tracepoint()
,需要在BPF程序中声明跟踪点参数。
一个例子:
# 定义BPF程序
bpf_text = """
#include <uapi/linux/ptrace.h>
struct urandom_read_args {
// from /sys/kernel/debug/tracing/events/random/urandom_read/format
u64 __unused__;
u32 got_bits;
u32 pool_left;
u32 input_left;
};
int printarg(struct urandom_read_args *args) {
bpf_trace_printk("%d\\n", args->got_bits);
return 0;
};
"""
# load BPF program
b = BPF(text=bpf_text)
b.attach_tracepoint("random:urandom_read", "printarg")
注意,printarg()
的第一个参数现在是我们定义的结构。
现场实例:
code,
search /examples,
search /tools
4、attach_uprobe()
语法格式:BPF.attach_uprobe(name="location", sym="symbol", fn_name="name" [, sym_off=int])
, BPF.attach_uprobe(name="location", sym_re="regex", fn_name="name")
, BPF.attach_uprobe(name="location", addr=int, fn_name="name")
使用用户级动态跟踪,从库或以Location
命名的二进制文件中检测用户级函数symbol()
,并在调用用户级函数时附加我们定义的C函数name()
。如果给出了sym_off
,则将函数附加到符号内的偏移量上。
实际地址addr
可以代替sym
,在这种情况下,sym
必须设置为其默认值。如果文件是non-PIE可执行文件,则addr
必须是虚拟地址,否则它必须是相对于文件加载地址的偏移量。
可在“sym_re”中提供正则表达式,而不是符号名称。然后,uprobe将附加到与所提供的正则表达式匹配的符号上。
库可以在没有lib前缀的名称参数中给出,也可以使用完整路径(/usr/lib/.)。只能在完整路径(/bin/sh)下给出二进制文件。
一个例子:
b.attach_uprobe(name="c", sym="strlen", fn_name="count")
这将使用来自libc的strlen()
函数,并在调用时调用我们的BPF函数count()
。注意“lib”在“libc”中不是必须要声明的。
其他例子:
b.attach_uprobe(name="c", sym="getaddrinfo", fn_name="do_entry")
b.attach_uprobe(name="/usr/bin/python", sym="main", fn_name="do_main")
可以不止一次调用attach_uprobe(),并将BPF函数附加到多个用户级函数。
有关如何从BPF中检测参数的,请参阅前面的uprobes部分。
现场实例:
search /examples,
search /tools
5、attach_uretprobe()
语法格式:BPF.attach_uretprobe(name="location", sym="symbol", fn_name="name")
使用对函数返回的用户级动态跟踪,从库或以location
命名的二进制文件中返回用户级函数symbol()
,并附加我们的C定义函数name()
,以便在用户级函数返回时调用。
一个例子:
b.attach_uretprobe(name="c", sym="strlen", fn_name="count")
这将使用来自libc的strlen()
函数,并在返回时调用我们的BPF函数count()
。
其他例子:
b.attach_uretprobe(name="c", sym="getaddrinfo", fn_name="do_return")
b.attach_uretprobe(name="/usr/bin/python", sym="main", fn_name="do_main")
可以不止一次调用attach_uretprobe(),并将BPF函数附加到多个用户级函数。
请参阅前面的uretprobes部分,了解如何从BPF中检测返回值。
现场实例:
search /examples,
search /tools
6、USDT.enable_probe()
语法格式:USDT.enable_probe(probe=probe, fn_name=name)
把一个BPF的c程序name
附加到USDT probe probe
上
一个例子:
# enable USDT probe from given PID
u = USDT(pid=int(pid))
u.enable_probe(probe="http__server__request", fn_name="do_trace")
从给定的PID中启用USDT探针
要检查二进制文件是否有USDT探针,以及它们是什么,可以运行readelf -n binary
并检查stap调试部分。
现场实例:
search /examples,
search /tools
7、attach_raw_tracepoint()
语法格式:BPF.attach_raw_tracepoint(tp="tracepoint", fn_name="name")
使用Trace Point
(Events
only,nocategory
)描述的内核raw tracepoint,并在命中时运行BPF函数Name()
。
这是使用追踪点的一种明确的方法。前面的原始跟踪点部分所涵盖的RAW_TRACEPOINT_PROBE
语法是一种替代方法。
一个例子:
b.attach_raw_tracepoint("sched_swtich", "do_trace")
现场实例:
search /tools
DEBUG OUTPUT
1、trace_print()
语法格式:BPF.trace_print(fmt="fields")
此方法不断读取全局共享/sys/kernel/debug/tracing/trace_pipe文件并打印其内容。这个文件可以通过BPF和bpf_trace_printk()函数写入,但是该方法有局限性,包括缺乏并发跟踪支持。前面所述的BPF_PERF_OUTPUT机制优先。
参数:
- fmt:可选参数,并且可以包含字段格式化字符串。默认为“none”。
一个例子:
# print trace_pipe output as-is:
b.trace_print()
# print PID and message:
b.trace_print(fmt="{1} {5}")
现场实例:
search /examples,
search /tools
2、trace_fields()
语法格式:BPF.trace_fields(nonblocking=False)
此方法从全局共享/sys/kernel/debug/tracing/trace_pipe文件中读取一行,并将其作为字段返回。这个文件可以通过BPF和bpf_trace_printk()函数写入,但是该方法有局限性,包括缺乏并发跟踪支持。前面所述的BPF_PER F_OUTP UT机制优先。
参数:
- nonblocking:可选的,默认为“False”。当设置为“True”时,程序不会阻塞等待输入。
一个例子:
while 1:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
except ValueError:
continue
[...]
现场实例:
search /examples,
search /tools
OUTPUT
BPF程序的正常输出是:
- per-event:使用PERF_EVENT_OUTPUT,open_perf_buffer(),和perf_buffer_poll()
- map 总结:使用items(),或者print_log2_hist(),这个在Maps章节
1、perf_buffer_poll()
语法格式:BPF.perf_buffer_poll(timeout=T)
此轮询来自所有的环缓冲区,调用open_perf_Buffer时为每个条目提供的回调函数。
超时参数是可选的,并以毫秒为单位测量。在没有定义的情况下,将无限期地继续进行。
一个例子:
# loop with callback to print_event
b["events"].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit();
现场实例:
code,
search /examples,
search /tools
MAPS
maps是BPF数据存储,在bcc中使用来实现一个表,然后在表的上层实现更高层次的对象,包括哈希和直方图。
1、get_table()
语法格式:BPF.get_table(name)
返回表对象。这不再使用,因为现在可以从BPF中将表读为项。
例子:
counts = b.get_table("counts")
counts = b["counts"]
二者是同等的效果。
2、open_perf_buffer()
语法格式:table.open_perf_buffers(callback, page_cnt=N, lost_cb=None)
这在BPF中定义为BPF_PERF_OUTPUT()的table操作,并将回调Python函数callback
关联起来,以便在Perf环缓冲区中可用数据时调用。这是推荐的机制的一部分,用于将每个事件的数据从内核传输到用户空间。perf环缓冲区的大小可以通过page_cnt
参数来指定,该参数必须是两个页数的幂,默认为8。如果回调处理数据的速度不够快,一些提交的数据可能会丢失。lost_cb
将被调用来记录/监视丢失的计数。如果loss_cb
是默认的none
值,它只会打印一行消息到stderr
。
一个例子:
# process event
def print_event(cpu, data, size):
event = ct.cast(data, ct.POINTER(Data)).contents
[...]
# loop with callback to print_event
b["events"].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
请注意,传输的数据结构将需要在BPF程序中在C中声明。例如:
// define output data structure in C
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
};
BPF_PERF_OUTPUT(events);
[...]
在Python中,您可以让BCC自动从C声明生成数据结构(推荐):
def print_event(cpu, data, size):
event = b["events"].event(data)
[...]
或手动定义:
# define output data structure in Python
TASK_COMM_LEN = 16 # linux/sched.h
class Data(ct.Structure):
_fields_ = [("pid", ct.c_ulonglong),
("ts", ct.c_ulonglong),
("comm", ct.c_char * TASK_COMM_LEN)]
def print_event(cpu, data, size):
event = ct.cast(data, ct.POINTER(Data)).contents
[...]
现场实例:
code,
search /examples,
search /tools
3、items()
语法格式:table.items()
返回table中key的数组。这可以与BPF_HASH map一起使用,以便在键上获取和迭代。
一个例子:
# print output
print("%10s %s" % ("COUNT", "STRING"))
counts = b.get_table("counts")
for k, v in sorted(counts.items(), key=lambda counts: counts[1].value):
print("%10d \"%s\"" % (v.value, k.c.encode('string-escape')))
此示例还使用sorted()
方法按值排序。
现场实例:
search /examples,
search /tools
4、values()
语法格式:table.values()
返回table中values的数组。
5、clear()
语法格式:table.clear()
清空table:删除所有目录
一个例子:
# print map summary every second:
while True:
time.sleep(1)
print("%-8s\n" % time.strftime("%H:%M:%S"), end="")
dist.print_log2_hist(sym + " return:")
dist.clear()
现场实例:
search /examples,
search /tools
6、print_log2_hist()
语法格式:table.print_log2_hist(val_type="value", section_header="Bucket ptr", section_print_fn=None)
在ASCII中打印一个表作为log2直方图。该表必须存储为log2,可以使用BPF函数“`bpf_log2l()”完成。
参数:
- val_type:可选的,列标题
- section_header:如果直方图有一个次要键,则会打印多个table,section_header可以用作每个table的标题描述
- section_print_fn:如果section_print_fn不是None,则将传递桶值。
一个例子:
b = BPF(text="""
BPF_HISTOGRAM(dist);
int kprobe__blk_account_io_completion(struct pt_regs *ctx, struct request *req)
{
dist.increment(bpf_log2l(req->__data_len / 1024));
return 0;
}
""")
[...]
b["dist"].print_log2_hist("kbytes")
输出:
kbytes : count distribution
0 -> 1 : 3 | |
2 -> 3 : 0 | |
4 -> 7 : 211 |********** |
8 -> 15 : 0 | |
16 -> 31 : 0 | |
32 -> 63 : 0 | |
64 -> 127 : 1 | |
128 -> 255 : 800 |**************************************|
该输出显示多模态分布,最大模式为128->255字节,计数为800。
这是一个很有效的途径来总结数据,总结的过程在内核中完成,最后只有count列传输到用户空间。
现场实例:
search /examples,
search /tools
6、print_linear_hist()
暂时略
HELPERS
bcc提供的一些辅助方法。请注意,由于我们在Python中,所以我们可以导入任何Python库及其方法,例如,库:argparse、collections、ctype、datetime、Re、socket、struct、subprocess、sys和time。
1、ksym()
语法格式:BPF.ksym(addr)
将内核内存地址转换为内核函数名,该函数名将被返回。
一个例子:
print("kernel function: " + b.ksym(addr))
现场实例:
search /examples,
search /tools
2、ksymname()
语法格式:BPF.ksymname(name)
将内核名称转换为地址。这是ksym的反面,当函数名未知时返回-1…
一个例子:
print("kernel address: %x" % b.ksymname("vfs_read"))
现场实例:
search /examples,
search /tools
3、sym()
语法格式:BPF.sym(addr, pid, show_module=False, show_offset=False)
将内存地址转换为一个PID的函数名,该函数名将被返回。小于零的PID将访问内核符号缓存。show_module
和show_offset
参数控制是否应显示符号所在的模块,以及是否应显示符号开头的指令偏移。这些额外的参数默认为false
。
一个例子:
print("function: " + b.sym(addr, pid))
现场实例:
search /examples,
search /tools
4、num_open_kprobes()
语法格式:BPF.num_open_kprobes()
返回打开的k[ret]探针的数量。对于在附加和分离探针时使用event_re的场景可能很有用。不包括perf_events reader。
一个例子:
b.attach_kprobe(event_re=pattern, fn_name="trace_count")
matched = b.num_open_kprobes()
if matched == 0:
print("0 functions matched by \"%s\". Exiting." % args.pattern)
exit()
现场实例:
search /examples,
search /tools
5、get_syscall_fnname()
语法格式: BPF.get_syscall_fnname(name : str)
返回syscall对应的内核函数名,此助手函数将尝试不同的前缀,并使用正确的前缀与syscall名称连接。请注意,不同版本的linux内核的返回值可能会有所不同,有时会造成麻烦…(见#2590)
一个例子:
print("The function name of %s in kernel is %s" % ("clone", b.get_syscall_fnname("clone")))
# sys_clone or __x64_sys_clone or ...
现场实例:
search /examples,
search /tools
BPF ERRORS
请参阅Documentation/networking/filter.txt下内核源中的Understanding eBPF verifier messages
部分。
1、Invalid mem access
这可能是因为试图直接读取内存,而不是在BPF堆栈上对内存进行操作。所有的内存读取必须通过bpf_probe_read()来复制内存到bpf堆栈中,在一些简单的去引用的情况下,bcc重写程序可以自动地将内存复制到bpf堆栈中。bpf_probe_read()做所有必要的检查。
例如:
bpf: Permission denied
0: (bf) r6 = r1
1: (79) r7 = *(u64 *)(r6 +80)
2: (85) call 14
3: (bf) r8 = r0
[...]
23: (69) r1 = *(u16 *)(r7 +16)
R7 invalid mem access 'inv'
Traceback (most recent call last):
File "./tcpaccept", line 179, in <module>
b = BPF(text=bpf_text)
File "/usr/lib/python2.7/dist-packages/bcc/__init__.py", line 172, in __init__
self._trace_autoload()
File "/usr/lib/python2.7/dist-packages/bcc/__init__.py", line 612, in _trace_autoload
fn = self.load_func(func_name, BPF.KPROBE)
File "/usr/lib/python2.7/dist-packages/bcc/__init__.py", line 212, in load_func
raise Exception("Failed to load BPF program %s" % func_name)
Exception: Failed to load BPF program kretprobe__inet_csk_accept
2、Cannot call GPL only function from proprietary program
当从非GPL BPF程序调用GPL唯一助手时,会发生此错误。若要修复此错误,请不要使用专有BPF程序中的GPL专用助手,或在GPL兼容许可证下重新保存BPF程序。检查哪些(BPF helpers)是GPL唯一的,以及哪些许可证被认为是GPL兼容的。
例如,从专有程序调用bpf_get_stackid()
,这是一个GPL专用BPF助手(# define BPF_LICENSE 专有
):
bpf: Failed to load program: Invalid argument
[...]
8: (85) call bpf_get_stackid#27
cannot call GPL only function from proprietary program
ENVIRONMENT VARIABLES
1、KERNEL SOURCE DIRECTORY
eBPF程序编译需要内核源程序或带有标头的内核头程序已汇编。如果您的内核源位于BCC的非标准位置然后找不到,就要给BCC提供绝对路径的位置,通过BCC_KERNEL_SOURCE
设置为它。
2、KERNEL VERSION OVERRIDING
默认情况下,BCC将LINUX_VERSION_CODE
存储在生成的eBPF对象中,然后在加载eBPF程序时将其传递到内核。有时这是非常不方便的,特别是当内核稍微更新时,例如LTS内核发布。轻微的不匹配将导致很多问题。通过将BCC_LINUX_VERSION_CODE
设置为正在运行的内核版本,可以绕过验证内核版本的检查。这对于使用kprobe的程序来说是必需的。这需要以格式编码:(VERSION*65536)+(PATCHLEVEL*256)+ SUBLEVEL
。例如,如果运行的内核是4.9.10'
,则可以设置export BCC_LINUX_VERSION_CODE=264458
以成功覆盖内核版本检查。
本文来自同门分享
参考链接:https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md