使⽤BPF跟踪Linux内核
1. 前⾔
我们可以使⽤BPF对Linux内核进⾏跟踪,收集我们想要的内核数据,从⽽对Linux中的程序进⾏分析和调试。与其它的跟踪技术相⽐,使⽤BPF的主要优点是⼏乎可以访问Linux内核和应⽤程序的任何信息,同时,BPF对系统性能影响很⼩,执⾏效率很⾼,⽽且开发⼈员不需要因为收集数据⽽修改程序。
本⽂将介绍保证BPF程序安全的BPF验证器,然后以BPF程序的⼯具集BCC为例,介绍常见BPF程序,具体为kprobes和tracepoints类型的BPF程序的使⽤及程序编写⽰例。
2. BPF验证器
BPF借助跟踪探针收集信息并进⾏调试和分析,与其它依赖于重新编译内核的⼯具相⽐,BPF程序的安全性更⾼。重新编译内核引⼊外部模块的⽅式,可能会因为程序的错误⽽产⽣系统奔溃。BPF程序的验证器会在BPF程序加载到内核之前分析程序,消除这种风险。
BPF验证器执⾏的第⼀项检查是对BPF虚拟机加载的代码进⾏静态分析,⽬的是确保程序能够按照预期结束。验证器在进⾏第⼀项检查时所做⼯作为:
程序不包含控制循环;
程序不会执⾏超过内核允许的最⼤指令数;
程序不包含任何⽆法到达的指令;
程序不会超出程序界限。
BPF验证器执⾏的第⼆项检查是对BPF程序进⾏预运⾏,所做⼯作为:
分析BPF程序执⾏的每条指令,确保不会执⾏⽆效指令;
检查所有内存指针是否可以正确访问和引⽤;
预运⾏将程序控制流的执⾏结果通知验证器,确保BPF程序最终都会执⾏BPF_EXIT指令。
3. 内核探针 kprobes
内核探针可以跟踪⼤多数内核函数,并且系统损耗最⼩。当跟踪的内核函数被调⽤时,附加到探针的BPF代码将被执⾏,之后内核将恢复正常模式。
3.1 kprobes类BPF程序的优缺点
优点
动态跟踪内核,可跟踪的内核函数众多,能够提取内核绝⼤部分信息。
缺点
没有稳定的应⽤程序⼆进制接⼝,可能随着内核版本的演进⽽更改。
3.2 kprobes
kprobe程序允许在执⾏内核函数之前插⼊BPF程序。当内核执⾏到kprobe挂载的内核函数时,先运⾏BPF程序,BPF程序运⾏结束后,返回继续开始执⾏内核函数。下⾯是⼀个使⽤kprobe的bcc程序⽰例,功能是监控内核函数kfree_skb函数,当此函数触发时,记录触发它的进程pid,进程名字和触发次数,并打印出触发此函数的进程pid,进程名字和触发次数:
#!/usr/bin/python3
# coding=utf-8
from __future__ import print_function
from bcc import BPF
from time import sleep
# define BPF program
bpf_program ="""
#include <uapi/linux/ptrace.h>
struct key_t{
u64 pid;
};
BPF_HASH(counts, struct key_t);
int trace_kfree_skb(struct pt_regs *ctx) {
u64 zero = 0, *val, pid;
pid = bpf_get_current_pid_tgid() >> 32;
struct key_t key = {};
key.pid = pid;
val = counts.lookup_or_try_init(&key, &zero);
if (val) {
(*val)++;
}
return 0;
}
"""
def pid_to_comm(pid):
try:
comm =open("/proc/%s/comm"% pid,"r").read().rstrip()
return comm
except IOError:
return str(pid)
# load BPF
b = BPF(text=bpf_program)
b.attach_kprobe(event="kfree_skb", fn_name="trace_kfree_skb")
# header
print("Tracing Ctrl-C to end.")
print("%-10s %-12s %-10s"%("PID","COMM","DROP_COUNTS"))
while1:
sleep(1)
for k, v in sorted(b["counts"].items(),key =lambda counts: counts[1].value):
print("%-10d %-12s %-10d"%(k.pid, pid_to_comm(k.pid), v.value))
该bcc程序主要包括两个部分,⼀部分是python语⾔,⼀部分是c语⾔。python部分主要做的⼯作是BPF程序的加载和操作BPF程序的map,并进⾏数据处理。c部分会被llvm编译器编译为BPF字节码,经过BPF验证器验证安全后,加载到内核中执⾏。python和c中出现的陌⽣函数可以查下⾯这两个⼿册,在此不再赘述:
python部分遇到的陌⽣函数可以查这个⼿册:
c部分中遇到的陌⽣函数可以查这个⼿册:
需要说明的是,该BPF程序类型是kprobe,它是在这⾥进⾏程序类型定义的:
b.attach_kprobe(event="kfree_skb", fn_name="trace_kfree_skb")
b.attach_kprobe()指定了该BPF程序类型为kprobe;
event="kfree_skb"指定了kprobe挂载的内核函数为kfree_skb;linux下的sleep函数
fn_name="trace_kfree_skb"指定了当检测到内核函数kfree_skb时,执⾏程序中的trace_kfree_skb函数;
BPF程序的第⼀个参数总为ctx,该参数称为上下⽂,提供了访问内核正在处理的信息,依赖于正在运⾏的BPF程序的类型。CPU将内核正在执⾏任务的不同信息保存在寄存器中,借助内核提供的宏可以访问这些寄存器,如PT_REGS_RC。
程序运⾏结果如下:
3.3 kretprobes
相⽐于内核探针kprobe程序,kretprobe程序是在内核函数有返回值时插⼊BPF程序。当内核执⾏到kretprobe挂载的内核函数时,先执⾏内核函数,当内核函数返回时执⾏BPF程序,运⾏结束后返回。
以上⾯的BPF程序为例,若要使⽤kretprobe,可以这样修改:
b.attach_kretprobe(event="kfree_skb", fn_name="trace_kfree_skb")
b.attach_kretprobe()指定了该BPF程序类型为kretprobe,kretprobe类型的BPF程序将在跟踪的内核函数有返回值时执⾏BPF程序;
event="kfree_skb"指定了kretprobe挂载的内核函数为kfree_skb;
fn_name="trace_kfree_skb"指定了当内核函数kfree_skb有返回值时,执⾏程序中的trace_kfree_skb函数;
4. 内核静态跟踪点 tracepoint
tracepoint是内核静态跟踪点,它与kprobe类程序的主要区别在于tracepoint由内核开发⼈员在内核中编写和修改。
3.1 tracepoint 程序的优缺点
优点
跟踪点是静态的,ABI更稳定,不随内核版本的变化⽽致不可⽤。
缺点
跟踪点是内核⼈员添加的,不会全⾯涵盖内核的所有⼦系统。
3.2 tracepoint 可⽤跟踪点
系统中所有的跟踪点都定义在/sys/kernel/debug/traceing/events⽬录中:
使⽤命令perf list 也可以列出可使⽤的tracepoint点:
对于bcc程序来说,以监控kfree_skb为例,tracepoint程序可以这样写:
b.attach_tracepoint(tp="skb:kfree_skb", fn_name="trace_kfree_skb")
bcc遵循tracepoint命名约定,⾸先是指定要跟踪的⼦系统,这⾥是“skb:”,然后是⼦系统中的跟踪点“kfree_skb”:
5. 总结
本⽂主要介绍了保证BPF程序安全的BPF验证器,然后以BPF程序的⼯具集BCC为例,分享了kprobes和tracepoints类型的BPF程序的使⽤及程序编写⽰例。本⽂分享的是内核跟踪,那么⽤户空间程序该如何跟踪呢,这将在后⾯的⽂章中逐步分享,感谢阅读。
参考资料:
若未安cc,请参考进⾏安装;
bcc程序编写
参考书《Linux内核观测技术 BPF》
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论