0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何使用gobpf和uprobe来为Go程序构建函数参数跟踪程序

Linux阅码场 来源:Linux内核之旅 作者:Zain Asgar, 陈恒奇 2021-04-03 16:15 次阅读

这是本系列文章的第一篇,讲述了我们如何在生产环境中使用 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 的一部分执行,并且可以将结果写入缓冲区。

fe942248-8cdd-11eb-8b86-12bb97331649.jpg

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

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

// 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

”, computeE(iters))))

})

// Start server.。.

}

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

[0] % objdump --syms app|grep computeE

00000000006609a0 g F .text 000000000000004b main.computeE

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

[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 内核数据结构。

fec975f6-8cdd-11eb-8b86-12bb97331649.png

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

ff0bbdbc-8cdd-11eb-8b86-12bb97331649.png

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

#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 函数的功能完善的端到端的参数跟踪程序!下面的视频片段展示了这一结果。

ff4b47e8-8cdd-11eb-8b86-12bb97331649.gif

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

(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 了。

(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 代码很复杂。没有大量高级工具,不太可能把它当作一般的调试手段。
编辑:lyn

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • LINUX内核
    +关注

    关注

    1

    文章

    316

    浏览量

    21651
  • 函数参数
    +关注

    关注

    0

    文章

    6

    浏览量

    5987
  • BPF
    BPF
    +关注

    关注

    0

    文章

    25

    浏览量

    4006

原文标题:在生产环境中使用 eBPF 调试 GO 程序

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    嵌入式学习-飞凌嵌入式ElfBoard ELF 1板卡-spi编程示例之spi编写程序

    传输数据。它使用`ioctl`系统调用和`SPI_IOC_MESSAGE`命令执行SPI数据传输。第四部分:`main`函数程序的入口点。它将`send_num`变量设置38,然
    发表于 11-07 09:36

    飞凌嵌入式ElfBoard ELF 1板卡-spi编程示例之spi编写程序

    传输数据。它使用`ioctl`系统调用和`SPI_IOC_MESSAGE`命令执行SPI数据传输。第四部分:`main`函数程序的入口点。它将`send_num`变量设置38,然
    发表于 11-06 09:15

    面试常考+1:函数指针与指针函数、数组指针与指针数组

    函数是指返回值指针的函数。语法:返回值类型*函数名(参数列表)。示例代码:#include#includeint*getNumber(in
    的头像 发表于 08-10 08:11 861次阅读
    面试常考+1:<b class='flag-5'>函数</b>指针与指针<b class='flag-5'>函数</b>、数组指针与指针数组

    如何看懂检测设备程序逻辑

    的地方,通常是主函数(main function)或启动函数(startup function)。在程序入口处,通常会进行一些初始化操作,如初始化全局变量、配置硬件设备等。 程序主体
    的头像 发表于 07-17 16:50 427次阅读

    python函数的万能参数

    我们通过一个简单的事例展示一下函数的万能参数,我们先写一个最简单的函数
    的头像 发表于 07-17 14:56 339次阅读
    python<b class='flag-5'>函数</b>的万能<b class='flag-5'>参数</b>

    使用Redis和Spring Ai构建rag应用程序

    随着AI技术的不断进步,开发者面临着如何有效利用现有工具和技术加速开发过程的挑战。Redis与SpringAI的结合为Java开发者提供了一个强大的平台,以便快速构建并部署响应式AI应用。探索这一
    的头像 发表于 04-29 08:04 1040次阅读
    使用Redis和Spring Ai<b class='flag-5'>构建</b>rag应用<b class='flag-5'>程序</b>

    Go语言中的函数、方法与接口详解

    Go 没有类,不过可以为结构体类型定义方法。方法就是一类带特殊的接收者参数函数。方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。(非结构体类型也可以定义方法)
    的头像 发表于 04-23 16:21 832次阅读

    使用Docker部署Go Web应用程序步骤

    大多数情况下Go应用程序被编译成单个二进制文件,web应用程序则会包括模版和配置文件。而当一个项目中有很多文件的时候,由于很多文件没有同步就会导致错误的发生并且产生很多的问题。
    发表于 04-20 09:33 505次阅读
    使用Docker部署<b class='flag-5'>Go</b> Web应用<b class='flag-5'>程序</b>步骤

    学习笔记|如何用Go程序采集温湿度传感器数据

    在共创社内部的交流中,先前有一位成员展示了如何借助C语言实现对AHT20温湿度传感器数据的读取。这一实例触发了另一位共创官的灵感,他决定采纳Go语言重新构建这一数据采集流程。接下来,我们将详细解析
    的头像 发表于 03-21 11:46 714次阅读
    学习笔记|如何用<b class='flag-5'>Go</b><b class='flag-5'>程序</b>采集温湿度传感器数据

    什么是pipeline?Go构建流数据pipeline的技术

    本文介绍了在 Go构建流数据pipeline的技术。 处理此类pipeline中的故障很棘手,因为pipeline中的每个阶段可能会阻止尝试向下游发送值,并且下游阶段可能不再关心传入的数据。
    的头像 发表于 03-11 10:16 613次阅读

    如何使用exit()、_exit()和_Exit()终止程序运行呢?

    在Linux系统下,你可以使用 exit()、_exit() 和 _Exit() 终止程序运行,特别是在出现错误或执行失败的情况下。
    的头像 发表于 02-22 12:20 938次阅读

    如何使用linux下gdb调试python程序

    如何使用linux下gdb调试python程序  在Linux下,可以使用GDB(GNU调试器)调试Python程序。GDB是一个强大的调试工具,可以帮助开发者诊断和修复
    的头像 发表于 01-31 10:41 2624次阅读

    GD32 MCU是如何进入中断函数

    用过GD32 MCU的小伙伴们都知道,程序是顺序执行的,但当有中断的时候程序会跳转到中断函数,执行完中断函数
    的头像 发表于 01-30 09:45 1099次阅读
    GD32 MCU是如何进入中断<b class='flag-5'>函数</b>的

    如何构建linux开发环境和编译软件工程、应用程序

    前文介绍了如何使用官方提供的镜像文件启动开发板,本文将说明如何构建linux开发环境和编译软件工程、应用程序
    的头像 发表于 01-03 12:31 2059次阅读
    如何<b class='flag-5'>构建</b>linux开发环境和编译软件工程、应用<b class='flag-5'>程序</b>

    怎么写出效率高、思路清晰的C语言程序

    要用C语言的思维方式进行程序的构架构建 要有良好的C语言算法基础,以此实现程序的逻辑构架 灵活运用C语言的指针操作
    的头像 发表于 01-02 14:20 572次阅读