Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >[译] 第 1 部分: 在生产环境中使用 eBPF 调试 Go 程序

[译] 第 1 部分: 在生产环境中使用 eBPF 调试 Go 程序

作者头像
ritchiechen
发布于 2021-01-12 15:19:12
发布于 2021-01-12 15:19:12
1.4K0
举报
文章被收录于专栏:Serverless+Serverless+

这是本系列文章的第一篇, 讲述了我们如何在生产环境中使用 eBPF 调试应用程序而无需重新编译/重新部署. 这篇文章介绍了如何使用 gobpf 和 uprobe 来为 Go 程序构建函数参数跟踪程序. 这项技术也可以扩展应用于其他编译型语言, 例如 C++, Rust 等. 本系列的后续文章将讨论如何使用 eBPF 来跟踪 HTTP/gRPC/SSL 等.

简介

在调试时, 我们通常对了解程序的状态感兴趣. 这使我们能够检查程序正在做什么, 并确定缺陷在代码中的位置. 观察状态的一种简单方法是使用调试器来捕获函数的参数. 对于 Go 程序来说, 我们经常使用 Delve 或者 GDB.

在开发环境中, Delve 和 GDB 工作得很好, 但是在生产环境中并不经常使用它们. 那些使调试器强大的特性也让它们不适合在生产环境中使用. 调试器会导致程序中断, 甚至允许修改状态, 这可能会导致软件产生意外故障.

为了更好地捕获函数参数, 我们将探索使用 eBPF(在 Linux 4.x+ 中可用) 以及高级的 Go 程序库 gobpf.

eBPF 是什么 ?

扩展的 BPF(eBPF) 是 Linux 4.x+ 里的一项内核技术. 你可以把它想像成一个运行在 Linux 内核中的轻量级的沙箱虚拟机, 可以提供对内核内存的经过验证的访问.

如下概述所示, eBPF 允许内核运行 BPF 字节码. 尽管使用的前端语言可能会有所不同, 但它通常是 C 的受限子集. 一般情况下, 使用 Clang 将 C 代码编译为 BPF 字节码, 然后验证这些字节码, 确保可以安全运行. 这些严格的验证确保了机器码不会有意或无意地破坏 Linux 内核, 并且 BPF 探针每次被触发时, 都只会执行有限的指令. 这些保证使 eBPF 可以用于性能关键的工作负载, 例如数据包过滤, 网络监控等.

从功能上讲, eBPF 允许你在某些事件(例如定时器, 网络事件或函数调用)触发时运行受限的 C 代码. 当在函数调用上触发时, 我们称这些函数为探针, 它们既可以用于内核里的函数调用(kprobe) 也可以用于用户态程序中的函数调用(uprobe). 本文重点介绍使用 uprobe 来动态跟踪函数参数.

Uprobe

uprobe 可以通过插入触发软中断的调试陷阱指令(x86 上的 int3)来拦截用户态程序. 这也是调试器的工作方式. uprobe 的流程与任何其他 BPF 程序基本相同, 如下图所示. 经过编译和验证的 BPF 程序将作为 uprobe 的一部分执行, 并且可以将结果写入缓冲区.

BPF for tracing (from Brendan Gregg)
BPF for tracing (from Brendan Gregg)

让我们看看 uprobe 是如何工作的. 要部署 uprobe 并捕获函数参数, 我们将使用这个简单的示例程序. 这个 Go 程序的相关部分如下所示.

main() 是一个简单的 HTTP 服务器, 在路径 /e 上公开单个GET 端点, 该端点使用迭代逼近来计算欧拉数(e). computeE接受单个查询参数(iterations), 该参数指定计算近似值要运行的迭代次数. 迭代次数越多, 近似值越准确, 但会消耗指令周期. 理解函数背后的数学并不是必需的. 我们只是想跟踪对computeE 的任何调用的参数.

代码语言:txt
AI代码解释
复制
// computeE computes the approximation of e by running a fixed number of iterations.
func computeE(iterations int64) float64 {
  res := 2.0
  fact := 1.0

  for i := int64(2); i < iterations; i++ {
    fact *= float64(i)
    res += 1 / fact
  }
  return res
}

func main() {
  http.HandleFunc("/e", func(w http.ResponseWriter, r *http.Request) {
    // Parse iters argument from get request, use default if not available.
    // ... removed for brevity ...
    w.Write([]byte(fmt.Sprintf("e = %0.4f\n", computeE(iters))))
  })
  // Start server...
}

要了解 uprobe 的工作原理, 让我们看一下二进制文件中如何跟踪符号. 由于 uprobe 通过插入调试陷阱指令来工作, 因此我们需要获取函数所在的地址. Linux 上的 Go 二进制文件使用 ELF 存储调试信息. 除非删除了调试数据, 否则即使在优化过的二进制文件中也可以找到这些信息. 我们可以使用 objdump 命令检查二进制文件中的符号:

代码语言:txt
AI代码解释
复制
[0] % objdump --syms app|grep computeE
00000000006609a0 g     F .text    000000000000004b              main.computeE

从这个输出中, 我们知道函数 computeE 位于地址 0x6609a0. 要看到它前后的指令, 我们可以使用 objdump 来反汇编二进制文件(通过添加 -d 选项实现). 反汇编后的代码如下:

代码语言:txt
AI代码解释
复制
[0] % objdump -d app | less
00000000006609a0 <main.computeE>:
  6609a0:       48 8b 44 24 08          mov    0x8(%rsp),%rax
  6609a5:       b9 02 00 00 00          mov    $0x2,%ecx
  6609aa:       f2 0f 10 05 16 a6 0f    movsd  0xfa616(%rip),%xmm0
  6609b1:       00
  6609b2:       f2 0f 10 0d 36 a6 0f    movsd  0xfa636(%rip),%xmm1

由此可见, 当 computeE 被调用时会发生什么. 第一条指令是 mov 0x8(%rsp), %rax. 它把 rsp 寄存器偏移 0x8 的内容移动到 rax 寄存器. 这实际上就是上面的输入参数 iterations. Go 的参数在栈上传递.

有了这些信息, 我们现在就可以继续深入, 编写代码来跟踪 computeE 的参数了.

构建跟踪程序

要捕获事件, 我们需要注册一个 uprobe 函数, 还需要一个可以读取输出的用户空间函数. 如下图所示. 我们将编写一个称为跟踪程序的二进制文件, 它负责注册 BPF 代码并读取 BPF 代码的结果. 如图所示, uprobe 简单地写入 perf buffer, 这是用于 perf 事件的 Linux 内核数据结构.

High-level overview showing the Tracer binary listening to perf events generated from the App
High-level overview showing the Tracer binary listening to perf events generated from the App

现在, 我们已了解了涉及到的各个部分, 下面让我们详细研究添加 uprobe 时发生的情况. 下图显示了 Linux 内核如何使用uprobe 修改二进制文件. 软中断指令(int3)作为第一条指令被插入 main.computeE 中. 这将导致软中断, 从而允许 Linux 内核执行我们的 BPF 函数. 然后我们将参数写入 perf buffer, 该缓冲区由跟踪程序异步读取.

Details of how a debug trap instruction is used call a BPF program
Details of how a debug trap instruction is used call a BPF program

BPF 函数相对简单, C代码如下所示. 我们注册这个函数, 每次调用 main.computeE 时都将调用它. 一旦调用, 我们只需读取函数参数并写入 perf buffer. 设置缓冲区需要很多样板代码, 可以在完整的示例中找到.

代码语言:txt
AI代码解释
复制
#include <uapi/linux/ptrace.h>

BPF_PERF_OUTPUT(trace);

inline int computeECalled(struct pt_regs *ctx) {
  // The input argument is stored in ax.
  long val = ctx->ax;
  trace.perf_submit(ctx, &val, sizeof(val));
  return 0;
}

现在我们有了一个用于 main.computeE 函数的功能完善的端到端的参数跟踪程序! 下面的视频片段展示了这一结果.

End-to-End demo
End-to-End demo

另一个很棒的事情是, 我们可以使用 GDB 来查看对二进制文件所做的修改. 在运行我们的跟踪程序之前, 我们输出地址 0x6609a0 的指令.

代码语言:txt
AI代码解释
复制
(gdb) display /4i 0x6609a0
10: x/4i 0x6609a0
   0x6609a0 <main.computeE>:    mov    0x8(%rsp),%rax
   0x6609a5 <main.computeE+5>:  mov    $0x2,%ecx
   0x6609aa <main.computeE+10>: movsd  0xfa616(%rip),%xmm0
   0x6609b2 <main.computeE+18>: movsd  0xfa636(%rip),%xmm1

而这是在我们运行跟踪程序之后. 我们可以清楚地看到, 第一个指令现在变成 int3 了.

代码语言:txt
AI代码解释
复制
(gdb) display /4i 0x6609a0
7: x/4i 0x6609a0
   0x6609a0 <main.computeE>:    int3
   0x6609a1 <main.computeE+1>:  mov    0x8(%rsp),%eax
   0x6609a5 <main.computeE+5>:  mov    $0x2,%ecx
   0x6609aa <main.computeE+10>: movsd  0xfa616(%rip),%xmm0

尽管我们为该特定示例对跟踪程序进行了硬编码, 但是这个过程是可以通用化的. Go 的许多方面(例如嵌套指针, 接口, 通道等)让这个过程变得有挑战性, 但是解决这些问题可以使用现有系统中不存在的另一种检测模式. 另外, 因为这一过程工作在二进制层面, 它也可以用于其他语言(C++, Rust 等)编译的二进制文件. 我们只需考虑它们各自 ABI 的差异.

下一步是什么 ?

使用 uprobe 进行 BPF 跟踪有其自身的优缺点. 当我们需要观察二进制程序的状态时, BPF 很有用, 甚至在连接调试器会产生问题或者坏处的环境(例如生产环境二进制程序). 最大的缺点是, 即使是最简单的程序状态的观测性, 也需要编写代码来实现. 编写和维护 BPF 代码很复杂. 没有大量高级工具, 不太可能把它当作一般的调试手段.

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
使用 eBPF 在生产环境调试 Go 应用
本文是描述我们如何在生产中使用 eBPF 调试应用程序的系列文章中的第一篇,无需重新编译/重新部署,这篇文章介绍了如何使用 gobpf[1] 和uprobes 为 Go 应用程序建立一个函数参数跟踪器,这种技术也可以扩展到其他编译语言,如 C++、Rust 等。
我是阳明
2020/11/03
1.9K0
使用 eBPF 在生产环境调试 Go 应用
【译】如何在生产环境跟踪 GO 函数的参数
原文地址:https://blog.px.dev/ebpf-function-tracing/
黑光技术
2023/02/15
9460
【译】如何在生产环境跟踪 GO 函数的参数
万字长文解读 Linux 内核追踪机制
Linux 存在众多 tracing tools,比如 ftrace、perf,他们可用于内核的调试、提高内核的可观测性。众多的工具也意味着繁杂的概念,诸如 tracepoint、trace events、kprobe、eBPF 等,甚至让人搞不清楚他们到底是干什么的。本文尝试理清这些概念。
刘盼
2023/08/22
2.5K0
万字长文解读 Linux 内核追踪机制
eBPF 入门实践教程十六:编写 eBPF 程序 Memleak 监控内存泄漏
eBPF(扩展的伯克利数据包过滤器)是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。
云微
2023/08/14
1.2K0
ebpf简介_ebpf编程
eBPF 是什么呢? 从它的全称“扩展的伯克利数据包过滤器 (Extended Berkeley Packet Filter)” 来看,它是一种数据包过滤技术,是从 BPF (Berkeley Packet Filter) 技术扩展而来的。顾名思义BPF来源于伯克利大学, 最早应用于网络数据包过滤器,它比当时最先进的抓包技术快20倍,其主要得利于它的两个设计:
全栈程序员站长
2022/11/08
6030
ebpf简介_ebpf编程
【译】如何使用 eBPF 检测分析用户态程序
这篇文章较好的介绍使用 eBPF 对几种语言开发的应用程序进行可观测分析。介绍的也比较详细,对这块有兴趣的同学可以深入学习。
黑光技术
2022/11/29
1.5K0
【译】如何使用 eBPF 检测分析用户态程序
Deepflow Agent代码阅读杂记
本文章是前端时间读代码时的随手记录,没有做系统整理,估计也不会填坑了,大家随便看看就好。
DifficultWork
2024/06/28
4830
用BPF实现用户态tracing
BPF是最近Linux内核领域热门的技术。传统的BPF指的是tcpdump命令用于过滤网络包的工具,现在BPF已经得到极大的扩展,不再是Berkeley Packet Filter的缩写对应的简单的网络包过滤工具。 从Kernel 4.9之后,BPF已经成为一个完善的内核扩展工具,BPF在内核里运行一个sandbox,用于执行BPF的字节码(bytecode), 在执行BPF程序前,BPF的检查器会对BPF程序的字节码进行安全检查(比如,指针要先判断不为空后再访问,代码里不能有循环,等等),以保证BPF程序不会导致系统崩溃,因为BPF程序执行时是在内核态。 因此,BPF可以很安全地在内核态执行用户编写的程序,而且有安全保证,这比编写内核模块安全太多了。 正是因为BPF能保证安全,并运行在内核态,可以大大简化很多以前很复杂的事情,目前BPF已经应用于性能分析、网络、安全、驱动、区块链等等领域。
王璞
2020/07/14
3.3K0
基于ebpf的性能工具-bpftrace脚本语法
bpftrace 通过高度抽象的封装来使用 eBPF,大多数功能只需要寥寥几笔就可以运行起来,可以很快让我们搞清楚 eBPF 是什么样的,而暂时不关心 eBPF 复杂的内部机理。由于 bpftrace 深受 AWK 和 c 的影响,bpftrace 使用起来于 AWK 非常相似,那些内核 hook 注入点几乎可以按普通字符串匹配来理解,非常容易上手。
Rice加饭
2023/09/02
2.3K0
基于ebpf的性能工具-bpftrace脚本语法
观察HTTP/2流量是困难的,但eBPF可以帮助
在当今充满微服务的世界中,获取服务之间发送的消息的可观察性对于理解和排除问题至关重要。
CNCF
2022/03/27
1.4K0
观察HTTP/2流量是困难的,但eBPF可以帮助
基于 eBPF 的 Linux 可观测性
最近发布的 Linux 内核带了一个针对内核的能力强大的 Linux 监控框架。它起源于历史上人们所说的的 BPF。
黑光技术
2020/05/14
2.3K0
基于 eBPF 的 Linux 可观测性
eBPF 入门开发实践指南五:在 eBPF 中使用 uprobe 捕获 bash 的 readline 函数调用
eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具,它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。
云微
2023/02/24
1K0
eBPF 入门开发实践教程五:在 eBPF 中使用 uprobe 捕获 bash 的 readline 函数调用
eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具,它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。
云微
2023/08/14
4530
聊聊eBPF的前世今生
在介绍eBPF (Extended Berkeley Packet Filter)之前,我们先来了解一下它的前身-BPF (Berkeley Packet Filter)伯克利数据包过滤器。
用户1278550
2023/09/04
1.1K0
聊聊eBPF的前世今生
基于 eBPF 实现容器运行时安全
BPF 全称是「Berkeley Packet Filter」,中文翻译为「伯克利包过滤器」。它源于 1992 年伯克利实验室,Steven McCanne 和 Van Jacobson 写得一篇名为《The BSD Packet Filter: A New Architecture for User-level Packet Capture》的论文。该论文描述是在 BSD 系统上设计了一种新的用户级的数据包过滤架构。在性能上,新的架构比当时基于栈过滤器的 CSPF 快 20 倍,比之前 Unix 的数据包过滤器,例如:SunOS 的 NIT(The Network Interface Tap )快 100 倍。
灵雀云
2021/04/23
2.9K0
基于 eBPF 实现容器运行时安全
eBPF动手实践系列三:基于原生libbpf库的eBPF编程改进方案
在上一篇文章《eBPF动手实践系列二:构建基于纯C语言的eBPF项目》中,我们初步实现了脱离内核源码进行纯C语言eBPF项目的构建。libbpf库在早期和内核源码结合的比较紧密,如今的libbpf库更加成熟,已经完全脱离内核源码独立发展。
Linux阅码场
2024/03/20
4690
eBPF动手实践系列三:基于原生libbpf库的eBPF编程改进方案
eBPF 概述:第 4 部分:在嵌入式系统运行
在本系列的第 1 部分和第 2 部分,我们介绍了 eBPF 虚拟机内部工作原理,在第 3 部分我们研究了基于底层虚拟机机制之上开发和使用 eBPF 程序的主流方式。
233333
2023/11/30
5800
eBPF 概述:第 4 部分:在嵌入式系统运行
【eBPF笔记中篇】运行原理、交互、event触发 解析(未完)
从之前的分析已经得知,.c的eBPF程序会通过BCC等工具编译并加载到内核中,但是具体在内核中,ebpf是如何工作的呢?
历久尝新
2022/02/15
3.4K0
【eBPF笔记中篇】运行原理、交互、event触发 解析(未完)
eBPF 入门实践教程十五:使用 USDT 捕获用户态 Java GC 事件耗时
eBPF (扩展的伯克利数据包过滤器) 是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。这个特性使得 eBPF 能够提供极高的灵活性和性能,使其在网络和系统性能分析方面具有广泛的应用。此外,eBPF 还支持使用 USDT (用户级静态定义跟踪点) 捕获用户态的应用程序行为。
云微
2023/08/14
7270
动态追踪技术(四):基于 Linux bcc/BPF 实现 Go 程序动态追踪
在这篇文章中,我将迅速调研一种跟踪的 Go 程序的新方法:基于 Linux 4.x eBPF 实现动态跟踪。如果你去搜索 Go 和 BPF,你会发现使用 BPF 接口的 Go 语言接口(例如,gobpf)。这不是我所探索的东西:我将使用 BPF 工具实现 Go 应用程序的性能分析和调试。
RiboseYim
2018/01/20
3K0
推荐阅读
相关推荐
使用 eBPF 在生产环境调试 Go 应用
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档