eBPF 入门开发实践教程零:介绍 eBPF 的基本概念、常见的开发工具
-1. eBPF 简介:安全和有效地扩展内核
-eBPF 是一项革命性的技术,起源于 Linux 内核,可以在操作系统的内核中运行沙盒程序。它被用来安全和有效地扩展内核的功能,而不需要改变内核的源代码或加载内核模块。eBPF 通过允许在操作系统内运行沙盒程序,应用程序开发人员可以在运行时,可编程地向操作系统动态添加额外的功能。然后,操作系统保证安全和执行效率,就像在即时编译(JIT)编译器和验证引擎的帮助下进行本地编译一样。eBPF 程序在内核版本之间是可移植的,并且可以自动更新,从而避免了工作负载中断和节点重启。
-今天,eBPF 被广泛用于各类场景:在现代数据中心和云原生环境中,可以提供高性能的网络包处理和负载均衡;以非常低的资源开销,做到对多种细粒度指标的可观测性,帮助应用程序开发人员跟踪应用程序,为性能故障排除提供洞察力;保障应用程序和容器运行时的安全执行,等等。可能性是无穷的,而 eBPF 在操作系统内核中所释放的创新才刚刚开始[3]。
-eBPF 的未来:内核的 JavaScript 可编程接口
-对于浏览器而言,JavaScript 的引入带来的可编程性开启了一场巨大的革命,使浏览器发展成为几乎独立的操作系统。现在让我们回到 eBPF:为了理解 eBPF 对 Linux 内核的可编程性影响,对 Linux 内核的结构以及它如何与应用程序和硬件进行交互有一个高层次的理解是有帮助的[4]。
-
Linux 内核的主要目的是抽象出硬件或虚拟硬件,并提供一个一致的 API(系统调用),允许应用程序运行和共享资源。为了实现这个目的,我们维护了一系列子系统和层,以分配这些责任[5]。每个子系统通常允许某种程度的配置,以考虑到用户的不同需求。如果不能配置所需的行为,就需要改变内核,从历史上看,改变内核的行为,或者让用户编写的程序能够在内核中运行,就有两种选择:
-| 本地支持内核模块 | 写一个内核模块 |
|---|---|
| 改变内核源代码,并说服Linux内核社区相信这种改变是必要的。等待几年,让新的内核版本成为一种商品。 | 定期修复它,因为每个内核版本都可能破坏它。由于缺乏安全边界,冒着破坏你的Linux内核的风险 |
实际上,两种方案都不常用,前者成本太高,后者则几乎没有可移植性。
-有了 eBPF,就有了一个新的选择,可以重新编程 Linux 内核的行为,而不需要改变内核的源代码或加载内核模块,同时保证在不同内核版本之间一定程度上的行为一致性和兼容性、以及安全性[6]。为了实现这个目的,eBPF 程序也需要有一套对应的 API,允许用户定义的应用程序运行和共享资源 --- 换句话说,某种意义上讲 eBPF 虚拟机也提供了一套类似于系统调用的机制,借助 eBPF 和用户态通信的机制,Wasm 虚拟机和用户态应用也可以获得这套“系统调用”的完整使用权,一方面能可编程地扩展传统的系统调用的能力,另一方面能在网络、文件系统等许多层次实现更高效的可编程 IO 处理。
-
正如上图所示,当今的 Linux 内核正在向一个新的内核模型演化:用户定义的应用程序可以在内核态和用户态同时执行,用户态通过传统的系统调用访问系统资源,内核态则通过 BPF Helper Calls 和系统的各个部分完成交互。截止 2023 年初,内核中的 eBPF 虚拟机中已经有 220 多个 Helper 系统接口,涵盖了非常多的应用场景。
-值得注意的是,BPF Helper Call 和系统调用二者并不是竞争关系,它们的编程模型和有性能优势的场景完全不同,也不会完全替代对方。对 Wasm 和 Wasi 相关生态来说,情况也类似,专门设计的 Wasi 接口需要经历一个漫长的标准化过程,但可能在特定场景能为用户态应用获取更佳的性能和可移植性保证,而 eBPF 在保证沙箱本质和可移植性的前提下,可以提供一个快速灵活的扩展系统接口的方案。
-目前的 eBPF 仍然处于早期阶段,但是借助当前 eBPF 提供的内核接口和用户态交互的能力,经由 Wasm-bpf 的系统接口转换,Wasm 虚拟机中的应用已经几乎有能力获取内核以及用户态任意一个函数调用的数据和返回值(kprobe,uprobe...);以很低的代价收集和理解所有系统调用,并获取所有网络操作的数据包和套接字级别的数据(tracepoint,socket...);在网络包处理解决方案中添加额外的协议分析器,并轻松地编程任何转发逻辑(XDP,TC...),以满足不断变化的需求,而无需离开Linux内核的数据包处理环境。
-不仅如此,eBPF 还有能力往用户空间任意进程的任意地址写入数据(bpf_probe_write_user[7]),有限度地修改内核函数的返回值(bpf_override_return[8]),甚至在内核态直接执行某些系统调用[9];所幸的是,eBPF 在加载进内核之前对字节码会进行严格的安全检查,确保没有内存越界等操作,同时,许多可能会扩大攻击面、带来安全风险的功能都是需要在编译内核时明确选择启用才能使用的;在 Wasm 虚拟机将字节码加载进内核之前,也可以明确选择启用或者禁用某些 eBPF 功能,以确保沙箱的安全性。
-除了内核态的 eBPF 运行时,eBPF 也可以拓展到用户空间,例如 bpftime,实现更高性能的用户态追踪、性能分析、插件等等。
-2. 关于如何学习 eBPF 相关的开发的一些建议
-本文不会对 eBPF 的原理做更详细的介绍,不过这里有一个学习规划和参考资料,也许会有一些价值:
-eBPF 入门(5-7h)
+eBPF 示例教程 0:核心概念与工具简介
+这是一个全面的 eBPF 开发教程的第一部分,旨在通过实用的 eBPF 开发指导您从初学者到高级用户。它涵盖了基本概念、实际代码示例以及在现代系统中的应用。我们将不再专注于传统工具如 BCC,而是使用现代框架如 libbpf、Cilium、libbpf-rs 和 eunomia-bpf,并提供 C、Go 和 Rust 的示例。
本教程的主要目标是提供清晰简洁的 eBPF 工具示例(起步只需 20 行代码!),帮助开发者快速掌握基本的 eBPF 开发技术。每个示例都是独立的,可以在目录结构中找到,每个目录代表一个独立的 eBPF 工具。您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/tutorials/ 获取更多示例和完整的教程源代码。
+eBPF 简介:安全高效的内核扩展
+eBPF(扩展的 Berkeley Packet Filter)是一项突破性的技术,允许开发者在内核空间中安全高效地运行小型程序。与传统方法需要修改内核源代码或加载新模块不同,eBPF 使得动态定制和优化网络行为成为可能,且不会中断系统操作。这种灵活性和高效性使 eBPF 成为克服传统网络栈限制的关键技术。
+eBPF 的强大之处是什么?
-
-
- Google 或者其他搜索引擎查找:eBPF -
- 询问 ChatGPT 之类的东西:eBPF 是什么? +
- 直接内核交互:eBPF 程序在内核中执行,与系统级事件如网络包、系统调用或追踪点交互。 +
- 安全执行:eBPF 通过验证器在程序运行前检查其逻辑,防止潜在的内核崩溃或安全漏洞。 +
- 最低开销:eBPF 通过使用即时编译器(JIT),将 eBPF 字节码转换为针对特定架构的优化机器码,实现近原生执行速度。 +
eBPF:过去、现在与未来
+过去:可编程网络的变革
+eBPF 于 2014 年推出,彻底改变了开发者处理网络的方式,允许小型可编程内核空间应用程序实时处理数据包。通过钩住关键内核点,eBPF 使得在网络包到达时应用自定义逻辑成为可能,从而提高了效率和灵活性。这使得组织能够在不需要自定义驱动程序或修改内核的情况下定制网络行为,为云原生和数据中心环境创造了理想的解决方案。
+现在:满足现代计算需求的多功能框架
+eBPF 已发展为一个多功能框架,超越了其最初的网络用途,现在涵盖了可观测性、追踪、安全性,甚至系统资源管理。eBPF 程序可以动态钩住内核事件,赋予开发者精确控制系统行为和性能优化的能力,而无需修改内核或重启系统。这使得 eBPF 成为系统管理员和开发者监控、优化和保护环境的必备工具。
+以下是 eBPF 目前广泛应用的一些关键领域:
+-
+
-
+
网络:eBPF 提供内核中实时、高速的数据包过滤和处理,允许创建自定义协议解析器和网络策略,无需新驱动程序或系统重启。这在云和数据中心环境中实现了高效的网络管理。
+
+ -
+
可观测性:eBPF 使开发者能够通过收集自定义指标和执行内核级数据聚合来深入了解系统行为。通过利用内核追踪点和函数调用,eBPF 有助于识别性能问题和定位难以发现的错误。
+
+ -
+
追踪与分析:eBPF 提供强大的追踪和分析能力,通过附加到内核函数、追踪点甚至用户空间探针,使开发者能够深入了解系统和应用程序的行为,从而优化性能和解决复杂的系统问题。
+
+ -
+
安全:eBPF 在实时安全监控中发挥重要作用。它能够深入检查系统调用、网络流量和其他内核活动,帮助执行动态安全策略和检测异常行为,为基础设施提供高效的保护。
+
+ -
+
调度器优化:eBPF 越来越多地用于增强 CPU 调度,能够监控 CPU 负载并优化任务在核心之间的分配。这可以更有效地利用 CPU 资源,提高系统响应能力。
+
+ -
+
HID(人机接口设备)驱动增强:开发者使用 eBPF 优化键盘、鼠标和触摸屏等设备的 HID 驱动程序。通过为处理输入事件添加自定义逻辑,eBPF 提高了对延迟敏感应用的响应速度。
+
+
各行业组织已大规模采用 eBPF:
+-
+
- Google:使用 eBPF 进行安全审计、数据包处理、实时性能监控以及优化其庞大基础设施的 CPU 调度。 +
- Netflix:利用 eBPF 进行网络流量分析,确保流媒体服务的高可用性和性能。 +
- Android:应用 eBPF 优化网络使用、功耗和资源分配,提升数百万设备的性能和电池寿命。 +
- S&P Global:通过 Cilium 使用 eBPF 管理跨多个云和本地系统的网络,确保可扩展性和安全性。 +
- Shopify:与 Falco 一起实施 eBPF 进行入侵检测,增强其电子商务平台的安全性。 +
- Cloudflare:使用 eBPF 进行网络可观测性、安全监控和性能优化,保护全球数百万网站。 +
eBPF 能够动态调整系统行为并扩展到用户空间,使其成为现代计算不可或缺的技术。无论是优化网络流量、提升安全性,还是增强系统性能,eBPF 都能帮助开发者高效、安全地应对实时需求。
+除了其内核模式运行时,eBPF 还可以扩展到用户空间。例如,bpftime 是一个用户空间 eBPF 运行时,允许在用户空间应用中进行高性能追踪、性能分析和插件支持。这种 eBPF 向用户空间的扩展有助于在各种超越内核级任务的用例中提高灵活性和性能。
+未来:eBPF 的扩展潜力
+展望未来,预计 eBPF 将成为操作系统更为重要的一部分。重点将放在提升其灵活性、模块化和易用性上,使其能够应用于更广泛的场景。内存管理、并发机制的创新以及与用户空间应用的更好集成已在路上。已经有项目在编译 Linux 内核的关键部分到 BPF 指令集,这可能彻底改变内核开发和分析的方式。
+动态栈、更好的用户空间可观测性工具(例如快速 Uprobes 和特定语言的栈行走器)以及更安全的程序终止机制等进展将继续增强 eBPF 的可靠性并扩展其使用场景。此外,新工具和库将简化 eBPF 开发,降低内核和应用开发者的入门门槛。
+开始学习教程
+本教程提供实用的 eBPF 开发实践,涵盖从初级到高级的主题。我们专注于在可观测性、网络和安全等领域的动手示例,使用 libbpf、libbpf-rs 和 eunomia-bpf 等框架,并提供 C、Go 和 Rust 的示例。
本教程适合谁?
+-
+
- 开发者 希望实现自定义内核解决方案。 +
- 系统管理员 旨在提升性能和安全性。 +
- 技术爱好者 探索前沿的内核技术。 +
你将学到什么?
+-
+
- 核心概念:eBPF 基础知识及其与 Linux 内核的集成。 +
- 实用技能:编写和部署 eBPF 程序。 +
- 高级主题:探索 eBPF 在安全、追踪和未来创新方面的应用。 +
+
目录
+-
+
-
+
eBPF 简介
+
+基本概念和入门所需的工具。
+ -
+
初学者示例
+
+简单的程序,如“Hello World”及使用 kprobe 和 uprobe 进行基础追踪。
+ -
+
可观测性
+
+侧重于使用 eBPF 监控网络流量、文件操作和进程行为的示例。
+ -
+
网络
+
+侧重于修改和优化网络流量的示例,如 XDP、TC 和 socket。
+ -
+
安全
+
+用于隐藏进程和文件、发送信号杀死进程以及跟踪进程事件以增强安全性的程序。
+ -
+
高级用例
+
+涉及性能分析、调度器优化和用户空间 eBPF(如 bpftime)的复杂示例。
+ -
+
深入主题
+
+探索 eBPF 在 Android 上的应用、使用 eBPF 进行网络加速以及通过系统调用修改来保护系统。
+
如何使用 eBPF 编程
+从头编写 eBPF 程序可能较为复杂。为简化这一过程,LLVM 于 2015 年引入了将高级语言代码编译为 eBPF 字节码的能力。自那时起,eBPF 社区构建了像 libbpf 这样的库来管理这些程序。这些库帮助将 eBPF 字节码加载到内核中并执行基本任务。Linux 内核源代码中 samples/bpf/ 目录包含了众多 eBPF 示例。
典型的 eBPF 程序包含两个部分:内核空间代码(*_kern.c)和用户空间代码(*_user.c)。内核空间代码定义逻辑,而用户空间代码负责加载和与内核交互。然而,像 libbpf-bootstrap 和 Go eBPF 库这样的工具简化了这一过程,允许一次性编译和更容易的开发。
eBPF 开发工具
+-
+
- BCC:一个基于 Python 的工具链,简化了 eBPF 程序的编写、编译和加载。它提供了许多预构建的追踪工具,但在依赖和兼容性方面存在一些限制。 +
- eBPF Go 库:一个 Go 库,解耦了获取 eBPF 字节码的过程与加载和管理 eBPF 程序的过程。 +
- libbpf-bootstrap:基于
libbpf的现代脚手架,提供了高效的工作流用于编写 eBPF 程序,提供简单的一次性编译过程以生成可重用的字节码。
+ - eunomia-bpf:一个用于编写仅包含内核空间代码的 eBPF 程序的工具链。它通过动态加载 eBPF 程序简化了 eBPF 程序的开发。 +
这些工具有助于减少开发 eBPF 程序的复杂性,使开发者更容易优化系统性能、安全性和可观测性。
+学习 eBPF 开发的一些技巧
+本文不会提供更详细的 eBPF 原理介绍,但以下是一个学习计划和参考资料,可能对您有帮助:
+eBPF 简介(5-7 小时)
+-
+
- 使用 Google 或其他搜索引擎搜索:eBPF +
- 询问类似 ChatGPT 的工具:什么是 eBPF?
推荐:
-
-
- 阅读 ebpf 简介:https://ebpf.io/ (30min) -
- 简要了解一下 ebpf 内核相关文档:https://prototype-kernel.readthedocs.io/en/latest/bpf/ (知道有问题去哪里查询,30min) -
- 阅读 ebpf 中文入门指南:https://www.ebpf.top/post/ebpf_intro (1h) -
- 有大量的参考资料:https://github.com/zoidbergwill/awesome-ebpf (2-3h) -
- 可以选自己感兴趣的 PPT 翻一翻:https://github.com/gojue/ebpf-slide (1-2h) +
- 阅读 eBPF 介绍:https://ebpf.io/(30 分钟) +
- 简要了解 eBPF 内核相关文档:https://docs.ebpf.io/(了解技术细节的查询来源,30 分钟)
回答三个问题:
-
-
- 了解 eBPF 是什么东西?为啥要有这个玩意,不能用内核模块? -
- 它有什么功能?能在 Linux 内核里面完成哪些事情?有哪些 eBPF 程序的类型和 helper(不需要知道全部,但是需要知道去哪里找)? -
- 能拿来做什么?比如说在哪些场景中进行运用?网络、安全、可观测性? +
- 了解 eBPF 是什么?我们为什么需要它?难道不能使用内核模块吗? +
- 它有哪些功能?它在 Linux 内核中能做什么?eBPF 程序和助手函数有哪些类型(不需要全部了解,但需要知道在哪里查找)? +
- 它能用于哪些场景?例如,可以在哪些情况下使用?网络、安全、可观测性?
了解如何开发 eBPF 程序(10-15h)
-了解并尝试一下 eBPF 开发框架:
+理解如何开发 eBPF 程序(10-15 小时)
+了解并尝试 eBPF 开发框架:
-
-
- bpftrace 教程,对于最简单的应用来说,bpftrace 可能是最方便的:https://eunomia.dev/zh/tutorials/bpftrace-tutorial/ (试试,1h) -
- BCC 开发各类小工具的例子:https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md (跑一遍,3-4h) -
- libbpf 的一些例子:https://github.com/libbpf/libbpf-bootstrap (选感兴趣的运行一下,并阅读一下源代码,2h) -
- 基于 C 语言 libbpf, Go 语言或者 Rust 语言和 eunomia-bpf 的教程:https://github.com/eunomia-bpf/bpf-developer-tutorial (阅读 1-20 的部分,3-8h) +
- bpftrace 教程:https://eunomia.dev/tutorials/bpftrace-tutorial/(尝试,1 小时) +
- 使用 BCC 开发各种工具的示例:https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md(运行,3-4 小时) +
- libbpf 的一些示例:https://github.com/libbpf/libbpf-bootstrap(运行任何有趣的示例并阅读源代码,2 小时) +
- 教程:https://github.com/eunomia-bpf/bpf-developer-tutorial(阅读第 1-10 部分,3-4 小时)
有任何问题或者想了解的东西,不管是不是和本项目相关,都可以在本项目的 discussions 里面开始讨论。
-回答一些问题,并且进行一些尝试(2-5h):
+其他开发框架:Go 或 Rust 语言,请自行搜索和尝试(0-2 小时)
+如果有问题或想了解的内容,无论是否与本项目相关,都可以在该项目的讨论区开始讨论。
+回答一些问题并尝试一些实验(2-5 小时):
-
-
- 如何开发一个最简单的 eBPF 程序? -
- 如何用 eBPF 追踪一个内核功能或函数?有很多种方法,举出对应的代码; -
- 有哪些方案能通过用户态和内核态通信?如何从用户态向内核态传送信息?如何从内核态向用户态传递信息?举出代码示例; -
- 编写一个你自己的 eBPF 程序,实现一个功能; -
- eBPF 程序的整个生命周期里面,分别在用户态和内核态做了哪些事情? +
- 如何开发最简单的 eBPF 程序? +
- 如何使用 eBPF 追踪内核功能或函数?有很多方法,提供相应的代码示例; +
- 用户模式和内核模式之间的通信解决方案有哪些?如何将信息从用户模式发送到内核模式?如何将信息从内核模式传递到用户模式?提供代码示例; +
- 编写您自己的 eBPF 程序以实现某个功能; +
- 在 eBPF 程序的整个生命周期中,用户模式和内核模式分别做了什么?
3. 如何使用 eBPF 编程
-原始的 eBPF 程序编写是非常繁琐和困难的。为了改变这一现状,llvm 于 2015 年推出了可以将由高级语言编写的代码编译为 eBPF 字节码的功能,同时,eBPF 社区将 bpf() 等原始的系统调用进行了初步地封装,给出了 libbpf 库。这些库会包含将字节码加载到内核中的函数以及一些其他的关键函数。在 Linux 的源码包的 samples/bpf/ 目录下,有大量 Linux 提供的基于 libbpf 的 eBPF 样例代码。
一个典型的基于 libbpf 的 eBPF 程序具有 *_kern.c 和 *_user.c 两个文件,*_kern.c 中书写在内核中的挂载点以及处理函数,*_user.c 中书写用户态代码,完成内核态代码注入以及与用户交互的各种任务。 更为详细的教程可以参考该视频。然而由于该方法仍然较难理解且入门存在一定的难度,因此现阶段的eBPF程序开发大多基于一些工具,比如:
-
-
- BCC -
- BPFtrace -
- libbpf-bootstrap -
- Go eBPF library -
以及还有比较新的工具,例如 eunomia-bpf.
编写 eBPF 程序
-eBPF 程序由内核态部分和用户态部分构成。内核态部分包含程序的实际逻辑,用户态部分负责加载和管理内核态部分。使用 eunomia-bpf 开发工具,只需编写内核态部分的代码。
-内核态部分的代码需要符合 eBPF 的语法和指令集。eBPF 程序主要由若干个函数组成,每个函数都有其特定的作用。可以使用的函数类型包括:
--
-
- kprobe:插探函数,在指定的内核函数前或后执行。 -
- tracepoint:跟踪点函数,在指定的内核跟踪点处执行。 -
- raw_tracepoint:原始跟踪点函数,在指定的内核原始跟踪点处执行。 -
- xdp:网络数据处理函数,拦截和处理网络数据包。 -
- perf_event:性能事件函数,用于处理内核性能事件。 -
- kretprobe:函数返回插探函数,在指定的内核函数返回时执行。 -
- tracepoint_return:跟踪点函数返回,在指定的内核跟踪点返回时执行。 -
- raw_tracepoint_return:原始跟踪点函数返回,在指定的内核原始跟踪 -
BCC
-BCC 全称为 BPF Compiler Collection,该项目是一个 python 库, -包含了完整的编写、编译、和加载 BPF 程序的工具链,以及用于调试和诊断性能问题的工具。
-自 2015 年发布以来,BCC 经过上百位贡献者地不断完善后,目前已经包含了大量随时可用的跟踪工具。其官方项目库 -提供了一个方便上手的教程,用户可以快速地根据教程完成 BCC 入门工作。
-用户可以在 BCC 上使用 Python、Lua 等高级语言进行编程。 -相较于使用 C 语言直接编程,这些高级语言具有极大的便捷性,用户只需要使用 C 来设计内核中的 -BPF 程序,其余包括编译、解析、加载等工作在内,均可由 BCC 完成。
-然而使用 BCC 存在一个缺点便是在于其兼容性并不好。基于 BCC 的 -eBPF 程序每次执行时候都需要进行编译,编译则需要用户配置相关的头文件和对应实现。在实际应用中, -相信大家也会有体会,编译依赖问题是一个很棘手的问题。也正是因此,在本项目的开发中我们放弃了 BCC, -选择了可以做到一次编译-多次运行的 libbpf-bootstrap 工具。
-eBPF Go library
-eBPF Go 库提供了一个通用的 eBPF 库,它解耦了获取 eBPF 字节码的过程和 eBPF 程序的加载和管理,并实现了类似 libbpf 一样的 CO- 功能。eBPF 程序通常是通过编写高级语言创建的,然后使用 clang/LLVM 编译器编译为 eBPF 字节码。
-libbpf
-libbpf-bootstrap 是一个基于 libbpf 库的 BPF 开发脚手架,从其
-github 上可以得到其源码。
libbpf-bootstrap 综合了 BPF 社区过去多年的实践,为开发者提了一个现代化的、便捷的工作流,实
-现了一次编译,重复使用的目的。
基于 libbpf-bootstrap 的 BPF 程序对于源文件有一定的命名规则,
-用于生成内核态字节码的 bpf 文件以 .bpf.c 结尾,用户态加载字节码的文件以 .c 结尾,且这两个文件的
-前缀必须相同。
基于 libbpf-bootstrap 的 BPF 程序在编译时会先将 *.bpf.c 文件编译为
-对应的 .o 文件,然后根据此文件生成 skeleton 文件,即 *.skel.h,这个文件会包含内核态中定义的一些
-数据结构,以及用于装载内核态代码的关键函数。在用户态代码 include 此文件之后调用对应的装载函数即可将
-字节码装载到内核中。同样的,libbpf-bootstrap 也有非常完备的入门教程,用户可以在该处
-得到详细的入门操作介绍。
eunomia-bpf
-eunomia-bpf 是一个开源的 eBPF 动态加载运行时和开发工具链,是为了简化 eBPF 程序的开发、构建、分发、运行而设计的,基于 libbpf 的 CO-RE 轻量级开发框架。
-使用 eunomia-bpf ,可以:
--
-
- 在编写 eBPF 程序或工具时只编写内核态代码,自动获取内核态导出信息,并作为模块动态加载; -
- eunomia-bpf 可以将预编译的 eBPF 程序打包为通用的 JSON 或 WASM 模块,跨架构和内核版本进行分发,无需重新编译即可动态加载运行。 -
eunomia-bpf 由一个编译工具链和一个运行时库组成, 对比传统的 BCC、原生 libbpf 等框架,简化了 eBPF 程序的开发流程,在大多数时候只需编写内核态代码,即可轻松构建、打包、发布完整的 eBPF 应用。
---
-- eunomia-bpf 项目 Github 地址: https://github.com/eunomia-bpf/eunomia-bpf
-- gitee 镜像: https://gitee.com/anolis/eunomia
-
参考资料
- eBPF 介绍:https://ebpf.io/ -
- BPF Compiler Collection (BCC):https://github.com/iovisor/bcc +
- BPF 编译器集合(BCC):https://github.com/iovisor/bcc
- eunomia-bpf:https://github.com/eunomia-bpf/eunomia-bpf
您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程源代码。我们会继续分享更多有关 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术。
-- -+
您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/tutorials/ 获取更多示例和完整的教程源代码。所有内容均为开源。我们将继续分享更多关于 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术。
https://github.com/eunomia-bpf/bpf-developer-tutorial
-eBPF 入门开发实践教程零:介绍 eBPF 的基本概念、常见的开发工具
-1. eBPF 简介:安全和有效地扩展内核
-eBPF 是一项革命性的技术,起源于 Linux 内核,可以在操作系统的内核中运行沙盒程序。它被用来安全和有效地扩展内核的功能,而不需要改变内核的源代码或加载内核模块。eBPF 通过允许在操作系统内运行沙盒程序,应用程序开发人员可以在运行时,可编程地向操作系统动态添加额外的功能。然后,操作系统保证安全和执行效率,就像在即时编译(JIT)编译器和验证引擎的帮助下进行本地编译一样。eBPF 程序在内核版本之间是可移植的,并且可以自动更新,从而避免了工作负载中断和节点重启。
-今天,eBPF 被广泛用于各类场景:在现代数据中心和云原生环境中,可以提供高性能的网络包处理和负载均衡;以非常低的资源开销,做到对多种细粒度指标的可观测性,帮助应用程序开发人员跟踪应用程序,为性能故障排除提供洞察力;保障应用程序和容器运行时的安全执行,等等。可能性是无穷的,而 eBPF 在操作系统内核中所释放的创新才刚刚开始[3]。
-eBPF 的未来:内核的 JavaScript 可编程接口
-对于浏览器而言,JavaScript 的引入带来的可编程性开启了一场巨大的革命,使浏览器发展成为几乎独立的操作系统。现在让我们回到 eBPF:为了理解 eBPF 对 Linux 内核的可编程性影响,对 Linux 内核的结构以及它如何与应用程序和硬件进行交互有一个高层次的理解是有帮助的[4]。
-
Linux 内核的主要目的是抽象出硬件或虚拟硬件,并提供一个一致的 API(系统调用),允许应用程序运行和共享资源。为了实现这个目的,我们维护了一系列子系统和层,以分配这些责任[5]。每个子系统通常允许某种程度的配置,以考虑到用户的不同需求。如果不能配置所需的行为,就需要改变内核,从历史上看,改变内核的行为,或者让用户编写的程序能够在内核中运行,就有两种选择:
-| 本地支持内核模块 | 写一个内核模块 |
|---|---|
| 改变内核源代码,并说服Linux内核社区相信这种改变是必要的。等待几年,让新的内核版本成为一种商品。 | 定期修复它,因为每个内核版本都可能破坏它。由于缺乏安全边界,冒着破坏你的Linux内核的风险 |
实际上,两种方案都不常用,前者成本太高,后者则几乎没有可移植性。
-有了 eBPF,就有了一个新的选择,可以重新编程 Linux 内核的行为,而不需要改变内核的源代码或加载内核模块,同时保证在不同内核版本之间一定程度上的行为一致性和兼容性、以及安全性[6]。为了实现这个目的,eBPF 程序也需要有一套对应的 API,允许用户定义的应用程序运行和共享资源 --- 换句话说,某种意义上讲 eBPF 虚拟机也提供了一套类似于系统调用的机制,借助 eBPF 和用户态通信的机制,Wasm 虚拟机和用户态应用也可以获得这套“系统调用”的完整使用权,一方面能可编程地扩展传统的系统调用的能力,另一方面能在网络、文件系统等许多层次实现更高效的可编程 IO 处理。
-
正如上图所示,当今的 Linux 内核正在向一个新的内核模型演化:用户定义的应用程序可以在内核态和用户态同时执行,用户态通过传统的系统调用访问系统资源,内核态则通过 BPF Helper Calls 和系统的各个部分完成交互。截止 2023 年初,内核中的 eBPF 虚拟机中已经有 220 多个 Helper 系统接口,涵盖了非常多的应用场景。
-值得注意的是,BPF Helper Call 和系统调用二者并不是竞争关系,它们的编程模型和有性能优势的场景完全不同,也不会完全替代对方。对 Wasm 和 Wasi 相关生态来说,情况也类似,专门设计的 Wasi 接口需要经历一个漫长的标准化过程,但可能在特定场景能为用户态应用获取更佳的性能和可移植性保证,而 eBPF 在保证沙箱本质和可移植性的前提下,可以提供一个快速灵活的扩展系统接口的方案。
-目前的 eBPF 仍然处于早期阶段,但是借助当前 eBPF 提供的内核接口和用户态交互的能力,经由 Wasm-bpf 的系统接口转换,Wasm 虚拟机中的应用已经几乎有能力获取内核以及用户态任意一个函数调用的数据和返回值(kprobe,uprobe...);以很低的代价收集和理解所有系统调用,并获取所有网络操作的数据包和套接字级别的数据(tracepoint,socket...);在网络包处理解决方案中添加额外的协议分析器,并轻松地编程任何转发逻辑(XDP,TC...),以满足不断变化的需求,而无需离开Linux内核的数据包处理环境。
-不仅如此,eBPF 还有能力往用户空间任意进程的任意地址写入数据(bpf_probe_write_user[7]),有限度地修改内核函数的返回值(bpf_override_return[8]),甚至在内核态直接执行某些系统调用[9];所幸的是,eBPF 在加载进内核之前对字节码会进行严格的安全检查,确保没有内存越界等操作,同时,许多可能会扩大攻击面、带来安全风险的功能都是需要在编译内核时明确选择启用才能使用的;在 Wasm 虚拟机将字节码加载进内核之前,也可以明确选择启用或者禁用某些 eBPF 功能,以确保沙箱的安全性。
-除了内核态的 eBPF 运行时,eBPF 也可以拓展到用户空间,例如 bpftime,实现更高性能的用户态追踪、性能分析、插件等等。
-2. 关于如何学习 eBPF 相关的开发的一些建议
-本文不会对 eBPF 的原理做更详细的介绍,不过这里有一个学习规划和参考资料,也许会有一些价值:
-eBPF 入门(5-7h)
+eBPF 示例教程 0:核心概念与工具简介
+这是一个全面的 eBPF 开发教程的第一部分,旨在通过实用的 eBPF 开发指导您从初学者到高级用户。它涵盖了基本概念、实际代码示例以及在现代系统中的应用。我们将不再专注于传统工具如 BCC,而是使用现代框架如 libbpf、Cilium、libbpf-rs 和 eunomia-bpf,并提供 C、Go 和 Rust 的示例。
本教程的主要目标是提供清晰简洁的 eBPF 工具示例(起步只需 20 行代码!),帮助开发者快速掌握基本的 eBPF 开发技术。每个示例都是独立的,可以在目录结构中找到,每个目录代表一个独立的 eBPF 工具。您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/tutorials/ 获取更多示例和完整的教程源代码。
+eBPF 简介:安全高效的内核扩展
+eBPF(扩展的 Berkeley Packet Filter)是一项突破性的技术,允许开发者在内核空间中安全高效地运行小型程序。与传统方法需要修改内核源代码或加载新模块不同,eBPF 使得动态定制和优化网络行为成为可能,且不会中断系统操作。这种灵活性和高效性使 eBPF 成为克服传统网络栈限制的关键技术。
+eBPF 的强大之处是什么?
-
-
- Google 或者其他搜索引擎查找:eBPF -
- 询问 ChatGPT 之类的东西:eBPF 是什么? +
- 直接内核交互:eBPF 程序在内核中执行,与系统级事件如网络包、系统调用或追踪点交互。 +
- 安全执行:eBPF 通过验证器在程序运行前检查其逻辑,防止潜在的内核崩溃或安全漏洞。 +
- 最低开销:eBPF 通过使用即时编译器(JIT),将 eBPF 字节码转换为针对特定架构的优化机器码,实现近原生执行速度。 +
eBPF:过去、现在与未来
+过去:可编程网络的变革
+eBPF 于 2014 年推出,彻底改变了开发者处理网络的方式,允许小型可编程内核空间应用程序实时处理数据包。通过钩住关键内核点,eBPF 使得在网络包到达时应用自定义逻辑成为可能,从而提高了效率和灵活性。这使得组织能够在不需要自定义驱动程序或修改内核的情况下定制网络行为,为云原生和数据中心环境创造了理想的解决方案。
+现在:满足现代计算需求的多功能框架
+eBPF 已发展为一个多功能框架,超越了其最初的网络用途,现在涵盖了可观测性、追踪、安全性,甚至系统资源管理。eBPF 程序可以动态钩住内核事件,赋予开发者精确控制系统行为和性能优化的能力,而无需修改内核或重启系统。这使得 eBPF 成为系统管理员和开发者监控、优化和保护环境的必备工具。
+以下是 eBPF 目前广泛应用的一些关键领域:
+-
+
-
+
网络:eBPF 提供内核中实时、高速的数据包过滤和处理,允许创建自定义协议解析器和网络策略,无需新驱动程序或系统重启。这在云和数据中心环境中实现了高效的网络管理。
+
+ -
+
可观测性:eBPF 使开发者能够通过收集自定义指标和执行内核级数据聚合来深入了解系统行为。通过利用内核追踪点和函数调用,eBPF 有助于识别性能问题和定位难以发现的错误。
+
+ -
+
追踪与分析:eBPF 提供强大的追踪和分析能力,通过附加到内核函数、追踪点甚至用户空间探针,使开发者能够深入了解系统和应用程序的行为,从而优化性能和解决复杂的系统问题。
+
+ -
+
安全:eBPF 在实时安全监控中发挥重要作用。它能够深入检查系统调用、网络流量和其他内核活动,帮助执行动态安全策略和检测异常行为,为基础设施提供高效的保护。
+
+ -
+
调度器优化:eBPF 越来越多地用于增强 CPU 调度,能够监控 CPU 负载并优化任务在核心之间的分配。这可以更有效地利用 CPU 资源,提高系统响应能力。
+
+ -
+
HID(人机接口设备)驱动增强:开发者使用 eBPF 优化键盘、鼠标和触摸屏等设备的 HID 驱动程序。通过为处理输入事件添加自定义逻辑,eBPF 提高了对延迟敏感应用的响应速度。
+
+
各行业组织已大规模采用 eBPF:
+-
+
- Google:使用 eBPF 进行安全审计、数据包处理、实时性能监控以及优化其庞大基础设施的 CPU 调度。 +
- Netflix:利用 eBPF 进行网络流量分析,确保流媒体服务的高可用性和性能。 +
- Android:应用 eBPF 优化网络使用、功耗和资源分配,提升数百万设备的性能和电池寿命。 +
- S&P Global:通过 Cilium 使用 eBPF 管理跨多个云和本地系统的网络,确保可扩展性和安全性。 +
- Shopify:与 Falco 一起实施 eBPF 进行入侵检测,增强其电子商务平台的安全性。 +
- Cloudflare:使用 eBPF 进行网络可观测性、安全监控和性能优化,保护全球数百万网站。 +
eBPF 能够动态调整系统行为并扩展到用户空间,使其成为现代计算不可或缺的技术。无论是优化网络流量、提升安全性,还是增强系统性能,eBPF 都能帮助开发者高效、安全地应对实时需求。
+除了其内核模式运行时,eBPF 还可以扩展到用户空间。例如,bpftime 是一个用户空间 eBPF 运行时,允许在用户空间应用中进行高性能追踪、性能分析和插件支持。这种 eBPF 向用户空间的扩展有助于在各种超越内核级任务的用例中提高灵活性和性能。
+未来:eBPF 的扩展潜力
+展望未来,预计 eBPF 将成为操作系统更为重要的一部分。重点将放在提升其灵活性、模块化和易用性上,使其能够应用于更广泛的场景。内存管理、并发机制的创新以及与用户空间应用的更好集成已在路上。已经有项目在编译 Linux 内核的关键部分到 BPF 指令集,这可能彻底改变内核开发和分析的方式。
+动态栈、更好的用户空间可观测性工具(例如快速 Uprobes 和特定语言的栈行走器)以及更安全的程序终止机制等进展将继续增强 eBPF 的可靠性并扩展其使用场景。此外,新工具和库将简化 eBPF 开发,降低内核和应用开发者的入门门槛。
+开始学习教程
+本教程提供实用的 eBPF 开发实践,涵盖从初级到高级的主题。我们专注于在可观测性、网络和安全等领域的动手示例,使用 libbpf、libbpf-rs 和 eunomia-bpf 等框架,并提供 C、Go 和 Rust 的示例。
本教程适合谁?
+-
+
- 开发者 希望实现自定义内核解决方案。 +
- 系统管理员 旨在提升性能和安全性。 +
- 技术爱好者 探索前沿的内核技术。 +
你将学到什么?
+-
+
- 核心概念:eBPF 基础知识及其与 Linux 内核的集成。 +
- 实用技能:编写和部署 eBPF 程序。 +
- 高级主题:探索 eBPF 在安全、追踪和未来创新方面的应用。 +
+
目录
+-
+
-
+
eBPF 简介
+
+基本概念和入门所需的工具。
+ -
+
初学者示例
+
+简单的程序,如“Hello World”及使用 kprobe 和 uprobe 进行基础追踪。
+ -
+
可观测性
+
+侧重于使用 eBPF 监控网络流量、文件操作和进程行为的示例。
+ -
+
网络
+
+侧重于修改和优化网络流量的示例,如 XDP、TC 和 socket。
+ -
+
安全
+
+用于隐藏进程和文件、发送信号杀死进程以及跟踪进程事件以增强安全性的程序。
+ -
+
高级用例
+
+涉及性能分析、调度器优化和用户空间 eBPF(如 bpftime)的复杂示例。
+ -
+
深入主题
+
+探索 eBPF 在 Android 上的应用、使用 eBPF 进行网络加速以及通过系统调用修改来保护系统。
+
如何使用 eBPF 编程
+从头编写 eBPF 程序可能较为复杂。为简化这一过程,LLVM 于 2015 年引入了将高级语言代码编译为 eBPF 字节码的能力。自那时起,eBPF 社区构建了像 libbpf 这样的库来管理这些程序。这些库帮助将 eBPF 字节码加载到内核中并执行基本任务。Linux 内核源代码中 samples/bpf/ 目录包含了众多 eBPF 示例。
典型的 eBPF 程序包含两个部分:内核空间代码(*_kern.c)和用户空间代码(*_user.c)。内核空间代码定义逻辑,而用户空间代码负责加载和与内核交互。然而,像 libbpf-bootstrap 和 Go eBPF 库这样的工具简化了这一过程,允许一次性编译和更容易的开发。
eBPF 开发工具
+-
+
- BCC:一个基于 Python 的工具链,简化了 eBPF 程序的编写、编译和加载。它提供了许多预构建的追踪工具,但在依赖和兼容性方面存在一些限制。 +
- eBPF Go 库:一个 Go 库,解耦了获取 eBPF 字节码的过程与加载和管理 eBPF 程序的过程。 +
- libbpf-bootstrap:基于
libbpf的现代脚手架,提供了高效的工作流用于编写 eBPF 程序,提供简单的一次性编译过程以生成可重用的字节码。
+ - eunomia-bpf:一个用于编写仅包含内核空间代码的 eBPF 程序的工具链。它通过动态加载 eBPF 程序简化了 eBPF 程序的开发。 +
这些工具有助于减少开发 eBPF 程序的复杂性,使开发者更容易优化系统性能、安全性和可观测性。
+学习 eBPF 开发的一些技巧
+本文不会提供更详细的 eBPF 原理介绍,但以下是一个学习计划和参考资料,可能对您有帮助:
+eBPF 简介(5-7 小时)
+-
+
- 使用 Google 或其他搜索引擎搜索:eBPF +
- 询问类似 ChatGPT 的工具:什么是 eBPF?
推荐:
-
-
- 阅读 ebpf 简介:https://ebpf.io/ (30min) -
- 简要了解一下 ebpf 内核相关文档:https://prototype-kernel.readthedocs.io/en/latest/bpf/ (知道有问题去哪里查询,30min) -
- 阅读 ebpf 中文入门指南:https://www.ebpf.top/post/ebpf_intro (1h) -
- 有大量的参考资料:https://github.com/zoidbergwill/awesome-ebpf (2-3h) -
- 可以选自己感兴趣的 PPT 翻一翻:https://github.com/gojue/ebpf-slide (1-2h) +
- 阅读 eBPF 介绍:https://ebpf.io/(30 分钟) +
- 简要了解 eBPF 内核相关文档:https://docs.ebpf.io/(了解技术细节的查询来源,30 分钟)
回答三个问题:
-
-
- 了解 eBPF 是什么东西?为啥要有这个玩意,不能用内核模块? -
- 它有什么功能?能在 Linux 内核里面完成哪些事情?有哪些 eBPF 程序的类型和 helper(不需要知道全部,但是需要知道去哪里找)? -
- 能拿来做什么?比如说在哪些场景中进行运用?网络、安全、可观测性? +
- 了解 eBPF 是什么?我们为什么需要它?难道不能使用内核模块吗? +
- 它有哪些功能?它在 Linux 内核中能做什么?eBPF 程序和助手函数有哪些类型(不需要全部了解,但需要知道在哪里查找)? +
- 它能用于哪些场景?例如,可以在哪些情况下使用?网络、安全、可观测性?
了解如何开发 eBPF 程序(10-15h)
-了解并尝试一下 eBPF 开发框架:
+理解如何开发 eBPF 程序(10-15 小时)
+了解并尝试 eBPF 开发框架:
-
-
- bpftrace 教程,对于最简单的应用来说,bpftrace 可能是最方便的:https://eunomia.dev/zh/tutorials/bpftrace-tutorial/ (试试,1h) -
- BCC 开发各类小工具的例子:https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md (跑一遍,3-4h) -
- libbpf 的一些例子:https://github.com/libbpf/libbpf-bootstrap (选感兴趣的运行一下,并阅读一下源代码,2h) -
- 基于 C 语言 libbpf, Go 语言或者 Rust 语言和 eunomia-bpf 的教程:https://github.com/eunomia-bpf/bpf-developer-tutorial (阅读 1-20 的部分,3-8h) +
- bpftrace 教程:https://eunomia.dev/tutorials/bpftrace-tutorial/(尝试,1 小时) +
- 使用 BCC 开发各种工具的示例:https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md(运行,3-4 小时) +
- libbpf 的一些示例:https://github.com/libbpf/libbpf-bootstrap(运行任何有趣的示例并阅读源代码,2 小时) +
- 教程:https://github.com/eunomia-bpf/bpf-developer-tutorial(阅读第 1-10 部分,3-4 小时)
有任何问题或者想了解的东西,不管是不是和本项目相关,都可以在本项目的 discussions 里面开始讨论。
-回答一些问题,并且进行一些尝试(2-5h):
+其他开发框架:Go 或 Rust 语言,请自行搜索和尝试(0-2 小时)
+如果有问题或想了解的内容,无论是否与本项目相关,都可以在该项目的讨论区开始讨论。
+回答一些问题并尝试一些实验(2-5 小时):
-
-
- 如何开发一个最简单的 eBPF 程序? -
- 如何用 eBPF 追踪一个内核功能或函数?有很多种方法,举出对应的代码; -
- 有哪些方案能通过用户态和内核态通信?如何从用户态向内核态传送信息?如何从内核态向用户态传递信息?举出代码示例; -
- 编写一个你自己的 eBPF 程序,实现一个功能; -
- eBPF 程序的整个生命周期里面,分别在用户态和内核态做了哪些事情? +
- 如何开发最简单的 eBPF 程序? +
- 如何使用 eBPF 追踪内核功能或函数?有很多方法,提供相应的代码示例; +
- 用户模式和内核模式之间的通信解决方案有哪些?如何将信息从用户模式发送到内核模式?如何将信息从内核模式传递到用户模式?提供代码示例; +
- 编写您自己的 eBPF 程序以实现某个功能; +
- 在 eBPF 程序的整个生命周期中,用户模式和内核模式分别做了什么?
3. 如何使用 eBPF 编程
-原始的 eBPF 程序编写是非常繁琐和困难的。为了改变这一现状,llvm 于 2015 年推出了可以将由高级语言编写的代码编译为 eBPF 字节码的功能,同时,eBPF 社区将 bpf() 等原始的系统调用进行了初步地封装,给出了 libbpf 库。这些库会包含将字节码加载到内核中的函数以及一些其他的关键函数。在 Linux 的源码包的 samples/bpf/ 目录下,有大量 Linux 提供的基于 libbpf 的 eBPF 样例代码。
一个典型的基于 libbpf 的 eBPF 程序具有 *_kern.c 和 *_user.c 两个文件,*_kern.c 中书写在内核中的挂载点以及处理函数,*_user.c 中书写用户态代码,完成内核态代码注入以及与用户交互的各种任务。 更为详细的教程可以参考该视频。然而由于该方法仍然较难理解且入门存在一定的难度,因此现阶段的eBPF程序开发大多基于一些工具,比如:
-
-
- BCC -
- BPFtrace -
- libbpf-bootstrap -
- Go eBPF library -
以及还有比较新的工具,例如 eunomia-bpf.
编写 eBPF 程序
-eBPF 程序由内核态部分和用户态部分构成。内核态部分包含程序的实际逻辑,用户态部分负责加载和管理内核态部分。使用 eunomia-bpf 开发工具,只需编写内核态部分的代码。
-内核态部分的代码需要符合 eBPF 的语法和指令集。eBPF 程序主要由若干个函数组成,每个函数都有其特定的作用。可以使用的函数类型包括:
--
-
- kprobe:插探函数,在指定的内核函数前或后执行。 -
- tracepoint:跟踪点函数,在指定的内核跟踪点处执行。 -
- raw_tracepoint:原始跟踪点函数,在指定的内核原始跟踪点处执行。 -
- xdp:网络数据处理函数,拦截和处理网络数据包。 -
- perf_event:性能事件函数,用于处理内核性能事件。 -
- kretprobe:函数返回插探函数,在指定的内核函数返回时执行。 -
- tracepoint_return:跟踪点函数返回,在指定的内核跟踪点返回时执行。 -
- raw_tracepoint_return:原始跟踪点函数返回,在指定的内核原始跟踪 -
BCC
-BCC 全称为 BPF Compiler Collection,该项目是一个 python 库, -包含了完整的编写、编译、和加载 BPF 程序的工具链,以及用于调试和诊断性能问题的工具。
-自 2015 年发布以来,BCC 经过上百位贡献者地不断完善后,目前已经包含了大量随时可用的跟踪工具。其官方项目库 -提供了一个方便上手的教程,用户可以快速地根据教程完成 BCC 入门工作。
-用户可以在 BCC 上使用 Python、Lua 等高级语言进行编程。 -相较于使用 C 语言直接编程,这些高级语言具有极大的便捷性,用户只需要使用 C 来设计内核中的 -BPF 程序,其余包括编译、解析、加载等工作在内,均可由 BCC 完成。
-然而使用 BCC 存在一个缺点便是在于其兼容性并不好。基于 BCC 的 -eBPF 程序每次执行时候都需要进行编译,编译则需要用户配置相关的头文件和对应实现。在实际应用中, -相信大家也会有体会,编译依赖问题是一个很棘手的问题。也正是因此,在本项目的开发中我们放弃了 BCC, -选择了可以做到一次编译-多次运行的 libbpf-bootstrap 工具。
-eBPF Go library
-eBPF Go 库提供了一个通用的 eBPF 库,它解耦了获取 eBPF 字节码的过程和 eBPF 程序的加载和管理,并实现了类似 libbpf 一样的 CO- 功能。eBPF 程序通常是通过编写高级语言创建的,然后使用 clang/LLVM 编译器编译为 eBPF 字节码。
-libbpf
-libbpf-bootstrap 是一个基于 libbpf 库的 BPF 开发脚手架,从其
-github 上可以得到其源码。
libbpf-bootstrap 综合了 BPF 社区过去多年的实践,为开发者提了一个现代化的、便捷的工作流,实
-现了一次编译,重复使用的目的。
基于 libbpf-bootstrap 的 BPF 程序对于源文件有一定的命名规则,
-用于生成内核态字节码的 bpf 文件以 .bpf.c 结尾,用户态加载字节码的文件以 .c 结尾,且这两个文件的
-前缀必须相同。
基于 libbpf-bootstrap 的 BPF 程序在编译时会先将 *.bpf.c 文件编译为
-对应的 .o 文件,然后根据此文件生成 skeleton 文件,即 *.skel.h,这个文件会包含内核态中定义的一些
-数据结构,以及用于装载内核态代码的关键函数。在用户态代码 include 此文件之后调用对应的装载函数即可将
-字节码装载到内核中。同样的,libbpf-bootstrap 也有非常完备的入门教程,用户可以在该处
-得到详细的入门操作介绍。
eunomia-bpf
-eunomia-bpf 是一个开源的 eBPF 动态加载运行时和开发工具链,是为了简化 eBPF 程序的开发、构建、分发、运行而设计的,基于 libbpf 的 CO-RE 轻量级开发框架。
-使用 eunomia-bpf ,可以:
--
-
- 在编写 eBPF 程序或工具时只编写内核态代码,自动获取内核态导出信息,并作为模块动态加载; -
- eunomia-bpf 可以将预编译的 eBPF 程序打包为通用的 JSON 或 WASM 模块,跨架构和内核版本进行分发,无需重新编译即可动态加载运行。 -
eunomia-bpf 由一个编译工具链和一个运行时库组成, 对比传统的 BCC、原生 libbpf 等框架,简化了 eBPF 程序的开发流程,在大多数时候只需编写内核态代码,即可轻松构建、打包、发布完整的 eBPF 应用。
---
-- eunomia-bpf 项目 Github 地址: https://github.com/eunomia-bpf/eunomia-bpf
-- gitee 镜像: https://gitee.com/anolis/eunomia
-
参考资料
- eBPF 介绍:https://ebpf.io/ -
- BPF Compiler Collection (BCC):https://github.com/iovisor/bcc +
- BPF 编译器集合(BCC):https://github.com/iovisor/bcc
- eunomia-bpf:https://github.com/eunomia-bpf/eunomia-bpf
您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程源代码。我们会继续分享更多有关 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术。
-- -+
您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/tutorials/ 获取更多示例和完整的教程源代码。所有内容均为开源。我们将继续分享更多关于 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术。
eBPF 入门开发实践教程一:Hello World,基本框架和开发流程
在本篇博客中,我们将深入探讨eBPF(Extended Berkeley Packet Filter)的基本框架和开发流程。eBPF是一种在Linux内核上运行的强大网络和性能分析工具,它为开发者提供了在内核运行时动态加载、更新和运行用户定义代码的能力。这使得开发者可以实现高效、安全的内核级别的网络监控、性能分析和故障排查等功能。
本文是eBPF入门开发实践教程的第二篇,我们将重点关注如何编写一个简单的eBPF程序,并通过实际例子演示整个开发流程。在阅读本教程之前,建议您先学习第一篇教程,以便对eBPF的基本概念有个大致的了解。
@@ -4523,7 +4542,7 @@ ndlock,lockdown,yama,integrity,apparmorGRUB_CMDLINE_LINUX="lsm=ndlock,lockdown,yama,integrity,apparmor,bpf"
并通过 update-grub2 命令更新 grub 配置(不同系统的对应命令可能不同),然后重启系统。
编写 eBPF 程序
+编写 eBPF 程序
// lsm-connect.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_core_read.h>
@@ -4629,7 +4648,7 @@ Retrying.
从协议栈上看,tc 位于链路层,其所在位置已经完成了 sk_buff 的分配,要晚于 xdp。为了实现对数据包发送和接收的控制,tc 使用队列结构来临时保存并组织数据包,在 tc 子系统中对应的数据结构和算法控制机制被抽象为 qdisc(Queueing discipline),其对外暴露数据包入队和出队的两个回调接口,并在内部隐藏排队算法实现。在 qdisc 中我们可以基于 filter 和 class 实现复杂的树形结构,其中 filter 被挂载到 qdisc 或 class 上用于实现具体的过滤逻辑,返回值决定了该数据包是否属于特定 class。
当数据包到达顶层 qdisc 时,其入队接口被调用,其上挂载的 filter 被依次执行直到一个 filter 匹配成功;此后数据包被送入该 filter 指向的 class,进入该 class 配置的 qdisc 处理流程中。tc 框架提供了所谓 classifier-action 机制,即在数据包匹配到特定 filter 时执行该 filter 所挂载的 action 对数据包进行处理,实现了完整的数据包分类和处理机制。
现有的 tc 为 eBPF 提供了 direct-action 模式,它使得一个作为 filter 加载的 eBPF 程序可以返回像 TC_ACT_OK 等 tc action 的返回值,而不是像传统的 filter 那样仅仅返回一个 classid 并把对数据包的处理交给 action 模块。现在,eBPF 程序可以被挂载到特定的 qdisc 上,并完成对数据包的分类和处理动作。
-编写 eBPF 程序
+编写 eBPF 程序
#include <vmlinux.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>
@@ -4774,7 +4793,7 @@ Packing ebpf object and config into package.json...
资源效率:XDP 不需要像 DPDK 等用户空间解决方案那样将整个 CPU 核心专用于数据包处理,因此它是高性能网络的更高效选择。
-编写 eBPF 程序
+编写 eBPF 程序
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
@@ -6820,7 +6839,7 @@ sudo ls -l /sys/fs/bpf/textreplace
郑昱笙
扩展伯克利数据包过滤器(eBPF)代表了我们与现代操作系统交互和扩展其能力方式的重大演变。作为一种强大的技术,它使得Linux内核能够响应事件运行沙盒程序,eBPF已成为系统可观察性、网络和安全特性的基石。
然而,像任何与内核紧密接口的系统一样,eBPF 运行时本身的安全性至关重要。在这篇博客中,我们将深入探讨常被忽视的 eBPF 安全性问题,探索旨在保护 eBPF 的机制本身如何被加固。我们将解析 eBPF 验证器的作用,审视当前的访问控制模型,并调查研究中的潜在改进机会。
-目录
+目录
- eBPF 运行时安全性:面临的挑战与前沿创新
@@ -8483,7 +8502,7 @@ The list of subcommands supported in your kernel can be found in file
bcc 参考指南
用于搜索 (Ctrl-F) 和参考。如需教程,请从 tutorial.md 开始。
该指南尚未完成。如果感觉有遗漏的内容,请查看 bcc 和内核源码。如果确认确实有遗漏,请发送拉取请求进行修复,并协助所有人。
-目录
+目录
- bcc 参考指南
-
diff --git a/searchindex.js b/searchindex.js
index 39efeb8..0068223 100644
--- a/searchindex.js
+++ b/searchindex.js
@@ -1 +1 @@
-Object.assign(window.search, {"doc_urls":["https://github.com/eunomia-bpf/bpf-developer-tutorial.html#httpsgithubcomeunomia-bpfbpf-developer-tutorial","0-introduce/index.html#ebpf-入门开发实践教程零介绍-ebpf-的基本概念常见的开发工具","0-introduce/index.html#1-ebpf-简介安全和有效地扩展内核","0-introduce/index.html#ebpf-的未来内核的-javascript-可编程接口","0-introduce/index.html#2-关于如何学习-ebpf-相关的开发的一些建议","0-introduce/index.html#ebpf-入门5-7h","0-introduce/index.html#了解如何开发-ebpf-程序10-15h","0-introduce/index.html#3-如何使用-ebpf-编程","0-introduce/index.html#编写-ebpf-程序","0-introduce/index.html#bcc","0-introduce/index.html#ebpf-go-library","0-introduce/index.html#libbpf","0-introduce/index.html#eunomia-bpf","0-introduce/index.html#参考资料","1-helloworld/index.html#ebpf-入门开发实践教程一hello-world基本框架和开发流程","1-helloworld/index.html#ebpf开发环境准备与基本开发流程","1-helloworld/index.html#安装必要的软件和工具","1-helloworld/index.html#下载安装-eunomia-bpf-开发工具","1-helloworld/index.html#hello-world---minimal-ebpf-program","1-helloworld/index.html#ebpf-程序的基本框架","1-helloworld/index.html#tracepoints","1-helloworld/index.html#github-模板轻松构建-ebpf-项目和开发环境","1-helloworld/index.html#总结","2-kprobe-unlink/index.html#ebpf-入门开发实践教程二在-ebpf-中使用-kprobe-监测捕获-unlink-系统调用","2-kprobe-unlink/index.html#kprobes-技术背景","2-kprobe-unlink/index.html#kprobe-示例","2-kprobe-unlink/index.html#总结","3-fentry-unlink/index.html#ebpf-入门开发实践教程三在-ebpf-中使用-fentry-监测捕获-unlink-系统调用","3-fentry-unlink/index.html#fentry","3-fentry-unlink/index.html#总结","4-opensnoop/index.html#ebpf-入门开发实践教程四在-ebpf-中捕获进程打开文件的系统调用集合使用全局变量过滤进程-pid","4-opensnoop/index.html#在-ebpf-中捕获进程打开文件的系统调用集合","4-opensnoop/index.html#使用全局变量在-ebpf-中过滤进程-pid","4-opensnoop/index.html#总结","5-uprobe-bashreadline/index.html#ebpf-入门开发实践教程五在-ebpf-中使用--uprobe-捕获-bash-的-readline-函数调用","5-uprobe-bashreadline/index.html#什么是uprobe","5-uprobe-bashreadline/index.html#使用-uprobe-捕获-bash-的-readline-函数调用","5-uprobe-bashreadline/index.html#总结","6-sigsnoop/index.html#ebpf-入门开发实践教程六捕获进程发送信号的系统调用集合使用-hash-map-保存状态","6-sigsnoop/index.html#sigsnoop","6-sigsnoop/index.html#总结","7-execsnoop/index.html#ebpf-入门实践教程七捕获进程执行事件通过-perf-event-array-向用户态打印输出","7-execsnoop/index.html#perf-buffer","7-execsnoop/index.html#execsnoop","7-execsnoop/index.html#总结","8-exitsnoop/index.html#ebpf-入门开发实践教程八在-ebpf-中使用-exitsnoop-监控进程退出事件使用-ring-buffer-向用户态打印输出","8-exitsnoop/index.html#ring-buffer","8-exitsnoop/index.html#ebpf-ringbuf-vs-ebpf-perfbuf","8-exitsnoop/index.html#exitsnoop","8-exitsnoop/index.html#compile-and-run","8-exitsnoop/index.html#总结","9-runqlat/index.html#ebpf-入门开发实践教程九捕获进程调度延迟以直方图方式记录","9-runqlat/index.html#runqlat-原理","9-runqlat/index.html#runqlat-代码实现","9-runqlat/index.html#runqlatbpfc","9-runqlat/index.html#runqlath","9-runqlat/index.html#编译运行","9-runqlat/index.html#总结","10-hardirqs/index.html#ebpf-入门开发实践教程十在-ebpf-中使用-hardirqs-或-softirqs-捕获中断事件","10-hardirqs/index.html#hardirqs-和-softirqs-是什么","10-hardirqs/index.html#实现原理","10-hardirqs/index.html#hardirqs-代码实现","10-hardirqs/index.html#运行代码","10-hardirqs/index.html#总结","11-bootstrap/index.html#ebpf-入门开发实践教程十一在-ebpf-中使用-libbpf-开发用户态程序并跟踪-exec-和-exit-系统调用","11-bootstrap/index.html#libbpf-库以及为什么需要使用它","11-bootstrap/index.html#什么是-bootstrap","11-bootstrap/index.html#bootstrap","11-bootstrap/index.html#内核态-ebpf-程序-bootstrapbpfc","11-bootstrap/index.html#用户态bootstrapc","11-bootstrap/index.html#安装依赖","11-bootstrap/index.html#编译运行","11-bootstrap/index.html#总结","12-profile/index.html#ebpf-入门实践教程十二使用-ebpf-程序-profile-进行性能分析","12-profile/index.html#ebpf-工具profile-性能分析示例","12-profile/index.html#实现原理","12-profile/index.html#内核态部分","12-profile/index.html#用户态部分","12-profile/index.html#总结","13-tcpconnlat/index.html#ebpf入门开发实践教程十三统计-tcp-连接延时并使用-libbpf-在用户态处理数据","13-tcpconnlat/index.html#背景","13-tcpconnlat/index.html#tcpconnlat-工具概述","13-tcpconnlat/index.html#tcp-连接原理","13-tcpconnlat/index.html#tcpconnlat-的-ebpf-实现","13-tcpconnlat/index.html#tcp_v4_connect-函数解析","13-tcpconnlat/index.html#内核态代码","13-tcpconnlat/index.html#用户态数据处理","13-tcpconnlat/index.html#编译运行","13-tcpconnlat/index.html#总结","14-tcpstates/index.html#ebpf入门实践教程十四记录-tcp-连接状态与-tcp-rtt","14-tcpstates/index.html#tcprtt-与-tcpstates","14-tcpstates/index.html#tcpstate","14-tcpstates/index.html#定义-bpf-maps","14-tcpstates/index.html#追踪-tcp-连接状态变化","14-tcpstates/index.html#更新时间戳","14-tcpstates/index.html#tcprtt","14-tcpstates/index.html#编译运行","14-tcpstates/index.html#总结","15-javagc/index.html#ebpf-入门实践教程十五使用-usdt-捕获用户态-java-gc-事件耗时","15-javagc/index.html#usdt-介绍","15-javagc/index.html#用户层面的追踪机制用户级动态跟踪和-usdt","15-javagc/index.html#java-gc-介绍","15-javagc/index.html#ebpf-实现机制","15-javagc/index.html#内核态程序","15-javagc/index.html#用户态程序","15-javagc/index.html#安装依赖","15-javagc/index.html#编译运行","15-javagc/index.html#总结","16-memleak/index.html#ebpf-入门实践教程十六编写-ebpf-程序-memleak-监控内存泄漏","16-memleak/index.html#背景及其重要性","16-memleak/index.html#调试内存泄漏的挑战","16-memleak/index.html#ebpf-的作用","16-memleak/index.html#memleak-的实现原理","16-memleak/index.html#内核态-ebpf-程序实现","16-memleak/index.html#memleak-内核态-ebpf-程序实现","16-memleak/index.html#用户态程序","16-memleak/index.html#编译运行","16-memleak/index.html#总结","17-biopattern/index.html#ebpf-入门实践教程十七编写-ebpf-程序统计随机顺序磁盘-io","17-biopattern/index.html#随机顺序磁盘-io","17-biopattern/index.html#biopattern","17-biopattern/index.html#ebpf-biopattern-实现原理","17-biopattern/index.html#用户态代码","17-biopattern/index.html#总结","18-further-reading/index.html#更多的参考资料论文项目等等","18-further-reading/index.html#xrp-in-kernel-storage-functions-with-ebpf","18-further-reading/index.html#specification-and-verification-in-the-field-applying-formal-methods-to-bpf-just-in-time-compilers-in-the-linux-kernel","18-further-reading/index.html#λ-io-a-unified-io-stack-for-computational-storage","18-further-reading/index.html#extension-framework-for-file-systems-in-user-space","18-further-reading/index.html#electrode-accelerating-distributed-protocols-with-ebpf","18-further-reading/index.html#bmc-accelerating-memcached-using-safe-in-kernel-caching-and-pre-stack-processing","18-further-reading/index.html#hxdp-efficient-software-packet-processing-on-fpga-nics","18-further-reading/index.html#network-centric-distributed-tracing-with-deepflow-troubleshooting-your-microservices-in-zero-code","18-further-reading/index.html#fast-in-kernel-traffic-sketching-in-ebpf","18-further-reading/index.html#spright-extracting-the-server-from-serverless-computing-high-performance-ebpf-based-event-driven-shared-memory-processing","18-further-reading/index.html#kgent-kernel-extensions-large-language-model-agent","18-further-reading/index.html#programmable-system-call-security-with-ebpf","18-further-reading/index.html#cross-container-attacks-the-bewildered-ebpf-on-clouds","18-further-reading/index.html#comparing-security-in-ebpf-and-webassembly","18-further-reading/index.html#a-flow-based-ids-using-machine-learning-in-ebpf","18-further-reading/index.html#femto-containers-lightweight-virtualization-and-fault-isolation-for-small-software-functions-on-low-power-iot-microcontrollers","19-lsm-connect/index.html#ebpf-入门实践教程使用-lsm-进行安全检测防御","19-lsm-connect/index.html#背景","19-lsm-connect/index.html#lsm-概述","19-lsm-connect/index.html#确认-bpf-lsm-是否可用","19-lsm-connect/index.html#编写-ebpf-程序","19-lsm-connect/index.html#编译运行","19-lsm-connect/index.html#总结","19-lsm-connect/index.html#参考","20-tc/index.html#ebpf-入门实践教程二十使用-ebpf-进行-tc-流量控制","20-tc/index.html#背景","20-tc/index.html#tc-概述","20-tc/index.html#编写-ebpf-程序","20-tc/index.html#编译运行","20-tc/index.html#总结","20-tc/index.html#参考","21-xdp/index.html#ebpf-入门实践教程二十一-使用-xdp-进行可编程数据包处理","21-xdp/index.html#什么是-xdp","21-xdp/index.html#为什么选择-xdp","21-xdp/index.html#xdp-与其他方法的比较","21-xdp/index.html#xdp--ebpf-的优势","21-xdp/index.html#xdp-的项目和应用案例","21-xdp/index.html#1--cilium","21-xdp/index.html#2--katran","21-xdp/index.html#3--cloudflare-的-xdp-ddos-保护","21-xdp/index.html#为什么选择-xdp-而不是其他方法","21-xdp/index.html#编写-ebpf-程序","21-xdp/index.html#编译运行","21-xdp/index.html#总结","21-xdp/index.html#参考资料","22-android/index.html#在-android-上使用-ebpf-程序","22-android/index.html#背景","22-android/index.html#测试环境","22-android/index.html#环境搭建","22-android/index.html#工具构建","22-android/index.html#结果","22-android/index.html#成功案例","22-android/index.html#一些可能的报错原因","22-android/index.html#总结","22-android/index.html#参考","30-sslsniff/index.html#ebpf-实践教程使用-uprobe-捕获多种库的-ssltls-明文数据","30-sslsniff/index.html#背景知识","30-sslsniff/index.html#ssl-和-tls","30-sslsniff/index.html#tls-的工作原理","30-sslsniff/index.html#ebpf-和-uprobe","30-sslsniff/index.html#用户态库","30-sslsniff/index.html#openssl-api-分析","30-sslsniff/index.html#1-ssl_read-函数","30-sslsniff/index.html#2-ssl_write-函数","30-sslsniff/index.html#ebpf-内核态代码编写","30-sslsniff/index.html#数据结构","30-sslsniff/index.html#hook-函数","30-sslsniff/index.html#hook到握手过程","30-sslsniff/index.html#用户态辅助代码分析与解读","30-sslsniff/index.html#1-支持的库挂载","30-sslsniff/index.html#2-详细挂载逻辑","30-sslsniff/index.html#编译与运行","30-sslsniff/index.html#启动-sslsniff","30-sslsniff/index.html#执行-curl-命令","30-sslsniff/index.html#sslsniff-输出","30-sslsniff/index.html#显示延迟和握手过程","30-sslsniff/index.html#16进制输出","30-sslsniff/index.html#总结","23-http/index.html#通过-ebpf-socket-filter-或-syscall-trace-追踪-http-请求等七层协议---ebpf-实践教程","23-http/index.html#追踪-http-http2-等七层协议的挑战","23-http/index.html#ebpf-中的-socket-filter-与-syscall-追踪深入解析与比较","23-http/index.html#ebpf-socket-filter","23-http/index.html#ebpf-syscall-tracing","23-http/index.html#ebpf-的-socket-filter-和-syscall-追踪的对比","23-http/index.html#使用-ebpf-socket-filter-来捕获-http-流量","23-http/index.html#潜在缺陷","23-http/index.html#用户态代码","23-http/index.html#编译运行","23-http/index.html#使用-ebpf-syscall-tracepoint-来捕获-http-流量","23-http/index.html#hook-位置和流程","23-http/index.html#需要-hook-的完整系统调用","23-http/index.html#总结","29-sockops/index.html#ebpf-开发实践使用-sockops-加速网络请求转发","29-sockops/index.html#利用-ebpf-的-sockops-进行性能优化","29-sockops/index.html#示例程序","29-sockops/index.html#编译-ebpf-程序","29-sockops/index.html#加载-ebpf-程序","29-sockops/index.html#使用-iperf3-或-curl-进行测试","29-sockops/index.html#收集追踪","29-sockops/index.html#卸载-ebpf-程序","29-sockops/index.html#参考资料","24-hide/index.html#ebpf-开发实践使用-ebpf-隐藏进程或文件信息","24-hide/index.html#背景知识与实现机制","24-hide/index.html#内核态-ebpf-程序实现","24-hide/index.html#用户态-ebpf-程序实现","24-hide/index.html#编译运行隐藏-pid","24-hide/index.html#总结","25-signal/index.html#ebpf-入门实践教程用-bpf_send_signal-发送信号终止恶意进程","25-signal/index.html#使用场景","25-signal/index.html#现有方案的不足","25-signal/index.html#新方案的优势","25-signal/index.html#内核态代码分析","25-signal/index.html#代码分析","25-signal/index.html#1-数据结构定义-signalh","25-signal/index.html#2-ebpf-程序-signalbpfc","25-signal/index.html#编译运行","25-signal/index.html#总结","25-signal/index.html#参考资料","26-sudo/index.html#使用-ebpf-添加-sudo-用户","26-sudo/index.html#参考资料","27-replace/index.html#使用-ebpf-替换任意程序读取或写入的文本","27-replace/index.html#参考资料","28-detach/index.html#在应用程序退出后运行-ebpf-程序ebpf-程序的生命周期","28-detach/index.html#ebpf-程序的生命周期","28-detach/index.html#kubernetes-中的-ebpf通过远程过程调用rpc部署-ebpf-程序","28-detach/index.html#使用-detach-在应用程序退出后通过任何程序替换-ebpf","28-detach/index.html#运行示例","28-detach/index.html#参考资料","18-further-reading/ebpf-security.zh.html#ebpf-运行时安全性面临的挑战与前沿创新","18-further-reading/ebpf-security.zh.html#目录","18-further-reading/ebpf-security.zh.html#ebpf如何通过验证器确保安全","18-further-reading/ebpf-security.zh.html#ebpf验证器是什么以及它的作用","18-further-reading/ebpf-security.zh.html#ebpf-验证器的工作原理","18-further-reading/ebpf-security.zh.html#verifier-的挑战","18-further-reading/ebpf-security.zh.html#强化-ebpf-验证器的其他研究工作","18-further-reading/ebpf-security.zh.html#ebpf访问控制的限制","18-further-reading/ebpf-security.zh.html#cap_bpf","18-further-reading/ebpf-security.zh.html#bpf命名空间","18-further-reading/ebpf-security.zh.html#无特权ebpf","18-further-reading/ebpf-security.zh.html#可信的非特权bpf","18-further-reading/ebpf-security.zh.html#一些其他的解决方案","18-further-reading/ebpf-security.zh.html#moat实现安全的bpf内核扩展隔离","18-further-reading/ebpf-security.zh.html#利用动态沙箱释放无特权ebpf的潜力","18-further-reading/ebpf-security.zh.html#内核扩展验证是不切实际的","18-further-reading/ebpf-security.zh.html#wasm-bpfwebassembly-ebpf库工具链及运行时","18-further-reading/ebpf-security.zh.html#bpftime用户空间ebpf运行时用于uprobe系统调用钩子及插件","18-further-reading/ebpf-security.zh.html#结论","34-syscall/index.html#ebpf-开发实践使用-ebpf-修改系统调用参数","34-syscall/index.html#修改-open-系统调用的文件名","34-syscall/index.html#修改-bash-execve-的进程名称","34-syscall/index.html#总结","35-user-ringbuf/index.html#ebpf开发实践使用-user-ring-buffer-向内核异步发送信息","35-user-ringbuf/index.html#用户态和内核态环形队列user-ring-buffer和kernel-ring-buffer","35-user-ringbuf/index.html#一实现在用户态和内核态间使用-ring-buffer-传送数据","35-user-ringbuf/index.html#创建环形缓冲区","35-user-ringbuf/index.html#编写内核态程序","35-user-ringbuf/index.html#编写用户态程序","35-user-ringbuf/index.html#初始化环形缓冲区并轮询","35-user-ringbuf/index.html#二编译和运行代码","35-user-ringbuf/index.html#总结","36-userspace-ebpf/index.html#用户空间-ebpf-运行时深度解析与应用实践","36-userspace-ebpf/index.html#ebpf内核的动态扩展运行时与字节码","36-userspace-ebpf/index.html#ebpf-究竟是何方神圣","36-userspace-ebpf/index.html#ebpf-对现代计算和网络的深远影响","36-userspace-ebpf/index.html#用户空间-ebpf-运行时ebpf-的新生代","36-userspace-ebpf/index.html#什么是用户空间-ebpf-运行时","36-userspace-ebpf/index.html#特定运行时简介","36-userspace-ebpf/index.html#为什么用户空间版本的-ebpf-会吸引如此多的关注","36-userspace-ebpf/index.html#性能提升","36-userspace-ebpf/index.html#灵活性与集成度","36-userspace-ebpf/index.html#安全性加固","36-userspace-ebpf/index.html#调试与许可的便利性","36-userspace-ebpf/index.html#使用案例现有的-ebpf-用户空间应用","36-userspace-ebpf/index.html#用户空间-ebpf-运行时-vs-wasm-运行时","36-userspace-ebpf/index.html#ebpf-在用户空间运行时-vs-wasm-运行时云原生计算的新纪元","36-userspace-ebpf/index.html#ebpf-与-wasm-之间的技术差异","36-userspace-ebpf/index.html#bpftime-快速入门","36-userspace-ebpf/index.html#总结与前景","36-userspace-ebpf/index.html#参考资料","37-uprobe-rust/index.html#ebpf-实践使用-uprobe-追踪用户态-rust-应用","37-uprobe-rust/index.html#uprobe","37-uprobe-rust/index.html#rust","37-uprobe-rust/index.html#最简单的例子symbol-name-mangling","37-uprobe-rust/index.html#一个奇怪的现象多次调用获取参数","37-uprobe-rust/index.html#参考资料","38-btf-uprobe/index.html#借助-ebpf-和-btf让用户态也能一次编译到处运行","38-btf-uprobe/index.html#为什么我们需要co-re","38-btf-uprobe/index.html#co-re的解决方案","38-btf-uprobe/index.html#co-re的优点","38-btf-uprobe/index.html#用户空间应用程序co-re的问题","38-btf-uprobe/index.html#用户空间程序的btf","38-btf-uprobe/index.html#使用用户空间程序的btf","38-btf-uprobe/index.html#结论","bcc-documents/kernel-versions.html#linux-内核版本的-bpf-功能","bcc-documents/kernel-versions.html#ebpf支持","bcc-documents/kernel-versions.html#jit编译","bcc-documents/kernel-versions.html#主要特性","bcc-documents/kernel-versions.html#程序类型","bcc-documents/kernel-versions.html#map-types--aka--表格-在-bcc-术语中","bcc-documents/kernel-versions.html#map-类型","bcc-documents/kernel-versions.html#map-userspace-api","bcc-documents/kernel-versions.html#xdp","bcc-documents/kernel-versions.html#程序类型-1","bcc-documents/kernel_config.html#bpf-特性的内核配置","bcc-documents/kernel_config.html#与-bpf-相关的内核配置","bcc-documents/reference_guide.html#bcc-参考指南","bcc-documents/reference_guide.html#目录","bcc-documents/reference_guide.html#bpf-c","bcc-documents/reference_guide.html#events--arguments","bcc-documents/reference_guide.html#1-kprobes","bcc-documents/reference_guide.html#2-kretprobes","bcc-documents/reference_guide.html#3-tracepoints","bcc-documents/reference_guide.html#4-uprobes","bcc-documents/reference_guide.html#6-usdt探测点","bcc-documents/reference_guide.html#7-原始跟踪点","bcc-documents/reference_guide.html#8-系统调用跟踪点","bcc-documents/reference_guide.html#9-kfuncs","bcc-documents/reference_guide.html#10-kretfuncs","bcc-documents/reference_guide.html#11-lsm-probes","bcc-documents/reference_guide.html#12-bpf迭代器","bcc-documents/reference_guide.html#数据","bcc-documents/reference_guide.html#1-bpf_probe_read_kernel","bcc-documents/reference_guide.html#2-bpf_probe_read_kernel_strshell","bcc-documents/reference_guide.html#3-bpf_ktime_get_ns","bcc-documents/reference_guide.html#4-bpf_get_current_pid_tgid","bcc-documents/reference_guide.html#5-bpf_get_current_uid_gid","bcc-documents/reference_guide.html#6-bpf_get_current_comm","bcc-documents/reference_guide.html#7-bpf_get_current_task","bcc-documents/reference_guide.html#8-bpf_log2l","bcc-documents/reference_guide.html#9-bpf_get_prandom_u32","bcc-documents/reference_guide.html#10-bpf_probe_read_user","bcc-documents/reference_guide.html#11-bpf_probe_read_user_str","bcc-documents/reference_guide.html#12-bpf_get_ns_current_pid_tgid","bcc-documents/reference_guide.html#调试","bcc-documents/reference_guide.html#1-bpf_override_return","bcc-documents/reference_guide.html#输出","bcc-documents/reference_guide.html#1-bpf_trace_printk","bcc-documents/reference_guide.html#2-bpf_perf_output","bcc-documents/reference_guide.html#3-perf_submit","bcc-documents/reference_guide.html#4-perf_submit_skb","bcc-documents/reference_guide.html#5-bpf_ringbuf_output","bcc-documents/reference_guide.html#6-ringbuf_output","bcc-documents/reference_guide.html#7-ringbuf_reserve","bcc-documents/reference_guide.html#8-ringbuf_submit","bcc-documents/reference_guide.html#9-ringbuf_discard","bcc-documents/reference_guide.html#maps","bcc-documents/reference_guide.html#1-bpf_table","bcc-documents/reference_guide.html#2-bpf_hash","bcc-documents/reference_guide.html#3-bpf_array","bcc-documents/reference_guide.html#4-bpf_histogram","bcc-documents/reference_guide.html#5-bpf_stack_trace","bcc-documents/reference_guide.html#6-bpf_perf_array","bcc-documents/reference_guide.html#7-bpf_percpu_hash","bcc-documents/reference_guide.html#8-bpf_percpu_array","bcc-documents/reference_guide.html#9-bpf_lpm_trie","bcc-documents/reference_guide.html#10-bpf_prog_array","bcc-documents/reference_guide.html#11-bpf_devmap","bcc-documents/reference_guide.html#12-bpf_cpumap","bcc-documents/reference_guide.html#13-bpf_xskmap","bcc-documents/reference_guide.html#14-bpf_array_of_maps","bcc-documents/reference_guide.html#15-bpf_hash_of_maps","bcc-documents/reference_guide.html#16-bpf_stack","bcc-documents/reference_guide.html#17-bpf_queue","bcc-documents/reference_guide.html#18-bpf_sockhash","bcc-documents/reference_guide.html#19-maplookup","bcc-documents/reference_guide.html#20-maplookup_or_try_init","bcc-documents/reference_guide.html#21-mapdelete","bcc-documents/reference_guide.html#22-mapupdate","bcc-documents/reference_guide.html#23-mapinsert","bcc-documents/reference_guide.html#24-mapincrement","bcc-documents/reference_guide.html#25-mapget_stackid","bcc-documents/reference_guide.html#26-mapperf_read","bcc-documents/reference_guide.html#27-mapcall","bcc-documents/reference_guide.html#28-mapredirect_map","bcc-documents/reference_guide.html#29-mappush","bcc-documents/reference_guide.html#30-mappop","bcc-documents/reference_guide.html#31-mappeek","bcc-documents/reference_guide.html#32-mapsock_hash_update","bcc-documents/reference_guide.html#33-mapmsg_redirect_hash","bcc-documents/reference_guide.html#34-mapsk_redirect_hash","bcc-documents/reference_guide.html#许可证","bcc-documents/reference_guide.html#rewriter","bcc-documents/reference_guide.html#bcc-python","bcc-documents/reference_guide.html#初始化","bcc-documents/reference_guide.html#1-bpf","bcc-documents/reference_guide.html#事件","bcc-documents/reference_guide.html#1-attach_kprobe","bcc-documents/reference_guide.html#2-attach_kretprobe","bcc-documents/reference_guide.html#3-attach_tracepoint","bcc-documents/reference_guide.html#4-attach_uprobe","bcc-documents/reference_guide.html#5-attach_uretprobe","bcc-documents/reference_guide.html#6-usdtenable_probe","bcc-documents/reference_guide.html#7-attach_raw_tracepoint","bcc-documents/reference_guide.html#8-attach_raw_socket","bcc-documents/reference_guide.html#9-attach_xdp","bcc-documents/reference_guide.html#10-attach_func","bcc-documents/reference_guide.html#12-detach_kprobe","bcc-documents/reference_guide.html#13-detach_kretprobe","bcc-documents/reference_guide.html#调试输出","bcc-documents/reference_guide.html#1-trace_print","bcc-documents/reference_guide.html#2-trace_fields","bcc-documents/reference_guide.html#输出-api","bcc-documents/reference_guide.html#1-perf_buffer_poll","bcc-documents/reference_guide.html#2-ring_buffer_poll","bcc-documents/reference_guide.html#3-ring_buffer_consume","bcc-documents/reference_guide.html#map-apis","bcc-documents/reference_guide.html#1-get_table","bcc-documents/reference_guide.html#2-open_perf_buffer","bcc-documents/reference_guide.html#4-values","bcc-documents/reference_guide.html#5-clear","bcc-documents/reference_guide.html#6-items_lookup_and_delete_batch","bcc-documents/reference_guide.html#7-items_lookup_batch","bcc-documents/reference_guide.html#8-items_delete_batch","bcc-documents/reference_guide.html#9-items_update_batch","bcc-documents/reference_guide.html#11-print_linear_hist语法-tableprint_linear_histval_typevalue-section_headerbucket-ptr-section_print_fnnone","bcc-documents/reference_guide.html#12-open_ring_buffer","bcc-documents/reference_guide.html#13-push","bcc-documents/reference_guide.html#14-pop","bcc-documents/reference_guide.html#15-peek","bcc-documents/reference_guide.html#辅助方法","bcc-documents/reference_guide.html#1-ksym","bcc-documents/reference_guide.html#2-ksymname","bcc-documents/reference_guide.html#3-sym","bcc-documents/reference_guide.html#4-num_open_kprobes","bcc-documents/reference_guide.html#5-get_syscall_fnname","bcc-documents/reference_guide.html#bpf-错误","bcc-documents/reference_guide.html#1-invalid-mem-access","bcc-documents/reference_guide.html#2-无法从专有程序调用-gpl-only-函数","bcc-documents/reference_guide.html#环境变量","bcc-documents/reference_guide.html#1-内核源代码目录","bcc-documents/reference_guide.html#2-内核版本覆盖","bcc-documents/special_filtering.html#特殊过滤","bcc-documents/special_filtering.html#按-cgroups过滤","bcc-documents/special_filtering.html#按命名空间选择挂载点进行过滤","bcc-documents/tutorial.html#bcc-教程","bcc-documents/tutorial.html#可观察性","bcc-documents/tutorial.html#0-使用bcc之前","bcc-documents/tutorial.html#1-性能分析","bcc-documents/tutorial.html#2-使用通用工具进行可观察性","bcc-documents/tutorial.html#网络","bcc-documents/tutorial_bcc_python_developer.html#bcc-python-开发者教程","bcc-documents/tutorial_bcc_python_developer.html#可观测性","bcc-documents/tutorial_bcc_python_developer.html#第1课-你好世界","bcc-documents/tutorial_bcc_python_developer.html#第二课-sys_sync","bcc-documents/tutorial_bcc_python_developer.html#第三课-hello_fieldspy","bcc-documents/tutorial_bcc_python_developer.html#lesson-4-sync_timingpy","bcc-documents/tutorial_bcc_python_developer.html#第5课-sync_countpy","bcc-documents/tutorial_bcc_python_developer.html#第6课-disksnooppy","bcc-documents/tutorial_bcc_python_developer.html#lesson-7-hello_perf_outputpy","bcc-documents/tutorial_bcc_python_developer.html#第八课-sync_perf_outputpy","bcc-documents/tutorial_bcc_python_developer.html#第九课-bitehistpy","bcc-documents/tutorial_bcc_python_developer.html#lesson-10-disklatencypy-lesson-11-vfsreadlatpy","bcc-documents/tutorial_bcc_python_developer.html#lesson-12-urandomreadpy","bcc-documents/tutorial_bcc_python_developer.html#第13课-disksnooppy已修复","bcc-documents/tutorial_bcc_python_developer.html#第14课-strlen_countpy","bcc-documents/tutorial_bcc_python_developer.html#第15课nodejs_http_serverpy","bcc-documents/tutorial_bcc_python_developer.html#第16课-task_switchc","bcc-documents/tutorial_bcc_python_developer.html#第17课-进一步研究","bcc-documents/tutorial_bcc_python_developer.html#网络","bpftrace-tutorial/index.html#bpftrace一行教程","bpftrace-tutorial/index.html#1-列出所有探针","bpftrace-tutorial/index.html#2-hello-world","bpftrace-tutorial/index.html#3-文件打开","bpftrace-tutorial/index.html#4-进程级系统调用计数","bpftrace-tutorial/index.html#5-read返回值分布统计","bpftrace-tutorial/index.html#6-内核动态跟踪read返回的字节数","bpftrace-tutorial/index.html#7-read调用的时间","bpftrace-tutorial/index.html#8-统计进程级别的事件","bpftrace-tutorial/index.html#9-分析内核实时函数栈","bpftrace-tutorial/index.html#10-调度器跟踪","bpftrace-tutorial/index.html#11-块级io跟踪","bpftrace-tutorial/index.html#12-内核结构跟踪"],"index":{"documentStore":{"docInfo":{"0":{"body":0,"breadcrumbs":8,"title":4},"1":{"body":0,"breadcrumbs":5,"title":2},"10":{"body":10,"breadcrumbs":6,"title":3},"100":{"body":179,"breadcrumbs":4,"title":1},"101":{"body":10,"breadcrumbs":5,"title":2},"102":{"body":3,"breadcrumbs":4,"title":1},"103":{"body":182,"breadcrumbs":3,"title":0},"104":{"body":200,"breadcrumbs":3,"title":0},"105":{"body":24,"breadcrumbs":3,"title":0},"106":{"body":51,"breadcrumbs":3,"title":0},"107":{"body":22,"breadcrumbs":3,"title":0},"108":{"body":4,"breadcrumbs":6,"title":3},"109":{"body":2,"breadcrumbs":3,"title":0},"11":{"body":24,"breadcrumbs":4,"title":1},"110":{"body":5,"breadcrumbs":3,"title":0},"111":{"body":48,"breadcrumbs":4,"title":1},"112":{"body":25,"breadcrumbs":4,"title":1},"113":{"body":0,"breadcrumbs":4,"title":1},"114":{"body":564,"breadcrumbs":5,"title":2},"115":{"body":88,"breadcrumbs":3,"title":0},"116":{"body":61,"breadcrumbs":3,"title":0},"117":{"body":21,"breadcrumbs":3,"title":0},"118":{"body":5,"breadcrumbs":6,"title":3},"119":{"body":22,"breadcrumbs":4,"title":1},"12":{"body":29,"breadcrumbs":5,"title":2},"120":{"body":82,"breadcrumbs":4,"title":1},"121":{"body":287,"breadcrumbs":5,"title":2},"122":{"body":154,"breadcrumbs":3,"title":0},"123":{"body":23,"breadcrumbs":3,"title":0},"124":{"body":33,"breadcrumbs":4,"title":0},"125":{"body":14,"breadcrumbs":9,"title":5},"126":{"body":20,"breadcrumbs":15,"title":11},"127":{"body":9,"breadcrumbs":10,"title":6},"128":{"body":7,"breadcrumbs":10,"title":6},"129":{"body":2,"breadcrumbs":9,"title":5},"13":{"body":21,"breadcrumbs":3,"title":0},"130":{"body":2,"breadcrumbs":14,"title":10},"131":{"body":5,"breadcrumbs":11,"title":7},"132":{"body":3,"breadcrumbs":13,"title":9},"133":{"body":5,"breadcrumbs":9,"title":5},"134":{"body":11,"breadcrumbs":18,"title":14},"135":{"body":4,"breadcrumbs":11,"title":7},"136":{"body":13,"breadcrumbs":9,"title":5},"137":{"body":24,"breadcrumbs":10,"title":6},"138":{"body":18,"breadcrumbs":8,"title":4},"139":{"body":6,"breadcrumbs":11,"title":7},"14":{"body":11,"breadcrumbs":6,"title":3},"140":{"body":17,"breadcrumbs":17,"title":13},"141":{"body":9,"breadcrumbs":6,"title":2},"142":{"body":14,"breadcrumbs":4,"title":0},"143":{"body":24,"breadcrumbs":5,"title":1},"144":{"body":19,"breadcrumbs":6,"title":2},"145":{"body":103,"breadcrumbs":5,"title":1},"146":{"body":122,"breadcrumbs":4,"title":0},"147":{"body":20,"breadcrumbs":4,"title":0},"148":{"body":10,"breadcrumbs":4,"title":0},"149":{"body":0,"breadcrumbs":6,"title":3},"15":{"body":0,"breadcrumbs":4,"title":1},"150":{"body":15,"breadcrumbs":3,"title":0},"151":{"body":39,"breadcrumbs":4,"title":1},"152":{"body":112,"breadcrumbs":4,"title":1},"153":{"body":75,"breadcrumbs":3,"title":0},"154":{"body":15,"breadcrumbs":3,"title":0},"155":{"body":6,"breadcrumbs":3,"title":0},"156":{"body":6,"breadcrumbs":5,"title":2},"157":{"body":9,"breadcrumbs":4,"title":1},"158":{"body":7,"breadcrumbs":4,"title":1},"159":{"body":10,"breadcrumbs":4,"title":1},"16":{"body":35,"breadcrumbs":3,"title":0},"160":{"body":18,"breadcrumbs":5,"title":2},"161":{"body":1,"breadcrumbs":4,"title":1},"162":{"body":5,"breadcrumbs":5,"title":2},"163":{"body":8,"breadcrumbs":5,"title":2},"164":{"body":12,"breadcrumbs":7,"title":4},"165":{"body":10,"breadcrumbs":4,"title":1},"166":{"body":64,"breadcrumbs":4,"title":1},"167":{"body":63,"breadcrumbs":3,"title":0},"168":{"body":12,"breadcrumbs":3,"title":0},"169":{"body":16,"breadcrumbs":3,"title":0},"17":{"body":71,"breadcrumbs":5,"title":2},"170":{"body":13,"breadcrumbs":4,"title":2},"171":{"body":60,"breadcrumbs":2,"title":0},"172":{"body":19,"breadcrumbs":2,"title":0},"173":{"body":76,"breadcrumbs":2,"title":0},"174":{"body":10,"breadcrumbs":2,"title":0},"175":{"body":2,"breadcrumbs":2,"title":0},"176":{"body":440,"breadcrumbs":2,"title":0},"177":{"body":45,"breadcrumbs":2,"title":0},"178":{"body":35,"breadcrumbs":2,"title":0},"179":{"body":3,"breadcrumbs":2,"title":0},"18":{"body":152,"breadcrumbs":8,"title":5},"180":{"body":10,"breadcrumbs":5,"title":3},"181":{"body":0,"breadcrumbs":2,"title":0},"182":{"body":12,"breadcrumbs":4,"title":2},"183":{"body":15,"breadcrumbs":3,"title":1},"184":{"body":19,"breadcrumbs":4,"title":2},"185":{"body":3,"breadcrumbs":2,"title":0},"186":{"body":6,"breadcrumbs":4,"title":2},"187":{"body":26,"breadcrumbs":4,"title":2},"188":{"body":28,"breadcrumbs":4,"title":2},"189":{"body":5,"breadcrumbs":3,"title":1},"19":{"body":17,"breadcrumbs":4,"title":1},"190":{"body":35,"breadcrumbs":2,"title":0},"191":{"body":166,"breadcrumbs":3,"title":1},"192":{"body":174,"breadcrumbs":3,"title":1},"193":{"body":3,"breadcrumbs":2,"title":0},"194":{"body":40,"breadcrumbs":3,"title":1},"195":{"body":213,"breadcrumbs":3,"title":1},"196":{"body":4,"breadcrumbs":2,"title":0},"197":{"body":3,"breadcrumbs":3,"title":1},"198":{"body":13,"breadcrumbs":3,"title":1},"199":{"body":20,"breadcrumbs":3,"title":1},"2":{"body":5,"breadcrumbs":5,"title":2},"20":{"body":5,"breadcrumbs":4,"title":1},"200":{"body":37,"breadcrumbs":2,"title":0},"201":{"body":14,"breadcrumbs":3,"title":1},"202":{"body":14,"breadcrumbs":2,"title":0},"203":{"body":19,"breadcrumbs":13,"title":7},"204":{"body":16,"breadcrumbs":8,"title":2},"205":{"body":0,"breadcrumbs":10,"title":4},"206":{"body":14,"breadcrumbs":9,"title":3},"207":{"body":9,"breadcrumbs":9,"title":3},"208":{"body":16,"breadcrumbs":10,"title":4},"209":{"body":526,"breadcrumbs":10,"title":4},"21":{"body":43,"breadcrumbs":5,"title":2},"210":{"body":0,"breadcrumbs":6,"title":0},"211":{"body":51,"breadcrumbs":6,"title":0},"212":{"body":75,"breadcrumbs":6,"title":0},"213":{"body":142,"breadcrumbs":10,"title":4},"214":{"body":1,"breadcrumbs":7,"title":1},"215":{"body":14,"breadcrumbs":7,"title":1},"216":{"body":18,"breadcrumbs":6,"title":0},"217":{"body":20,"breadcrumbs":3,"title":2},"218":{"body":21,"breadcrumbs":3,"title":2},"219":{"body":199,"breadcrumbs":1,"title":0},"22":{"body":43,"breadcrumbs":3,"title":0},"220":{"body":14,"breadcrumbs":2,"title":1},"221":{"body":222,"breadcrumbs":2,"title":1},"222":{"body":22,"breadcrumbs":3,"title":2},"223":{"body":194,"breadcrumbs":1,"title":0},"224":{"body":2,"breadcrumbs":2,"title":1},"225":{"body":11,"breadcrumbs":1,"title":0},"226":{"body":4,"breadcrumbs":3,"title":2},"227":{"body":12,"breadcrumbs":1,"title":0},"228":{"body":661,"breadcrumbs":2,"title":1},"229":{"body":145,"breadcrumbs":2,"title":1},"23":{"body":14,"breadcrumbs":8,"title":4},"230":{"body":178,"breadcrumbs":2,"title":1},"231":{"body":10,"breadcrumbs":1,"title":0},"232":{"body":9,"breadcrumbs":3,"title":2},"233":{"body":4,"breadcrumbs":1,"title":0},"234":{"body":5,"breadcrumbs":1,"title":0},"235":{"body":8,"breadcrumbs":1,"title":0},"236":{"body":5,"breadcrumbs":1,"title":0},"237":{"body":0,"breadcrumbs":1,"title":0},"238":{"body":22,"breadcrumbs":3,"title":2},"239":{"body":134,"breadcrumbs":4,"title":3},"24":{"body":58,"breadcrumbs":5,"title":1},"240":{"body":48,"breadcrumbs":1,"title":0},"241":{"body":7,"breadcrumbs":1,"title":0},"242":{"body":4,"breadcrumbs":1,"title":0},"243":{"body":24,"breadcrumbs":4,"title":2},"244":{"body":2,"breadcrumbs":2,"title":0},"245":{"body":42,"breadcrumbs":2,"title":1},"246":{"body":2,"breadcrumbs":1,"title":0},"247":{"body":21,"breadcrumbs":5,"title":2},"248":{"body":16,"breadcrumbs":4,"title":1},"249":{"body":23,"breadcrumbs":6,"title":3},"25":{"body":221,"breadcrumbs":5,"title":1},"250":{"body":79,"breadcrumbs":5,"title":2},"251":{"body":42,"breadcrumbs":3,"title":0},"252":{"body":20,"breadcrumbs":3,"title":0},"253":{"body":4,"breadcrumbs":2,"title":1},"254":{"body":17,"breadcrumbs":1,"title":0},"255":{"body":0,"breadcrumbs":2,"title":1},"256":{"body":0,"breadcrumbs":2,"title":1},"257":{"body":0,"breadcrumbs":2,"title":1},"258":{"body":2,"breadcrumbs":2,"title":1},"259":{"body":21,"breadcrumbs":2,"title":1},"26":{"body":19,"breadcrumbs":4,"title":0},"260":{"body":4,"breadcrumbs":2,"title":1},"261":{"body":0,"breadcrumbs":2,"title":1},"262":{"body":16,"breadcrumbs":2,"title":1},"263":{"body":0,"breadcrumbs":2,"title":1},"264":{"body":16,"breadcrumbs":2,"title":1},"265":{"body":0,"breadcrumbs":1,"title":0},"266":{"body":1,"breadcrumbs":2,"title":1},"267":{"body":3,"breadcrumbs":2,"title":1},"268":{"body":2,"breadcrumbs":1,"title":0},"269":{"body":10,"breadcrumbs":4,"title":3},"27":{"body":10,"breadcrumbs":8,"title":4},"270":{"body":9,"breadcrumbs":2,"title":1},"271":{"body":14,"breadcrumbs":1,"title":0},"272":{"body":11,"breadcrumbs":3,"title":2},"273":{"body":219,"breadcrumbs":2,"title":1},"274":{"body":133,"breadcrumbs":3,"title":2},"275":{"body":7,"breadcrumbs":1,"title":0},"276":{"body":14,"breadcrumbs":8,"title":4},"277":{"body":56,"breadcrumbs":9,"title":5},"278":{"body":14,"breadcrumbs":6,"title":2},"279":{"body":32,"breadcrumbs":4,"title":0},"28":{"body":198,"breadcrumbs":5,"title":1},"280":{"body":121,"breadcrumbs":4,"title":0},"281":{"body":58,"breadcrumbs":4,"title":0},"282":{"body":44,"breadcrumbs":4,"title":0},"283":{"body":45,"breadcrumbs":4,"title":0},"284":{"body":14,"breadcrumbs":4,"title":0},"285":{"body":13,"breadcrumbs":2,"title":1},"286":{"body":0,"breadcrumbs":2,"title":1},"287":{"body":18,"breadcrumbs":2,"title":1},"288":{"body":0,"breadcrumbs":2,"title":1},"289":{"body":0,"breadcrumbs":3,"title":2},"29":{"body":24,"breadcrumbs":4,"title":0},"290":{"body":9,"breadcrumbs":2,"title":1},"291":{"body":87,"breadcrumbs":1,"title":0},"292":{"body":1,"breadcrumbs":2,"title":1},"293":{"body":3,"breadcrumbs":1,"title":0},"294":{"body":2,"breadcrumbs":1,"title":0},"295":{"body":2,"breadcrumbs":1,"title":0},"296":{"body":5,"breadcrumbs":1,"title":0},"297":{"body":65,"breadcrumbs":2,"title":1},"298":{"body":3,"breadcrumbs":4,"title":3},"299":{"body":3,"breadcrumbs":4,"title":3},"3":{"body":43,"breadcrumbs":5,"title":2},"30":{"body":10,"breadcrumbs":6,"title":3},"300":{"body":21,"breadcrumbs":3,"title":2},"301":{"body":81,"breadcrumbs":2,"title":1},"302":{"body":12,"breadcrumbs":1,"title":0},"303":{"body":49,"breadcrumbs":1,"title":0},"304":{"body":10,"breadcrumbs":5,"title":3},"305":{"body":27,"breadcrumbs":3,"title":1},"306":{"body":4,"breadcrumbs":3,"title":1},"307":{"body":65,"breadcrumbs":5,"title":3},"308":{"body":138,"breadcrumbs":2,"title":0},"309":{"body":3,"breadcrumbs":2,"title":0},"31":{"body":159,"breadcrumbs":4,"title":1},"310":{"body":30,"breadcrumbs":4,"title":2},"311":{"body":0,"breadcrumbs":4,"title":2},"312":{"body":3,"breadcrumbs":4,"title":2},"313":{"body":2,"breadcrumbs":4,"title":2},"314":{"body":2,"breadcrumbs":4,"title":2},"315":{"body":186,"breadcrumbs":3,"title":1},"316":{"body":430,"breadcrumbs":3,"title":1},"317":{"body":22,"breadcrumbs":2,"title":0},"318":{"body":0,"breadcrumbs":7,"title":2},"319":{"body":2,"breadcrumbs":6,"title":1},"32":{"body":106,"breadcrumbs":5,"title":2},"320":{"body":48,"breadcrumbs":6,"title":1},"321":{"body":164,"breadcrumbs":5,"title":0},"322":{"body":225,"breadcrumbs":5,"title":0},"323":{"body":0,"breadcrumbs":9,"title":4},"324":{"body":163,"breadcrumbs":6,"title":1},"325":{"body":81,"breadcrumbs":8,"title":3},"326":{"body":964,"breadcrumbs":6,"title":1},"327":{"body":145,"breadcrumbs":5,"title":0},"328":{"body":0,"breadcrumbs":5,"title":1},"329":{"body":100,"breadcrumbs":5,"title":1},"33":{"body":28,"breadcrumbs":3,"title":0},"330":{"body":4,"breadcrumbs":4,"title":1},"331":{"body":238,"breadcrumbs":3,"title":0},"332":{"body":2,"breadcrumbs":5,"title":2},"333":{"body":0,"breadcrumbs":5,"title":2},"334":{"body":34,"breadcrumbs":5,"title":2},"335":{"body":16,"breadcrumbs":5,"title":2},"336":{"body":34,"breadcrumbs":5,"title":2},"337":{"body":39,"breadcrumbs":5,"title":2},"338":{"body":23,"breadcrumbs":5,"title":2},"339":{"body":48,"breadcrumbs":4,"title":1},"34":{"body":10,"breadcrumbs":9,"title":5},"340":{"body":36,"breadcrumbs":4,"title":1},"341":{"body":20,"breadcrumbs":5,"title":2},"342":{"body":22,"breadcrumbs":5,"title":2},"343":{"body":31,"breadcrumbs":6,"title":3},"344":{"body":36,"breadcrumbs":5,"title":2},"345":{"body":0,"breadcrumbs":3,"title":0},"346":{"body":10,"breadcrumbs":5,"title":2},"347":{"body":13,"breadcrumbs":5,"title":2},"348":{"body":5,"breadcrumbs":5,"title":2},"349":{"body":9,"breadcrumbs":5,"title":2},"35":{"body":17,"breadcrumbs":5,"title":1},"350":{"body":8,"breadcrumbs":5,"title":2},"351":{"body":17,"breadcrumbs":5,"title":2},"352":{"body":22,"breadcrumbs":5,"title":2},"353":{"body":6,"breadcrumbs":5,"title":2},"354":{"body":3,"breadcrumbs":5,"title":2},"355":{"body":8,"breadcrumbs":5,"title":2},"356":{"body":11,"breadcrumbs":5,"title":2},"357":{"body":18,"breadcrumbs":5,"title":2},"358":{"body":0,"breadcrumbs":3,"title":0},"359":{"body":19,"breadcrumbs":5,"title":2},"36":{"body":204,"breadcrumbs":7,"title":3},"360":{"body":0,"breadcrumbs":3,"title":0},"361":{"body":7,"breadcrumbs":5,"title":2},"362":{"body":29,"breadcrumbs":5,"title":2},"363":{"body":13,"breadcrumbs":5,"title":2},"364":{"body":10,"breadcrumbs":5,"title":2},"365":{"body":16,"breadcrumbs":5,"title":2},"366":{"body":9,"breadcrumbs":5,"title":2},"367":{"body":5,"breadcrumbs":5,"title":2},"368":{"body":7,"breadcrumbs":5,"title":2},"369":{"body":8,"breadcrumbs":5,"title":2},"37":{"body":23,"breadcrumbs":4,"title":0},"370":{"body":0,"breadcrumbs":4,"title":1},"371":{"body":17,"breadcrumbs":5,"title":2},"372":{"body":15,"breadcrumbs":5,"title":2},"373":{"body":12,"breadcrumbs":5,"title":2},"374":{"body":15,"breadcrumbs":5,"title":2},"375":{"body":11,"breadcrumbs":5,"title":2},"376":{"body":16,"breadcrumbs":5,"title":2},"377":{"body":18,"breadcrumbs":5,"title":2},"378":{"body":13,"breadcrumbs":5,"title":2},"379":{"body":18,"breadcrumbs":5,"title":2},"38":{"body":10,"breadcrumbs":6,"title":3},"380":{"body":13,"breadcrumbs":5,"title":2},"381":{"body":10,"breadcrumbs":5,"title":2},"382":{"body":13,"breadcrumbs":5,"title":2},"383":{"body":8,"breadcrumbs":5,"title":2},"384":{"body":16,"breadcrumbs":5,"title":2},"385":{"body":15,"breadcrumbs":5,"title":2},"386":{"body":20,"breadcrumbs":5,"title":2},"387":{"body":20,"breadcrumbs":5,"title":2},"388":{"body":27,"breadcrumbs":5,"title":2},"389":{"body":4,"breadcrumbs":5,"title":2},"39":{"body":256,"breadcrumbs":4,"title":1},"390":{"body":5,"breadcrumbs":5,"title":2},"391":{"body":3,"breadcrumbs":5,"title":2},"392":{"body":4,"breadcrumbs":5,"title":2},"393":{"body":4,"breadcrumbs":5,"title":2},"394":{"body":13,"breadcrumbs":5,"title":2},"395":{"body":12,"breadcrumbs":5,"title":2},"396":{"body":4,"breadcrumbs":5,"title":2},"397":{"body":39,"breadcrumbs":5,"title":2},"398":{"body":52,"breadcrumbs":5,"title":2},"399":{"body":6,"breadcrumbs":5,"title":2},"4":{"body":1,"breadcrumbs":5,"title":2},"40":{"body":30,"breadcrumbs":3,"title":0},"400":{"body":3,"breadcrumbs":5,"title":2},"401":{"body":4,"breadcrumbs":5,"title":2},"402":{"body":12,"breadcrumbs":5,"title":2},"403":{"body":9,"breadcrumbs":5,"title":2},"404":{"body":65,"breadcrumbs":5,"title":2},"405":{"body":97,"breadcrumbs":3,"title":0},"406":{"body":0,"breadcrumbs":4,"title":1},"407":{"body":0,"breadcrumbs":5,"title":2},"408":{"body":0,"breadcrumbs":3,"title":0},"409":{"body":74,"breadcrumbs":5,"title":2},"41":{"body":16,"breadcrumbs":7,"title":4},"410":{"body":0,"breadcrumbs":3,"title":0},"411":{"body":6,"breadcrumbs":5,"title":2},"412":{"body":7,"breadcrumbs":5,"title":2},"413":{"body":45,"breadcrumbs":5,"title":2},"414":{"body":26,"breadcrumbs":5,"title":2},"415":{"body":15,"breadcrumbs":5,"title":2},"416":{"body":13,"breadcrumbs":5,"title":2},"417":{"body":4,"breadcrumbs":5,"title":2},"418":{"body":28,"breadcrumbs":5,"title":2},"419":{"body":45,"breadcrumbs":5,"title":2},"42":{"body":4,"breadcrumbs":5,"title":2},"420":{"body":31,"breadcrumbs":5,"title":2},"421":{"body":7,"breadcrumbs":5,"title":2},"422":{"body":7,"breadcrumbs":5,"title":2},"423":{"body":0,"breadcrumbs":3,"title":0},"424":{"body":14,"breadcrumbs":5,"title":2},"425":{"body":20,"breadcrumbs":5,"title":2},"426":{"body":7,"breadcrumbs":4,"title":1},"427":{"body":13,"breadcrumbs":5,"title":2},"428":{"body":9,"breadcrumbs":5,"title":2},"429":{"body":10,"breadcrumbs":5,"title":2},"43":{"body":187,"breadcrumbs":4,"title":1},"430":{"body":0,"breadcrumbs":5,"title":2},"431":{"body":4,"breadcrumbs":5,"title":2},"432":{"body":81,"breadcrumbs":5,"title":2},"433":{"body":1,"breadcrumbs":5,"title":2},"434":{"body":14,"breadcrumbs":5,"title":2},"435":{"body":24,"breadcrumbs":5,"title":2},"436":{"body":24,"breadcrumbs":5,"title":2},"437":{"body":2,"breadcrumbs":5,"title":2},"438":{"body":62,"breadcrumbs":5,"title":2},"439":{"body":59,"breadcrumbs":9,"title":6},"44":{"body":28,"breadcrumbs":3,"title":0},"440":{"body":56,"breadcrumbs":5,"title":2},"441":{"body":3,"breadcrumbs":5,"title":2},"442":{"body":3,"breadcrumbs":5,"title":2},"443":{"body":3,"breadcrumbs":5,"title":2},"444":{"body":0,"breadcrumbs":3,"title":0},"445":{"body":6,"breadcrumbs":5,"title":2},"446":{"body":6,"breadcrumbs":5,"title":2},"447":{"body":9,"breadcrumbs":5,"title":2},"448":{"body":10,"breadcrumbs":5,"title":2},"449":{"body":9,"breadcrumbs":5,"title":2},"45":{"body":9,"breadcrumbs":8,"title":5},"450":{"body":5,"breadcrumbs":4,"title":1},"451":{"body":71,"breadcrumbs":7,"title":4},"452":{"body":25,"breadcrumbs":5,"title":2},"453":{"body":0,"breadcrumbs":3,"title":0},"454":{"body":4,"breadcrumbs":4,"title":1},"455":{"body":11,"breadcrumbs":4,"title":1},"456":{"body":0,"breadcrumbs":2,"title":0},"457":{"body":106,"breadcrumbs":3,"title":1},"458":{"body":75,"breadcrumbs":2,"title":0},"459":{"body":5,"breadcrumbs":3,"title":1},"46":{"body":8,"breadcrumbs":5,"title":2},"460":{"body":0,"breadcrumbs":2,"title":0},"461":{"body":30,"breadcrumbs":4,"title":2},"462":{"body":530,"breadcrumbs":3,"title":1},"463":{"body":278,"breadcrumbs":3,"title":1},"464":{"body":0,"breadcrumbs":2,"title":0},"465":{"body":6,"breadcrumbs":6,"title":2},"466":{"body":1,"breadcrumbs":4,"title":0},"467":{"body":55,"breadcrumbs":5,"title":1},"468":{"body":11,"breadcrumbs":5,"title":1},"469":{"body":63,"breadcrumbs":5,"title":1},"47":{"body":14,"breadcrumbs":8,"title":5},"470":{"body":87,"breadcrumbs":7,"title":3},"471":{"body":0,"breadcrumbs":6,"title":2},"472":{"body":98,"breadcrumbs":6,"title":2},"473":{"body":125,"breadcrumbs":7,"title":3},"474":{"body":1,"breadcrumbs":5,"title":1},"475":{"body":92,"breadcrumbs":5,"title":1},"476":{"body":104,"breadcrumbs":10,"title":6},"477":{"body":158,"breadcrumbs":7,"title":3},"478":{"body":3,"breadcrumbs":6,"title":2},"479":{"body":126,"breadcrumbs":6,"title":2},"48":{"body":195,"breadcrumbs":4,"title":1},"480":{"body":112,"breadcrumbs":5,"title":1},"481":{"body":80,"breadcrumbs":6,"title":2},"482":{"body":10,"breadcrumbs":5,"title":1},"483":{"body":1,"breadcrumbs":4,"title":0},"484":{"body":9,"breadcrumbs":3,"title":1},"485":{"body":8,"breadcrumbs":3,"title":1},"486":{"body":13,"breadcrumbs":5,"title":3},"487":{"body":27,"breadcrumbs":3,"title":1},"488":{"body":25,"breadcrumbs":3,"title":1},"489":{"body":40,"breadcrumbs":4,"title":2},"49":{"body":99,"breadcrumbs":5,"title":2},"490":{"body":50,"breadcrumbs":4,"title":2},"491":{"body":85,"breadcrumbs":4,"title":2},"492":{"body":40,"breadcrumbs":3,"title":1},"493":{"body":24,"breadcrumbs":3,"title":1},"494":{"body":30,"breadcrumbs":3,"title":1},"495":{"body":68,"breadcrumbs":4,"title":2},"496":{"body":54,"breadcrumbs":3,"title":1},"5":{"body":27,"breadcrumbs":6,"title":3},"50":{"body":19,"breadcrumbs":3,"title":0},"51":{"body":12,"breadcrumbs":4,"title":1},"52":{"body":85,"breadcrumbs":4,"title":1},"53":{"body":0,"breadcrumbs":4,"title":1},"54":{"body":495,"breadcrumbs":4,"title":1},"55":{"body":26,"breadcrumbs":4,"title":1},"56":{"body":225,"breadcrumbs":3,"title":0},"57":{"body":17,"breadcrumbs":3,"title":0},"58":{"body":17,"breadcrumbs":7,"title":4},"59":{"body":2,"breadcrumbs":5,"title":2},"6":{"body":35,"breadcrumbs":6,"title":3},"60":{"body":25,"breadcrumbs":3,"title":0},"61":{"body":348,"breadcrumbs":4,"title":1},"62":{"body":27,"breadcrumbs":3,"title":0},"63":{"body":21,"breadcrumbs":3,"title":0},"64":{"body":9,"breadcrumbs":8,"title":5},"65":{"body":35,"breadcrumbs":4,"title":1},"66":{"body":19,"breadcrumbs":4,"title":1},"67":{"body":6,"breadcrumbs":4,"title":1},"68":{"body":565,"breadcrumbs":5,"title":2},"69":{"body":574,"breadcrumbs":4,"title":1},"7":{"body":27,"breadcrumbs":5,"title":2},"70":{"body":24,"breadcrumbs":3,"title":0},"71":{"body":77,"breadcrumbs":3,"title":0},"72":{"body":8,"breadcrumbs":3,"title":0},"73":{"body":10,"breadcrumbs":6,"title":3},"74":{"body":75,"breadcrumbs":5,"title":2},"75":{"body":6,"breadcrumbs":3,"title":0},"76":{"body":199,"breadcrumbs":3,"title":0},"77":{"body":343,"breadcrumbs":3,"title":0},"78":{"body":24,"breadcrumbs":3,"title":0},"79":{"body":11,"breadcrumbs":6,"title":3},"8":{"body":9,"breadcrumbs":4,"title":1},"80":{"body":13,"breadcrumbs":3,"title":0},"81":{"body":4,"breadcrumbs":4,"title":1},"82":{"body":21,"breadcrumbs":4,"title":1},"83":{"body":51,"breadcrumbs":5,"title":2},"84":{"body":322,"breadcrumbs":4,"title":1},"85":{"body":385,"breadcrumbs":3,"title":0},"86":{"body":169,"breadcrumbs":3,"title":0},"87":{"body":55,"breadcrumbs":3,"title":0},"88":{"body":22,"breadcrumbs":3,"title":0},"89":{"body":12,"breadcrumbs":7,"title":4},"9":{"body":21,"breadcrumbs":4,"title":1},"90":{"body":80,"breadcrumbs":5,"title":2},"91":{"body":189,"breadcrumbs":4,"title":1},"92":{"body":10,"breadcrumbs":5,"title":2},"93":{"body":12,"breadcrumbs":4,"title":1},"94":{"body":131,"breadcrumbs":3,"title":0},"95":{"body":179,"breadcrumbs":4,"title":1},"96":{"body":342,"breadcrumbs":3,"title":0},"97":{"body":17,"breadcrumbs":3,"title":0},"98":{"body":10,"breadcrumbs":7,"title":4},"99":{"body":5,"breadcrumbs":4,"title":1}},"docs":{"0":{"body":"","breadcrumbs":"https://github.com/eunomia-bpf/bpf-developer-tutorial » https://github.com/eunomia-bpf/bpf-developer-tutorial","id":"0","title":"https://github.com/eunomia-bpf/bpf-developer-tutorial"},"1":{"body":"","breadcrumbs":"lesson 0-introduce » eBPF 入门开发实践教程零:介绍 eBPF 的基本概念、常见的开发工具","id":"1","title":"eBPF 入门开发实践教程零:介绍 eBPF 的基本概念、常见的开发工具"},"10":{"body":"eBPF Go 库提供了一个通用的 eBPF 库,它解耦了获取 eBPF 字节码的过程和 eBPF 程序的加载和管理,并实现了类似 libbpf 一样的 CO- 功能。eBPF 程序通常是通过编写高级语言创建的,然后使用 clang/LLVM 编译器编译为 eBPF 字节码。","breadcrumbs":"lesson 0-introduce » eBPF Go library","id":"10","title":"eBPF Go library"},"100":{"body":"在用户层面进行动态跟踪,即用户级动态跟踪(User-Level Dynamic Tracing)允许我们对任何用户级别的代码进行插桩。比如,我们可以通过在 MySQL 服务器的 dispatch_command() 函数上进行插桩,来跟踪服务器的查询请求: # ./uprobe 'p:cmd /opt/bin/mysqld:_Z16dispatch_command19enum_server_commandP3THDPcj +0(%dx):string'\nTracing uprobe cmd (p:cmd /opt/bin/mysqld:0x2dbd40 +0(%dx):string). Ctrl-C to end. mysqld-2855 [001] d... 19957757.590926: cmd: (0x6dbd40) arg1=\"show tables\" mysqld-2855 [001] d... 19957759.703497: cmd: (0x6dbd40) arg1=\"SELECT * FROM numbers\"\n[...] 这里我们使用了 uprobe 工具,它利用了 Linux 的内置功能:ftrace(跟踪器)和 uprobes(用户级动态跟踪,需要较新的 Linux 版本,例如 4.0 左右)。其他的跟踪器,如 perf_events 和 SystemTap,也可以实现此功能。 许多其他的 MySQL 函数也可以被跟踪以获取更多的信息。我们可以列出和计算这些函数的数量: # ./uprobe -l /opt/bin/mysqld | more\naccount_hash_get_key\nadd_collation\nadd_compiled_collation\nadd_plugin_noargs\nadjust_time_range\n[...]\n# ./uprobe -l /opt/bin/mysqld | wc -l\n21809 这有 21,000 个函数。我们也可以跟踪库函数,甚至是单个的指令偏移。 用户级动态跟踪的能力是非常强大的,它可以解决无数的问题。然而,使用它也有一些困难:需要确定需要跟踪的代码,处理函数参数,以及应对代码的更改。 用户级静态定义跟踪(User-level Statically Defined Tracing, USDT)则可以在某种程度上解决这些问题。USDT 探针(或者称为用户级 \"marker\")是开发者在代码的关键位置插入的跟踪宏,提供稳定且已经过文档说明的 API。这使得跟踪工作变得更加简单。 使用 USDT,我们可以简单地跟踪一个名为 mysql:query__start 的探针,而不是去跟踪那个名为 _Z16dispatch_command19enum_server_commandP3THDPcj 的 C++ 符号,也就是 dispatch_command() 函数。当然,我们仍然可以在需要的时候去跟踪 dispatch_command() 以及其他 21,000 个 mysqld 函数,但只有当 USDT 探针无法解决问题的时候我们才需要这么做。 在 Linux 中的 USDT,无论是哪种形式的静态跟踪点,其实都已经存在了几十年。它最近由于 Sun 的 DTrace 工具的流行而再次受到关注,这使得许多常见的应用程序,包括 MySQL、PostgreSQL、Node.js、Java 等都加入了 USDT。SystemTap 则开发了一种可以消费这些 DTrace 探针的方式。 你可能正在运行一个已经包含了 USDT 探针的 Linux 应用程序,或者可能需要重新编译(通常是 --enable-dtrace)。你可以使用 readelf 来进行检查,例如对于 Node.js: # readelf -n node\n[...]\nNotes at offset 0x00c43058 with length 0x00000494: Owner Data size Description stapsdt 0x0000003c NT_STAPSDT (SystemTap probe descriptors) Provider: node Name: gc__start Location: 0x0000000000bf44b4, Base: 0x0000000000f22464, Semaphore: 0x0000000001243028 Arguments: 4@%esi 4@%edx 8@%rdi\n[...] stapsdt 0x00000082 NT_STAPSDT (SystemTap probe descriptors) Provider: node Name: http__client__request Location: 0x0000000000bf48ff, Base: 0x0000000000f22464, Semaphore: 0x0000000001243024 Arguments: 8@%rax 8@%rdx 8@-136(%rbp) -4@-140(%rbp) 8@-72(%rbp) 8@-80(%rbp) -4@-144(%rbp)\n[...] 这就是使用 --enable-dtrace 重新编译的 node,以及安装了提供 \"dtrace\" 功能来构建 USDT 支持的 systemtap-sdt-dev 包。这里显示了两个探针:node:gc__start(开始进行垃圾回收)和 node:http__client__request。 在这一点上,你可以使用 SystemTap 或者 LTTng 来跟踪这些探针。然而,内置的 Linux 跟踪器,比如 ftrace 和 perf_events,目前还无法做到这一点(尽管 perf_events 的支持正在开发中)。 USDT 在内核态 eBPF 运行时,也可能产生比较大的性能开销,这时候也可以考虑使用用户态 eBPF 运行时,例如 bpftime 。bpftime 是一个基于 LLVM JIT/AOT 的用户态 eBPF 运行时,它可以在用户态运行 eBPF 程序,和内核态的 eBPF 兼容,避免了内核态和用户态之间的上下文切换,从而提高了 eBPF 程序的执行效率。对于 uprobe 而言,bpftime 的性能开销比 kernel 小一个数量级。","breadcrumbs":"lesson 15-javagc » 用户层面的追踪机制:用户级动态跟踪和 USDT","id":"100","title":"用户层面的追踪机制:用户级动态跟踪和 USDT"},"101":{"body":"Java 作为一种高级编程语言,其自动垃圾回收(GC)是其核心特性之一。Java GC 的目标是自动地回收那些不再被程序使用的内存空间,从而减轻程序员在内存管理方面的负担。然而,GC 过程可能会引发应用程序的停顿,对程序的性能和响应时间产生影响。因此,对 Java GC 事件进行监控和分析,对于理解和优化 Java 应用的性能是非常重要的。 在接下来的教程中,我们将演示如何使用 eBPF 和 USDT 来监控和分析 Java GC 事件的耗时,希望这些内容对你在使用 eBPF 进行应用性能分析方面的工作有所帮助。","breadcrumbs":"lesson 15-javagc » Java GC 介绍","id":"101","title":"Java GC 介绍"},"102":{"body":"Java GC 的 eBPF 程序分为内核态和用户态两部分,我们会分别介绍这两部分的实现机制。","breadcrumbs":"lesson 15-javagc » eBPF 实现机制","id":"102","title":"eBPF 实现机制"},"103":{"body":"/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */\n/* Copyright (c) 2022 Chen Tao */\n#include
\n#include \n#include \n#include \n#include \"javagc.h\" struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 100); __type(key, uint32_t); __type(value, struct data_t);\n} data_map SEC(\".maps\"); struct { __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); __type(key, int); __type(value, int);\n} perf_map SEC(\".maps\"); __u32 time; static int gc_start(struct pt_regs *ctx)\n{ struct data_t data = {}; data.cpu = bpf_get_smp_processor_id(); data.pid = bpf_get_current_pid_tgid() >> 32; data.ts = bpf_ktime_get_ns(); bpf_map_update_elem(&data_map, &data.pid, &data, 0); return 0;\n} static int gc_end(struct pt_regs *ctx)\n{ struct data_t data = {}; struct data_t *p; __u32 val; data.cpu = bpf_get_smp_processor_id(); data.pid = bpf_get_current_pid_tgid() >> 32; data.ts = bpf_ktime_get_ns(); p = bpf_map_lookup_elem(&data_map, &data.pid); if (!p) return 0; val = data.ts - p->ts; if (val > time) { data.ts = val; bpf_perf_event_output(ctx, &perf_map, BPF_F_CURRENT_CPU, &data, sizeof(data)); } bpf_map_delete_elem(&data_map, &data.pid); return 0;\n} SEC(\"usdt\")\nint handle_gc_start(struct pt_regs *ctx)\n{ return gc_start(ctx);\n} SEC(\"usdt\")\nint handle_gc_end(struct pt_regs *ctx)\n{ return gc_end(ctx);\n} SEC(\"usdt\")\nint handle_mem_pool_gc_start(struct pt_regs *ctx)\n{ return gc_start(ctx);\n} SEC(\"usdt\")\nint handle_mem_pool_gc_end(struct pt_regs *ctx)\n{ return gc_end(ctx);\n} char LICENSE[] SEC(\"license\") = \"Dual BSD/GPL\"; 首先,我们定义了两个映射(map): data_map:这个 hashmap 存储每个进程 ID 的垃圾收集开始时间。data_t 结构体包含进程 ID、CPU ID 和时间戳。 perf_map:这是一个 perf event array,用于将数据发送回用户态程序。 然后,我们有四个处理函数:gc_start、gc_end 和两个 USDT 处理函数 handle_mem_pool_gc_start 和 handle_mem_pool_gc_end。这些函数都用 BPF 的 SEC(\"usdt\") 宏注解,以便在 Java 进程中捕获到与垃圾收集相关的 USDT 事件。 gc_start 函数在垃圾收集开始时被调用。它首先获取当前的 CPU ID、进程 ID 和时间戳,然后将这些数据存入 data_map。 gc_end 函数在垃圾收集结束时被调用。它执行与 gc_start 类似的操作,但是它还从 data_map 中检索开始时间,并计算垃圾收集的持续时间。如果持续时间超过了设定的阈值(变量 time),那么它将数据发送回用户态程序。 handle_gc_start 和 handle_gc_end 是针对垃圾收集开始和结束事件的处理函数,它们分别调用了 gc_start 和 gc_end。 handle_mem_pool_gc_start 和 handle_mem_pool_gc_end 是针对内存池的垃圾收集开始和结束事件的处理函数,它们也分别调用了 gc_start 和 gc_end。 最后,我们有一个 LICENSE 数组,声明了该 BPF 程序的许可证,这是加载 BPF 程序所必需的。","breadcrumbs":"lesson 15-javagc » 内核态程序","id":"103","title":"内核态程序"},"104":{"body":"用户态程序的主要目标是加载和运行eBPF程序,以及处理来自内核态程序的数据。它是通过 libbpf 库来完成这些操作的。这里我们省略了一些通用的加载和运行 eBPF 程序的代码,只展示了与 USDT 相关的部分。 第一个函数 get_jvmso_path 被用来获取运行的Java虚拟机(JVM)的 libjvm.so 库的路径。首先,它打开了 /proc/ /maps 文件,该文件包含了进程地址空间的内存映射信息。然后,它在文件中搜索包含 libjvm.so 的行,然后复制该行的路径到提供的参数中。 static int get_jvmso_path(char *path)\n{ char mode[16], line[128], buf[64]; size_t seg_start, seg_end, seg_off; FILE *f; int i = 0; sprintf(buf, \"/proc/%d/maps\", env.pid); f = fopen(buf, \"r\"); if (!f) return -1; while (fscanf(f, \"%zx-%zx %s %zx %*s %*d%[^\\n]\\n\", &seg_start, &seg_end, mode, &seg_off, line) == 5) { i = 0; while (isblank(line[i])) i++; if (strstr(line + i, \"libjvm.so\")) { break; } } strcpy(path, line + i); fclose(f); return 0;\n} 接下来,我们看到的是将 eBPF 程序(函数 handle_gc_start 和 handle_gc_end)附加到Java进程的相关USDT探针上。每个程序都通过调用 bpf_program__attach_usdt 函数来实现这一点,该函数的参数包括BPF程序、进程ID、二进制路径以及探针的提供者和名称。如果探针挂载成功,bpf_program__attach_usdt 将返回一个链接对象,该对象将存储在skeleton的链接成员中。如果挂载失败,程序将打印错误消息并进行清理。 skel->links.handle_mem_pool_gc_start = bpf_program__attach_usdt(skel->progs.handle_gc_start, env.pid, binary_path, \"hotspot\", \"mem__pool__gc__begin\", NULL); if (!skel->links.handle_mem_pool_gc_start) { err = errno; fprintf(stderr, \"attach usdt mem__pool__gc__begin failed: %s\\n\", strerror(err)); goto cleanup; } skel->links.handle_mem_pool_gc_end = bpf_program__attach_usdt(skel->progs.handle_gc_end, env.pid, binary_path, \"hotspot\", \"mem__pool__gc__end\", NULL); if (!skel->links.handle_mem_pool_gc_end) { err = errno; fprintf(stderr, \"attach usdt mem__pool__gc__end failed: %s\\n\", strerror(err)); goto cleanup; } skel->links.handle_gc_start = bpf_program__attach_usdt(skel->progs.handle_gc_start, env.pid, binary_path, \"hotspot\", \"gc__begin\", NULL); if (!skel->links.handle_gc_start) { err = errno; fprintf(stderr, \"attach usdt gc__begin failed: %s\\n\", strerror(err)); goto cleanup; } skel->links.handle_gc_end = bpf_program__attach_usdt(skel->progs.handle_gc_end, env.pid, binary_path, \"hotspot\", \"gc__end\", NULL); if (!skel->links.handle_gc_end) { err = errno; fprintf(stderr, \"attach usdt gc__end failed: %s\\n\", strerror(err)); goto cleanup; } 最后一个函数 handle_event 是一个回调函数,用于处理从perf event array收到的数据。这个函数会被 perf event array 触发,并在每次接收到新的事件时调用。函数首先将数据转换为 data_t 结构体,然后将当前时间格式化为字符串,并打印出事件的时间戳、CPU ID、进程 ID,以及垃圾回收的持续时间。 static void handle_event(void *ctx, int cpu, void *data, __u32 data_sz)\n{ struct data_t *e = (struct data_t *)data; struct tm *tm = NULL; char ts[16]; time_t t; time(&t); tm = localtime(&t); strftime(ts, sizeof(ts), \"%H:%M:%S\", tm); printf(\"%-8s %-7d %-7d %-7lld\\n\", ts, e->cpu, e->pid, e->ts/1000);\n}","breadcrumbs":"lesson 15-javagc » 用户态程序","id":"104","title":"用户态程序"},"105":{"body":"构建示例需要 clang、libelf 和 zlib。包名在不同的发行版中可能会有所不同。 在 Ubuntu/Debian 上,你需要执行以下命令: sudo apt install clang libelf1 libelf-dev zlib1g-dev 在 CentOS/Fedora 上,你需要执行以下命令: sudo dnf install clang elfutils-libelf elfutils-libelf-devel zlib-devel","breadcrumbs":"lesson 15-javagc » 安装依赖","id":"105","title":"安装依赖"},"106":{"body":"在对应的目录中,运行 Make 即可编译运行上述代码: $ make\n$ sudo ./javagc -p 12345\nTracing javagc time... Hit Ctrl-C to end.\nTIME CPU PID GC TIME\n10:00:01 10% 12345 50ms\n10:00:02 12% 12345 55ms\n10:00:03 9% 12345 47ms\n10:00:04 13% 12345 52ms\n10:00:05 11% 12345 50ms 完整源代码: https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/15-javagc 参考资料: https://www.brendangregg.com/blog/2015-07-03/hacking-linux-usdt-ftrace.html https://github.com/iovisor/bcc/blob/master/libbpf-tools/javagc.c","breadcrumbs":"lesson 15-javagc » 编译运行","id":"106","title":"编译运行"},"107":{"body":"通过本篇 eBPF 入门实践教程,我们学习了如何使用 eBPF 和 USDT 动态跟踪和分析 Java 的垃圾回收(GC)事件。我们了解了如何在用户态应用程序中设置 USDT 跟踪点,以及如何编写 eBPF 程序来捕获这些跟踪点的信息,从而更深入地理解和优化 Java GC 的行为和性能。 此外,我们也介绍了一些关于 Java GC、USDT 和 eBPF 的基础知识和实践技巧,这些知识和技巧对于想要在网络和系统性能分析领域深入研究的开发者来说是非常有价值的。 如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。 The original link of this article: https://eunomia.dev/tutorials/15-javagc","breadcrumbs":"lesson 15-javagc » 总结","id":"107","title":"总结"},"108":{"body":"eBPF(扩展的伯克利数据包过滤器)是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。 在本篇教程中,我们将探讨如何使用 eBPF 编写 Memleak 程序,以监控程序的内存泄漏。","breadcrumbs":"lesson 16-memleak » eBPF 入门实践教程十六:编写 eBPF 程序 Memleak 监控内存泄漏","id":"108","title":"eBPF 入门实践教程十六:编写 eBPF 程序 Memleak 监控内存泄漏"},"109":{"body":"内存泄漏是计算机编程中的一种常见问题,其严重程度不应被低估。内存泄漏发生时,程序会逐渐消耗更多的内存资源,但并未正确释放。随着时间的推移,这种行为会导致系统内存逐渐耗尽,从而显著降低程序及系统的整体性能。 内存泄漏有多种可能的原因。这可能是由于配置错误导致的,例如程序错误地配置了某些资源的动态分配。它也可能是由于软件缺陷或错误的内存管理策略导致的,如在程序执行过程中忘记释放不再需要的内存。此外,如果一个应用程序的内存使用量过大,那么系统性能可能会因页面交换(swapping)而大幅下降,甚至可能导致应用程序被系统强制终止(Linux 的 OOM killer)。","breadcrumbs":"lesson 16-memleak » 背景及其重要性","id":"109","title":"背景及其重要性"},"11":{"body":"libbpf-bootstrap 是一个基于 libbpf 库的 BPF 开发脚手架,从其 github 上可以得到其源码。 libbpf-bootstrap 综合了 BPF 社区过去多年的实践,为开发者提了一个现代化的、便捷的工作流,实 现了一次编译,重复使用的目的。 基于 libbpf-bootstrap 的 BPF 程序对于源文件有一定的命名规则, 用于生成内核态字节码的 bpf 文件以 .bpf.c 结尾,用户态加载字节码的文件以 .c 结尾,且这两个文件的 前缀必须相同。 基于 libbpf-bootstrap 的 BPF 程序在编译时会先将 *.bpf.c 文件编译为 对应的 .o 文件,然后根据此文件生成 skeleton 文件,即 *.skel.h,这个文件会包含内核态中定义的一些 数据结构,以及用于装载内核态代码的关键函数。在用户态代码 include 此文件之后调用对应的装载函数即可将 字节码装载到内核中。同样的,libbpf-bootstrap 也有非常完备的入门教程,用户可以在 该处 得到详细的入门操作介绍。","breadcrumbs":"lesson 0-introduce » libbpf","id":"11","title":"libbpf"},"110":{"body":"调试内存泄漏问题是一项复杂且挑战性的任务。这涉及到详细检查应用程序的配置、内存分配和释放情况,通常需要应用专门的工具来帮助诊断。例如,有一些工具可以在应用程序启动时将 malloc() 函数调用与特定的检测工具关联起来,如 Valgrind memcheck,这类工具可以模拟 CPU 来检查所有内存访问,但可能会导致应用程序运行速度大大减慢。另一个选择是使用堆分析器,如 libtcmalloc,它相对较快,但仍可能使应用程序运行速度降低五倍以上。此外,还有一些工具,如 gdb,可以获取应用程序的核心转储并进行后处理以分析内存使用情况。然而,这些工具通常在获取核心转储时需要暂停应用程序,或在应用程序终止后才能调用 free() 函数。","breadcrumbs":"lesson 16-memleak » 调试内存泄漏的挑战","id":"110","title":"调试内存泄漏的挑战"},"111":{"body":"在这种背景下,eBPF 的作用就显得尤为重要。eBPF 提供了一种高效的机制来监控和追踪系统级别的事件,包括内存的分配和释放。通过 eBPF,我们可以跟踪内存分配和释放的请求,并收集每次分配的调用堆栈。然后,我们可以分 析这些信息,找出执行了内存分配但未执行释放操作的调用堆栈,这有助于我们找出导致内存泄漏的源头。这种方式的优点在于,它可以实时地在运行的应用程序中进行,而无需暂停应用程序或进行复杂的前后处理。 memleak eBPF 工具可以跟踪并匹配内存分配和释放的请求,并收集每次分配的调用堆栈。随后,memleak 可以打印一个总结,表明哪些调用堆栈执行了分配,但是并没有随后进行释放。例如,我们运行命令: # ./memleak -p $(pidof allocs)\nAttaching to pid 5193, Ctrl+C to quit.\n[11:16:33] Top 2 stacks with outstanding allocations: 80 bytes in 5 allocations from stack main+0x6d [allocs] __libc_start_main+0xf0 [libc-2.21.so] [11:16:34] Top 2 stacks with outstanding allocations: 160 bytes in 10 allocations from stack main+0x6d [allocs] __libc_start_main+0xf0 [libc-2.21.so] 运行这个命令后,我们可以看到分配但未释放的内存来自于哪些堆栈,并且可以看到这些未释放的内存的大小和数量。 随着时间的推移,很显然,allocs 进程的 main 函数正在泄漏内存,每次泄漏 16 字节。幸运的是,我们不需要检查每个分配,我们得到了一个很好的总结,告诉我们哪个堆栈负责大量的泄漏。","breadcrumbs":"lesson 16-memleak » eBPF 的作用","id":"111","title":"eBPF 的作用"},"112":{"body":"在基本层面上,memleak 的工作方式类似于在内存分配和释放路径上安装监控设备。它通过在内存分配和释放函数中插入 eBPF 程序来达到这个目标。这意味着,当这些函数被调用时,memleak 就会记录一些重要信息,如调用者的进程 ID(PID)、分配的内存地址以及分配的内存大小等。当释放内存的函数被调用时,memleak 则会在其内部的映射表(map)中删除相应的内存分配记录。这种机制使得 memleak 能够准确地追踪到哪些内存块已被分配但未被释放。 对于用户态的常用内存分配函数,如 malloc 和 calloc 等,memleak 利用了用户态探测(uprobe)技术来实现监控。uprobe 是一种用于用户空间应用程序的动态追踪技术,它可以在运行时不修改二进制文件的情况下在任意位置设置断点,从而实现对特定函数调用的追踪。Uprobe 在内核态 eBPF 运行时,也可能产生比较大的性能开销,这时候也可以考虑使用用户态 eBPF 运行时,例如 bpftime 。bpftime 是一个基于 LLVM JIT/AOT 的用户态 eBPF 运行时,它可以在用户态运行 eBPF 程序,和内核态的 eBPF 兼容,避免了内核态和用户态之间的上下文切换,从而提高了 eBPF 程序的执行效率。对于 uprobe 而言,bpftime 的性能开销比 kernel 小一个数量级。 对于内核态的内存分配函数,如 kmalloc 等,memleak 则选择使用了 tracepoint 来实现监控。Tracepoint 是一种在 Linux 内核中提供的动态追踪技术,它可以在内核运行时动态地追踪特定的事件,而无需重新编译内核或加载内核模块。","breadcrumbs":"lesson 16-memleak » memleak 的实现原理","id":"112","title":"memleak 的实现原理"},"113":{"body":"","breadcrumbs":"lesson 16-memleak » 内核态 eBPF 程序实现","id":"113","title":"内核态 eBPF 程序实现"},"114":{"body":"memleak 的内核态 eBPF 程序包含一些用于跟踪内存分配和释放的关键函数。在我们深入了解这些函数之前,让我们首先观察 memleak 所定义的一些数据结构,这些结构在其内核态和用户态程序中均有使用。 #ifndef __MEMLEAK_H\n#define __MEMLEAK_H #define ALLOCS_MAX_ENTRIES 1000000\n#define COMBINED_ALLOCS_MAX_ENTRIES 10240 struct alloc_info { __u64 size; // 分配的内存大小 __u64 timestamp_ns; // 分配时的时间戳,单位为纳秒 int stack_id; // 分配时的调用堆栈ID\n}; union combined_alloc_info { struct { __u64 total_size : 40; // 所有未释放分配的总大小 __u64 number_of_allocs : 24; // 所有未释放分配的总次数 }; __u64 bits; // 结构的位图表示\n}; #endif /* __MEMLEAK_H */ 这里定义了两个主要的数据结构:alloc_info 和 combined_alloc_info。 alloc_info 结构体包含了一个内存分配的基本信息,包括分配的内存大小 size、分配发生时的时间戳 timestamp_ns,以及触发分配的调用堆栈 ID stack_id。 combined_alloc_info 是一个联合体(union),它包含一个嵌入的结构体和一个 __u64 类型的位图表示 bits。嵌入的结构体有两个成员:total_size 和 number_of_allocs,分别代表所有未释放分配的总大小和总次数。其中 40 和 24 分别表示 total_size 和 number_of_allocs这两个成员变量所占用的位数,用来限制其大小。通过这样的位数限制,可以节省combined_alloc_info结构的存储空间。同时,由于total_size和number_of_allocs在存储时是共用一个unsigned long long类型的变量bits,因此可以通过在成员变量bits上进行位运算来访问和修改total_size和number_of_allocs,从而避免了在程序中定义额外的变量和函数的复杂性。 接下来,memleak 定义了一系列用于保存内存分配信息和分析结果的 eBPF 映射(maps)。这些映射都以 SEC(\".maps\") 的形式定义,表示它们属于 eBPF 程序的映射部分。 const volatile size_t min_size = 0;\nconst volatile size_t max_size = -1;\nconst volatile size_t page_size = 4096;\nconst volatile __u64 sample_rate = 1;\nconst volatile bool trace_all = false;\nconst volatile __u64 stack_flags = 0;\nconst volatile bool wa_missing_free = false; struct { __uint(type, BPF_MAP_TYPE_HASH); __type(key, pid_t); __type(value, u64); __uint(max_entries, 10240);\n} sizes SEC(\".maps\"); struct { __uint(type, BPF_MAP_TYPE_HASH); __type(key, u64); /* address */ __type(value, struct alloc_info); __uint(max_entries, ALLOCS_MAX_ENTRIES);\n} allocs SEC(\".maps\"); struct { __uint(type, BPF_MAP_TYPE_HASH); __type(key, u64); /* stack id */ __type(value, union combined_alloc_info); __uint(max_entries, COMBINED_ALLOCS_MAX_ENTRIES);\n} combined_allocs SEC(\".maps\"); struct { __uint(type, BPF_MAP_TYPE_HASH); __type(key, u64); __type(value, u64); __uint(max_entries, 10240);\n} memptrs SEC(\".maps\"); struct { __uint(type, BPF_MAP_TYPE_STACK_TRACE); __type(key, u32);\n} stack_traces SEC(\".maps\"); static union combined_alloc_info initial_cinfo; 这段代码首先定义了一些可配置的参数,如 min_size, max_size, page_size, sample_rate, trace_all, stack_flags 和 wa_missing_free,分别表示最小分配大小、最大分配大小、页面大小、采样率、是否追踪所有分配、堆栈标志和是否工作在缺失释放(missing free)模式。 接着定义了五个映射: sizes:这是一个哈希类型的映射,键为进程 ID,值为 u64 类型,存储每个进程的分配大小。 allocs:这也是一个哈希类型的映射,键为分配的地址,值为 alloc_info 结构体,存储每个内存分配的详细信息。 combined_allocs:这是另一个哈希类型的映射,键为堆栈 ID,值为 combined_alloc_info 联合体,存储所有未释放分配的总大小和总次数。 memptrs:这也是一个哈希类型的映射,键和值都为 u64 类型,用于在用户空间和内核空间之间传递内存指针。 stack_traces:这是一个堆栈追踪类型的映射,键为 u32 类型,用于存储堆栈 ID。 以用户态的内存分配追踪部分为例,主要是挂钩内存相关的函数调用,如 malloc, free, calloc, realloc, mmap 和 munmap,以便在调用这些函数时进行数据记录。在用户态,memleak 主要使用了 uprobes 技术进行挂载。 每个函数调用被分为 \"enter\" 和 \"exit\" 两部分。\"enter\" 部分记录的是函数调用的参数,如分配的大小或者释放的地址。\"exit\" 部分则主要用于获取函数的返回值,如分配得到的内存地址。 这里,gen_alloc_enter, gen_alloc_exit, gen_free_enter 是实现记录行为的函数,他们分别用于记录分配开始、分配结束和释放开始的相关信息。 函数原型示例如下: SEC(\"uprobe\")\nint BPF_KPROBE(malloc_enter, size_t size)\n{ // 记录分配开始的相关信息 return gen_alloc_enter(size);\n} SEC(\"uretprobe\")\nint BPF_KRETPROBE(malloc_exit)\n{ // 记录分配结束的相关信息 return gen_alloc_exit(ctx);\n} SEC(\"uprobe\")\nint BPF_KPROBE(free_enter, void *address)\n{ // 记录释放开始的相关信息 return gen_free_enter(address);\n} 其中,malloc_enter 和 free_enter 是分别挂载在 malloc 和 free 函数入口处的探针(probes),用于在函数调用时进行数据记录。而 malloc_exit 则是挂载在 malloc 函数的返回处的探针,用于记录函数的返回值。 这些函数使用了 BPF_KPROBE 和 BPF_KRETPROBE 这两个宏来声明,这两个宏分别用于声明 kprobe(内核探针)和 kretprobe(内核返回探针)。具体来说,kprobe 用于在函数调用时触发,而 kretprobe 则是在函数返回时触发。 gen_alloc_enter 函数是在内存分配请求的开始时被调用的。这个函数主要负责在调用分配内存的函数时收集一些基本的信息。下面我们将深入探讨这个函数的实现。 static int gen_alloc_enter(size_t size)\n{ if (size < min_size || size > max_size) return 0; if (sample_rate > 1) { if (bpf_ktime_get_ns() % sample_rate != 0) return 0; } const pid_t pid = bpf_get_current_pid_tgid() >> 32; bpf_map_update_elem(&sizes, &pid, &size, BPF_ANY); if (trace_all) bpf_printk(\"alloc entered, size = %lu\\n\", size); return 0;\n} SEC(\"uprobe\")\nint BPF_KPROBE(malloc_enter, size_t size)\n{ return gen_alloc_enter(size);\n} 首先,gen_alloc_enter 函数接收一个 size 参数,这个参数表示请求分配的内存的大小。如果这个值不在 min_size 和 max_size 之间,函数将直接返回,不再进行后续的操作。这样可以使工具专注于追踪特定范围的内存分配请求,过滤掉不感兴趣的分配请求。 接下来,函数检查采样率 sample_rate。如果 sample_rate 大于1,意味着我们不需要追踪所有的内存分配请求,而是周期性地追踪。这里使用 bpf_ktime_get_ns 获取当前的时间戳,然后通过取模运算来决定是否需要追踪当前的内存分配请求。这是一种常见的采样技术,用于降低性能开销,同时还能够提供一个代表性的样本用于分析。 之后,函数使用 bpf_get_current_pid_tgid 函数获取当前进程的 PID。注意这里的 PID 实际上是进程和线程的组合 ID,我们通过右移 32 位来获取真正的进程 ID。 函数接下来更新 sizes 这个 map,这个 map 以进程 ID 为键,以请求的内存分配大小为值。BPF_ANY 表示如果 key 已存在,那么更新 value,否则就新建一个条目。 最后,如果启用了 trace_all 标志,函数将打印一条信息,说明发生了内存分配。 BPF_KPROBE 宏用于 最后定义了 BPF_KPROBE(malloc_enter, size_t size),它会在 malloc 函数被调用时被 BPF uprobe 拦截执行,并通过 gen_alloc_enter 来记录内存分配大小。 我们刚刚分析了内存分配的入口函数 gen_alloc_enter,现在我们来关注这个过程的退出部分。具体来说,我们将讨论 gen_alloc_exit2 函数以及如何从内存分配调用中获取返回的内存地址。 static int gen_alloc_exit2(void *ctx, u64 address)\n{ const pid_t pid = bpf_get_current_pid_tgid() >> 32; struct alloc_info info; const u64* size = bpf_map_lookup_elem(&sizes, &pid); if (!size) return 0; // missed alloc entry __builtin_memset(&info, 0, sizeof(info)); info.size = *size; bpf_map_delete_elem(&sizes, &pid); if (address != 0) { info.timestamp_ns = bpf_ktime_get_ns(); info.stack_id = bpf_get_stackid(ctx, &stack_traces, stack_flags); bpf_map_update_elem(&allocs, &address, &info, BPF_ANY); update_statistics_add(info.stack_id, info.size); } if (trace_all) { bpf_printk(\"alloc exited, size = %lu, result = %lx\\n\", info.size, address); } return 0;\n}\nstatic int gen_alloc_exit(struct pt_regs *ctx)\n{ return gen_alloc_exit2(ctx, PT_REGS_RC(ctx));\n} SEC(\"uretprobe\")\nint BPF_KRETPROBE(malloc_exit)\n{ return gen_alloc_exit(ctx);\n} gen_alloc_exit2 函数在内存分配操作完成时被调用,这个函数接收两个参数,一个是上下文 ctx,另一个是内存分配函数返回的内存地址 address。 首先,它获取当前线程的 PID,然后使用这个 PID 作为键在 sizes 这个 map 中查找对应的内存分配大小。如果没有找到(也就是说,没有对应的内存分配操作的入口),函数就会直接返回。 接着,函数清除 info 结构体的内容,并设置它的 size 字段为之前在 map 中找到的内存分配大小。并从 sizes 这个 map 中删除相应的元素,因为此时内存分配操作已经完成,不再需要这个信息。 接下来,如果 address 不为 0(也就是说,内存分配操作成功了),函数就会进一步收集一些额外的信息。首先,它获取当前的时间戳作为内存分配完成的时间,并获取当前的堆栈跟踪。这些信息都会被储存在 info 结构体中,并随后更新到 allocs 这个 map 中。 最后,函数调用 update_statistics_add 更新统计数据,如果启用了所有内存分配操作的跟踪,函数还会打印一些关于内存分配操作的信息。 请注意,gen_alloc_exit 函数是 gen_alloc_exit2 的一个包装,它将 PT_REGS_RC(ctx) 作为 address 参数传递给 gen_alloc_exit2。在我们的讨论中,我们刚刚提到在gen_alloc_exit2函数中,调用了update_statistics_add` 函数以更新内存分配的统计数据。下面我们详细看一下这个函数的具体实现。 static void update_statistics_add(u64 stack_id, u64 sz)\n{ union combined_alloc_info *existing_cinfo; existing_cinfo = bpf_map_lookup_or_try_init(&combined_allocs, &stack_id, &initial_cinfo); if (!existing_cinfo) return; const union combined_alloc_info incremental_cinfo = { .total_size = sz, .number_of_allocs = 1 }; __sync_fetch_and_add(&existing_cinfo->bits, incremental_cinfo.bits);\n} update_statistics_add 函数接收两个参数:当前的堆栈 ID stack_id 以及内存分配的大小 sz。这两个参数都在内存分配事件中收集到,并且用于更新内存分配的统计数据。 首先,函数尝试在 combined_allocs 这个 map 中查找键值为当前堆栈 ID 的元素,如果找不到,就用 initial_cinfo(这是一个默认的 combined_alloc_info 结构体,所有字段都为零)来初始化新的元素。 接着,函数创建一个 incremental_cinfo,并设置它的 total_size 为当前内存分配的大小,设置 number_of_allocs 为 1。这是因为每次调用 update_statistics_add 函数都表示有一个新的内存分配事件发生,而这个事件的内存分配大小就是 sz。 最后,函数使用 __sync_fetch_and_add 函数原子地将 incremental_cinfo 的值加到 existing_cinfo 中。请注意这个步骤是线程安全的,即使有多个线程并发地调用 update_statistics_add 函数,每个内存分配事件也能正确地记录到统计数据中。 总的来说,update_statistics_add 函数实现了内存分配统计的更新逻辑,通过维护每个堆栈 ID 的内存分配总量和次数,我们可以深入了解到程序的内存分配行为。 在我们对内存分配的统计跟踪过程中,我们不仅要统计内存的分配,还要考虑内存的释放。在上述代码中,我们定义了一个名为 update_statistics_del 的函数,其作用是在内存释放时更新统计信息。而 gen_free_enter 函数则是在进程调用 free 函数时被执行。 static void update_statistics_del(u64 stack_id, u64 sz)\n{ union combined_alloc_info *existing_cinfo; existing_cinfo = bpf_map_lookup_elem(&combined_allocs, &stack_id); if (!existing_cinfo) { bpf_printk(\"failed to lookup combined allocs\\n\"); return; } const union combined_alloc_info decremental_cinfo = { .total_size = sz, .number_of_allocs = 1 }; __sync_fetch_and_sub(&existing_cinfo->bits, decremental_cinfo.bits);\n} update_statistics_del 函数的参数为堆栈 ID 和要释放的内存块大小。函数首先在 combined_allocs 这个 map 中使用当前的堆栈 ID 作为键来查找相应的 combined_alloc_info 结构体。如果找不到,就输出错误信息,然后函数返回。如果找到了,就会构造一个名为 decremental_cinfo 的 combined_alloc_info 结构体,设置它的 total_size 为要释放的内存大小,设置 number_of_allocs 为 1。然后使用 __sync_fetch_and_sub 函数原子地从 existing_cinfo 中减去 decremental_cinfo 的值。请注意,这里的 number_of_allocs 是负数,表示减少了一个内存分配。 static int gen_free_enter(const void *address)\n{ const u64 addr = (u64)address; const struct alloc_info *info = bpf_map_lookup_elem(&allocs, &addr); if (!info) return 0; bpf_map_delete_elem(&allocs, &addr); update_statistics_del(info->stack_id, info->size); if (trace_all) { bpf_printk(\"free entered, address = %lx, size = %lu\\n\", address, info->size); } return 0;\n} SEC(\"uprobe\")\nint BPF_KPROBE(free_enter, void *address)\n{ return gen_free_enter(address);\n} 接下来看 gen_free_enter 函数。它接收一个地址作为参数,这个地址是内存分配的结果,也就是将要释放的内存的起始地址。函数首先在 allocs 这个 map 中使用这个地址作为键来查找对应的 alloc_info 结构体。如果找不到,那么就直接返回,因为这意味着这个地址并没有被分配过。如果找到了,那么就删除这个元素,并且调用 update_statistics_del 函数来更新统计数据。最后,如果启用了全局追踪,那么还会输出一条信息,包括这个地址以及它的大小。 在我们追踪和统计内存分配的同时,我们也需要对内核态的内存分配和释放进行追踪。在Linux内核中,kmem_cache_alloc函数和kfree函数分别用于内核态的内存分配和释放。 SEC(\"tracepoint/kmem/kfree\")\nint memleak__kfree(void *ctx)\n{ const void *ptr; if (has_kfree()) { struct trace_event_raw_kfree___x *args = ctx; ptr = BPF_CORE_READ(args, ptr); } else { struct trace_event_raw_kmem_free___x *args = ctx; ptr = BPF_CORE_READ(args, ptr); } return gen_free_enter(ptr);\n} 上述代码片段定义了一个函数memleak__kfree,这是一个bpf程序,会在内核调用kfree函数时执行。首先,该函数检查是否存在kfree函数。如果存在,则会读取传递给kfree函数的参数(即要释放的内存块的地址),并保存到变量ptr中;否则,会读取传递给kmem_free函数的参数(即要释放的内存块的地址),并保存到变量ptr中。接着,该函数会调用之前定义的gen_free_enter函数来处理该内存块的释放。 SEC(\"tracepoint/kmem/kmem_cache_alloc\")\nint memleak__kmem_cache_alloc(struct trace_event_raw_kmem_alloc *ctx)\n{ if (wa_missing_free) gen_free_enter(ctx->ptr); gen_alloc_enter(ctx->bytes_alloc); return gen_alloc_exit2(ctx, (u64)(ctx->ptr));\n} 这段代码定义了一个函数 memleak__kmem_cache_alloc,这也是一个bpf程序,会在内核调用 kmem_cache_alloc 函数时执行。如果标记 wa_missing_free 被设置,则调用 gen_free_enter 函数处理可能遗漏的释放操作。然后,该函数会调用 gen_alloc_enter 函数来处理内存分配,最后调用gen_alloc_exit2函数记录分配的结果。 这两个 bpf 程序都使用了 SEC 宏定义了对应的 tracepoint,以便在相应的内核函数被调用时得到执行。在Linux内核中,tracepoint 是一种可以在内核中插入的静态钩子,可以用来收集运行时的内核信息,它在调试和性能分析中非常有用。 在理解这些代码的过程中,要注意 BPF_CORE_READ 宏的使用。这个宏用于在 bpf 程序中读取内核数据。在 bpf 程序中,我们不能直接访问内核内存,而需要使用这样的宏来安全地读取数据。","breadcrumbs":"lesson 16-memleak » memleak 内核态 eBPF 程序实现","id":"114","title":"memleak 内核态 eBPF 程序实现"},"115":{"body":"在理解 BPF 内核部分之后,我们转到用户空间程序。用户空间程序与BPF内核程序紧密配合,它负责将BPF程序加载到内核,设置和管理BPF map,以及处理从BPF程序收集到的数据。用户态程序较长,我们这里可以简要参考一下它的挂载点。 int attach_uprobes(struct memleak_bpf *skel)\n{ ATTACH_UPROBE_CHECKED(skel, malloc, malloc_enter); ATTACH_URETPROBE_CHECKED(skel, malloc, malloc_exit); ATTACH_UPROBE_CHECKED(skel, calloc, calloc_enter); ATTACH_URETPROBE_CHECKED(skel, calloc, calloc_exit); ATTACH_UPROBE_CHECKED(skel, realloc, realloc_enter); ATTACH_URETPROBE_CHECKED(skel, realloc, realloc_exit); ATTACH_UPROBE_CHECKED(skel, mmap, mmap_enter); ATTACH_URETPROBE_CHECKED(skel, mmap, mmap_exit); ATTACH_UPROBE_CHECKED(skel, posix_memalign, posix_memalign_enter); ATTACH_URETPROBE_CHECKED(skel, posix_memalign, posix_memalign_exit); ATTACH_UPROBE_CHECKED(skel, memalign, memalign_enter); ATTACH_URETPROBE_CHECKED(skel, memalign, memalign_exit); ATTACH_UPROBE_CHECKED(skel, free, free_enter); ATTACH_UPROBE_CHECKED(skel, munmap, munmap_enter); // the following probes are intentinally allowed to fail attachment // deprecated in libc.so bionic ATTACH_UPROBE(skel, valloc, valloc_enter); ATTACH_URETPROBE(skel, valloc, valloc_exit); // deprecated in libc.so bionic ATTACH_UPROBE(skel, pvalloc, pvalloc_enter); ATTACH_URETPROBE(skel, pvalloc, pvalloc_exit); // added in C11 ATTACH_UPROBE(skel, aligned_alloc, aligned_alloc_enter); ATTACH_URETPROBE(skel, aligned_alloc, aligned_alloc_exit); return 0;\n} 在这段代码中,我们看到一个名为attach_uprobes的函数,该函数负责将uprobes(用户空间探测点)挂载到内存分配和释放函数上。在Linux中,uprobes是一种内核机制,可以在用户空间程序中的任意位置设置断点,这使得我们可以非常精确地观察和控制用户空间程序的行为。 这里,每个内存相关的函数都通过两个uprobes进行跟踪:一个在函数入口(enter),一个在函数退出(exit)。因此,每当这些函数被调用或返回时,都会触发一个uprobes事件,进而触发相应的BPF程序。 在具体的实现中,我们使用了ATTACH_UPROBE和ATTACH_URETPROBE两个宏来附加uprobes和uretprobes(函数返回探测点)。每个宏都需要三个参数:BPF程序的骨架(skel),要监视的函数名,以及要触发的BPF程序的名称。 这些挂载点包括常见的内存分配函数,如malloc、calloc、realloc、mmap、posix_memalign、memalign、free等,以及对应的退出点。另外,我们也观察一些可能的分配函数,如valloc、pvalloc、aligned_alloc等,尽管它们可能不总是存在。 这些挂载点的目标是捕获所有可能的内存分配和释放事件,从而使我们的内存泄露检测工具能够获取到尽可能全面的数据。这种方法可以让我们不仅能跟踪到内存分配和释放,还能得到它们发生的上下文信息,例如调用栈和调用次数,从而帮助我们定位和修复内存泄露问题。 注意,一些内存分配函数可能并不存在或已弃用,比如valloc、pvalloc等,因此它们的附加可能会失败。在这种情况下,我们允许附加失败,并不会阻止程序的执行。这是因为我们更关注的是主流和常用的内存分配函数,而这些已经被弃用的函数往往在实际应用中较少使用。 完整的源代码: https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/16-memleak 关于如何安装依赖,请参考: https://eunomia.dev/tutorials/11-bootstrap/","breadcrumbs":"lesson 16-memleak » 用户态程序","id":"115","title":"用户态程序"},"116":{"body":"$ make\n$ sudo ./memleak using default object: libc.so.6\nusing page size: 4096\ntracing kernel: true\nTracing outstanding memory allocs... Hit Ctrl-C to end\n[17:17:27] Top 10 stacks with outstanding allocations:\n1236992 bytes in 302 allocations from stack 0 [ ] 1 [ ] 2 [ ] 3 [ ] 4 [ ] 5 [ ] 6 [ ] \n...","breadcrumbs":"lesson 16-memleak » 编译运行","id":"116","title":"编译运行"},"117":{"body":"通过本篇 eBPF 入门实践教程,您已经学习了如何编写 Memleak eBPF 监控程序,以实时监控程序的内存泄漏。您已经了解了 eBPF 在内存监控方面的应用,学会了使用 BPF API 编写 eBPF 程序,创建和使用 eBPF maps,并且明白了如何用 eBPF 工具监测和分析内存泄漏问题。我们展示了一个详细的例子,帮助您理解 eBPF 代码的运行流程和原理。 您可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。 接下来的教程将进一步探讨 eBPF 的高级特性,我们会继续分享更多有关 eBPF 开发实践的内容。希望这些知识和技巧能帮助您更好地了解和使用 eBPF,以解决实际工作中遇到的问题。 参考资料: https://github.com/iovisor/bcc/blob/master/libbpf-tools/memleak.c","breadcrumbs":"lesson 16-memleak » 总结","id":"117","title":"总结"},"118":{"body":"eBPF(扩展的伯克利数据包过滤器)是 Linux 内核中的一种新技术,允许用户在内核空间中执行自定义程序,而无需更改内核代码。这为系统管理员和开发者提供了强大的工具,可以深入了解和监控系统的行为,从而进行优化。 在本篇教程中,我们将探索如何使用 eBPF 编写程序来统计随机和顺序的磁盘 I/O。磁盘 I/O 是计算机性能的关键指标之一,特别是在数据密集型应用中。","breadcrumbs":"lesson 17-biopattern » eBPF 入门实践教程十七:编写 eBPF 程序统计随机/顺序磁盘 I/O","id":"118","title":"eBPF 入门实践教程十七:编写 eBPF 程序统计随机/顺序磁盘 I/O"},"119":{"body":"随着技术的进步和数据量的爆炸性增长,磁盘 I/O 成为了系统性能的关键瓶颈。应用程序的性能很大程度上取决于其如何与存储层进行交互。因此,深入了解和优化磁盘 I/O,特别是随机和顺序的 I/O,变得尤为重要。 随机 I/O :随机 I/O 发生在应用程序从磁盘的非连续位置读取或写入数据时。这种 I/O 模式的主要特点是磁盘头需要频繁地在不同的位置之间移动,导致其通常比顺序 I/O 的速度慢。典型的产生随机 I/O 的场景包括数据库查询、文件系统的元数据操作以及虚拟化环境中的并发任务。 顺序 I/O :与随机 I/O 相反,顺序 I/O 是当应用程序连续地读取或写入磁盘上的数据块。这种 I/O 模式的优势在于磁盘头可以在一个方向上连续移动,从而大大提高了数据的读写速度。视频播放、大型文件的下载或上传以及连续的日志记录都是产生顺序 I/O 的典型应用。 为了实现存储性能的最优化,了解随机和顺序的磁盘 I/O 是至关重要的。例如,随机 I/O 敏感的应用程序在 SSD 上的性能通常远超于传统硬盘,因为 SSD 在处理随机 I/O 时几乎没有寻址延迟。相反,对于大量顺序 I/O 的应用,如何最大化磁盘的连续读写速度则更为关键。 在本教程的后续部分,我们将详细探讨如何使用 eBPF 工具来实时监控和统计这两种类型的磁盘 I/O。这不仅可以帮助我们更好地理解系统的 I/O 行为,还可以为进一步的性能优化提供有力的数据支持。","breadcrumbs":"lesson 17-biopattern » 随机/顺序磁盘 I/O","id":"119","title":"随机/顺序磁盘 I/O"},"12":{"body":"eunomia-bpf 是一个开源的 eBPF 动态加载运行时和开发工具链,是为了简化 eBPF 程序的开发、构建、分发、运行而设计的,基于 libbpf 的 CO-RE 轻量级开发框架。 使用 eunomia-bpf ,可以: 在编写 eBPF 程序或工具时只编写内核态代码,自动获取内核态导出信息,并作为模块动态加载; eunomia-bpf 可以将预编译的 eBPF 程序打包为通用的 JSON 或 WASM 模块,跨架构和内核版本进行分发,无需重新编译即可动态加载运行。 eunomia-bpf 由一个编译工具链和一个运行时库组成, 对比传统的 BCC、原生 libbpf 等框架,简化了 eBPF 程序的开发流程,在大多数时候只需编写内核态代码,即可轻松构建、打包、发布完整的 eBPF 应用。 eunomia-bpf 项目 Github 地址: https://github.com/eunomia-bpf/eunomia-bpf gitee 镜像: https://gitee.com/anolis/eunomia","breadcrumbs":"lesson 0-introduce » eunomia-bpf","id":"12","title":"eunomia-bpf"},"120":{"body":"Biopattern 可以统计随机/顺序磁盘I/O次数的比例。 首先,确保你已经正确安装了 libbpf 和相关的工具集,可以在这里找到对应的源代码: bpf-developer-tutorial 关于如何安装依赖,请参考: https://eunomia.dev/tutorials/11-bootstrap/ 导航到 biopattern 的源代码目录,并使用 make 命令进行编译: cd ~/bpf-developer-tutorial/src/17-biopattern\nmake 编译成功后,你应该可以在当前目录下看到 biopattern 的可执行文件。基本的运行命令如下: sudo ./biopattern [interval] [count] 例如,要每秒打印一次输出,并持续10秒,你可以运行: $ sudo ./biopattern 1 10\nTracing block device I/O requested seeks... Hit Ctrl-C to end.\nDISK %RND %SEQ COUNT KBYTES\nsr0 0 100 3 0\nsr1 0 100 8 0\nsda 0 100 1 4\nsda 100 0 26 136\nsda 0 100 1 4 输出列的含义如下: DISK:被追踪的磁盘名称。 %RND:随机 I/O 的百分比。 %SEQ:顺序 I/O 的百分比。 COUNT:在指定的时间间隔内的 I/O 请求次数。 KBYTES:在指定的时间间隔内读写的数据量(以 KB 为单位)。 从上述输出中,我们可以得出以下结论: sr0 和 sr1 设备在观测期间主要进行了顺序 I/O,但数据量很小。 sda 设备在某些时间段内只进行了随机 I/O,而在其他时间段内只进行了顺序 I/O。 这些信息可以帮助我们了解系统的 I/O 模式,从而进行针对性的优化。","breadcrumbs":"lesson 17-biopattern » Biopattern","id":"120","title":"Biopattern"},"121":{"body":"首先,让我们看一下 biopattern 的核心 eBPF 内核态代码: #include \n#include \n#include \n#include \"biopattern.h\"\n#include \"maps.bpf.h\"\n#include \"core_fixes.bpf.h\" const volatile bool filter_dev = false;\nconst volatile __u32 targ_dev = 0; struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 64); __type(key, u32); __type(value, struct counter);\n} counters SEC(\".maps\"); SEC(\"tracepoint/block/block_rq_complete\")\nint handle__block_rq_complete(void *args)\n{ struct counter *counterp, zero = {}; sector_t sector; u32 nr_sector; u32 dev; if (has_block_rq_completion()) { struct trace_event_raw_block_rq_completion___x *ctx = args; sector = BPF_CORE_READ(ctx, sector); nr_sector = BPF_CORE_READ(ctx, nr_sector); dev = BPF_CORE_READ(ctx, dev); } else { struct trace_event_raw_block_rq_complete___x *ctx = args; sector = BPF_CORE_READ(ctx, sector); nr_sector = BPF_CORE_READ(ctx, nr_sector); dev = BPF_CORE_READ(ctx, dev); } if (filter_dev && targ_dev != dev) return 0; counterp = bpf_map_lookup_or_try_init(&counters, &dev, &zero); if (!counterp) return 0; if (counterp->last_sector) { if (counterp->last_sector == sector) __sync_fetch_and_add(&counterp->sequential, 1); else __sync_fetch_and_add(&counterp->random, 1); __sync_fetch_and_add(&counterp->bytes, nr_sector * 512); } counterp->last_sector = sector + nr_sector; return 0;\n} char LICENSE[] SEC(\"license\") = \"GPL\"; 全局变量定义 const volatile bool filter_dev = false; const volatile __u32 targ_dev = 0; 这两个全局变量用于设备过滤。filter_dev 决定是否启用设备过滤,而 targ_dev 是我们想要追踪的目标设备的标识符。 BPF map 定义: struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 64); __type(key, u32); __type(value, struct counter); } counters SEC(\".maps\"); 这部分代码定义了一个 BPF map,类型为哈希表。该映射的键是设备的标识符,而值是一个 counter 结构体,用于存储设备的 I/O 统计信息。 追踪点函数: SEC(\"tracepoint/block/block_rq_complete\") int handle__block_rq_complete(void *args) { struct counter *counterp, zero = {}; sector_t sector; u32 nr_sector; u32 dev; if (has_block_rq_completion()) { struct trace_event_raw_block_rq_completion___x *ctx = args; sector = BPF_CORE_READ(ctx, sector); nr_sector = BPF_CORE_READ(ctx, nr_sector); dev = BPF_CORE_READ(ctx, dev); } else { struct trace_event_raw_block_rq_complete___x *ctx = args; sector = BPF_CORE_READ(ctx, sector); nr_sector = BPF_CORE_READ(ctx, nr_sector); dev = BPF_CORE_READ(ctx, dev); } if (filter_dev && targ_dev != dev) return 0; counterp = bpf_map_lookup_or_try_init(&counters, &dev, &zero); if (!counterp) return 0; if (counterp->last_sector) { if (counterp->last_sector == sector) __sync_fetch_and_add(&counterp->sequential, 1); else __sync_fetch_and_add(&counterp->random, 1); __sync_fetch_and_add(&counterp->bytes, nr_sector * 512); } counterp->last_sector = sector + nr_sector; return 0; } 在 Linux 中,每次块设备的 I/O 请求完成时,都会触发一个名为 block_rq_complete 的追踪点。这为我们提供了一个机会,通过 eBPF 来捕获这些事件,并进一步分析 I/O 的模式。 主要逻辑分析: 提取 I/O 请求信息 :从传入的参数中获取 I/O 请求的相关信息。这里有两种可能的上下文结构,取决于 has_block_rq_completion 的返回值。这是因为不同版本的 Linux 内核可能会有不同的追踪点定义。无论哪种情况,我们都从上下文中提取出扇区号 (sector)、扇区数量 (nr_sector) 和设备标识符 (dev)。 设备过滤 :如果启用了设备过滤 (filter_dev 为 true),并且当前设备不是目标设备 (targ_dev),则直接返回。这允许用户只追踪特定的设备,而不是所有设备。 统计信息更新 : - 查找或初始化统计信息 :使用 bpf_map_lookup_or_try_init 函数查找或初始化与当前设备相关的统计信息。如果映射中没有当前设备的统计信息,它会使用 zero 结构体进行初始化。 - 判断 I/O 模式 :根据当前 I/O 请求与上一个 I/O 请求的扇区号,我们可以判断当前请求是随机的还是顺序的。如果两次请求的扇区号相同,那么它是顺序的;否则,它是随机的。然后,我们使用 __sync_fetch_and_add 函数更新相应的统计信息。这是一个原子操作,确保在并发环境中数据的一致性。 - 更新数据量 :我们还更新了该设备的总数据量,这是通过将扇区数量 (nr_sector) 乘以 512(每个扇区的字节数)来实现的。 - 更新最后一个 I/O 请求的扇区号 :为了下一次的比较,我们更新了 last_sector 的值。 在 Linux 内核的某些版本中,由于引入了一个新的追踪点 block_rq_error,追踪点的命名和结构发生了变化。这意味着,原先的 block_rq_complete 追踪点的结构名称从 trace_event_raw_block_rq_complete 更改为 trace_event_raw_block_rq_completion。这种变化可能会导致 eBPF 程序在不同版本的内核上出现兼容性问题。 为了解决这个问题,biopattern 工具引入了一种机制来动态检测当前内核使用的是哪种追踪点结构,即 has_block_rq_completion 函数。 定义两种追踪点结构 : struct trace_event_raw_block_rq_complete___x { dev_t dev; sector_t sector; unsigned int nr_sector; } __attribute__((preserve_access_index)); struct trace_event_raw_block_rq_completion___x { dev_t dev; sector_t sector; unsigned int nr_sector; } __attribute__((preserve_access_index)); 这里定义了两种追踪点结构,分别对应于不同版本的内核。每种结构都包含设备标识符 (dev)、扇区号 (sector) 和扇区数量 (nr_sector)。 动态检测追踪点结构 : static __always_inline bool has_block_rq_completion() { if (bpf_core_type_exists(struct trace_event_raw_block_rq_completion___x)) return true; return false; } has_block_rq_completion 函数使用 bpf_core_type_exists 函数来检测当前内核是否存在 trace_event_raw_block_rq_completion___x 结构。如果存在,函数返回 true,表示当前内核使用的是新的追踪点结构;否则,返回 false,表示使用的是旧的结构。在对应的 eBPF 代码中,会根据两种不同的定义分别进行处理,这也是适配不同内核版本之间的变更常见的方案。","breadcrumbs":"lesson 17-biopattern » eBPF Biopattern 实现原理","id":"121","title":"eBPF Biopattern 实现原理"},"122":{"body":"biopattern 工具的用户态代码负责从 BPF 映射中读取统计数据,并将其展示给用户。通过这种方式,系统管理员可以实时监控每个设备的 I/O 模式,从而更好地理解和优化系统的 I/O 性能。 主循环: /* main: poll */ while (1) { sleep(env.interval); err = print_map(obj->maps.counters, partitions); if (err) break; if (exiting || --env.times == 0) break; } 这是 biopattern 工具的主循环,它的工作流程如下: 等待 :使用 sleep 函数等待指定的时间间隔 (env.interval)。 打印映射 :调用 print_map 函数打印 BPF 映射中的统计数据。 退出条件 :如果收到退出信号 (exiting 为 true) 或者达到指定的运行次数 (env.times 达到 0),则退出循环。 打印映射函数: static int print_map(struct bpf_map *counters, struct partitions *partitions) { __u32 total, lookup_key = -1, next_key; int err, fd = bpf_map__fd(counters); const struct partition *partition; struct counter counter; struct tm *tm; char ts[32]; time_t t; while (!bpf_map_get_next_key(fd, &lookup_key, &next_key)) { err = bpf_map_lookup_elem(fd, &next_key, &counter); if (err < 0) { fprintf(stderr, \"failed to lookup counters: %d\\n\", err); return -1; } lookup_key = next_key; total = counter.sequential + counter.random; if (!total) continue; if (env.timestamp) { time(&t); tm = localtime(&t); strftime(ts, sizeof(ts), \"%H:%M:%S\", tm); printf(\"%-9s \", ts); } partition = partitions__get_by_dev(partitions, next_key); printf(\"%-7s %5ld %5ld %8d %10lld\\n\", partition ? partition->name : \"Unknown\", counter.random * 100L / total, counter.sequential * 100L / total, total, counter.bytes / 1024); } lookup_key = -1; while (!bpf_map_get_next_key(fd, &lookup_key, &next_key)) { err = bpf_map_delete_elem(fd, &next_key); if (err < 0) { fprintf(stderr, \"failed to cleanup counters: %d\\n\", err); return -1; } lookup_key = next_key; } return 0; } print_map 函数负责从 BPF 映射中读取统计数据,并将其打印到控制台。其主要逻辑如下: 遍历 BPF 映射 :使用 bpf_map_get_next_key 和 bpf_map_lookup_elem 函数遍历 BPF 映射,获取每个设备的统计数据。 计算总数 :计算每个设备的随机和顺序 I/O 的总数。 打印统计数据 :如果启用了时间戳 (env.timestamp 为 true),则首先打印当前时间。接着,打印设备名称、随机 I/O 的百分比、顺序 I/O 的百分比、总 I/O 数量和总数据量(以 KB 为单位)。 清理 BPF 映射 :为了下一次的统计,使用 bpf_map_get_next_key 和 bpf_map_delete_elem 函数清理 BPF 映射中的所有条目。","breadcrumbs":"lesson 17-biopattern » 用户态代码","id":"122","title":"用户态代码"},"123":{"body":"在本教程中,我们深入探讨了如何使用 eBPF 工具 biopattern 来实时监控和统计随机和顺序的磁盘 I/O。我们首先了解了随机和顺序磁盘 I/O 的重要性,以及它们对系统性能的影响。接着,我们详细介绍了 biopattern 的工作原理,包括如何定义和使用 BPF maps,如何处理不同版本的 Linux 内核中的追踪点变化,以及如何在 eBPF 程序中捕获和分析磁盘 I/O 事件。 您可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。 完整代码: https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/17-biopattern bcc 工具: https://github.com/iovisor/bcc/blob/master/libbpf-tools/biopattern.c","breadcrumbs":"lesson 17-biopattern » 总结","id":"123","title":"总结"},"124":{"body":"可以在这里找到更多关于 eBPF 的信息: 一个关于 eBPF 相关内容和信息的详细列表: https://github.com/zoidbergwill/awesome-ebpf eBPF 相关项目、教程: https://ebpf.io/ 这是我近年来读过的与 eBPF 相关的论文列表,可能对于对 eBPF 相关研究感兴趣的人有所帮助。 eBPF(扩展的伯克利数据包过滤器)是一种新兴的技术,允许在 Linux 内核中安全地执行用户提供的程序。近年来,它因加速网络处理、增强可观察性和实现可编程数据包处理而得到了广泛的应用。此文档列出了过去几年关于 eBPF 的一些关键研究论文。这些论文涵盖了 eBPF 的几个方面,包括加速分布式系统、存储和网络,正式验证 eBPF 的 JIT 编译器和验证器,将 eBPF 用于入侵检测,以及从 eBPF 程序自动生成硬件设计。 一些关键亮点: eBPF 允许在内核中执行自定义函数,以加速分布式协议、存储引擎和网络应用,与传统的用户空间实现相比,可以提高吞吐量和降低延迟。 eBPF 组件(如 JIT 和验证器)的正式验证确保了正确性,并揭示了实际实现中的错误。 eBPF 的可编程性和效率使其适合在内核中完全构建入侵检测和网络监控应用。 从 eBPF 程序中自动生成硬件设计允许软件开发人员快速生成网络卡中的优化数据包处理管道。 这些论文展示了 eBPF 在加速系统、增强安全性和简化网络编程方面的多功能性。随着 eBPF 的采用不断增加,它是一个与性能、安全性、硬件集成和易用性相关的系统研究的重要领域。 如果您有任何建议或添加论文的意见,请随时开放一个问题或PR。此列表创建于 2023.10,未来将添加新的论文。 如果您对 eBPF 有些进一步的兴趣的话,也可以查看我们在 eunomia-bpf 的开源项目和 bpf-developer-tutorial 的 eBPF 教程。我也在寻找 2024/2025 年系统和网络领域的 PhD 相关机会,这是我的 Github 和 邮箱 。","breadcrumbs":"lesson 18-further-reading » 更多的参考资料:论文、项目等等","id":"124","title":"更多的参考资料:论文、项目等等"},"125":{"body":"随着微秒级 NVMe 存储设备的出现,Linux 内核存储堆栈开销变得显著,几乎使访问时间翻倍。我们介绍了 XRP,一个框架,允许应用程序从 eBPF 在 NVMe 驱动程序中的钩子执行用户定义的存储功能,如索引查找或聚合,安全地绕过大部分内核的存储堆栈。为了保持文件系统的语义,XRP 将少量的内核状态传播到其 NVMe 驱动程序钩子,在那里调用用户注册的 eBPF 函数。我们展示了如何利用 XRP 显著提高两个键值存储,BPF-KV,一个简单的 B+ 树键值存储,和 WiredTiger,一个流行的日志结构合并树存储引擎的吞吐量和延迟。 OSDI '22 最佳论文: https://www.usenix.org/conference/osdi22/presentation/zhong","breadcrumbs":"lesson 18-further-reading » XRP: In-Kernel Storage Functions with eBPF","id":"125","title":"XRP: In-Kernel Storage Functions with eBPF"},"126":{"body":"本文描述了我们将形式方法应用于 Linux 内核中的一个关键组件,即 Berkeley 数据包过滤器 (BPF) 虚拟机的即时编译器 (\"JIT\") 的经验。我们使用 Jitterbug 验证这些 JIT,这是第一个提供 JIT 正确性的精确规范的框架,能够排除实际错误,并提供一个自动化的证明策略,该策略可以扩展到实际实现。使用 Jitterbug,我们设计、实施并验证了一个新的针对 32 位 RISC-V 的 BPF JIT,在五个其他部署的 JIT 中找到并修复了 16 个之前未知的错误,并开发了新的 JIT 优化;所有这些更改都已上传到 Linux 内核。结果表明,在一个大型的、未经验证的系统中,通过仔细设计规范和证明策略,可以构建一个经过验证的组件。 OSDI 20: https://www.usenix.org/conference/osdi20/presentation/nelson","breadcrumbs":"lesson 18-further-reading » Specification and verification in the field: Applying formal methods to BPF just-in-time compilers in the Linux kernel","id":"126","title":"Specification and verification in the field: Applying formal methods to BPF just-in-time compilers in the Linux kernel"},"127":{"body":"新兴的计算存储设备为存储内计算提供了一个机会。它减少了主机与设备之间的数据移动开销,从而加速了数据密集型应用程序。在这篇文章中,我们介绍 λ-IO,一个统一的 IO 堆栈,跨主机和设备管理计算和存储资源。我们提出了一套设计 - 接口、运行时和调度 - 来解决三个关键问题。我们在全堆栈软件和硬件环境中实施了 λ-IO,并使用合成和实际应用程序对其 进行评估,与 Linux IO 相比,显示出高达 5.12 倍的性能提升。 FAST23: https://www.usenix.org/conference/fast23/presentation/yang-zhe","breadcrumbs":"lesson 18-further-reading » λ-IO: A Unified IO Stack for Computational Storage","id":"127","title":"λ-IO: A Unified IO Stack for Computational Storage"},"128":{"body":"用户文件系统相对于其内核实现提供了许多优势,例如开发的简易性和更好的系统可靠性。然而,它们会导致重大的性能损失。我们观察到现有的用户文件系统框架非常通用;它们由一个位于内核中的最小干预层组成,该层简单地将所有低级请求转发到用户空间。虽然这种设计提供了灵活性,但由于频繁的内核-用户上下文切换,它也严重降低了性能。 这项工作介绍了 ExtFUSE,一个用于开发可扩展用户文件系统的框架,该框架还允许应用程序在内核中注册\"薄\"的专用请求处理程序,以满足其特定的操作需求,同时在用户空间中保留复杂的功能。我们使用两个 FUSE 文件系统对 ExtFUSE 进行评估,结果表明 ExtFUSE 可以通过平均不到几百行的改动来提高用户文件系统的性能。ExtFUSE 可在 GitHub 上找到。 ATC 19: https://www.usenix.org/conference/atc19/presentation/bijlani","breadcrumbs":"lesson 18-further-reading » Extension Framework for File Systems in User space","id":"128","title":"Extension Framework for File Systems in User space"},"129":{"body":"在标准的Linux内核网络栈下实现分布式协议可以享受到负载感知的CPU缩放、高兼容性以及强大的安全性和隔离性。但由于过多的用户-内核切换和内核网络栈遍历,其性能较低。我们介绍了Electrode,这是一套为分布式协议设计的基于eBPF的性能优化。这些优化在网络栈之前在内核中执行,但实现了与用户空间中实现的相似功能(例如,消息广播,收集ack的仲裁),从而避免了用户-内核切换和内核网络栈遍历所带来的开销。我们展示,当应用于经典的Multi-Paxos状态机复制协议时,Electrode可以提高其吞吐量高达128.4%,并将延迟降低高达41.7%。 NSDI 23: 链接","breadcrumbs":"lesson 18-further-reading » Electrode: Accelerating Distributed Protocols with eBPF","id":"129","title":"Electrode: Accelerating Distributed Protocols with eBPF"},"13":{"body":"eBPF 介绍: https://ebpf.io/ BPF Compiler Collection (BCC): https://github.com/iovisor/bcc eunomia-bpf: https://github.com/eunomia-bpf/eunomia-bpf 您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程源代码。我们会继续分享更多有关 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术。 原文地址: https://eunomia.dev/zh/tutorials/0-introduce/ 转载请注明","breadcrumbs":"lesson 0-introduce » 参考资料","id":"13","title":"参考资料"},"130":{"body":"内存键值存储是帮助扩展大型互联网服务的关键组件,通过提供对流行数据的低延迟访问。Memcached是最受欢迎的键值存储之一,由于Linux网络栈固有的性能限制,当使用高速网络接口时,其性能不高。虽然可以使用DPDK基础方案绕过Linux网络栈,但这种方法需要对软件栈进行完全重新设计,而且在客户端负载较低时也会导致高CPU利用率。 为了克服这些限制,我们提出了BMC,这是一个为Memcached设计的内核缓存,可以在执行标准网络栈之前服务于请求。对BMC缓存的请求被视为NIC中断的一部分,这允许性能随着为NIC队列服务的核心数量而扩展。为确保安全,BMC使用eBPF实现。尽管eBPF具有安全约束,但我们展示了实现复杂缓存服务是可能的。因为BMC在商用硬件上运行,并且不需要修改Linux内核或Memcached应用程序,所以它可以在现有系统上广泛部署。BMC优化了Facebook样式的小型请求的处理时间。在这个目标工作负载上,我们的评估显示,与原始的Memcached应用程序相比,BMC的吞吐量提高了高达18倍,与使用SO_REUSEPORT套接字标志的优化版Memcached相比,提高了高达6倍。此外,我们的结果还显示,对于非目标工作负载,BMC的开销可以忽略不计,并且不会降低吞吐量。 NSDI 21: 链接","breadcrumbs":"lesson 18-further-reading » BMC: Accelerating Memcached using Safe In-kernel Caching and Pre-stack Processing","id":"130","title":"BMC: Accelerating Memcached using Safe In-kernel Caching and Pre-stack Processing"},"131":{"body":"FPGA加速器在NIC上使得从CPU卸载昂贵的数据包处理任务成为可能。但是,FPGA有限的资源可能需要在多个应用程序之间共享,而编程它们则很困难。 我们提出了一种在FPGA上运行Linux的eXpress Data Path程序的解决方案,这些程序使用eBPF编写,仅使用可用硬件资源的一部分,同时匹配高端CPU的性能。eBPF的迭代执行模型不适合FPGA加速器。尽管如此,我们展示了,当针对一个特定的FPGA执行器时,一个eBPF程序的许多指令可以被压缩、并行化或完全删除,从而显著提高性能。我们利用这一点设计了hXDP,它包括(i)一个优化编译器,该编译器并行化并将eBPF字节码转换为我们定义的扩展eBPF指令集架构;(ii)一个在FPGA上执行这些指令的软处理器;以及(iii)一个基于FPGA的基础设施,提供XDP的maps和Linux内核中定义的helper函数。 我们在FPGA NIC上实现了hXDP,并评估了其运行真实世界的未经修改的eBPF程序的性能。我们的实现以156.25MHz的速度时钟,使用约15%的FPGA资源,并可以运行动态加载的程序。尽管有这些适度的要求,但它达到了高端CPU核心的数据包处理吞吐量,并提供了10倍低的数据包转发延迟。 OSDI 20: 链接","breadcrumbs":"lesson 18-further-reading » hXDP: Efficient Software Packet Processing on FPGA NICs","id":"131","title":"hXDP: Efficient Software Packet Processing on FPGA NICs"},"132":{"body":"微服务正变得越来越复杂,给传统的性能监控解决方案带来了新的挑战。一方面,微服务的快速演变给现有的分布式跟踪框架的使用和维护带来了巨大的负担。另一方面,复杂的基础设施增加了网络性能问题的概率,并在网络侧创造了更多的盲点。在这篇论文中,我们介绍了 DeepFlow,一个用于微服务故障排除的以网络为中心的分布式跟踪框架。DeepFlow 通过一个以网络为中心的跟踪平面和隐式的上下文传播提供开箱即用的跟踪。此外,它消除了网络基础设施中的盲点,以低成本方式捕获网络指标,并增强了不同组件和层之间的关联性。我们从分析和实证上证明,DeepFlow 能够准确地定位微服务性能异常,而开销几乎可以忽略不计。DeepFlow 已经为超过26家公司发现了71多个关键性能异常,并已被数百名开发人员所使用。我们的生产评估显示,DeepFlow 能够为用户节省数小时的仪表化工作,并将故障排除时间从数小时缩短到几分钟。 SIGCOMM 23: https://dl.acm.org/doi/10.1145/3603269.3604823","breadcrumbs":"lesson 18-further-reading » Network-Centric Distributed Tracing with DeepFlow: Troubleshooting Your Microservices in Zero Code","id":"132","title":"Network-Centric Distributed Tracing with DeepFlow: Troubleshooting Your Microservices in Zero Code"},"133":{"body":"扩展的伯克利数据包过滤器(eBPF)是一个基础设施,允许在不重新编译的情况下动态加载并直接在 Linux 内核中运行微程序。 在这项工作中,我们研究如何在 eBPF 中开发高性能的网络测量。我们以绘图为案例研究,因为它们具有支持广泛任务的能力,同时提供低内存占用和准确性保证。我们实现了 NitroSketch,一个用于用户空间网络的最先进的绘图,并表明用户空间网络的最佳实践不能直接应用于 eBPF,因为它的性能特点不同。通过应用我们学到的经验教训,我们将其性能提高了40%,与初级实现相比。 SIGCOMM 23: https://dl.acm.org/doi/abs/10.1145/3594255.3594256","breadcrumbs":"lesson 18-further-reading » Fast In-kernel Traffic Sketching in eBPF","id":"133","title":"Fast In-kernel Traffic Sketching in eBPF"},"134":{"body":"无服务器计算在云环境中承诺提供高效、低成本的计算能力。然而,现有的解决方案,如Knative这样的开源平台,包含了繁重的组件,破坏了无服务器计算的目标。此外,这种无服务器平台缺乏数据平面优化,无法实现高效的、高性能的功能链,这也是流行的微服务开发范式的设施。它们为构建功能链使用的不必要的复杂和重复的功能严重降低了性能。\"冷启动\"延迟是另一个威慑因素。 我们描述了 SPRIGHT,一个轻量级、高性能、响应式的无服务器框架。SPRIGHT 利用共享内存处理显著提高了数据平面的可伸缩性,通过避免不必要的协议处理和序列化-反序列化开销。SPRIGHT 大量利用扩展的伯克利数据包过滤器 (eBPF) 进行事件驱动处理。我们创造性地使用 eBPF 的套接字消息机制支持共享内存处理,其开销严格与负载成正比。与常驻、基于轮询的DPDK相比,SPRIGHT 在真实工作负载下实现了相同的数据平面性能,但 CPU 使用率降低了10倍。此外,eBPF 为 SPRIGHT 带来了好处,替换了繁重的无服务器组件,使我们能够以微不足道的代价保持函数处于\"暖\"状态。 我们的初步实验结果显示,与 Knative 相比,SPRIGHT 在吞吐量和延迟方面实现了一个数量级的提高,同时大大减少了 CPU 使用,并消除了 \"冷启动\"的需要。 https://dl.acm.org/doi/10.1145/3544216.3544259","breadcrumbs":"lesson 18-further-reading » SPRIGHT: extracting the server from serverless computing! high-performance eBPF-based event-driven, shared-memory processing","id":"134","title":"SPRIGHT: extracting the server from serverless computing! high-performance eBPF-based event-driven, shared-memory processing"},"135":{"body":"修改和扩展操作系统的能力是提高系统安全性、可靠性和性能的重要功能。扩展的伯克利数据包过滤器(eBPF)生态系统已经成为扩展Linux内核的标准机制,并且最近已被移植到Windows。eBPF程序将新逻辑注入内核,使系统在现有逻辑之前或之后执行这些逻辑。虽然eBPF生态系统提供了一种灵活的内核扩展机制,但目前开发人员编写eBPF程序仍然困难。eBPF开发人员必须深入了解操作系统的内部结构,以确定在何处放置逻辑,并应对eBPF验证器对其eBPF程序的控制流和数据访问施加的编程限制。本文介绍了KEN,一种通过允许使用自然语言编写内核扩展来缓解编写eBPF程序难度的替代框架。KEN利用大语言模型(LLMs)的最新进展,根据用户的英文提示生成eBPF程序。为了确保LLM的输出在语义上等同于用户的提示,KEN结合了LLM增强的程序理解、符号执行和一系列反馈循环。KEN的关键创新在于这些技术的结合。特别是,该系统以一种新颖的结构使用符号执行,使其能够结合程序综合和程序理解的结果,并建立在LLMs在每个任务中单独展示的成功基础上。为了评估KEN,我们开发了一个新的自然语言提示eBPF程序的语料库。我们显示,KEN在80%的情况下生成了正确的eBPF程序,这比LLM增强的程序综合基线提高了2.67倍。 eBPF'24: https://dl.acm.org/doi/10.1145/3672197.3673434 和arxiv https://arxiv.org/abs/2312.05531","breadcrumbs":"lesson 18-further-reading » Kgent: Kernel Extensions Large Language Model Agent","id":"135","title":"Kgent: Kernel Extensions Large Language Model Agent"},"136":{"body":"利用 eBPF 进行可编程的系统调用安全 系统调用过滤是一种广泛用于保护共享的 OS 内核免受不受信任的用户应用程序威胁的安全机制。但是,现有的系统调用过滤技术要么由于用户空间代理带来的上下文切换开销过于昂贵,要么缺乏足够的可编程性来表达高级策略。Seccomp 是 Linux 的系统调用过滤模块,广泛用于现代的容器技术、移动应用和系统管理服务。尽管采用了经典的 BPF 语言(cBPF),但 Seccomp 中的安全策略主要限于静态的允许列表,主要是因为 cBPF 不支持有状态的策略。因此,许多关键的安全功能无法准确地表达,和/或需要修改内核。 在这篇论文中,我们介绍了一个可编程的系统调用过滤机制,它通过利用扩展的 BPF 语言(eBPF)使得更高级的安全策略得以表达。更具体地说,我们创建了一个新的 Seccomp eBPF 程序类型,暴露、修改或创建新的 eBPF 助手函数来安全地管理过滤状态、访问内核和用户状态,以及利用同步原语。重要的是,我们的系统与现有的内核特权和能力机制集成,使非特权用户能够安全地安装高级过滤器。我们的评估表明,我们基于 eBPF 的过滤可以增强现有策略(例如,通过时间专化,减少早期执行阶段的攻击面积高达55.4%)、缓解实际漏洞并加速过滤器。 https://arxiv.org/abs/2302.10366","breadcrumbs":"lesson 18-further-reading » Programmable System Call Security with eBPF","id":"136","title":"Programmable System Call Security with eBPF"},"137":{"body":"在云上困惑的 eBPF 之间的容器攻击 扩展的伯克利数据包过滤器(eBPF)为用户空间程序提供了强大而灵活的内核接口,通过在内核空间直接运行字节码来扩展内核功能。它已被云服务广泛使用,以增强容器安全性、网络管理和系统可观察性。然而,我们发现在 Linux 主机上广泛讨论的攻击性 eBPF 可以为容器带来新的攻击面。通过 eBPF 的追踪特性,攻击者可以破坏容器的隔离并攻击主机,例如,窃取敏感数据、进行 DoS 攻击,甚至逃逸容器。在这篇论文中,我们研究基于 eBPF 的跨容器攻击,并揭示其在实际服务中的安全影响。利用 eBPF 攻击,我们成功地妨害了五个在线的 Jupyter/交互式 Shell 服务和 Google Cloud Platform 的 Cloud Shell。此外,我们发现三家领先的云供应商提供的 Kubernetes 服务在攻击者通过 eBPF 逃逸容器后可以被利用来发起跨节点攻击。具体来说,在阿里巴巴的 Kubernetes 服务中,攻击者可以通过滥用他们过度特权的云指标或管理 Pods 来妨害整个集群。不幸的是,容器上的 eBPF 攻击鲜为人知,并且现有的入侵检测系统几乎无法发现它们。此外,现有的 eBPF 权限模型无法限制 eBPF 并确保在共享内核的容器环境中安全使用。为此,我们提出了一个新的 eBPF 权限模型,以对抗容器中的 eBPF 攻击。 https://www.usenix.org/conference/usenixsecurity23/presentation/he","breadcrumbs":"lesson 18-further-reading » Cross Container Attacks: The Bewildered eBPF on Clouds","id":"137","title":"Cross Container Attacks: The Bewildered eBPF on Clouds"},"138":{"body":"比较 eBPF 和 WebAssembly 中的安全性 本文研究了 eBPF 和 WebAssembly(Wasm)的安全性,这两种技术近年来得到了广泛的采用,尽管它们是为非常不同的用途和环境而设计的。当 eBPF 主要用于 Linux 等操作系统内核时,Wasm 是一个为基于堆栈的虚拟机设计的二进制指令格式,其用途超出了 web。鉴于 eBPF 的增长和不断扩大的雄心,Wasm 可能提供有启发性的见解,因为它围绕在如 web 浏览器和云等复杂和敌对环境中安全执行任意不受信任的程序进行设计。我们分析了两种技术的安全目标、社区发展、内存模型和执行模型,并进行了比较安全性评估,探讨了内存安全性、控制流完整性、API 访问和旁路通道。我们的结果表明,eBPF 有一个首先关注性能、其次关注安全的历史,而 Wasm 更强调安全,尽管要支付一些运行时开销。考虑 eBPF 的基于语言的限制和一个用于 API 访问的安全模型是未来工作的有益方向。 https://dl.acm.org/doi/abs/10.1145/3609021.3609306 更多内容可以在第一个 eBPF 研讨会中找到: https://conferences.sigcomm.org/sigcomm/2023/workshop-ebpf.html","breadcrumbs":"lesson 18-further-reading » Comparing Security in eBPF and WebAssembly","id":"138","title":"Comparing Security in eBPF and WebAssembly"},"139":{"body":"基于eBPF中的机器学习的流式入侵检测系统 eBPF 是一种新技术,允许动态加载代码片段到 Linux 内核中。它可以大大加速网络,因为它使内核能够处理某些数据包而无需用户空间程序的参与。到目前为止,eBPF 主要用于简单的数据包过滤应用,如防火墙或拒绝服务保护。我们证明在 eBPF 中完全基于机器学习开发流式网络入侵检测系统是可行的。我们的解决方案使用决策树,并为每个数据包决定它是否恶意,考虑到网络流的整个先前上下文。与作为用户空间程序实现的同一解决方案相比,我们实现了超过 20% 的性能提升。 https://arxiv.org/abs/2102.09980","breadcrumbs":"lesson 18-further-reading » A flow-based IDS using Machine Learning in eBPF","id":"139","title":"A flow-based IDS using Machine Learning in eBPF"},"14":{"body":"在本篇博客中,我们将深入探讨eBPF(Extended Berkeley Packet Filter)的基本框架和开发流程。eBPF是一种在Linux内核上运行的强大网络和性能分析工具,它为开发者提供了在内核运行时动态加载、更新和运行用户定义代码的能力。这使得开发者可以实现高效、安全的内核级别的网络监控、性能分析和故障排查等功能。 本文是eBPF入门开发实践教程的第二篇,我们将重点关注如何编写一个简单的eBPF程序,并通过实际例子演示整个开发流程。在阅读本教程之前,建议您先学习第一篇教程,以便对eBPF的基本概念有个大致的了解。 在开发eBPF程序时,有多种开发框架可供选择,如 BCC(BPF Compiler Collection)libbpf、cilium/ebpf、eunomia-bpf 等。虽然不同工具的特点各异,但它们的基本开发流程大致相同。在接下来的内容中,我们将深入了解这些流程,并以 Hello World 程序为例,带领读者逐步掌握eBPF开发的基本技巧。 本教程将帮助您了解eBPF程序的基本结构、编译和加载过程、用户空间与内核空间的交互方式以及调试与优化技巧。通过学习本教程,您将掌握eBPF开发的基本知识,并为后续进一步学习和实践奠定坚实的基础。","breadcrumbs":"lesson 1-helloworld » eBPF 入门开发实践教程一:Hello World,基本框架和开发流程","id":"14","title":"eBPF 入门开发实践教程一:Hello World,基本框架和开发流程"},"140":{"body":"针对低功耗 IoT 微控制器上的小型软件功能的轻量级虚拟化和故障隔离: Femto-容器 低功耗的 IoT 微控制器上运行的操作系统运行时通常提供基础的 API、基本的连接性和(有时)一个(安全的)固件更新机制。相比之下,在硬件约束较少的场合,网络化软件已进入无服务器、微服务和敏捷的时代。考虑到弥合这一差距,我们在论文中设计了 Femto-容器,这是一种新的中间件运行时,可以嵌入到各种低功耗 IoT 设备中。Femto-容器使得可以在低功耗 IoT 设备上通过网络安全地部署、执行和隔离小型虚拟软件功能。我们实施了 Femto-容器,并在 RIOT 中提供了集成,这是一个受欢迎的开源 IoT 操作系统。然后,我们评估了我们的实现性能,它已被正式验证用于故障隔离,确保 RIOT 受到加载并在 Femto-容器中执行的逻辑的保护。我们在各种受欢迎的微控制器架构(Arm Cortex-M、ESP32 和 RISC-V)上的实验表明,Femto-容器在内存占用开销、能源消耗和安全性方面提供了有吸引力的权衡。 https://dl.acm.org/doi/abs/10.1145/3528535.3565242","breadcrumbs":"lesson 18-further-reading » Femto-containers: lightweight virtualization and fault isolation for small software functions on low-power IoT microcontrollers","id":"140","title":"Femto-containers: lightweight virtualization and fault isolation for small software functions on low-power IoT microcontrollers"},"141":{"body":"eBPF (扩展的伯克利数据包过滤器) 是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。这个特性使得 eBPF 能够提供极高的灵活性和性能,使其在网络和系统性能分析方面具有广泛的应用。安全方面的 eBPF 应用也是如此,本文将介绍如何使用 eBPF LSM(Linux Security Modules)机制实现一个简单的安全检查程序。","breadcrumbs":"lesson 19-lsm-connect » eBPF 入门实践教程:使用 LSM 进行安全检测防御","id":"141","title":"eBPF 入门实践教程:使用 LSM 进行安全检测防御"},"142":{"body":"LSM 从 Linux 2.6 开始成为官方内核的一个安全框架,基于此的安全实现包括 SELinux 和 AppArmor 等。在 Linux 5.7 引入 BPF LSM 后,系统开发人员已经能够自由地实现函数粒度的安全检查能力,本文就提供了这样一个案例:限制通过 socket connect 函数对特定 IPv4 地址进行访问的 BPF LSM 程序。(可见其控制精度是很高的)","breadcrumbs":"lesson 19-lsm-connect » 背景","id":"142","title":"背景"},"143":{"body":"LSM(Linux Security Modules)是 Linux 内核中用于支持各种计算机安全模型的框架。LSM 在 Linux 内核安全相关的关键路径上预置了一批 hook 点,从而实现了内核和安全模块的解耦,使不同的安全模块可以自由地在内核中加载/卸载,无需修改原有的内核代码就可以加入安全检查功能。 在过去,使用 LSM 主要通过配置已有的安全模块(如 SELinux 和 AppArmor)或编写自己的内核模块;而在 Linux 5.7 引入 BPF LSM 机制后,一切都变得不同了:现在,开发人员可以通过 eBPF 编写自定义的安全策略,并将其动态加载到内核中的 LSM 挂载点,而无需配置或编写内核模块。 现在 LSM 支持的 hook 点包括但不限于: 对文件的打开、创建、删除和移动等; 文件系统的挂载; 对 task 和 process 的操作; 对 socket 的操作(创建、绑定 socket,发送和接收消息等); 更多 hook 点可以参考 lsm_hooks.h 。","breadcrumbs":"lesson 19-lsm-connect » LSM 概述","id":"143","title":"LSM 概述"},"144":{"body":"首先,请确认内核版本高于 5.7。接下来,可以通过 $ cat /boot/config-$(uname -r) | grep BPF_LSM\nCONFIG_BPF_LSM=y 判断是否内核是否支持 BPF LSM。上述条件都满足的情况下,可以通过 $ cat /sys/kernel/security/lsm\nndlock,lockdown,yama,integrity,apparmor 查看输出是否包含 bpf 选项,如果输出不包含(像上面的例子),可以通过修改 /etc/default/grub: GRUB_CMDLINE_LINUX=\"lsm=ndlock,lockdown,yama,integrity,apparmor,bpf\" 并通过 update-grub2 命令更新 grub 配置(不同系统的对应命令可能不同),然后重启系统。","breadcrumbs":"lesson 19-lsm-connect » 确认 BPF LSM 是否可用","id":"144","title":"确认 BPF LSM 是否可用"},"145":{"body":"// lsm-connect.bpf.c\n#include \"vmlinux.h\"\n#include \n#include \n#include char LICENSE[] SEC(\"license\") = \"GPL\"; #define EPERM 1\n#define AF_INET 2 const __u32 blockme = 16843009; // 1.1.1.1 -> int SEC(\"lsm/socket_connect\")\nint BPF_PROG(restrict_connect, struct socket *sock, struct sockaddr *address, int addrlen, int ret)\n{ // Satisfying \"cannot override a denial\" rule if (ret != 0) { return ret; } // Only IPv4 in this example if (address->sa_family != AF_INET) { return 0; } // Cast the address to an IPv4 socket address struct sockaddr_in *addr = (struct sockaddr_in *)address; // Where do you want to go? __u32 dest = addr->sin_addr.s_addr; bpf_printk(\"lsm: found connect to %d\", dest); if (dest == blockme) { bpf_printk(\"lsm: blocking %d\", dest); return -EPERM; } return 0;\n} 这是一段 C 实现的 eBPF 内核侧代码,它会阻碍所有试图通过 socket 对 1.1.1.1 的连接操作,其中: SEC(\"lsm/socket_connect\") 宏指出该程序期望的挂载点; 程序通过 BPF_PROG 宏定义(详情可查看 tools/lib/bpf/bpf_tracing.h ); restrict_connect 是 BPF_PROG 宏要求的程序名; ret 是该挂载点上(潜在的)当前函数之前的 LSM 检查程序的返回值; 整个程序的思路不难理解: 首先,若其他安全检查函数返回值不为 0(不通过),则无需检查,直接返回不通过; 接下来,判断是否为 IPV4 的连接请求,并比较试图连接的地址是否为 1.1.1.1; 若请求地址为 1.1.1.1 则拒绝连接,否则允许连接; 在程序运行期间,所有通过 socket 的连接操作都会被输出到 /sys/kernel/debug/tracing/trace_pipe。","breadcrumbs":"lesson 19-lsm-connect » 编写 eBPF 程序","id":"145","title":"编写 eBPF 程序"},"146":{"body":"通过容器编译: docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest 或是通过 ecc 编译: $ ecc lsm-connect.bpf.c\nCompiling bpf object...\nPacking ebpf object and config into package.json... 并通过 ecli 运行: sudo ecli run package.json 接下来,可以打开另一个 terminal,并尝试访问 1.1.1.1: $ ping 1.1.1.1\nping: connect: Operation not permitted\n$ curl 1.1.1.1\ncurl: (7) Couldn't connect to server\n$ wget 1.1.1.1\n--2023-04-23 08:41:18-- (try: 2) http://1.1.1.1/\nConnecting to 1.1.1.1:80... failed: Operation not permitted.\nRetrying. 同时,我们可以查看 bpf_printk 的输出: $ sudo cat /sys/kernel/debug/tracing/trace_pipe ping-7054 [000] d...1 6313.430872: bpf_trace_printk: lsm: found connect to 16843009 ping-7054 [000] d...1 6313.430874: bpf_trace_printk: lsm: blocking 16843009 curl-7058 [000] d...1 6316.346582: bpf_trace_printk: lsm: found connect to 16843009 curl-7058 [000] d...1 6316.346584: bpf_trace_printk: lsm: blocking 16843009 wget-7061 [000] d...1 6318.800698: bpf_trace_printk: lsm: found connect to 16843009 wget-7061 [000] d...1 6318.800700: bpf_trace_printk: lsm: blocking 16843009 完整源代码: https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/19-lsm-connect","breadcrumbs":"lesson 19-lsm-connect » 编译运行","id":"146","title":"编译运行"},"147":{"body":"本文介绍了如何使用 BPF LSM 来限制通过 socket 对特定 IPv4 地址的访问。我们可以通过修改 GRUB 配置文件来开启 LSM 的 BPF 挂载点。在 eBPF 程序中,我们通过 BPF_PROG 宏定义函数,并通过 SEC 宏指定挂载点;在函数实现上,遵循 LSM 安全检查模块中 \"cannot override a denial\" 的原则,并根据 socket 连接请求的目的地址对该请求进行限制。 如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。","breadcrumbs":"lesson 19-lsm-connect » 总结","id":"147","title":"总结"},"148":{"body":"https://github.com/leodido/demo-cloud-native-ebpf-day https://aya-rs.dev/book/programs/lsm/#writing-lsm-bpf-program","breadcrumbs":"lesson 19-lsm-connect » 参考","id":"148","title":"参考"},"149":{"body":"","breadcrumbs":"lesson 20-tc » eBPF 入门实践教程二十:使用 eBPF 进行 tc 流量控制","id":"149","title":"eBPF 入门实践教程二十:使用 eBPF 进行 tc 流量控制"},"15":{"body":"在开始编写eBPF程序之前,我们需要准备一个合适的开发环境,并了解eBPF程序的基本开发流程。本部分将详细介绍这些内容。","breadcrumbs":"lesson 1-helloworld » eBPF开发环境准备与基本开发流程","id":"15","title":"eBPF开发环境准备与基本开发流程"},"150":{"body":"Linux 的流量控制子系统(Traffic Control, tc)在内核中存在了多年,类似于 iptables 和 netfilter 的关系,tc 也包括一个用户态的 tc 程序和内核态的 trafiic control 框架,主要用于从速率、顺序等方面控制数据包的发送和接收。从 Linux 4.1 开始,tc 增加了一些新的挂载点,并支持将 eBPF 程序作为 filter 加载到这些挂载点上。","breadcrumbs":"lesson 20-tc » 背景","id":"150","title":"背景"},"151":{"body":"从协议栈上看,tc 位于链路层,其所在位置已经完成了 sk_buff 的分配,要晚于 xdp。为了实现对数据包发送和接收的控制,tc 使用队列结构来临时保存并组织数据包,在 tc 子系统中对应的数据结构和算法控制机制被抽象为 qdisc(Queueing discipline),其对外暴露数据包入队和出队的两个回调接口,并在内部隐藏排队算法实现。在 qdisc 中我们可以基于 filter 和 class 实现复杂的树形结构,其中 filter 被挂载到 qdisc 或 class 上用于实现具体的过滤逻辑,返回值决定了该数据包是否属于特定 class。 当数据包到达顶层 qdisc 时,其入队接口被调用,其上挂载的 filter 被依次执行直到一个 filter 匹配成功;此后数据包被送入该 filter 指向的 class,进入该 class 配置的 qdisc 处理流程中。tc 框架提供了所谓 classifier-action 机制,即在数据包匹配到特定 filter 时执行该 filter 所挂载的 action 对数据包进行处理,实现了完整的数据包分类和处理机制。 现有的 tc 为 eBPF 提供了 direct-action 模式,它使得一个作为 filter 加载的 eBPF 程序可以返回像 TC_ACT_OK 等 tc action 的返回值,而不是像传统的 filter 那样仅仅返回一个 classid 并把对数据包的处理交给 action 模块。现在,eBPF 程序可以被挂载到特定的 qdisc 上,并完成对数据包的分类和处理动作。","breadcrumbs":"lesson 20-tc » tc 概述","id":"151","title":"tc 概述"},"152":{"body":"#include \n#include \n#include \n#include #define TC_ACT_OK 0\n#define ETH_P_IP 0x0800 /* Internet Protocol packet */ /// @tchook {\"ifindex\":1, \"attach_point\":\"BPF_TC_INGRESS\"}\n/// @tcopts {\"handle\":1, \"priority\":1}\nSEC(\"tc\")\nint tc_ingress(struct __sk_buff *ctx)\n{ void *data_end = (void *)(__u64)ctx->data_end; void *data = (void *)(__u64)ctx->data; struct ethhdr *l2; struct iphdr *l3; if (ctx->protocol != bpf_htons(ETH_P_IP)) return TC_ACT_OK; l2 = data; if ((void *)(l2 + 1) > data_end) return TC_ACT_OK; l3 = (struct iphdr *)(l2 + 1); if ((void *)(l3 + 1) > data_end) return TC_ACT_OK; bpf_printk(\"Got IP packet: tot_len: %d, ttl: %d\", bpf_ntohs(l3->tot_len), l3->ttl); return TC_ACT_OK;\n} char __license[] SEC(\"license\") = \"GPL\"; 这段代码定义了一个 eBPF 程序,它可以通过 Linux TC(Transmission Control)来捕获数据包并进行处理。在这个程序中,我们限定了只捕获 IPv4 协议的数据包,然后通过 bpf_printk 函数打印出数据包的总长度和 Time-To-Live(TTL)字段的值。 需要注意的是,我们在代码中使用了一些 BPF 库函数,例如 bpf_htons 和 bpf_ntohs 函数,它们用于进行网络字节序和主机字节序之间的转换。此外,我们还使用了一些注释来为 TC 提供附加点和选项信息。例如,在这段代码的开头,我们使用了以下注释: /// @tchook {\"ifindex\":1, \"attach_point\":\"BPF_TC_INGRESS\"}\n/// @tcopts {\"handle\":1, \"priority\":1} 这些注释告诉 TC 将 eBPF 程序附加到网络接口的 ingress 附加点,并指定了 handle 和 priority 选项的值。关于 libbpf 中 tc 相关的 API 可以参考 patchwork 中的介绍。 总之,这段代码实现了一个简单的 eBPF 程序,用于捕获数据包并打印出它们的信息。","breadcrumbs":"lesson 20-tc » 编写 eBPF 程序","id":"152","title":"编写 eBPF 程序"},"153":{"body":"通过容器编译: docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest 或是通过 ecc 编译: $ ecc tc.bpf.c\nCompiling bpf object...\nPacking ebpf object and config into package.json... 并通过 ecli 运行: sudo ecli run ./package.json 可以通过如下方式查看程序的输出: $ sudo cat /sys/kernel/debug/tracing/trace_pipe node-1254811 [007] ..s1 8737831.671074: 0: Got IP packet: tot_len: 79, ttl: 64 sshd-1254728 [006] ..s1 8737831.674334: 0: Got IP packet: tot_len: 79, ttl: 64 sshd-1254728 [006] ..s1 8737831.674349: 0: Got IP packet: tot_len: 72, ttl: 64 node-1254811 [007] ..s1 8737831.674550: 0: Got IP packet: tot_len: 71, ttl: 64","breadcrumbs":"lesson 20-tc » 编译运行","id":"153","title":"编译运行"},"154":{"body":"本文介绍了如何向 TC 流量控制子系统挂载 eBPF 类型的 filter 来实现对链路层数据包的排队处理。基于 eunomia-bpf 提供的通过注释向 libbpf 传递参数的方案,我们可以将自己编写的 tc BPF 程序以指定选项挂载到目标网络设备,并借助内核的 sk_buff 结构对数据包进行过滤处理。 如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。","breadcrumbs":"lesson 20-tc » 总结","id":"154","title":"总结"},"155":{"body":"http://just4coding.com/2022/08/05/tc/ https://arthurchiao.art/blog/understanding-tc-da-mode-zh/","breadcrumbs":"lesson 20-tc » 参考","id":"155","title":"参考"},"156":{"body":"在本教程中,我们将介绍 XDP(eXpress Data Path),并通过一个简单的例子帮助你入门。之后,我们将探讨更高级的 XDP 应用,例如负载均衡器、防火墙及其他实际应用。如果你对 eBPF 或 XDP 感兴趣,请在 Github 上为我们点赞!","breadcrumbs":"lesson 21-xdp » eBPF 入门实践教程二十一: 使用 XDP 进行可编程数据包处理","id":"156","title":"eBPF 入门实践教程二十一: 使用 XDP 进行可编程数据包处理"},"157":{"body":"XDP 是 Linux 内核中的一种高性能可编程数据路径,专为网络接口级的数据包处理而设计。通过将 eBPF 程序直接附加到网络设备驱动程序上,XDP 能够在数据包到达内核网络栈之前拦截并处理它们。这使得 XDP 能够进行极低延迟和高效的数据包处理,非常适合如 DDoS 防护、负载均衡和流量过滤等任务。实际上,XDP 每核心的吞吐量可以高达 每秒 2400 万包(Mpps) 。","breadcrumbs":"lesson 21-xdp » 什么是 XDP?","id":"157","title":"什么是 XDP?"},"158":{"body":"XDP 运行在比传统 Linux 网络组件(如 cBPF)更低的层级,在网络设备驱动程序的软中断上下文中执行。它能够在数据包被内核标准网络栈处理之前对其进行处理,避免了创建 Linux 中表示网络数据包的 skb_buff 结构。这种早期处理为简单但频繁的操作(如丢弃恶意数据包或负载均衡服务器)带来了显著的性能提升。 与其他数据包处理机制相比,XDP 在性能和可用性之间取得了平衡,它利用了 Linux 内核的安全性和可靠性,同时通过可编程的 eBPF 提供了灵活性。","breadcrumbs":"lesson 21-xdp » 为什么选择 XDP?","id":"158","title":"为什么选择 XDP?"},"159":{"body":"在 XDP 出现之前,一些解决方案通过完全绕过内核来加速数据包处理。其中一个显著的例子是 DPDK (数据平面开发工具包)。DPDK 允许用户空间应用程序直接控制网络设备,从而实现非常高的性能。然而,这种方法也存在一些权衡: 缺乏内核集成 :DPDK 及其他内核绕过解决方案无法利用现有的内核网络功能,开发者必须在用户空间重新实现许多协议和功能。 安全边界 :这些绕过技术破坏了内核的安全模型,使得难以利用内核提供的安全工具。 用户空间与内核的转换开销 :当用户空间数据包处理需要与传统内核网络交互时(例如基于套接字的应用程序),数据包必须重新注入到内核中,增加了开销和复杂性。 专用 CPU 使用 :为了处理高流量,DPDK 和类似解决方案通常需要专用的 CPU 核心来处理数据包,这限制了通用系统的可扩展性和效率。 另一个替代 XDP 的方法是使用 Linux 网络栈中的 内核模块 或 挂钩 。虽然这种方法可以很好地集成现有的内核功能,但它需要大量的内核修改,且由于在数据包处理管道的后期运行,无法提供与 XDP 相同的性能优势。","breadcrumbs":"lesson 21-xdp » XDP 与其他方法的比较","id":"159","title":"XDP 与其他方法的比较"},"16":{"body":"要开发eBPF程序,您需要安装以下软件和工具: Linux 内核:由于eBPF是内核技术,因此您需要具备较新版本的Linux内核(至少 4.8 及以上版本,建议至少在 5.15 以上),以支持eBPF功能。 建议使用最新的 Ubuntu 版本(例如 Ubuntu 23.10)以获得最佳的学习体验,较旧的内核 eBPF 功能支持可能相对不全。 LLVM 和 Clang:这些工具用于编译eBPF程序。安装最新版本的LLVM和Clang可以确保您获得最佳的eBPF支持。 eBPF 程序主要由两部分构成:内核态部分和用户态部分。内核态部分包含 eBPF 程序的实际逻辑,用户态部分负责加载、运行和监控内核态程序。 当您选择了合适的开发框架后,如BCC(BPF Compiler Collection)、libbpf、cilium/ebpf或eunomia-bpf等,您可以开始进行用户态和内核态程序的开发。以BCC工具为例,我们将介绍eBPF程序的基本开发流程: 安装BCC工具:根据您的Linux发行版,按照BCC官方文档的指南安装BCC工具和相关依赖。 编写eBPF程序(C语言):使用C语言编写一个简单的eBPF程序,例如Hello World程序。该程序可以在内核空间执行并完成特定任务,如统计网络数据包数量。 编写用户态程序(Python或C等):使用Python、C等语言编写用户态程序,用于加载、运行eBPF程序以及与之交互。在这个程序中,您需要使用BCC提供的API来加载和操作内核态的eBPF程序。 编译eBPF程序:使用BCC工具,将C语言编写的eBPF程序编译成内核可以执行的字节码。BCC会在运行时动态从源码编译eBPF程序。 加载并运行eBPF程序:在用户态程序中,使用BCC提供的API加载编译好的eBPF程序到内核空间,然后运行该程序。 与eBPF程序交互:用户态程序通过BCC提供的API与eBPF程序交互,实现数据收集、分析和展示等功能。例如,您可以使用BCC API读取eBPF程序中的map数据,以获取网络数据包统计信息。 卸载eBPF程序:当不再需要eBPF程序时,用户态程序应使用BCC API将其从内核空间卸载。 调试与优化:使用 bpftool 等工具进行eBPF程序的调试和优化,提高程序性能和稳定性。 通过以上流程,您可以使用BCC工具开发、编译、运行和调试eBPF程序。请注意,其他框架(如libbpf、cilium/ebpf和eunomia-bpf)的开发流程大致相似但略有不同,因此在选择框架时,请参考相应的官方文档和示例。 通过这个过程,你可以开发出一个能够在内核中运行的 eBPF 程序。eunomia-bpf 是一个开源的 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。它基于 libbpf 的 CO-RE 轻量级开发框架,支持通过用户态 WASM 虚拟机控制 eBPF 程序的加载和执行,并将预编译的 eBPF 程序打包为通用的 JSON 或 WASM 模块进行分发。我们会使用 eunomia-bpf 进行演示。","breadcrumbs":"lesson 1-helloworld » 安装必要的软件和工具","id":"16","title":"安装必要的软件和工具"},"160":{"body":"XDP 与 eBPF 结合提供了介于内核绕过方案(如 DPDK)和内核集成方案之间的中间地带。以下是 XDP + eBPF 脱颖而出的原因: 高性能 :通过在网络接口卡(NIC)驱动程序级别拦截数据包,XDP 可以实现接近线速的性能,用于丢弃、重定向或负载均衡数据包,同时保持低资源消耗。 内核集成 :与 DPDK 不同,XDP 在 Linux 内核中工作,允许与现有的内核网络栈和工具(如 iptables、nftables 或套接字)无缝交互。无需在用户空间重新实现网络协议。 安全性 :eBPF 虚拟机确保用户定义的 XDP 程序是被隔离的,不会对内核造成不稳定影响。eBPF 的安全模型防止恶意或有缺陷的代码损害系统,提供了一个安全的可编程数据包处理环境。 不需要专用 CPU :XDP 允许数据包处理而无需将整个 CPU 核心专用于网络任务。这提高了系统的整体效率,允许更灵活的资源分配。 总的来说,XDP + eBPF 提供了一种强大的可编程数据包处理解决方案,结合了高性能与内核集成的灵活性和安全性。它消除了完全绕过内核方案的缺点,同时保留了内核安全性和功能的优势。","breadcrumbs":"lesson 21-xdp » XDP + eBPF 的优势","id":"160","title":"XDP + eBPF 的优势"},"161":{"body":"XDP 已经在许多高调的项目中得到应用,这些项目展示了它在实际网络场景中的强大功能和灵活性:","breadcrumbs":"lesson 21-xdp » XDP 的项目和应用案例","id":"161","title":"XDP 的项目和应用案例"},"162":{"body":"描述 :Cilium 是一个为云原生环境(尤其是 Kubernetes)设计的开源网络、安全和可观测性工具。它利用 XDP 实现高性能的数据包过滤和负载均衡。 应用案例 :Cilium 将数据包过滤和安全策略卸载到 XDP,实现高吞吐量和低延迟的容器化环境流量管理,同时不牺牲可扩展性。 链接 : Cilium","breadcrumbs":"lesson 21-xdp » 1. Cilium","id":"162","title":"1. Cilium"},"163":{"body":"描述 :Katran 是由 Facebook 开发的第 4 层负载均衡器,优化了高可扩展性和性能。它使用 XDP 处理数据包转发,开销极小。 应用案例 :Katran 每秒处理数百万个数据包,高效地将流量分配到后端服务器上,利用 XDP 在大规模数据中心中实现低延迟和高性能的负载均衡。 链接 : Katran GitHub","breadcrumbs":"lesson 21-xdp » 2. Katran","id":"163","title":"2. Katran"},"164":{"body":"描述 :Cloudflare 已经实现了基于 XDP 的实时 DDoS 缓解。通过在 NIC 级别处理数据包,Cloudflare 能够在恶意流量进入网络栈之前过滤掉攻击流量,最小化 DDoS 攻击对其系统的影响。 应用案例 :Cloudflare 利用 XDP 在管道早期丢弃恶意数据包,保护其基础设施免受大规模 DDoS 攻击,同时保持对合法流量的高可用性。 链接 : Cloudflare 博客关于 XDP 这些项目展示了 XDP 在不同领域的可扩展和高效的数据包处理能力,从安全和负载均衡到云原生网络。","breadcrumbs":"lesson 21-xdp » 3. Cloudflare 的 XDP DDoS 保护","id":"164","title":"3. Cloudflare 的 XDP DDoS 保护"},"165":{"body":"与传统方法(如 iptables、nftables 或 tc)相比,XDP 提供了几个明显的优势: 速度与低开销 :XDP 直接在 NIC 驱动程序中运行,绕过了内核的大部分开销,使数据包处理更快。 可定制性 :XDP 允许开发人员通过 eBPF 创建自定义的数据包处理程序,提供比传统工具(如 iptables)更大的灵活性和细粒度控制。 资源效率 :XDP 不需要像 DPDK 等用户空间解决方案那样将整个 CPU 核心专用于数据包处理,因此它是高性能网络的更高效选择。","breadcrumbs":"lesson 21-xdp » 为什么选择 XDP 而不是其他方法?","id":"165","title":"为什么选择 XDP 而不是其他方法?"},"166":{"body":"#include \"vmlinux.h\"\n#include /// @ifindex 1\n/// @flags 0\n/// @xdpopts {\"old_prog_fd\":0}\nSEC(\"xdp\")\nint xdp_pass(struct xdp_md* ctx) { void* data = (void*)(long)ctx->data; void* data_end = (void*)(long)ctx->data_end; int pkt_sz = data_end - data; bpf_printk(\"packet size is %d\", pkt_sz); return XDP_PASS;\n} char __license[] SEC(\"license\") = \"GPL\"; 这是一段 C 语言实现的 eBPF 内核侧代码,它能够通过 xdp 捕获所有经过目标网络设备的数据包,计算其大小并输出到 trace_pipe 中。 值得注意的是,在代码中我们使用了以下注释: /// @ifindex 1\n/// @flags 0\n/// @xdpopts {\"old_prog_fd\":0} 这是由 eunomia-bpf 提供的功能,我们可以通过这样的注释告知 eunomia-bpf 加载器此 xdp 程序想要挂载的目标网络设备编号,挂载的标志和选项。 这些变量的设计基于 libbpf 提供的 API,可以通过 patchwork 查看接口的详细介绍。 SEC(\"xdp\") 宏指出 BPF 程序的类型,ctx 是此 BPF 程序执行的上下文,用于包处理流程。 在程序的最后,我们返回了 XDP_PASS,这表示我们的 xdp 程序会将经过目标网络设备的包正常交付给内核的网络协议栈。可以通过 XDP actions 了解更多 xdp 的处理动作。","breadcrumbs":"lesson 21-xdp » 编写 eBPF 程序","id":"166","title":"编写 eBPF 程序"},"167":{"body":"通过容器编译: docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest 或是通过 ecc 编译: $ ecc xdp.bpf.c\nCompiling bpf object...\nPacking ebpf object and config into package.json... 并通过 ecli 运行: sudo ecli run package.json 可以通过如下方式查看程序的输出: $ sudo cat /sys/kernel/tracing/trace_pipe node-1939 [000] d.s11 1601.190413: bpf_trace_printk: packet size is 177 node-1939 [000] d.s11 1601.190479: bpf_trace_printk: packet size is 66 ksoftirqd/1-19 [001] d.s.1 1601.237507: bpf_trace_printk: packet size is 66 node-1939 [000] d.s11 1601.275860: bpf_trace_printk: packet size is 344","breadcrumbs":"lesson 21-xdp » 编译运行","id":"167","title":"编译运行"},"168":{"body":"本文介绍了如何使用 xdp 来处理经过特定网络设备的包,基于 eunomia-bpf 提供的通过注释向 libbpf 传递参数的方案,我们可以将自己编写的 xdp BPF 程序以指定选项挂载到目标设备,并在网络包进入内核网络协议栈之前就对其进行处理,从而获取高性能的可编程包处理能力。 如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。","breadcrumbs":"lesson 21-xdp » 总结","id":"168","title":"总结"},"169":{"body":"http://arthurchiao.art/blog/xdp-paper-acm-2018-zh/ http://arthurchiao.art/blog/linux-net-stack-implementation-rx-zh/ https://github.com/xdp-project/xdp-tutorial/tree/master/basic01-xdp-pass","breadcrumbs":"lesson 21-xdp » 参考资料","id":"169","title":"参考资料"},"17":{"body":"可以通过以下步骤下载和安装 eunomia-bpf: 下载 ecli 工具,用于运行 eBPF 程序: $ wget https://aka.pw/bpf-ecli -O ecli && chmod +x ./ecli\n$ ./ecli -h\nUsage: ecli [--help] [--version] [--json] [--no-cache] url-and-args 下载编译器工具链,用于将 eBPF 内核代码编译为 config 文件或 WASM 模块: $ wget https://github.com/eunomia-bpf/eunomia-bpf/releases/latest/download/ecc && chmod +x ./ecc\n$ ./ecc -h\neunomia-bpf compiler\nUsage: ecc [OPTIONS] [EXPORT_EVENT_HEADER] 注:假如在 aarch64 平台上,请从 release 下载 ecc-aarch64 和 ecli-aarch64 . 也可以使用 docker 镜像进行编译: $ docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest # 使用 docker 进行编译。`pwd` 应该包含 *.bpf.c 文件和 *.h 文件。\nexport PATH=PATH:~/.eunomia/bin\nCompiling bpf object...\nPacking ebpf object and config into /src/package.json...","breadcrumbs":"lesson 1-helloworld » 下载安装 eunomia-bpf 开发工具","id":"17","title":"下载安装 eunomia-bpf 开发工具"},"170":{"body":"本文主要记录了笔者在 Android Studio Emulator 中测试高版本 Android Kernel 对基于 libbpf 的 CO-RE 技术支持程度的探索过程、结果和遇到的问题。 测试采用的方式是在 Android Shell 环境下构建 Debian 环境,并基于此尝试构建 eunomia-bpf 工具链、运行其测试用例。","breadcrumbs":"在 Android 上使用 eBPF 程序 » 在 Android 上使用 eBPF 程序","id":"170","title":"在 Android 上使用 eBPF 程序"},"171":{"body":"截至目前(2023-04),Android 还未对 eBPF 程序的动态加载做出较好的支持,无论是以 bcc 为代表的带编译器分发方案,还是基于 btf 和 libbpf 的 CO-RE 方案,都在较大程度上离不开 Linux 环境的支持,无法在 Android 系统上很好地运行 [1] 。 虽然如此,在 Android 平台上尝试 eBPF 也已经有了一些成功案例,除谷歌官方提供的修改 Android.bp 以将 eBPF 程序随整个系统一同构建并挂载的方案 [2] ,也有人提出基于 Android 内核构建 Linux 环境进而运行 eBPF 工具链的思路,并开发了相关工具。 目前已有的资料,大多基于 adeb/eadb 在 Android 内核基础上构建 Linux 沙箱,并对 bcc 和 bpftrace 相关工具链进行测试,而对 CO-RE 方案的测试工作较少。在 Android 上使用 bcc 工具目前有较多参考资料,如: SeeFlowerX: https://blog.seeflower.dev/category/eBPF/ evilpan: https://bbs.kanxue.com/thread-271043.htm 其主要思路是利用 chroot 在 Android 内核上运行一个 Debian 镜像,并在其中构建整个 bcc 工具链,从而使用 eBPF 工具。如果想要使用 bpftrace,原理也是类似的。 事实上,高版本的 Android 内核已支持 btf 选项,这意味着 eBPF 领域中新兴的 CO-RE 技术也应当能够运用到基于 Android 内核的 Linux 系统中。本文将基于此对 eunomia-bpf 在模拟器环境下进行测试运行。 eunomia-bpf 是一个结合了 libbpf 和 WebAssembly 技术的开源项目,旨在简化 eBPF 程序的编写、编译和部署。该项目可被视作 CO-RE 的一种实践方式,其核心依赖是 libbpf,相信对 eunomia-bpf 的测试工作能够为其他 CO-RE 方案提供参考。","breadcrumbs":"在 Android 上使用 eBPF 程序 » 背景","id":"171","title":"背景"},"172":{"body":"Android Emulator(Android Studio Flamingo | 2022.2.1) AVD: Pixel 6 Android Image: Tiramisu Android 13.0 x86_64(5.15.41-android13-8-00055-g4f5025129fe8-ab8949913)","breadcrumbs":"在 Android 上使用 eBPF 程序 » 测试环境","id":"172","title":"测试环境"},"173":{"body":"[3] 从 eadb 仓库 的 releases 页面获取 debianfs-amd64-full.tar.gz 作为 Linux 环境的 rootfs,同时还需要获取该项目的 assets 目录来构建环境; 从 Android Studio 的 Device Manager 配置并启动 Android Virtual Device; 通过 Android Studio SDK 的 adb 工具将 debianfs-amd64-full.tar.gz 和 assets 目录推送到 AVD 中: ./adb push debianfs-amd64-full.tar.gz /data/local/tmp/deb.tar.gz ./adb push assets /data/local/tmp/assets 通过 adb 进入 Android shell 环境并获取 root 权限: ./adb shell su 在 Android shell 中构建并进入 debian 环境: mkdir -p /data/eadb mv /data/local/tmp/assets/* /data/eadb mv /data/local/tmp/deb.tar.gz /data/eadb/deb.tar.gz rm -r /data/local/tmp/assets chmod +x /data/eadb/device-* /data/eadb/device-unpack /data/eadb/run /data/eadb/debian 至此,测试 eBPF 所需的 Linux 环境已经构建完毕。此外,在 Android shell 中(未进入 debian 时)可以通过 zcat /proc/config.gz 并配合 grep 查看内核编译选项。 目前,eadb 打包的 debian 环境存在 libc 版本低,缺少的工具依赖较多等情况;并且由于内核编译选项不同,一些 eBPF 功能可能也无法使用。","breadcrumbs":"在 Android 上使用 eBPF 程序 » 环境搭建","id":"173","title":"环境搭建"},"174":{"body":"在 debian 环境中将 eunomia-bpf 仓库 clone 到本地,具体的构建过程,可以参考仓库的 build.md 。在本次测试中,笔者选用了 ecc 编译生成 package.json 的方式,该工具的构建和使用方式请参考 仓库页面 。 在构建过程中,可能需要自行安装包括但不限于 curl,pkg-config,libssl-dev 等工具。","breadcrumbs":"在 Android 上使用 eBPF 程序 » 工具构建","id":"174","title":"工具构建"},"175":{"body":"有部分 eBPF 程序可以成功在 Android 上运行,但也会有部分应用因为种种原因无法成功被执行。","breadcrumbs":"在 Android 上使用 eBPF 程序 » 结果","id":"175","title":"结果"},"176":{"body":"bootstrap 运行输出如下: TIME PID PPID EXIT_CODE DURATION_NS COMM FILENAME EXIT_EVENT\n09:09:19 10217 479 0 0 sh /system/bin/sh 0\n09:09:19 10217 479 0 0 ps /system/bin/ps 0\n09:09:19 10217 479 0 54352100 ps 1\n09:09:21 10219 479 0 0 sh /system/bin/sh 0\n09:09:21 10219 479 0 0 ps /system/bin/ps 0\n09:09:21 10219 479 0 44260900 ps 1 tcpstates 开始监测后在 Linux 环境中通过 wget 下载 Web 页面: TIME SADDR DADDR SKADDR TS_US DELTA_US PID OLDSTATE NEWSTATE FAMILY SPORT DPORT TASK\n09:07:46 0x4007000200005000000000000f02000a 0x5000000000000f02000a8bc53f77 18446635827774444352 3315344998 0 10115 7 2 2 0 80 wget\n09:07:46 0x40020002d98e50003d99f8090f02000a 0xd98e50003d99f8090f02000a8bc53f77 18446635827774444352 3315465870 120872 0 2 1 2 55694 80 swapper/0\n09:07:46 0x40010002d98e50003d99f8090f02000a 0xd98e50003d99f8090f02000a8bc53f77 18446635827774444352 3315668799 202929 10115 1 4 2 55694 80 wget\n09:07:46 0x40040002d98e50003d99f8090f02000a 0xd98e50003d99f8090f02000a8bc53f77 18446635827774444352 3315670037 1237 0 4 5 2 55694 80 swapper/0\n09:07:46 0x40050002000050003d99f8090f02000a 0x50003d99f8090f02000a8bc53f77 18446635827774444352 3315670225 188 0 5 7 2 55694 80 swapper/0\n09:07:47 0x400200020000bb01565811650f02000a 0xbb01565811650f02000a6aa0d9ac 18446635828348806592 3316433261 0 2546 2 7 2 49970 443 ChromiumNet\n09:07:47 0x400200020000bb01db794a690f02000a 0xbb01db794a690f02000aea2afb8e 18446635827774427776 3316535591 0 1469 2 7 2 37386 443 ChromiumNet 开始检测后在 Android Studio 模拟界面打开 Chrome 浏览器并访问百度页面: TIME SADDR DADDR SKADDR TS_US DELTA_US PID OLDSTATE NEWSTATE FAMILY SPORT DPORT TASK\n07:46:58 0x400700020000bb01000000000f02000a 0xbb01000000000f02000aeb6f2270 18446631020066638144 192874641 0 3305 7 2 2 0 443 NetworkService\n07:46:58 0x40020002d28abb01494b6ebe0f02000a 0xd28abb01494b6ebe0f02000aeb6f2270 18446631020066638144 192921938 47297 3305 2 1 2 53898 443 NetworkService\n07:46:58 0x400700020000bb01000000000f02000a 0xbb01000000000f02000ae7e7e8b7 18446631020132433920 193111426 0 3305 7 2 2 0 443 NetworkService\n07:46:58 0x40020002b4a0bb0179ff85e80f02000a 0xb4a0bb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193124670 13244 3305 2 1 2 46240 443 NetworkService\n07:46:58 0x40010002b4a0bb0179ff85e80f02000a 0xb4a0bb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193185397 60727 3305 1 4 2 46240 443 NetworkService\n07:46:58 0x40040002b4a0bb0179ff85e80f02000a 0xb4a0bb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193186122 724 3305 4 5 2 46240 443 NetworkService\n07:46:58 0x400500020000bb0179ff85e80f02000a 0xbb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193186244 122 3305 5 7 2 46240 443 NetworkService\n07:46:59 0x40010002d01ebb01d0c52f5c0f02000a 0xd01ebb01d0c52f5c0f02000a51449c27 18446631020103553856 194110884 0 5130 1 8 2 53278 443 ThreadPoolForeg\n07:46:59 0x400800020000bb01d0c52f5c0f02000a 0xbb01d0c52f5c0f02000a51449c27 18446631020103553856 194121000 10116 3305 8 7 2 53278 443 NetworkService\n07:46:59 0x400700020000bb01000000000f02000a 0xbb01000000000f02000aeb6f2270 18446631020099513920 194603677 0 3305 7 2 2 0 443 NetworkService\n07:46:59 0x40020002d28ebb0182dd92990f02000a 0xd28ebb0182dd92990f02000aeb6f2270 18446631020099513920 194649313 45635 12 2 1 2 53902 443 ksoftirqd/0\n07:47:00 0x400700020000bb01000000000f02000a 0xbb01000000000f02000a26f6e878 18446631020132433920 195193350 0 3305 7 2 2 0 443 NetworkService\n07:47:00 0x40020002ba32bb01e0e09e3a0f02000a 0xba32bb01e0e09e3a0f02000a26f6e878 18446631020132433920 195206992 13642 0 2 1 2 47666 443 swapper/0\n07:47:00 0x400700020000bb01000000000f02000a 0xbb01000000000f02000ae7e7e8b7 18446631020132448128 195233125 0 3305 7 2 2 0 443 NetworkService\n07:47:00 0x40020002b4a8bb0136cac8dd0f02000a 0xb4a8bb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195246569 13444 3305 2 1 2 46248 443 NetworkService\n07:47:00 0xf02000affff00000000000000000000 0x1aca06cffff00000000000000000000 18446631019225912320 195383897 0 947 7 2 10 0 80 Thread-11\n07:47:00 0x40010002b4a8bb0136cac8dd0f02000a 0xb4a8bb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195421584 175014 3305 1 4 2 46248 443 NetworkService\n07:47:00 0x40040002b4a8bb0136cac8dd0f02000a 0xb4a8bb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195422361 777 3305 4 5 2 46248 443 NetworkService\n07:47:00 0x400500020000bb0136cac8dd0f02000a 0xbb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195422450 88 3305 5 7 2 46248 443 NetworkService\n07:47:01 0x400700020000bb01000000000f02000a 0xbb01000000000f02000aea2afb8e 18446631020099528128 196321556 0 1315 7 2 2 0 443 ChromiumNet","breadcrumbs":"在 Android 上使用 eBPF 程序 » 成功案例","id":"176","title":"成功案例"},"177":{"body":"opensnoop 例如 opensnoop 工具,可以在 Android 上成功构建,但运行报错: libbpf: failed to determine tracepoint 'syscalls/sys_enter_open' perf event ID: No such file or directory\nlibbpf: prog 'tracepoint__syscalls__sys_enter_open': failed to create tracepoint 'syscalls/sys_enter_open' perf event: No such file or directory\nlibbpf: prog 'tracepoint__syscalls__sys_enter_open': failed to auto-attach: -2\nfailed to attach skeleton\nError: BpfError(\"load and attach ebpf program failed\") 后经查看发现内核未开启 CONFIG_FTRACE_SYSCALLS 选项,导致无法使用 syscalls 的 tracepoint。","breadcrumbs":"在 Android 上使用 eBPF 程序 » 一些可能的报错原因","id":"177","title":"一些可能的报错原因"},"178":{"body":"在 Android shell 中查看内核编译选项可以发现 CONFIG_DEBUG_INFO_BTF 默认是打开的,在此基础上 eunomia-bpf 项目提供的 example 已有一些能够成功运行的案例,例如可以监测 exec 族函数的执行和 tcp 连接的状态。 对于无法运行的一些,原因主要是以下两个方面: 内核编译选项未支持相关 eBPF 功能; eadb 打包的 Linux 环境较弱,缺乏必须依赖; 目前在 Android 系统中使用 eBPF 工具基本上仍然需要构建完整的 Linux 运行环境,但 Android 内核本身对 eBPF 的支持已较为全面,本次测试证明较高版本的 Android 内核支持 BTF 调试信息和依赖 CO-RE 的 eBPF 程序的运行。 Android 系统 eBPF 工具的发展需要官方新特性的加入,目前看来通过 Android APP 直接使用 eBPF 工具需要的工作量较大,同时由于 eBPF 工具需要 root 权限,普通 Android 用户的使用会面临较多困难。 如果希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。","breadcrumbs":"在 Android 上使用 eBPF 程序 » 总结","id":"178","title":"总结"},"179":{"body":"https://source.android.google.cn/docs/core/architecture/kernel/bpf https://mp.weixin.qq.com/s/mul4n5D3nXThjxuHV7GpMA https://blog.seeflower.dev/archives/138/","breadcrumbs":"在 Android 上使用 eBPF 程序 » 参考","id":"179","title":"参考"},"18":{"body":"我们会先从一个简单的 eBPF 程序开始,它会在内核中打印一条消息。我们会使用 eunomia-bpf 的编译器工具链将其编译为 bpf 字节码文件,然后使用 ecli 工具加载并运行该程序。作为示例,我们可以暂时省略用户态程序的部分。 /* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */\n#define BPF_NO_GLOBAL_DATA\n#include \n#include \n#include typedef unsigned int u32;\ntypedef int pid_t;\nconst pid_t pid_filter = 0; char LICENSE[] SEC(\"license\") = \"Dual BSD/GPL\"; SEC(\"tp/syscalls/sys_enter_write\")\nint handle_tp(void *ctx)\n{ pid_t pid = bpf_get_current_pid_tgid() >> 32; if (pid_filter && pid != pid_filter) return 0; bpf_printk(\"BPF triggered sys_enter_write from PID %d.\\n\", pid); return 0;\n} 这段程序通过定义一个 handle_tp 函数并使用 SEC 宏把它附加到 sys_enter_write tracepoint(即在进入 write 系统调用时执行)。该函数通过使用 bpf_get_current_pid_tgid 和 bpf_printk 函数获取调用 write 系统调用的进程 ID,并在内核日志中打印出来。 bpf_printk(): 一种将信息输出到trace_pipe(/sys/kernel/debug/tracing/trace_pipe)简单机制。 在一些简单用例中这样使用没有问题, but它也有一些限制:最多3 参数; 第一个参数必须是%s(即字符串);同时trace_pipe在内核中全局共享,其他并行使用trace_pipe的程序有可能会将 trace_pipe 的输出扰乱。 一个更好的方式是通过 BPF_PERF_OUTPUT(), 稍后将会讲到。 void *ctx:ctx本来是具体类型的参数, 但是由于我们这里没有使用这个参数,因此就将其写成void *类型。 return 0;:必须这样,返回0 (如果要知道why, 参考 #139 https://github.com/iovisor/bcc/issues/139 )。 要编译和运行这段程序,可以使用 ecc 工具和 ecli 命令。首先在 Ubuntu/Debian 上,执行以下命令: sudo apt install clang llvm 使用 ecc 编译程序: $ ./ecc minimal.bpf.c\nCompiling bpf object...\nPacking ebpf object and config into package.json... 或使用 docker 镜像进行编译: docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest 然后使用 ecli 运行编译后的程序: $ sudo ./ecli run package.json\nRunning eBPF program... 运行这段程序后,可以通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出: $ sudo cat /sys/kernel/debug/tracing/trace_pipe | grep \"BPF triggered sys_enter_write\" <...>-3840345 [010] d... 3220701.101143: bpf_trace_printk: write system call from PID 3840345. <...>-3840345 [010] d... 3220701.101143: bpf_trace_printk: write system call from PID 3840345. 按 Ctrl+C 停止 ecli 进程之后,可以看到对应的输出也停止。 注意:如果正在使用的 Linux 发行版(例如 Ubuntu )默认情况下没有启用跟踪子系统可能看不到任何输出,使用以下指令打开这个功能: $ sudo su\n# echo 1 > /sys/kernel/debug/tracing/tracing_on","breadcrumbs":"lesson 1-helloworld » Hello World - minimal eBPF program","id":"18","title":"Hello World - minimal eBPF program"},"180":{"body":"随着TLS在现代网络环境中的广泛应用,跟踪微服务RPC消息已经变得愈加棘手。传统的流量嗅探技术常常受限于只能获取到加密后的数据,导致无法真正观察到通信的原始内容。这种限制为系统的调试和分析带来了不小的障碍。 但现在,我们有了新的解决方案。使用 eBPF 技术,通过其能力在用户空间进行探测,提供了一种方法重新获得明文数据,使得我们可以直观地查看加密前的通信内容。然而,每个应用可能使用不同的库,每个库都有多个版本,这种多样性给跟踪带来了复杂性。 在本教程中,我们将带您了解一种跨多种用户态 SSL/TLS 库的 eBPF 追踪技术,它不仅可以同时跟踪 GnuTLS 和 OpenSSL 等用户态库,而且相比以往,大大降低了对新版本库的维护工作。完整的源代码可以在这里查看: https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/30-sslsniff 。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » eBPF 实践教程:使用 uprobe 捕获多种库的 SSL/TLS 明文数据","id":"180","title":"eBPF 实践教程:使用 uprobe 捕获多种库的 SSL/TLS 明文数据"},"181":{"body":"在深入本教程的主题之前,我们需要理解一些核心概念,这些概念将为我们后面的讨论提供基础。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 背景知识","id":"181","title":"背景知识"},"182":{"body":"SSL (Secure Sockets Layer): 由 Netscape 在 1990 年代早期开发,为网络上的两台机器之间提供数据加密传输。然而,由于某些已知的安全问题,SSL的使用已被其后继者TLS所替代。 TLS (Transport Layer Security): 是 SSL 的继任者,旨在提供更强大和更安全的数据加密方式。TLS 工作通过一个握手过程,在这个过程中,客户端和服务器之间会选择一个加密算法和相应的密钥。一旦握手完成,数据传输开始,所有数据都使用选择的算法和密钥加密。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » SSL 和 TLS","id":"182","title":"SSL 和 TLS"},"183":{"body":"Transport Layer Security (TLS) 是一个密码学协议,旨在为计算机网络上的通信提供安全性。它主要目标是通过密码学,例如证书的使用,为两个或更多通信的计算机应用程序提供安全性,包括隐私(机密性)、完整性和真实性。TLS 由两个子层组成:TLS 记录协议和TLS 握手协议。 握手过程 当客户端与启用了TLS的服务器连接并请求建立安全连接时,握手过程开始。握手允许客户端和服务器通过不对称密码来建立连接的安全性参数,完整流程如下: 初始握手 :客户端连接到启用了TLS的服务器,请求安全连接,并提供它支持的密码套件列表(加密算法和哈希函数)。 选择密码套件 :从提供的列表中,服务器选择它也支持的密码套件和哈希函数,并通知客户端已做出的决定。 提供数字证书 :通常,服务器接下来会提供形式为数字证书的身份验证。此证书包含服务器名称、信任的证书授权机构(为证书的真实性提供担保)以及服务器的公共加密密钥。 验证证书 :客户端在继续之前确认证书的有效性。 生成会话密钥 :为了生成用于安全连接的会话密钥,客户端有以下两种方法: 使用服务器的公钥加密一个随机数(PreMasterSecret)并将结果发送到服务器(只有服务器才能使用其私钥解密);双方然后使用该随机数生成一个独特的会话密钥,用于会话期间的数据加密和解密。 使用 Diffie-Hellman 密钥交换(或其变体椭圆曲线DH)来安全地生成一个随机且独特的会话密钥,用于加密和解密,该密钥具有前向保密的额外属性:即使在未来公开了服务器的私钥,也不能用它来解密当前的会话,即使第三方拦截并记录了会话。 一旦上述步骤成功完成,握手过程便结束,加密的连接开始。此连接使用会话密钥进行加密和解密,直到连接关闭。如果上述任何步骤失败,则TLS握手失败,连接将不会建立。 OSI模型中的TLS TLS 和 SSL 不完全适合 OSI 模型或 TCP/IP 模型的任何单一层次。TLS 在“某些可靠的传输协议(例如,TCP)之上运行”,这意味着它位于传输层之上。它为更高的层提供加密,这通常是表示层的功能。但是,使用TLS 的应用程序通常视其为传输层,即使使用TLS的应用程序必须积极控制启动 TLS 握手和交换的认证证书的处理。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » TLS 的工作原理","id":"183","title":"TLS 的工作原理"},"184":{"body":"eBPF (Extended Berkeley Packet Filter): 是一种内核技术,允许用户在内核空间中运行预定义的程序,不需要修改内核源代码或重新加载模块。它创建了一个桥梁,使得用户空间和内核空间可以交互,从而为系统监控、性能分析和网络流量分析等任务提供了无前例的能力。 uprobes 是eBPF的一个重要特性,允许我们在用户空间应用程序中动态地插入探测点,特别适用于跟踪SSL/TLS库中的函数调用。Uprobe 在内核态 eBPF 运行时,也可能产生比较大的性能开销,这时候也可以考虑使用用户态 eBPF 运行时,例如 bpftime 。bpftime 是一个基于 LLVM JIT/AOT 的用户态 eBPF 运行时,它可以在用户态运行 eBPF 程序,和内核态的 eBPF 兼容,避免了内核态和用户态之间的上下文切换,从而提高了 eBPF 程序的执行效率。对于 uprobe 而言,bpftime 的性能开销比 kernel 小一个数量级。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » eBPF 和 uprobe","id":"184","title":"eBPF 和 uprobe"},"185":{"body":"SSL/TLS协议的实现主要依赖于用户态库。以下是一些常见的库: OpenSSL: 一个开源的、功能齐全的加密库,广泛应用于许多开源和商业项目中。 BoringSSL: 是Google维护的OpenSSL的一个分支,重点是简化和优化,适用于Google的需求。 GnuTLS: 是GNU项目的一部分,提供了SSL,TLS和DTLS协议的实现。与OpenSSL和BoringSSL相比,GnuTLS在API设计、模块结构和许可证上有所不同。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 用户态库","id":"185","title":"用户态库"},"186":{"body":"OpenSSL 是一个广泛应用的开源库,提供了 SSL 和 TLS 协议的完整实现,并广泛用于各种应用程序中以确保数据传输的安全性。其中,SSL_read() 和 SSL_write() 是两个核心的 API 函数,用于从 TLS/SSL 连接中读取和写入数据。本章节,我们将深入这两个函数,帮助你理解其工作机制。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » OpenSSL API 分析","id":"186","title":"OpenSSL API 分析"},"187":{"body":"当我们想从一个已建立的 SSL 连接中读取数据时,可以使用 SSL_read 或 SSL_read_ex 函数。函数原型如下: int SSL_read_ex(SSL *ssl, void *buf, size_t num, size_t *readbytes);\nint SSL_read(SSL *ssl, void *buf, int num); SSL_read 和 SSL_read_ex 试图从指定的 ssl 中读取最多 num 字节的数据到缓冲区 buf 中。成功时,SSL_read_ex 会在 *readbytes 中存储实际读取到的字节数。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 1. SSL_read 函数","id":"187","title":"1. SSL_read 函数"},"188":{"body":"当我们想往一个已建立的 SSL 连接中写入数据时,可以使用 SSL_write 或 SSL_write_ex 函数。 函数原型: int SSL_write_ex(SSL *s, const void *buf, size_t num, size_t *written);\nint SSL_write(SSL *ssl, const void *buf, int num); SSL_write 和 SSL_write_ex 会从缓冲区 buf 中将最多 num 字节的数据写入到指定的 ssl 连接中。成功时,SSL_write_ex 会在 *written 中存储实际写入的字节数。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 2. SSL_write 函数","id":"188","title":"2. SSL_write 函数"},"189":{"body":"在我们的例子中,我们使用 eBPF 来 hook ssl_read 和 ssl_write 函数,从而在数据读取或写入 SSL 连接时执行自定义操作。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » eBPF 内核态代码编写","id":"189","title":"eBPF 内核态代码编写"},"19":{"body":"如上所述, eBPF 程序的基本框架包括: 包含头文件:需要包含 和 等头文件。 定义许可证:需要定义许可证,通常使用 \"Dual BSD/GPL\"。 定义 BPF 函数:需要定义一个 BPF 函数,例如其名称为 handle_tp,其参数为 void *ctx,返回值为 int。通常用 C 语言编写。 使用 BPF 助手函数:在例如 BPF 函数中,可以使用 BPF 助手函数 bpf_get_current_pid_tgid() 和 bpf_printk()。 返回值","breadcrumbs":"lesson 1-helloworld » eBPF 程序的基本框架","id":"19","title":"eBPF 程序的基本框架"},"190":{"body":"首先,我们定义了一个数据结构 probe_SSL_data_t 用于在内核态和用户态之间传输数据: #define MAX_BUF_SIZE 8192\n#define TASK_COMM_LEN 16 struct probe_SSL_data_t { __u64 timestamp_ns; // 时间戳(纳秒) __u64 delta_ns; // 函数执行时间 __u32 pid; // 进程 ID __u32 tid; // 线程 ID __u32 uid; // 用户 ID __u32 len; // 读/写数据的长度 int buf_filled; // 缓冲区是否填充完整 int rw; // 读或写(0为读,1为写) char comm[TASK_COMM_LEN]; // 进程名 __u8 buf[MAX_BUF_SIZE]; // 数据缓冲区 int is_handshake; // 是否是握手数据\n};","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 数据结构","id":"190","title":"数据结构"},"191":{"body":"我们的目标是 hook 到 SSL_read 和 SSL_write 函数。我们定义了一个函数 SSL_exit 来处理这两个函数的返回值。该函数会根据当前进程和线程的 ID,确定是否需要追踪并收集数据。 static int SSL_exit(struct pt_regs *ctx, int rw) { int ret = 0; u32 zero = 0; u64 pid_tgid = bpf_get_current_pid_tgid(); u32 pid = pid_tgid >> 32; u32 tid = (u32)pid_tgid; u32 uid = bpf_get_current_uid_gid(); u64 ts = bpf_ktime_get_ns(); if (!trace_allowed(uid, pid)) { return 0; } /* store arg info for later lookup */ u64 *bufp = bpf_map_lookup_elem(&bufs, &tid); if (bufp == 0) return 0; u64 *tsp = bpf_map_lookup_elem(&start_ns, &tid); if (!tsp) return 0; u64 delta_ns = ts - *tsp; int len = PT_REGS_RC(ctx); if (len <= 0) // no data return 0; struct probe_SSL_data_t *data = bpf_map_lookup_elem(&ssl_data, &zero); if (!data) return 0; data->timestamp_ns = ts; data->delta_ns = delta_ns; data->pid = pid; data->tid = tid; data->uid = uid; data->len = (u32)len; data->buf_filled = 0; data->rw = rw; data->is_handshake = false; u32 buf_copy_size = min((size_t)MAX_BUF_SIZE, (size_t)len); bpf_get_current_comm(&data->comm, sizeof(data->comm)); if (bufp != 0) ret = bpf_probe_read_user(&data->buf, buf_copy_size, (char *)*bufp); bpf_map_delete_elem(&bufs, &tid); bpf_map_delete_elem(&start_ns, &tid); if (!ret) data->buf_filled = 1; else buf_copy_size = 0; bpf_perf_event_output(ctx, &perf_SSL_events, BPF_F_CURRENT_CPU, data, EVENT_SIZE(buf_copy_size)); return 0;\n} 这里的 rw 参数标识是读还是写。0 代表读,1 代表写。 数据收集流程 获取当前进程和线程的 ID,以及当前用户的 ID。 通过 trace_allowed 判断是否允许追踪该进程。 获取起始时间,以计算函数的执行时间。 尝试从 bufs 和 start_ns maps 中查找相关的数据。 如果成功读取了数据,则创建或查找 probe_SSL_data_t 结构来填充数据。 将数据从用户空间复制到缓冲区,并确保不超过预定的大小。 最后,将数据发送到用户空间。 注意:我们使用了两个用户返回探针 uretprobe 来分别 hook SSL_read 和 SSL_write 的返回: SEC(\"uretprobe/SSL_read\")\nint BPF_URETPROBE(probe_SSL_read_exit) { return (SSL_exit(ctx, 0)); // 0 表示读操作\n} SEC(\"uretprobe/SSL_write\")\nint BPF_URETPROBE(probe_SSL_write_exit) { return (SSL_exit(ctx, 1)); // 1 表示写操作\n}","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » Hook 函数","id":"191","title":"Hook 函数"},"192":{"body":"在 SSL/TLS 中,握手(handshake)是一个特殊的过程,用于在客户端和服务器之间建立安全的连接。为了分析此过程,我们 hook 到了 do_handshake 函数,以跟踪握手的开始和结束。 进入握手 我们使用 uprobe 为 do_handshake 设置一个 probe: SEC(\"uprobe/do_handshake\")\nint BPF_UPROBE(probe_SSL_do_handshake_enter, void *ssl) { u64 pid_tgid = bpf_get_current_pid_tgid(); u32 pid = pid_tgid >> 32; u32 tid = (u32)pid_tgid; u64 ts = bpf_ktime_get_ns(); u32 uid = bpf_get_current_uid_gid(); if (!trace_allowed(uid, pid)) { return 0; } /* store arg info for later lookup */ bpf_map_update_elem(&start_ns, &tid, &ts, BPF_ANY); return 0;\n} 这段代码的主要功能如下: 获取当前的 pid, tid, ts 和 uid。 使用 trace_allowed 检查进程是否被允许追踪。 将当前时间戳存储在 start_ns 映射中,用于稍后计算握手过程的持续时间。 退出握手 同样,我们为 do_handshake 的返回设置了一个 uretprobe: SEC(\"uretprobe/do_handshake\")\nint BPF_URETPROBE(probe_SSL_do_handshake_exit) { u32 zero = 0; u64 pid_tgid = bpf_get_current_pid_tgid(); u32 pid = pid_tgid >> 32; u32 tid = (u32)pid_tgid; u32 uid = bpf_get_current_uid_gid(); u64 ts = bpf_ktime_get_ns(); int ret = 0; /* use kernel terminology here for tgid/pid: */ u32 tgid = pid_tgid >> 32; /* store arg info for later lookup */ if (!trace_allowed(tgid, pid)) { return 0; } u64 *tsp = bpf_map_lookup_elem(&start_ns, &tid); if (tsp == 0) return 0; ret = PT_REGS_RC(ctx); if (ret <= 0) // handshake failed return 0; struct probe_SSL_data_t *data = bpf_map_lookup_elem(&ssl_data, &zero); if (!data) return 0; data->timestamp_ns = ts; data->delta_ns = ts - *tsp; data->pid = pid; data->tid = tid; data->uid = uid; data->len = ret; data->buf_filled = 0; data->rw = 2; data->is_handshake = true; bpf_get_current_comm(&data->comm, sizeof(data->comm)); bpf_map_delete_elem(&start_ns, &tid); bpf_perf_event_output(ctx, &perf_SSL_events, BPF_F_CURRENT_CPU, data, EVENT_SIZE(0)); return 0;\n} 此函数的逻辑如下: 获取当前的 pid, tid, ts 和 uid。 使用 trace_allowed 再次检查是否允许追踪。 查找 start_ns 映射中的时间戳,用于计算握手的持续时间。 使用 PT_REGS_RC(ctx) 获取 do_handshake 的返回值,判断握手是否成功。 查找或初始化与当前线程关联的 probe_SSL_data_t 数据结构。 更新数据结构的字段,包括时间戳、持续时间、进程信息等。 通过 bpf_perf_event_output 将数据发送到用户态。 我们的 eBPF 代码不仅跟踪了 ssl_read 和 ssl_write 的数据传输,还特别关注了 SSL/TLS 的握手过程。这些信息对于深入了解和优化安全连接的性能至关重要。 通过这些 hook 函数,我们可以获得关于握手成功与否、握手所需的时间以及相关的进程信息的数据。这为我们提供了关于系统 SSL/TLS 行为的深入见解,可以帮助我们在需要时进行更深入的分析和优化。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » Hook到握手过程","id":"192","title":"Hook到握手过程"},"193":{"body":"在 eBPF 的生态系统中,用户态和内核态代码经常协同工作。内核态代码负责数据的采集,而用户态代码则负责设置、管理和处理这些数据。在本节中,我们将解读上述用户态代码如何配合 eBPF 追踪 SSL/TLS 交互。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 用户态辅助代码分析与解读","id":"193","title":"用户态辅助代码分析与解读"},"194":{"body":"上述代码片段中,根据环境变量 env 的设定,程序可以选择针对三种常见的加密库(OpenSSL、GnuTLS 和 NSS)进行挂载。这意味着我们可以在同一个工具中对多种库的调用进行追踪。 为了实现这一功能,首先利用 find_library_path 函数确定库的路径。然后,根据库的类型,调用对应的 attach_ 函数来将 eBPF 程序挂载到库函数上。 if (env.openssl) { char *openssl_path = find_library_path(\"libssl.so\"); printf(\"OpenSSL path: %s\\n\", openssl_path); attach_openssl(obj, openssl_path); } if (env.gnutls) { char *gnutls_path = find_library_path(\"libgnutls.so\"); printf(\"GnuTLS path: %s\\n\", gnutls_path); attach_gnutls(obj, gnutls_path); } if (env.nss) { char *nss_path = find_library_path(\"libnspr4.so\"); printf(\"NSS path: %s\\n\", nss_path); attach_nss(obj, nss_path); } 这里主要包含 OpenSSL、GnuTLS 和 NSS 三个库的挂载逻辑。NSS 是为组织设计的一套安全库,支持创建安全的客户端和服务器应用程序。它们最初是由 Netscape 开发的,现在由 Mozilla 维护。其他两个库前面已经介绍过了,这里不再赘述。","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 1. 支持的库挂载","id":"194","title":"1. 支持的库挂载"},"195":{"body":"具体的 attach 函数如下: #define __ATTACH_UPROBE(skel, binary_path, sym_name, prog_name, is_retprobe) \\ do { \\ LIBBPF_OPTS(bpf_uprobe_opts, uprobe_opts, .func_name = #sym_name, \\ .retprobe = is_retprobe); \\ skel->links.prog_name = bpf_program__attach_uprobe_opts( \\ skel->progs.prog_name, env.pid, binary_path, 0, &uprobe_opts); \\ } while (false) int attach_openssl(struct sslsniff_bpf *skel, const char *lib) { ATTACH_UPROBE_CHECKED(skel, lib, SSL_write, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, SSL_write, probe_SSL_write_exit); ATTACH_UPROBE_CHECKED(skel, lib, SSL_read, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, SSL_read, probe_SSL_read_exit); if (env.latency && env.handshake) { ATTACH_UPROBE_CHECKED(skel, lib, SSL_do_handshake, probe_SSL_do_handshake_enter); ATTACH_URETPROBE_CHECKED(skel, lib, SSL_do_handshake, probe_SSL_do_handshake_exit); } return 0;\n} int attach_gnutls(struct sslsniff_bpf *skel, const char *lib) { ATTACH_UPROBE_CHECKED(skel, lib, gnutls_record_send, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, gnutls_record_send, probe_SSL_write_exit); ATTACH_UPROBE_CHECKED(skel, lib, gnutls_record_recv, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, gnutls_record_recv, probe_SSL_read_exit); return 0;\n} int attach_nss(struct sslsniff_bpf *skel, const char *lib) { ATTACH_UPROBE_CHECKED(skel, lib, PR_Write, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, PR_Write, probe_SSL_write_exit); ATTACH_UPROBE_CHECKED(skel, lib, PR_Send, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, PR_Send, probe_SSL_write_exit); ATTACH_UPROBE_CHECKED(skel, lib, PR_Read, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, PR_Read, probe_SSL_read_exit); ATTACH_UPROBE_CHECKED(skel, lib, PR_Recv, probe_SSL_rw_enter); ATTACH_URETPROBE_CHECKED(skel, lib, PR_Recv, probe_SSL_read_exit); return 0;\n} 我们进一步观察 attach_ 函数,可以看到它们都使用了 ATTACH_UPROBE_CHECKED 和 ATTACH_URETPROBE_CHECKED 宏来实现具体的挂载逻辑。这两个宏分别用于设置 uprobe(函数入口)和 uretprobe(函数返回)。 考虑到不同的库有不同的 API 函数名称(例如,OpenSSL 使用 SSL_write,而 GnuTLS 使用 gnutls_record_send),所以我们需要为每个库写一个独立的 attach_ 函数。 例如,在 attach_openssl 函数中,我们为 SSL_write 和 SSL_read 设置了 probe。如果用户还希望追踪握手的延迟 (env.latency) 和握手过程 (env.handshake),那么我们还会为 SSL_do_handshake 设置 probe。 在eBPF生态系统中,perf_buffer是一个用于从内核态传输数据到用户态的高效机制。这对于内核态eBPF程序来说是十分有用的,因为它们不能直接与用户态进行交互。使用perf_buffer,我们可以在内核态eBPF程序中收集数据,然后在用户态异步地读取这些数据。我们使用 perf_buffer__poll 函数来读取内核态上报的数据,如下所示: while (!exiting) { err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS); if (err < 0 && err != -EINTR) { warn(\"error polling perf buffer: %s\\n\", strerror(-err)); goto cleanup; } err = 0; } 最后,在 print_event 函数中,我们将数据打印到标准输出: // Function to print the event from the perf buffer\nvoid print_event(struct probe_SSL_data_t *event, const char *evt) { ... if (buf_size != 0) { if (env.hexdump) { // 2 characters for each byte + null terminator char hex_data[MAX_BUF_SIZE * 2 + 1] = {0}; buf_to_hex((uint8_t *)buf, buf_size, hex_data); printf(\"\\n%s\\n\", s_mark); for (size_t i = 0; i < strlen(hex_data); i += 32) { printf(\"%.32s\\n\", hex_data + i); } printf(\"%s\\n\\n\", e_mark); } else { printf(\"\\n%s\\n%s\\n%s\\n\\n\", s_mark, buf, e_mark); } }\n} 完整的源代码可以在这里查看: https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/30-sslsniff","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 2. 详细挂载逻辑","id":"195","title":"2. 详细挂载逻辑"},"196":{"body":"关于如何安装依赖,请参考: https://eunomia.dev/tutorials/11-bootstrap/ 要开始使用 sslsniff,首先要进行编译: make 完成后,请按照以下步骤操作:","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 编译与运行","id":"196","title":"编译与运行"},"197":{"body":"在一个终端中,执行以下命令来启动 sslsniff: sudo ./sslsniff","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 启动 sslsniff","id":"197","title":"启动 sslsniff"},"198":{"body":"在另一个终端中,执行: curl https://example.com 正常情况下,你会看到类似以下的输出: Example Domain ......","breadcrumbs":"使用 uprobe 捕获多种库的 SSL/TLS 明文数据 » 执行 CURL 命令","id":"198","title":"执行 CURL 命令"},"199":{"body":"当执行 curl 命令后,sslsniff 会显示以下内容: READ/RECV 0.132786160 curl 47458 1256 ----- DATA ----- ...Example Domain
...