bpftrace学习

本文最后更新于:2026年4月3日 晚上

bpftrace的语法类似awk,熟悉awk的朋友这下有福了,一切看起来都是那么亲切。

语法结构

1
bpftrace -e 'BEGIN{} 探针,探针 /条件/{执行语句}  探针 /条件/{执行语句} ... END{}'

和awk语法相似,只是中间的每个执行语句是针对当时的探针触发点。
条件不像awk那样支持正则,只有简单的大小判断,字符串相等判断。

一行式实例学习

hello world

1
bpftrace -e 'BEGIN {print("hello world")}'

输出为:

1
2
bpftrace -e 'BEGIN {print("hello world")}'Attaching 1 probe...
hello world

跟踪打开的文件

1
2
3
4
5
# bpftrace -e 'tracepoint:syscalls:sys_enter_openat {printf("%s open %s\n", comm, str(args->filename));}'
Attaching 1 probe...
code open /proc/117533/cmdline
code open /proc/119202/cmdline
ToDesk_Service open /run/systemd/seats/

这里有同学可能要问,打开文件系统调用不是open吗,这里怎么使用openat,我怎么知道该用openat?
不清楚用哪个open没有关系,可以用以下办法查看有哪些open:

1
2
3
4
5
6
# bpftrace -l "tracepoint:syscalls:sys_enter_open*"
tracepoint:syscalls:sys_enter_open
tracepoint:syscalls:sys_enter_open_by_handle_at
tracepoint:syscalls:sys_enter_open_tree
tracepoint:syscalls:sys_enter_openat
tracepoint:syscalls:sys_enter_openat2

然后我们来试一下,有哪些open会被触发:

1
2
3
4
5
6
7
# bpftrace -e 'tracepoint:syscalls:sys_enter_open* {printf("%s %s \n", probe, comm);}'
Attaching 5 probes...
tracepoint:syscalls:sys_enter_openat bpftrace
tracepoint:syscalls:sys_enter_openat bpftrace
tracepoint:syscalls:sys_enter_openat systemd-oomd
tracepoint:syscalls:sys_enter_openat systemd-oomd
tracepoint:syscalls:sys_enter_openat systemd-oomd

然后发现只有sys_enter_openat被触发了。

统计open调用次数

统计各个进程open系统调用的次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# bpftrace -e 'tracepoint:syscalls:sys_enter_openat {@[comm] = count();}'
Attaching 1 probe...
^C

@[chrome]: 30
@[gnome-terminal-]: 34
@[MemoryInfra]: 60
@[Chrome_IOThread]: 90
@[copyq]: 93
@[systemd-journal]: 103
@[systemd-oomd]: 144
@[docker]: 256
@[ToDesk_Service]: 305
@[code]: 310

知识点:

  • @表示map类型,后面可以接变量名字,也可以不接,comm表示map的key。
  • count()是maps函数,每次跟踪点触发计数一次,计数结果保存在map中。
  • maps会在bpftrace结束后自动打印。

统计read返回值分布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# bpftrace -e 'tracepoint:syscalls:sys_exit_read /pid == 1963/ {@bytes = hist(args.ret)}'
Attaching 1 probe...
^C

@bytes:
[0] 20 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[1] 0 | |
[2, 4) 0 | |
[4, 8) 0 | |
[8, 16) 0 | |
[16, 32) 0 | |
[32, 64) 0 | |
[64, 128) 0 | |
[128, 256) 10 |@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[256, 512) 10 |@@@@@@@@@@@@@@@@@@@@@@@@@@ |

知识点:

  • /pid==1963/是过滤器
  • @bytes也是map,只是没有key
  • sys_exit_read表示系统调用read退出是的跟踪点,args.ret表示返回值
  • hist是maps函数,统计直方图,它是以2次方的间隔开始统计。还有类似的函数count(), sum(),avg(),min()和max()。

sum,max,min,avg使用例子:

1
2
3
4
5
6
7
8
9
10
# bpftrace -e 'tracepoint:syscalls:sys_exit_read /pid == 1963/ {@min = min(args.ret); @max=max(args.ret); @sum=sum(args.ret); @avg=avg(args.ret);}'
Attaching 1 probe...
^C

@avg: 184

@max: 3205
@min: 0
@sum: 13126

系统调用read的耗时统计

同时跟踪read进入和退出,统计时间消耗

1
2
3
4
5
6
7
8
9
# bpftrace -e 'tracepoint:syscalls:sys_enter_read /pid==1963/{@start=nsecs} tracepoint:syscalls:sys_exit_read /pid==1963/ {@ns=hist(nsecs-@start)}'
Attaching 2 probes...
^C

@ns:
[512, 1K) 13 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[1K, 2K) 6 |@@@@@@@@@@@@@@@@@@@@@@@@ |
[2K, 4K) 6 |@@@@@@@@@@@@@@@@@@@@@@@@ |
[4K, 8K) 3 |@@@@@@@@@@@@ |

知识点

  • nsecs:系统启动到现在的纳秒数

定时退出bpftrace

1
2
3
4
5
6
7
8
9
# bpftrace -e 'interval:ms:500 {@t++;printf("hello, %dms\n", @t*500)} interval:s:2 {exit()}'
Attaching 2 probes...
hello, 500ms
hello, 1000ms
hello, 1500ms
hello, 2000ms


@t: 4

知识点

  • interval是一个周期探针,周期性触发一次,执行后面的动作。interval:ms:500表示周期为500ms的探针,interval:s:2表示周期为2s的探针。
  • exit() 退出bpftrace程序

采样查看内核栈

以99hz的频率,打断所有cpu,记录调用栈,统计次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# bpftrace -e 'profile:hz:99 {@[kstack] = count()}'
Attaching 1 probe...
^C

@[
sockfd_lookup_light+98
__sys_recvmsg+103
__x64_sys_recvmsg+29
x64_sys_call+8847
do_syscall_64+127
entry_SYSCALL_64_after_hwframe+120
]: 1
@[
_raw_spin_unlock_irqrestore+33
logi_dj_recv_forward_input_report+323
logi_dj_raw_event+307
hid_input_report+246
hid_irq_in+552
__usb_hcd_giveback_urb+172
usb_giveback_urb_bh+168
tasklet_action_common.isra.0+218
tasklet_hi_action+31
handle_softirqs+219
__irq_exit_rcu+217
irq_exit_rcu+14
common_interrupt+164
asm_common_interrupt+39
mwait_idle+75
arch_cpu_idle+9
default_idle_call+44
cpuidle_idle_call+339
do_idle+135
cpu_startup_entry+42
start_secondary+297
secondary_startup_64_no_verify+388
]: 1

知识点

  • profile:hz:99 所有cpu都以99hz的频率打断触发采样
  • kstack 当前的调用栈

查看某个内核函数入参和返回值

打印ip_rcv函数的入参和返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# bpftrace -e 'kfunc:ip_rcv {printf("%s len=%d\n", args->dev->name, args->skb->len)}
kretfunc:ip_rcv {printf("ret=%d\n", retval)}'
Attaching 2 probes...
ret=0
ret=0
lo len=77
ret=0
lo len=81
ret=0
lo len=52
ret=0
enp6s0 len=370
ret=0
lo len=52
ret=0
lo len=52
ret=0

知识点
kretfunc:函数名 - 表示在函数出口添加探测点
retval - 是函数返回值,不一定是int,也可能是其他类型,也能打印。有的函数没有返回值,就没有这个变量。

语法提要

根据上面的一行式,总结一些用到的语法到下面

内置变量和函数

  • comm - 事件点所处的进程名
  • pid - 进程id
  • tid - 线程id
  • args - 包含跟踪点所有参数的结构体,有哪些成员可以通过-lv查看
  • str() - 将指针指向的内容转为字符串,如果类型本身就是数组,那么不用str()函数。
  • probe - 当前触发的probe完整名字
  • count() - 是maps函数,每次跟踪点触发计数一次
  • hist() - 是maps函数,统计直方图,它是以2次方的间隔开始统计。还有类似的函数count(), sum(),avg(),min()和max()。
  • nsecs - 系统启动到现在的纳秒数
  • interval:ms:500 - 一个周期间隔探针,用来创建脚本级别的间隔或超时时间
  • exit() - 退出bpftrace程序
  • profile:hz:99 所有cpu都以99hz的频率打断触发采样
  • kstack - 当前的调用栈

变量

@表示map类型,后面可以接变量名字,也可以不接,map可以有key,也可以没有key. maps会在bpftrace结束后自动打印出结果。map的作用域是全局的,生命周期和bpftrace一样长。
$表示临时变量,只在每次probe触发时那段代码有效。用于辅助计算存储。

bpftrace的原理

本质的过程是利用内核的ebpf机制,动态插入代码运行。过程简化如下:
bpftrace脚本–>编译为bpf字节码–>内核verifier验证–>JIT编译为机器码–>挂载到probe点执行

几大probe的底层原理

kprobe:在目标函数入口把第一条指令替换成 int3(x86 软件断点),触发时保存寄存器上下文,执行 BPF 程序,再恢复执行。kretprobe 则是在函数入口替换返回地址,函数 ret 时跳到 BPF 程序。
kfunc(fentry/fexit):内核编译时在每个函数头部预留了 nop 指令槽(__fentry__),attach 时把 nop 替换成跳转指令,比 kprobe 少一次中断开销。
tracepoint:内核源码里用 TRACE_EVENT 宏静态定义,编译成 __tracepoint_xxx 结构体,触发时遍历回调链表执行 BPF 程序。
uprobe:和 kprobe 类似,在用户态目标函数地址处替换成 int3,进程执行到该地址时陷入内核,执行 BPF 程序后返回用户态继续执行。

tracepoint、kprobe、kfunc区别

总结就是:有tracepoint用tracepint,能用kfunc用kfunc,否则用kprobe

tracepoint

是代码中预埋的钩子,名称稳定,有参数类型信息,跨内核版本兼容性好。是静态的,通过写代码埋进去的。
所有的系统调用都有tracepoint,跟踪系统调用肯定用tracepoint
通过-v可以直接查看参数

1
2
3
4
5
6
7
8
9
10
11
12
13
# bpftrace -lv "tracepoint:*:sys_enter_openat*"
tracepoint:syscalls:sys_enter_openat
int __syscall_nr
int dfd
const char * filename
int flags
umode_t mode
tracepoint:syscalls:sys_enter_openat2
int __syscall_nr
int dfd
const char * filename
struct open_how * how
size_t usize

使用参数也很简单

1
2
3
4
5
# bpftrace -e 'tracepoint:syscalls:sys_enter_openat {printf("%s open %s\n", comm, str(args->filename));}'
Attaching 1 probe...
code open /proc/117533/cmdline
code open /proc/119202/cmdline
ToDesk_Service open /run/systemd/seats/

kprobe/kretprobe

是动态的,动态在任意内核函数入口/出口插桩。访问参数需要看内核代码参数顺序,通过arg0,arg1来访问参数,而且它们都是uint64的,需要自己转换类型。
举例如下:

1
2
3
4
5
6
7
8
# bpftrace -e 'kprobe:ip6_route_input {
> $skb = (struct sk_buff *)arg0;
> printf("len=%d\n", $skb->len);
}'
Attaching 1 probe...
len=372
len=391

arg0需要自己转为sk_buff结构。

kfunc/kretfunc

kfunc是目前最新的技术,是kprobe的现代替代。它比kprobe性能更好,而且参数可见,访问更方便

1
2
3
# bpftrace -lv 'kfunc:ip6_route_input'
kfunc:vmlinux:ip6_route_input
struct sk_buff * skb

依然进行上面的跟踪:

1
2
3
4
5
# bpftrace -e 'kfunc:ip6_route_input {printf("len=%d\n",args->skb->len);}'
Attaching 1 probe...
len=104
len=104

更方便,不需要去看内核源代码确认参数位置,也不需要结构体转换,直接可以打印参数

疑问

uprobe打印字符串为空

c代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int test(char *name,int n)
{
int sum = 0;
for(int i = 0; i < n; i++){
sum += i;
}
return sum;
}

int main(int argc, char *argv[])
{
printf("test: %d\n", test("test-100000", 100000));
return 0;
}

bpftrace脚本如下:

1
2
bpftrace -e 'uprobe:./uprobe_test:test {@start=nsecs;@name=str(arg0);print(str(arg0))} 
uretprobe:./uprobe_test:test {$use=nsecs-@start;printf("%s: %dus\n", @name, $use/1000)}'

结果输出如下:

1
2
3
4
Attaching 2 probes...

: 112us
^C

uprobe点和uretprobe点都没有输出arg0参数。
这可能是字符串的坑点,暂不清楚原因,arg1 int参数可以正常输出。

参考

tutorial_one_liners
language

关联

[[bpftrace]]


人生苦短,远离bug Leon, 2026-04-02

bpftrace学习
https://leon0625.github.io/2026/04/02/5cb628ed86bb/
作者
leon.liu
发布于
2026年4月2日
许可协议