使用 eBPF 跟踪系统调用

1,300 阅读9分钟

使用 eBPF 跟踪系统调用的特色图片 - 第 1 部分

介绍:

在本文中,我们将深入研究 eBPF(扩展伯克利数据包过滤器)的详细信息,并探讨其在跟踪系统调用中的意义。这个特殊的博客将分为两部分;在第一篇博客中,我们将讨论 eBPF,在后续部分中,我们将深入研究探针。 eBPF 是一项强大的技术,允许动态且高效地跟踪操作系统内核空间内的事件。您可能听说过缩写词 BPF 和 eBPF 可以互换使用。这就是为什么在讨论 Falco 如何以及为何使用这项技术之前,我们的目标是解决 BPF 和 eBPF。

BPF(伯克利数据包过滤器)

BPF是一种用于网络数据包过滤和分析的技术。它是实现防火墙和入侵检测系统等网络安全功能的强大工具。它还可用于实时检查网络流量、检测可疑模式并采取适当的措施来保护网络。

eBPF(扩展伯克利数据包过滤器)

扩展伯克利滤波器 (eBPF) 是原始 BPF 技术的演变。它通过提供更强大、更灵活的方式来执行动态跟踪、网络分析和性能监控,扩展了 BPF 的功能。它允许开发人员编写程序并将其加载到内核中,这些程序可以附加到系统中的各种挂钩和事件。这些程序可以提供实时洞察和对系统活动的控制。

eBPF 程序

编译和运行 eBPF 程序的过程涉及几个步骤:

  1. eBPF程序通过编译器转换为字节码,准备被加载程序加载。

  2. eBPF 验证程序检查程序的安全性、正确性以及对特定规则和约束的遵守情况。首先,它对所有可能的执行路径执行深度优先搜索,以确保程序始终继续完成。接下来,它对字节码进行静态分析,确保程序不违反内存访问规则,并且不会导致不稳定。

  3. 一旦eBPF程序通过验证过程,就可以加载到内核中。加载器确保程序被安全地加载并附加到系统中所需的挂钩或目标。

  4. 在运行时,eBPF字节码通过JIT(Just-in-time)编译进一步优化。此步骤将 eBPF 字节码转换为 CPU 可以执行的机器代码。

内核模块

除了 eBPF 之外,我们之前讨论的 Linux 中进程跟踪的另一种方法是使用内核模块。内核模块允许开发人员编写可以加载到内核中以扩展其功能的自定义代码。

通过利用内核模块,可以连接到内核进程管理代码的各个点并捕获有关进程执行的详细信息。这包括进程创建、终止和上下文切换等事件。

通过访问内核的内部数据结构和函数,该模块可以收集有价值的见解,例如进程 ID、父子关系、执行时间、系统调用等。

为什么要使用eBPF呢?

eBPF 的集成为 Falco 等项目带来了显着的优势,使他们能够安全、高效地实时监控和分析系统调用。您可能想知道,当 Falco 已经通过其处理系统调用事件的 kprobe(内核探针)具有实时检测功能时,为什么需要 eBPF。

纳入 eBPF 支持的一个令人信服的原因是使 Falco 能够在现代云原生环境中无缝运行,在这种环境中,传统的内核探测可能会遇到限制或面临控制平面节点施加的限制。

通过采用 eBPF,Falco 以安全的方式确保其实时检测能力的连续性,无论底层环境如何,都可以快速准确地识别安全事件。

eBPF 程序与内核模块

安全和隔离

eBPF 程序在加载到内核之前要经过彻底的验证过程。此步骤提供了额外的保护层,有助于防止安全漏洞。相比之下,内核模块可以直接访问内核代码,如果实现不正确,可能会对系统构成威胁。

性能

eBPF程序被JIT编译成机器代码,这显着提高了性能。 JIT编译针对特定CPU架构优化程序,实现高效执行。尽管做出了所有这些努力,eBPF 插装总是会比内核模块插装在系统中造成更大的开销,原因是在内核模块插装中没有对 BPF 子系统的调用。

可观察性和调试

eBPF 提供强大的跟踪和可观察能力。 eBPF 程序可以附加到各种事件,例如网络数据包、系统调用或内核函数,从而允许详细了解系统行为。这使得 eBPF 成为调试、性能分析和安全监控的宝贵工具。内核模块通常需要更具侵入性和复杂的机制来实现类似的可观察性。

将 eBPF 程序附加到挂钩和事件

Linux 内核中定义了各种检测点。检测是计算机程序中的一个特定点,其中插入附加代码(称为检测代码)来收集有关程序执行的信息。可以使用 JIT 编译在运行时注入检测代码。内核探针、跟踪点、用户空间探针、kretprobe 都是检测点的示例。

这是一个在执行 execve 系统调用时运行的 eBPF 程序。

执行 execve 系统调用时运行的 eBPF 程序

  • 在 eBPF 编程环境中,头文件macro SEC()中的 frombpf/bpf_helper.h起着至关重要的作用。它允许程序员指定函数或变量将被放置在eBPF object file.当使用bpf() system call.

  • 通过将函数和变量组织到命名部分中,eBPF 加载器可以有效地定位和加载所需的代码和数据。具体来说,在处理跟踪点事件时,SEC格式遵循 模式SEC("tp/<category>/<name>"),其中<category><name>代表各自的跟踪点类别和事件名称。

  • tp/syscalls/sys_enter_execve指记录进程何时生成 execve 系统调用的跟踪点。

  • 文件中包含所有可用跟踪点的列表/sys/kernel/debug/tracing/available_events。文件中每一行的格式为<category>:<name>.例如,系统调用:sys_enter_execve。

在编译程序之前,我们需要做一些基本配置:

基本配置。

我们来编译一下程序。可以使用以下命令来执行此任务:

编译程序的命令。

现在,我们需要编写一个加载程序来加载并附加该程序。该加载程序用于加载 eBPF 程序并将其附加到 Linux 内核。它打开并加载 eBPF 对象文件,在过程中检查错误,在加载的对象中查找特定的 eBPF 程序,并将其附加到内核。一旦附加,eBPF 程序将在某些事件发生时执行。程序最后进入无限循环,表示会继续运行,直到被手动终止。

eBPF 的加载程序。

让我们编译并运行这个程序

为 eBPF 编译加载程序的命令。

要获取该函数生成的日志bpf_printk,我们可以读取该文件: /sys/kernel/tracing/trace_pipe

正在读取“/sys/kernel/tracing/trace_pipe”文件。

我们在读取“trace_pipe”文件后收到的消息。

从跟踪管道手动读取消息似乎不是一种有效的方法。为 eBPF 程序建立一种向加载程序发送消息的机制将是有利的。一种可行的解决方案是利用环形缓冲区。让我们回顾一下有关环形缓冲区的更多详细信息。

环形缓冲区

eBPF 环形缓冲区,也称为环形缓冲区bpf_ringbuf,是 Linux 内核提供的一种机制,用于 eBPF 程序和用户空间程序之间的高效通信。

它允许在内核和用户空间应用程序中运行的 eBPF 程序之间交换数据和事件。它是一个MPSC (Multi Producer Single Consumer)队列,可以同时在多个 CPU 之间安全地共享。

eBPF 环形缓冲区在所有 CPU 之间共享,为管理内存利用率提供了统一且高效的解决方案,缓解了 perfbuf 中常见的过度使用或分配不足的问题。

让我们看一下一些函数,我们将使用它们来编写将数据发送到用户空间的 eBPF 程序。

bpf_ringbuf_保留

该函数用于size在 BPF 环形缓冲区中保留字节空间。

用于在 BPF 环形缓冲区中保留“size”字节空间的函数。

bpf_probe_read_user_str

此函数用于将空终止字符串从用户空间内存读入目标dst。 dst 参数是指向内核空间中目标缓冲区的指针。unsafe_ptr是指向用户空间中源字符串的指针。

用于将空终止字符串从用户空间内存读取到目标“dst”的函数。

bpf_ringbuf_提交

此函数用于提交先前在ringbuf.

函数ehich用于提交先前保留在ringbuf中的数据。

bpf_object__find_map_fd_by_name

该函数用于查找命名映射的文件描述符。

用于查找命名映射的文件描述符的函数。

bpf_program__attach_tracepoint

该函数用于将 eBPF 程序附加到内核跟踪点。

用于将 eBPF 程序附加到内核跟踪点的函数。

ring_buffer__new

该函数用于创建和打开一个新的ringbuf管理器。

用于创建和打开新的ringbuf管理器的函数。

ring_buf__consume

用于从 中删除或使用数据ring buffer

用于从环形缓冲区中删除或消耗数据的程序。

BTF(BPF 类型格式)

它提供了一种描述 eBPF 程序使用的数据结构类型的方法,从而可以改进类型安全性、调试和自省。

现在,让我们编写一个使用ringbuf 将数据发送到用户空间的程序。

使用ringbuf向用户空间发送数据的程序

创建程序后,我们可以编写一个加载器来加载这个 eBPF 程序。

用于加载此 eBPF 程序的加载程序。

无限循环对于确保程序不断检查环形缓冲区中的新事件是必要的。如果没有循环,程序将仅消耗初始调用时已在缓冲区中的事件ring_buffer__consume()。通过循环和ring_buffer__consume()重复调用,程序可以在事件可用时立即检索事件并实时处理它们。循环内的调用sleep(1)通过在每次调用之间引入一秒的延迟来减少程序的 CPU 使用率ring_buffer__consume()

编译并运行上述程序的命令。

编译并执行上述程序后的结果。

太棒了,我们能够恢复进程名称和 PID!

结论

总之,本文全面概述了 eBPF(扩展伯克利数据包过滤器)及其在跟踪系统调用中的重要性。我们探讨了从 BPF 到 eBPF 的演变,讨论了 Falco 使用该技术的原因,并深入研究了使用 eBPF 程序和环形缓冲区在内核和用户空间应用程序之间进行高效数据通信的过程。

当我们在第一部分中了解 eBPF 的功能时,我们发现了与传统内核模块相比,它在安全性、性能和可观察性方面的优势。 eBPF 使我们能够安全、高效地实时监控和分析系统调用,使其成为现代云原生环境中的宝贵工具。

在本博客系列即将推出的第二部分中,我们将通过深入探讨探针领域和其他高级主题来进一步扩展我们的探索。我们将更深入地探讨如何利用 eBPF 探针来增强系统跟踪、性能分析和安全监控。请继续关注有关利用 eBPF 的力量的更多见解和实用指南。