This page looks best with JavaScript enabled

BCC、bpftrace && BPF Performance Tools

 ·  ☕ 8 min read · 👀... views

简单学习了一下BCC和bpftrace 增加了bypass go tls的bpftrace写法 见eBPF Uprobe bypass Go tls——抓取https明文流量

BCC & bpftrace内部实现

BCC由一个C++前端API用于内核态的BPF程序的编制、一个C++后端驱动使用Clang/LLVM编译BPF程序并将其装载到内核上挂载到时间上并对BPF映射表进行读写、用于编写BCC工具的语言前端:Python、C++ 组成。

bpftrace前端使用lex和yacc对bpftrace编程语言进行此法和语法分析,使用clang来解析结构体。后端则将bpftrace程序编译成LLVM IR,然后再通过LLVM库将其编译成BPF代码。

事件源及其实现

kprobe(int3中断) 具体使用依赖于前端跟踪器:包括perf、systemtap以及BPF追踪器如BCC和bpftrace
kretprobe(返回地址trampoline)

tracepoint (nop 启用时改jump trampoline) 性能损耗更低 比kprobe稳定
许多应用默认不开启USDT 可以使用Folly C++库添加或者使用systemtap-sdt-dev包提供的头文件和工具(nop 启用改int3)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# BCC
attach_kprobe()
attach_kretprobe()
attach_uprobe()
attach_uretprobe()
TRACEPOINT_PROBE()
USDT().enable_probe()
# bpftrace
'kprobe:subsystem:eventname { to do }'
'uprobe:/path/to/file:method { to do }'
'tracepoint:sched:sched_process_exec { to do }'
'usdt:/path/to/file:method { to do }'
'software:event:count'
'hardware:event:count'

动态USDT

  • 预编译共享库 函数预先插入USDT
  • 需要时dlopen()加载动态库
  • 调用函数

PMC时CPU上硬件可编程的计数器 广泛用于性能分析如缓存命中率 指令执行效率 阻塞指令周期等
两种工作模式:计数和溢出采样(超过一定值发送信号)

PMC由于存在中断延迟或者乱序执行 Intel开发一种解决方案叫PEBS精确计数

BCC和bpftrace使用perf_events作为他们的环形缓冲区 然后又增加了PMC的支持 现在又通过perf_event_open() 来对所有事件进行观测

perf也开发了一个使用BPF的接口 使其成为BPF追踪器 是唯一一个内置在Linux中的BPF前端

bpftrace还包括BEGIN和END即bpftrace启动和退出的事件

Software

  • cpu-clock or cpu
  • task-clock
  • page-faults or faults
  • context-switches or cs
  • cpu-migrations
  • minor-faults
  • major-faults
  • alignment-faults
  • emulation-faults
  • dummy
  • bpf-output

Hardware

  • cpu-cycles or cycles
  • instructions
  • cache-references
  • cache-misses
  • branch-instructions or branches
  • branch-misses
  • bus-cycles
  • frontend-stalls
  • backend-stalls
  • ref-cycles

bpftool

kernel代码tools/bpf中包含bpftool 其有很多功能模块

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# perf 子命令显示哪些BPF程序正在通过pref_event_open()
bpftool perf
# prog show 子命令会列出全部的程序(不止基于pref_event_open()的)
bpftool prog show
# xlated 可以 dump 进程对应的BPF汇编指令
bpftool prog dump xlated id 234
# 如果包含BTF信息 显示源码
bpftool prog dump xlated id 263
# 如果包含BTF信息 显示行号
bpftool prog dump xlated id 263 linum
# opcodes 显示BPF指令opcode
bpftool prog dump xlated id 263 opcodes
# visual dot格式输出控制流信息 可以用GraphViz绘图
bpftool prog dump xlated id 263 visual
# jited 显示x86_64汇编
bpftool prog dump jited id 263

bpftrace使用

基础

bpftrace有很多内置变量和函数,具体见https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md

基础变量(global、$local和per[tid])

内置变量(username、pid、tid、comm(进程名)、nsecs(纳秒为单位的时间戳)..)

关联数组(key[value])

频率计数(count() 或者 ++)

临时变量 $

BPF映射表变量 @(可在不同动作之间传递)

调用栈信息(kstack、ustack)

返回值(retval) 追踪函数名(func) 探针名(probe) CgroupId(cgroup)

参数(args、arg0.arg1 argN)

参数(sarg0.sarg1 sargN)那些用栈传值的程序(1.17以前的Go)

bpftrace选项

bpftrace -e 'probe /filter/ { action };

bpftrace -l 可以使用通配符列出所有动态插桩点和静态插桩点

tracepoint的命名方式是subsystem:eventname 如 kmem:kmalloc

1
2
3
4
sudo bpftrace -l 'kprobe:*'
sudo bpftrace -l 'kretprobe:*'
sudo bpftrace -l 'tracepoint:syscalls:*'
sudo bpftrace -l "usdt:/lib/x86_64-linux-gnu/libc.so.6:*"

动态插桩分成kprobe kretprobe uprobe uretprobe
tracepoint通配可以看到系统调用都被分为sys_open_*sys_close_*

bpftrace -e 可以执行简短bpftrace脚本

1
2
3
4
5
➜  tools git:(master) sudo bpftrace -e 'tracepoint:syscalls:sys_enter_open* { @[probe] = count(); }'
Attaching 5 probes...
^C

@[tracepoint:syscalls:sys_enter_openat]: 33

追踪参数

tracepoint通常带有参数 bpftrace可以通过args访问这些参数的信息,如

1
'tracepoint:net:netif_rx' args->len

这些参数可以通过-v查看 -l列出 -v详细模式

1
2
sudo bpftrace -lv tracepoint:syscalls:sys_enter_read
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_clone { printf("->clone() by %s PID %d\n", comm, pid); } tracepoint:syscalls:sys_exit_close { printf("<-clone() return %d, %s PID %d\n", args->ret, comm, pid); }' 

kprobe的参数"arg0,arg1…argN"是进入函数的参数 类型均为uint64 如果他们指向C结构体的指针 可以强制类型转化为对应的结构体。

也就是说静态插桩如tracepoint用的是args,动态插桩如kprobe用的是arg0…

BPF并发控制、栈回溯

并行多线程可能同时更新映射表数据从而导致数据丢失,BCC和bpftrace前端使用了per CPU的独立数组映射 避免并行线程对共享数据的更新

1
2
3
4
5
6
7
8
➜  linux sudo strace -febpf bpftrace -e 'k:vfs_read { @ = count(); }'
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_PERCPU_ARRAY,
...
➜  linux sudo strace -febpf bpftrace -e 'k:vfs_read { @++; }'
...
bpf(BPF_MAP_CREATE, {map_type=BPF_MAP_TYPE_HASH,
...

由于验证器的存在BPF是受限的图灵完备 跟ethereum的gas类似有限制无限循环的功能

栈大小限制在512字节也就是0x200

同xv6实验 BPF也可以用栈回溯获取调用信息(RBP gcc默认关闭功能 但是x64性能提升不明显)
栈回溯还可以使用debuginfo、LBR、ORC等方法
debuginfo DWARF格式 浪费计算资源 LBR intel基于硬件的分支回溯 ORC新的调试信息格式

BCC和bpftrace调试

BCC

1
b = BPF(text=prog,debug=flags)

DEBUG_LLVM_IR = 0x1 compiled LLVM IR
DEBUG_BPF = 0x2 loaded BPF bytecode and register state on branches
DEBUG_PREPROCESSOR = 0x4 pre-processor result
DEBUG_SOURCE = 0x8 ASM instructions embedded with source
DEBUG_BPF_REGISTER_STATE = 0x10 register state on all instructions in addition to DEBUG_BPF
DEBUG_BTF = 0x20 print the messages from the libbpf library.

BCC的bpflist工具 内核的bpftool工具

bpftrace

-d输出AST和LLVM IR

-dd详情模式

 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
➜  bpftool sudo bpftrace -d -e 'k:vfs_read { @[pid] = count(); }'

AST after: parser
-------------------
Program
 kprobe:vfs_read
  =
   map: @ :: type[none, ctx: 0]
    builtin: pid :: type[none, ctx: 0]
   call: count :: type[none, ctx: 0]


AST after: Semantic
-------------------
Program
 kprobe:vfs_read
  =
   map: @ :: type[count, ctx: 0]
    builtin: pid :: type[unsigned int64, ctx: 0]
   call: count :: type[count, ctx: 0]


AST after: NodeCounter
-------------------
Program
 kprobe:vfs_read
  =
   map: @ :: type[count, ctx: 0]
    builtin: pid :: type[unsigned int64, ctx: 0]
   call: count :: type[count, ctx: 0]


AST after: ResourceAnalyser
-------------------
Program
 kprobe:vfs_read
  =
   map: @ :: type[count, ctx: 0]
    builtin: pid :: type[unsigned int64, ctx: 0]
   call: count :: type[count, ctx: 0]

; ModuleID = 'bpftrace'
source_filename = "bpftrace"
target datalayout = "e-m:e-p:64:64-i64:64-i128:128-n32:64-S128"
target triple = "bpf-pc-linux"

; Function Attrs: nounwind
declare i64 @llvm.bpf.pseudo(i64 %0, i64 %1) #0

define i64 @"kprobe:vfs_read"(i8* nocapture readnone %0) local_unnamed_addr section "s_kprobe:vfs_read_1" {
entry:
  %"@_val" = alloca i64, align 8
  %"@_key" = alloca i64, align 8
  %get_pid_tgid = tail call i64 inttoptr (i64 14 to i64 ()*)()
  %1 = lshr i64 %get_pid_tgid, 32
  %2 = bitcast i64* %"@_key" to i8*
  call void @llvm.lifetime.start.p0i8(i64 -1, i8* nonnull %2)
  store i64 %1, i64* %"@_key", align 8
  %pseudo = tail call i64 @llvm.bpf.pseudo(i64 1, i64 0)
  %lookup_elem = call i8* inttoptr (i64 1 to i8* (i64, i64*)*)(i64 %pseudo, i64* nonnull %"@_key")
  %map_lookup_cond.not = icmp eq i8* %lookup_elem, null
  br i1 %map_lookup_cond.not, label %lookup_merge, label %lookup_success
  ...
➜  bpftool sudo bpftrace -d -e 'k:vfs_read { @[pid] = count(); }' | wc -l 
88
➜  bpftool sudo bpftrace -dd -e 'k:vfs_read { @[pid] = count(); }' | wc -l
163

-v开启详情模式 书上说会输出programe id和字节码 可以配合bpftool prog使用

实际上只输出了programe id

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
➜  bpftool sudo bpftrace -v -e 'k:vfs_read { @[pid] = count(); }'        
INFO: node count: 7
Attaching 1 probe...

Program ID: 64

The verifier log: 
processed 30 insns (limit 1000000) max_states_per_insn 0 total_states 2 peak_states 2 mark_read 1

Attaching kprobe:vfs_read
^C

@[919]: 1
@[1300]: 1
...

安全

https://github.com/brendangregg/bpf-perf-tools-book/tree/master/originals/Ch11_Security

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
bashreadline.bt
capable.bt
elfsnoop.bt
eperm.bt
modsnoop.bt
setuids.bt
shellsnoop.bt
shellsnoop.py
tcpreset.bt
ttysnoop.bt

# 监控bash输入
sudo bpftrace -e 'uretprobe:/bin/bash:readline { printf("%-6d %s\n",pid,str(retval)) }'

容器

命名空间 cgroup进行资源控制 所有容器共享同一个内核

内核中有针对cgroup事件的跟踪点 包括 cgroup:cgroup_setup_root、cgroup:cgroup_attach_task

也可以使用BPF_PROG_TYPE_CGROUP_SKB程序类型和 附加到cgroup入口点和出口点上处理网络数据包

BPF跟踪需要root特权 这对于大部分容器环境来说 意味着BPF跟踪工具只能在宿主机上执行不能在容器内执行

容器使用一些命名空间的组合 详细信息可以通过内核的nsproxy.h结构体读取 linux/nsproxy.h

传统工具:systemd-cgtop、kubectl top、 docker stats、/sys/fs/cgroups、pref

BPF工具:runqlat、pidnss、blkthrot、overlayfs

 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
#include <linux/sched.h>
#include <linux/nsproxy.h>
#include <linux/utsname.h>
#include <linux/pid_namespace.h>

BEGIN
{
	printf("Tracing PID namespace switches. Ctrl-C to end\n");
}

kprobe:finish_task_switch
{
	$prev = (struct task_struct *)arg0;
	$curr = (struct task_struct *)curtask;
	$prev_pidns = $prev->nsproxy->pid_ns_for_children->ns.inum;
	$curr_pidns = $curr->nsproxy->pid_ns_for_children->ns.inum;
	if ($prev_pidns != $curr_pidns) {
		@[$prev_pidns, $prev->nsproxy->uts_ns->name.nodename] = count();
	}
}

END
{
	printf("\nVictim PID namespace switch counts [PIDNS, nodename]:\n");
}

cgroup blk 控制器基于硬限制来限制I/O的时间 统计被限制次数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <linux/cgroup-defs.h>
#include <linux/blk-cgroup.h>

BEGIN
{
	printf("Tracing block I/O throttles by cgroup. Ctrl-C to end\n");
}

kprobe:blk_throtl_bio
{
	@blkg[tid] = arg1;
}

kretprobe:blk_throtl_bio
/@blkg[tid]/
{
	$blkg = (struct blkcg_gq *)@blkg[tid];
	if (retval) {
		@throttled[$blkg->blkcg->css.id] = count();
	} else {
		@notthrottled[$blkg->blkcg->css.id] = count();
	}
	delete(@blkg[tid]);
}

跟踪overlay文件系统的读写延迟

 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
37
38
39
40
41
42
43
44
45
#include <linux/nsproxy.h>
#include <linux/pid_namespace.h>

kprobe:ovl_read_iter
/((struct task_struct *)curtask)->nsproxy->pid_ns_for_children->ns.inum == $1/
{
	@read_start[tid] = nsecs;
}

kretprobe:ovl_read_iter
/((struct task_struct *)curtask)->nsproxy->pid_ns_for_children->ns.inum == $1/
{
	$duration_us = (nsecs - @read_start[tid]) / 1000;
	@read_latency_us = hist($duration_us);
	delete(@read_start[tid]);
}

kprobe:ovl_write_iter
/((struct task_struct *)curtask)->nsproxy->pid_ns_for_children->ns.inum == $1/
{
	@write_start[tid] = nsecs;
}

kretprobe:ovl_write_iter
/((struct task_struct *)curtask)->nsproxy->pid_ns_for_children->ns.inum == $1/
{
	$duration_us = (nsecs - @write_start[tid]) / 1000;
	@write_latency_us = hist($duration_us);
	delete(@write_start[tid]);
}

interval:ms:1000
{
	time("\n%H:%M:%S --------------------\n");
	print(@write_latency_us);
	print(@read_latency_us);
	clear(@write_latency_us);
	clear(@read_latency_us);
}

END
{
	clear(@write_start);
	clear(@read_start);
}

其他

1
2
man getaddrinfo
man setitimer
Share on

ruokeqx
WRITTEN BY
ruokeqx