使用ebpf对Linux内核和程序进行跟踪3:BCC工具开发提示
2022-12-17 19:35:50 Author: 奶牛安全(查看原文) 阅读量:8 收藏


BCC程序架构

framework

上面架构图展示了BCC的执行流程:

  1. BPF通过python接口.attach*来调用底下libbpf库的bpf_attach_*接口来让内核挂钩事件,这些接口可以见https://github.com/iovisor/bcc/blob/master/docs/reference_guide.mdhttps://github.com/iovisor/bcc/blob/master/src/cc/libbpf.h
  2. python脚本的嵌入C代码通过python接口BPF(text=...)Rewriter调用Clang/LLVM产生BPF字节码,再通过bpf_prog_load接口把字节码加入到内核里,在内核里,BPF虚拟机会调用Verifier对代码进行校验,再由BPF虚拟机执行。在x86体系里,由于CPU幽灵漏洞原因,大多会调用JIT把它编译成机器码再执行。
  3. python脚本会使用.print_log2_hist之类的接口调用libbpf库的bpf_create_map/bpf_*_elem接口来和内核的BPF代码进行交互,进行数据传输。见https://github.com/iovisor/bcc/blob/master/docs/reference_guide.mdMap APIs章节和https://man7.org/linux/man-pages/man2/bpf.2.html
  4. python脚本通过.perf_buffer_poll来调用libbpf库的perf_reader_poll来读取BPF在内核里的性能缓存
  5. USDT事件可以通过调用libbpfbcc_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 跟踪点)而不是动态跟踪(kprobesuprobes)。回想一下,动态跟踪有一个不稳定的 API(因为我们挂钩到函数的名称,它可以随软件版本而改变),所以如果它正在跟踪的代码发生变化,你的工具就会失效。
  • BPF C 程序中而不是在用户空间中做尽可能多的工作。与处理用户空间中的大部分工作相比,在内核中为每个事件完成工作要快得多。

事件频率和开销

关键要记住,获得这种可观察性并不是没有代价。每个启用的探测/跟踪点在每次被命中时都会产生一些需要完成的工作,无论是在内核空间还是用户空间,都会产生 CPU 开销。

确定跟踪程序的CPU开销的三个主要因素是:

  • 目标事件的频率
  • 每个事件的处理工作量
  • 系统上的 CPU 数量

一个程序在每个CPU的开销公式如下:

overhead = (frequency * workload) / numCPUs

换句话说,在单个 CPU 上每秒跟踪 100 万个事件可能会使系统龟速,而 128 个 CPU 的系统可能几乎不受影响。

暗号:cec28


文章来源: http://mp.weixin.qq.com/s?__biz=MzU4NjY0NTExNA==&mid=2247488122&idx=1&sn=62f22e6a5bb0079319833dfb2e45c130&chksm=fdf9796fca8ef079728f25897d4b682d75535a704d1d8036afc6296b539d8bf5141c1d8b5b9b#rd
如有侵权请联系:admin#unsafe.sh