BPF之前端工具BCC与bpftrace

BPF前端工具BCC与bpftrace

一、概述

BCC和bpftrace到底是什么,与BPF是什么关系呢?

经过上一篇的介绍,BPF是内核中的执行引擎,BCC和bpftrace则是两个前端工具,比如用户可以直接使用的命令行工具。

BCC与bpftrace又有何区别?

bpftrace是基于BPF和BCC开发出来的,bpftrace适合临时创造单行程序和短小脚本进行观测,而BCC更适合编写复杂的工具和守护进程。BCC提供的API分为内核态和用户态的,而bpftrace只有一种API即bpftrace编程语言。

BCC与bpftrace都是基于libbcc和libbpf库进行构建的。

二、BCC

BCC全称是BPF Compiler Collection,BPF编译器集合,简称BCC,它包含了构建BPF软件的编译器框架和库,是BPF的主要前端项目

组件

BCC组件可以用Brendan Gregg的一张图来表示:
在这里插入图片描述

安装

BPF组件是在内核4.1至4.9之间开发,最好选择内核4.9及以上的系统。

内核配置需要开启(一般默认开启了):

CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_EVENTS=y
CONFIG_BPF_JIT=y
CONFIG_HAVE_EBPF_JIT=y
Ubuntu

ubuntu上安装一般可能出现的问题较少,包名字叫bpfcc-tools

sudo apt install bpfcc-tools linux-headers-$(uname -r)
# 或者
sudo snap install gcc
RHEL

Centos7我试了很久,安装问题总是不断,不推荐。

sudo yum install bcc-tools
# 问题可能出在:
# 1.内核版本过低; 2. 缺少对应的kernel-devel(yum install kernel-devel-$(unamr -r); 3. 缺少kernel-headers)

工具

BCC工具有很多,单一用途或多用途工具都有,可以主要按以下分类:

功能点工具
调试手段trace、argdist、funccount、stackcount、opensnoop
CPU相关execsnoop、runqlat、runqlen、cpudist、profile、offcputime、syscount、softirq、hardirq
内存相关menleak
文件系统opensnoop、filelife、vfsstat、fileslower、cachestat、writeback、dcstat、xfsslower、xfsdist、ext4dist
磁盘I/Obiolatency、biosnoop、biotop、bitesize
网络相关tcpconnect、tcpaccept、tcplife、tcpretrans
安全相关capable

三、bpftrace

组件

在这里插入图片描述

所有的bpftrace工具都是以.bt作为文件后缀名。前端使用lex和yacc来对bpftrace语言进行词法和语法分析,使用Clang来解析结构体。后端则将bpftrace程序编译成LLVM中间表示形式,再通过LLVM库将其编译为BPF代码。

安装

Ubuntu
sudo apt update
sudo apt install bpftrace

工具

主要分类:

功能点工具
调试手段execsnoop.bt、threadsnoop.bt、opensnoop.bt、killsnoop.bt、signals.bt
CPU相关execsnoop.bt、runqlat.bt、runqlen.bt、cpuwalk.bt、offcputime.bt
内存相关oomkill.bt、failts.bt、vmscan.bt、swapin.bt
文件系统vfsstat.bt、filelife.bt、xfsdist.bt
存储I/Obiosnoop.bt、biolatency.bt、bitesize.bt、biostacks.bt、scsilatency.bt、nvmelatency.bt
网络相关tcpaccept.bt、tcpconnect.bt、tcpdrop.bt、tcpretrans.bt、gethostlatency.bt
安全相关ttysnoop.bt、elfsnoop.bt、setuids.bt
编程相关jnistacks.bt、javacalls.bt
应用相关threadsnoop.bt、pmheld.bt、naptime.bt、mysqld_qslower.bt
内核相关mlock.bt、mheld.bt、kmem.bt、kpages.bt、workq.bt
容器相关pidnss.bt、blkthrot.bt
虚拟机xenhyper.bt、cpustolen.bt、kvmexits.bt

bpftrace编程

基础语法
1. 程序结构

bpftrace程序结构是一系列探针加对应的动作

probes { actions }
probes { actions }
...

探针被激活时,相应动作会被执行。也可以在探针前设置一个可选的过滤表达式

probes /filter/ { actions }
/pattern/ { actions }
2. 注释

单行代码使用"//"来注释

// this is a comment

多行代码注释和C语言一样

/*
 * this is 
 * a muti-line
 * comment
 */
3. 探针格式

探针(probe)以类型名开始,然后是一系列冒号分隔的标识符

type:identifier1[:identifier2[...]]

标识符的组织形式由探针类型决定,比如

kprobe:vfs_read
uprobe:/bin/bash:readline
  • kprobe探针类型,内核态函数插桩,只需要一个标识符:内核函数名
  • uprobe探针类型,用户态函数插桩,需要两个标识符:二进制文件路径和函数名

可以使用逗号将多个探针并列,指向同一个执行动作,如

probe1,probe2,... { actions }

有两个特殊的探针类型不需要标识符:BEGIN和END,它们会在bpftrace程序启动和结束时触发(类似awk命令)

4. 探针通配符

有些探针接受通配符,比如下面这个,会对有的以"vfs_"开头的内核函数进行插桩

kprobe:vfs_*

但如果同时开启很多的函数插桩,对性能有影响。环境变量"BPFTRACE_MAX_PROBES"可以设置探针数量上限。

也可以插桩之前使用"bpftrace -l"命令进行统计有多少个探针数量比配,做个评估

bpftrace -l "kprobe:vfs_*" | wc -l
5. 过滤器

过滤器是一个布尔表达式,决定一个动作是否被执行

//过滤进程pid为123
/pid == 123/

//筛选pid为非零
/pid/

//过滤器也可使布尔运算组合
/pid > 100 && pid < 1000/
6. 执行动作

一个动作既可以是单条语句,也可以是分号分隔的多条语句

{ action one; action two; action three }

全部语句最后也可以加分号。语句采用bpftrace语法,类似C语言,可以操作变量和执行bpftrace函数调用

//创建变量x并赋值42,然后打印出来
{ $x = 42; printf("$x is %d", $x); }
7. 变量
变量类型含义举例
内置变量bpftrace预先定义好并提供pid,comm(进程名),nsecs(纳秒时间戳),curtask(当前线程task_struct结构体地址)
临时变量临时计算使用,"$"开头 x = 1 ; < b r / > x = 1;<br /> x=1;<br/>y = “hello”;
$z = (struct task_struct*) curtask;
映射表变量"@"前缀,用于存储对象@start[tid] = nsecs; //内置变量nsecs赋值给一个名为start、以tid为key的映射表,这允许每个线程存储自己的时间戳
@path[pid, f d ] = s t r ( a r g 0 ) ; / / 这是一个复合键的映射表,同时使用内置变量 p i d 和 fd] = str(arg0); //这是一个复合键的映射表,同时使用内置变量pid和 fd]=str(arg0);//这是一个复合键的映射表,同时使用内置变量pidfd组合key
8. 映射表函数

映射表可以通过特定的统计函数赋值,这些函数以特殊方式来存储和打印数据,这里使用了每个CPU独立的映射表

@x = count(); //对时间进行累计统计,打印时会答应出累计结果

下面这个语句也会对事件进行计数,但使用的是全局映射表,非每个CPU独立的映射表,@x的类型是整数

@x++;

其他举例:

@y = sum($x); //对$x求和,打印时打印出总数
@z = hist($x); //将$x保存在一个以2的幂为区间的直方图中
delete(@start[tid]); //从@start中删除key为tid的值
9. 实例

统计内核函数vfs_read()计时并以直方图打印

#!/usr/bin/bpftrace

kprobe:vfs_read
{
    @start[tid] = nsecs;
}

kretprobe:vfs_read
/@start[tid]/
{
    $duration_us = (nsecs - @start[tid]) / 1000;
    @us = hist($duration_us);
    delete(@start[tid]);
}

执行效果:
在这里插入图片描述

探针类型
类型缩写描述访问参数
tracepointt内核静态插桩点内置变量args
usdtU用户静态定义插桩点内置变量args
kprobek内核动态函数插桩arg0, arg1, …, argN
kretprobekr内核动态函数返回值插桩retval
uprobeu用户态动态函数插桩arg0, arg1, …, argN
uretprobeur用户态动态函数返回值插桩retval
softwares内核软件事件
hardwareh硬件基于计数器的插桩
profilep对全部CPU进行时间采样
intervali周期性报告(从一个CPU上)
BEGINbpftrace启动
ENDbpftrace推出
1. tracepoint

tracepoint会对内核跟踪点进行插桩,格式

tracepoint: tracepoint_name

tracepoint_name是跟踪点的全名,包括用来将跟踪点所在的类别和事件名字分隔开的冒号。如tracepoint:net:netif_rx是对net:netif_rx这个跟踪点进行插桩。

跟踪点通常带有参数,bpftrace可以通过内置变量args来访问这些参数。如net:netif_rx有一个代表数据包长度的参数len,可以通过args->len 来访问。可以通过"bpftrace -lv"来查看

root@pc:test# bpftrace -lv tracepoint:net:netif_rx
tracepoint:net:netif_rx
    void * skbaddr
    unsigned int len
    __data_loc char[] name

比如sys_enter_read是对系统调用read(2)的开始处插桩,man手册可以看到read(2)的帮助文档:

ssize_t read(int fd, void *buf, size_t count);

其有三个参数,那么对于sys_enter_read跟踪点来说,bpftrace中可以用args->fd, args->buf, args->count来访问这三个参数。

root@pc:test# bpftrace -lv tracepoint:syscalls:sys_enter_read
tracepoint:syscalls:sys_enter_read
    int __syscall_nr
    unsigned int fd
    char * buf
    size_t count

一个有趣的例子,对系统调用clone(2)的开始和推出进行插桩,test_sys_clone.bt代码

#!/usr/bin/bpftrace

tracepoint:syscalls:sys_enter_clone
{
    printf("\n-> clone() by %s PID %d\n", comm, pid);
}

tracepoint:syscalls:sys_exit_clone
{
    printf("<- clone() return %d, %s PID %d\n", args->ret, comm, pid);
}

执行,同时开启另一个terminal窗口

root@pc:test# ./sys_clone.bt 
Attaching 2 probes...

-> clone() by gnome-terminal- PID 3287
<- clone() return 76637, gnome-terminal- PID 3287
<- clone() return 0, gnome-terminal- PID 76637

-> clone() by bash PID 76637
<- clone() return 76639, bash PID 76637
<- clone() return 0, bash PID 76639

-> clone() by lesspipe PID 76639
<- clone() return 76640, lesspipe PID 76639
<- clone() return 0, lesspipe PID 76640

-> clone() by lesspipe PID 76639
<- clone() return 76641, lesspipe PID 76639
<- clone() return 0, lesspipe PID 76641

-> clone() by lesspipe PID 76641
<- clone() return 76642, lesspipe PID 76641
<- clone() return 0, lesspipe PID 76642

-> clone() by bash PID 76637
<- clone() return 76643, bash PID 76637
<- clone() return 0, bash PID 76643
^C

有趣的是clone(2)总是进入一次,返回两次,一次是父进程中返回子进程PID,一次是子进程中返回0

2. usdt

usdt探针是对用户态静态探针点进行插桩,格式为:

usdt:binary_path:probe_name
usdt:library_path:probe_name
usdt:binary_path:probe_namespace:probe_name
usdt:library_path:probe_namespace:probe_name

usdt可以对完整路径的二进制文件或者共享库进行插桩。probe_name为USDT的探针名字。

USDT探针的任意参数,都可以用bpftrace内置变量args访问

3. kprobe和kretprobe

内核态动态插桩,格式为

kprobe:function_name
kretprobe:function_name

kprobe的参数"arg0, arg1, … argN"是函数入参,都为64位无符号整型。

kretprobe的参数:内置的retval是函数的返回值。retval永远是64无符号整型,如果和函数返回值不一致,需要强制转化。

4. uprobe和uretprobe

用户态动态插桩,格式为

uprobe:binary_path:function_name
uprobe:library_path:function_name
uretprobe:binary_path:function_name
uretprobe:library_path:function_name

uprobe的参数:arg0,arg1,…,argN 是进入函数时的入参,类型均为64位无符号整型,如果是执行C结构体的指针,可以强制类型转换为对应结构体,目前还不支持BTF,将来可能会支持,那就可以像内核一样自己描述自己的结构体类型。

uretprobe的参数:内置变量retval是函数的返回值,retval恒为64位无符号整型,如果和函数返回值不一样,需要强制转换。

举例,一段简单的C代码,debugtest.c

root@pc:bpf-test# cat debugtest.c 
#include <stdio.h>

void test_func1(int num)
{
    printf("recv number %d\n", num);
}

int main(int argc, char *argv[])
{
    test_func1(2);
    return 0;
}

将debugtest.c编译,带debug编译

gcc -g debugtest.c -o debugtest

然后使用bpftrace跟踪test_func1()被调用时的输入参数num

bpftrace -e 'uprobe:./debugtest:test_func1 { printf("num is %d\n", arg0); }'

执行效果如下,在一个终端中跟踪,另一个终端中执行./debugtest
在这里插入图片描述

当然,bpftrace也可以直接跟踪地址,只要带"–unsafe"参数即可

root@pc:bpf-test# nm debugtest
000000000000038c r __abi_tag
0000000000004010 B __bss_start
0000000000004010 b completed.0
                 w __cxa_finalize@GLIBC_2.2.5
0000000000004000 D __data_start
0000000000004000 W data_start
0000000000001090 t deregister_tm_clones
0000000000001100 t __do_global_dtors_aux
0000000000003dc0 d __do_global_dtors_aux_fini_array_entry
0000000000004008 D __dso_handle
0000000000003dc8 d _DYNAMIC
0000000000004010 D _edata
0000000000004018 B _end
0000000000001198 T _fini
0000000000001140 t frame_dummy
0000000000003db8 d __frame_dummy_init_array_entry
0000000000002118 r __FRAME_END__
0000000000003fb8 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
0000000000002014 r __GNU_EH_FRAME_HDR
0000000000001000 T _init
0000000000002000 R _IO_stdin_used
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 U __libc_start_main@GLIBC_2.34
0000000000001174 T main
                 U printf@GLIBC_2.2.5
00000000000010c0 t register_tm_clones
0000000000001060 T _start
0000000000001149 T test_func1
0000000000004010 D __TMC_END__

这里看到,debugtest中test_func1()函数地址在0x1149位置,同样一个终端跟踪0x1149这个地址,另一个终端执行./debugtest,
在这里插入图片描述

甚至,在strip debugtest删除debugsymbol之后,还可以使用0x1149这个地址来追踪test_func1()函数。

控制流

bpftrace中支持3中类型的测试:过滤器filter、ternary运算符和if语句

1. 过滤器
probe /filter/ { actions }
2. 三元操作符
test ? true_statement : false_statement
3. if语句
if (test) { true_statement }
if (test) { true_statement } else { false_statement }

目前还不支持else if语句。

运算符

bpftrace支持布尔运算,以及下面一些运算符

运算符含义
=赋值
+、-、*、/加减乘除
++、–加1、减1
&、|、^按位与、按位或、按位与或
!逻辑非
<<、>>向左位移、向右位移
+=、-=、*=、/=、%=、&=、^=、<<=、>>=复合运算符
变量
1. 内置变量

bpftrace中内置变量一般用于对信息的只读访问

内置变量类型含义
pidint进程ID
tidint线程ID
uidint用户ID
usernamestring用户名
nesecsint时间戳,纳秒级
elapsedint时间戳,纳秒,自bpftrace启动开始计时
cpuintCPU ID
commstring进程名
kstackstring内核调用栈
ustackstring用户态调用栈
arg0, …, argNint某些探针类型的参数
argsstruct某些探针类型的参数
retvalint某些探针类型的返回值
funcstring被跟踪函数名
probestring当前探针全名
curtaskint内核task_struct的地址,类型是64位无符号整型
cgroupintcgroup ID
$1, …, $Nint、chat*bpftrace程序的位置参数

所有的int型目前都是64位无符号整型

举例:

  • 使用pid、comm、uid来跟踪谁在调用setuid()这个系统调用

    bpftrace -e 't:syscalls:sys_enter_setuid { printf("setuid by PID %d (%s), UID %d\n", pid, comm, uid); }'
    bpftrace -e 't:syscalls:sys_enter_setuid { printf("setuid by %s returned %d\n", comm, args->ret); }'
    

在这里插入图片描述

  • 使用pid来跟踪指定pid的进程在哪个CPU上被进程调度切换掉给哪个进程

    //比如这里pid 79095是我运行的htop进程,查看其在哪个CPU上调度切换给哪个进程
    bpftrace -e 't:sched:sched_switch /pid == 79095/ { printf("cpu %d switch from %d(%s) to %d(%s)\n", cpu, args->prev_pid, args->prev_comm, args->next_pid, args->next_comm); }'
    

在这里插入图片描述

  • 使用kstack打印内核态调用栈,比如打印htop(pid为79182)的进程被切换时内核调用栈

    bpftrace -e 't:sched:sched_switch /pid == 79182/ { printf("sched switch call stack %s\n", kstack); }'
    

在这里插入图片描述

2. 临时变量

格式如下:

$name

这些变量可以在一个动作中进行临时计算。类型取决于第一次被赋值,可以是整型、字符串、结构体的指针或者结构体

3. 映射表变量

格式如下:

@name
@name[key]
@name[key1, key2[, ...]]

这些变量使用BPF映射表作为存储,BPF映射表是一种哈希表(关联数组),可以用于不同的存储类型。值可以用一个或多个键值来存储。映射表使用的键/值类型必须前后保持一致。与临时变量一样,映射表的类型也取决于第一次赋值

举例:

@start = nsecs;  //@start是整型,因为被赋值了内置变量纳秒时间戳nsecs
@last[tid] = nsecs;   //@last是整型,因为被赋值纳秒时间戳nsecs,同时要求键类型为整型,因为这里用了整数键tid
@bytes = hist(retval);   //@bytes是一个特殊类型:以2的幂为区间的直方图,会管理并打印直方图
@who[pid, comm] = count();  //@who映射表中有两个键,整数pid和字符串comm,它的值是一个统计函数count()
函数

bpftrace中提供了针对各种任务的函数,这里列出最重要的一些

函数描述
printf(char *fmt [, …])打印
time(char *fmt)格式化打印时间
join(char *arr[ ])打印字符串数组,以空格分隔
str(char *s [, int len])从指针s返回字符串,长度参数可选
kstack(int limit)返回一个深度最大为limit的内核态调用栈
ustack(int limit)返回一个深度最大为limit的用户态调用栈
ksym(void *p)分析内核地址,并返回字符串形式的符号
usym(void *p)分析用户空间地址,并返回字符串形式的符号
kaddr(char *name)将内核符号名字翻译为地址
uaddr(char)将用户空间符号翻译为地址
reg(char *name)将返回值存储到指定寄存器中
ntop([int af, ] int addr)返回一个字符串表示的IP地址
system(char *fmt [, …])执行一个shell命令
cat(char *filename)打印文件内容
exit()退出bpftrace

异步处理,上面一些函数是异步处理的:内核将事件加入队列,然后有用户态程序后面处理

printf()、time()、cat()、join()、system()是异步处理

kstack()、ustack()、ksym()、usym()会同步记录地址,但符号转义是异步处理。

映射表函数

bpftrace中一些重要的映射表函数:

函数描述
count()对出现次数进行统计计数
sum(int n)求和
avg(int n)求平均
min(int n)记录最小值
max(int n)记录最大值
stats(int n)返回事件次数、平均值和总和
hist(int n)打印2的幂次方直方图
lhist(int n, int min, int max, int step)打印线性直方图
delete(@m[key])删除映射表中的键/值对
print(@m [, top [, div] ])删除映射表,可带参数limit和除数
clear(@m)删除映射表中全部的键
zero(@m)将映射表中的所有值设置为0

异步处理的函数有:print()、clear()、zero(),所以这几个函数执行会有延迟。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值