BCC程序架构
上面架构图展示了BCC
的执行流程:
-
BPF
通过python
接口.attach*
来调用底下libbpf
库的bpf_attach_*
接口来让内核挂钩事件,这些接口可以见https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md
和https://github.com/iovisor/bcc/blob/master/src/cc/libbpf.h
-
在 python
脚本的嵌入C
代码通过python
接口BPF(text=...)
到Rewriter
调用Clang/LLVM
产生BPF
字节码,再通过bpf_prog_load
接口把字节码加入到内核里,在内核里,BPF
虚拟机会调用Verifier
对代码进行校验,再由BPF
虚拟机执行。在x86
体系里,由于CPU
幽灵漏洞原因,大多会调用JIT
把它编译成机器码再执行。 -
python
脚本会使用.print_log2_hist
之类的接口调用libbpf
库的bpf_create_map/bpf_*_elem
接口来和内核的BPF
代码进行交互,进行数据传输。见https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md
的Map APIs
章节和https://man7.org/linux/man-pages/man2/bpf.2.html
-
python
脚本通过.perf_buffer_poll
来调用libbpf
库的perf_reader_poll
来读取BPF
在内核里的性能缓存 -
USDT
事件可以通过调用libbpf
的bcc_usdt_enable_probe
来打开跟踪
开发提示
以下是在编写自己的自定义 BCC
程序时应该注意的关于 BCC
工具开发的六个重要方面:
-
BPF C
(嵌入式C
)是受限的:没有循环或内核函数调用。您只能使用bpf_*
内核辅助函数和一些编译器内置函数。
但是,如果循环有确定的次数,则可以展开循环。例如,strcmp
将不起作用,但如果知道要与其他字符串进行比较的字符串的长度,则可以解决该问题。以下是执行字符串比较的展开循环解决方法的示例:
#define MY_STR_LEN 10
static inline bool equal_to_mystr(char *str) {
char comparand[MY_STR_LEN];
bpf_probe_read(&comparand, sizeof(comparand), str);
char mystr[] = "my string!";
for (int i = 0; i < MY_STR_LEN; ++i)
if (comparand[i] != mystr[i])
return false;
return true;
}
同样,不能使用 memcpy
从内存区域写入和写入内存区域。相反,必须使用 BCC
的内置函数 __builtin_memcpy(&dest, str, sizeof(dest))
。
-
所有内存都必须通过 bpf_probe_read()
读取,它会进行必要的安全检查。如果想取消引用a->b->c->d
,可以尝试这样做,因为BCC
有一个重写器可以将它翻译成必要的bpf_probe_read()
。然而,显式调用bpf_probe_read()
始终是一个安全的选择并被推荐。内存只能读到BPF
映射的BPF
堆栈。堆栈的大小有限,因此使用BPF
映射来存储大型对象和/或保存大量事件的数据。 -
从内核到用户空间(在 BPF C 程序中)获取数据主要有三种方式。 BPF_PERF_OUTPUT(...)
和output_name.perf_submit(...)
: -
通过自定义数据结构将每个事件的详细信息发送到用户空间 BPF_HISTOGRAM(...)
或其它BPF
映射: -
映射是一个键值散列,可以从中构建更高级的数据结构 -
通常用于汇总统计(例如直方图) -
定期从用户空间读取时很高效(而不是使用 BPF_PERF_OUTPUT
) bpf_trace_printk(...): -
仅用于调试,因为所有 bpf_trace_printk()
调用和一些跟踪器(例如ftrace
)写入同一个公共trace_pipe
-
尽可能使用静态跟踪(内核跟踪点、 USDT
跟踪点)而不是动态跟踪(kprobes
、uprobes
)。回想一下,动态跟踪有一个不稳定的API
(因为我们挂钩到函数的名称,它可以随软件版本而改变),所以如果它正在跟踪的代码发生变化,你的工具就会失效。 -
在 BPF C
程序中而不是在用户空间中做尽可能多的工作。与处理用户空间中的大部分工作相比,在内核中为每个事件完成工作要快得多。
事件频率和开销
关键要记住,获得这种可观察性并不是没有代价。每个启用的探测/跟踪点在每次被命中时都会产生一些需要完成的工作,无论是在内核空间还是用户空间,都会产生 CPU
开销。
确定跟踪程序的CPU
开销的三个主要因素是:
-
目标事件的频率 -
每个事件的处理工作量 -
系统上的 CPU 数量
一个程序在每个CPU
的开销公式如下:
overhead = (frequency * workload) / numCPUs
换句话说,在单个 CPU 上每秒跟踪 100 万个事件可能会使系统龟速,而 128 个 CPU 的系统可能几乎不受影响。
暗号:cec28
原文始发于微信公众号(奶牛安全):使用ebpf对Linux内核和程序进行跟踪3:BCC工具开发提示
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论