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

    文章

    311

    浏览量

    21389
  • 函数参数
    +关注

    关注

    0

    文章

    6

    浏览量

    5957
  • BPF
    BPF
    +关注

    关注

    0

    文章

    24

    浏览量

    3842

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

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

收藏 人收藏

    评论

    相关推荐

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

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

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

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

    用C语言构建高效的嵌入式程序

    嵌入式工程师在编写C语言程序时,需要注重效率和清晰的思路。本文将通过解析经典问题“猴子选大王”来展示如何用C语言思维方式构建高效、清晰的程序
    的头像 发表于 12-21 09:27 296次阅读

    c语言源程序的基本单位

    C语言源程序的基本单位是函数函数是一组有关联的语句和表达式的集合,它们一起完成特定的任务。在C语言中,程序的执行是以函数为单位的,每个
    的头像 发表于 11-26 09:05 927次阅读

    一个c源程序至少包括一个函数

    了一组操作的代码。它们可以接受传递给它们的参数,并根据参数执行特定的任务。在C语言中,函数可以被定义在程序的任何位置,但是为了使程序的逻辑更
    的头像 发表于 11-26 09:01 582次阅读

    c语言源程序main函数的位置

    C语言源程序中的main函数程序的入口点,它被认为是C语言程序的起点。在执行程序时,操作系统将首先定位到main
    的头像 发表于 11-24 10:23 927次阅读

    c语言源程序的基本单位

    由一个或多个函数组成。每个函数都有一个函数名和一对大括号{},大括号中是函数的代码块。函数可以接受一些
    的头像 发表于 11-24 10:20 718次阅读

    PTM程序跟踪宏单元介绍

    PTM,程序跟踪宏单元 PTM 是一个模块,它根据程序流程跟踪 (PFT) 体系结构执行实时指令流跟踪
    的头像 发表于 10-30 14:43 390次阅读

    汇编的子程序函数参数和返回值怎么判断?

    怎么知道一个子程序有没有函数参数,有几个函数参数函数参数
    发表于 10-19 07:21

    如何才能获取LabVIEW程序中的传递参数呢?

    有些场景下,我们用LabVIEW开发的应用程序,需要通过命令行来调用,并向该应用程序传递参数,那么在程序中如何才能获取这些参数呢?
    的头像 发表于 10-11 09:26 689次阅读
    如何才能获取LabVIEW<b class='flag-5'>程序</b>中的传递<b class='flag-5'>参数</b>呢?

    python函数函数之间的调用

    ( 2 )x(f) 运行结果: 无结果 分析:因为第9行中的x(f)中的f没有带括号,f只是一个普通的参数,所以程序只调用执行了x(f)函数,没有调用执行f()函数。x(f)
    的头像 发表于 10-04 17:17 356次阅读

    如何使用Tokio 和 Tracing模块构建异步的网络应用程序

    在 Rust 语言中,Tokio 是一个非常流行的异步运行时,它提供了高效的异步 I/O 操作和任务调度。而 Tracing 则是一个用于应用程序跟踪的框架,它可以帮助我们理解应用程序的行为和性能
    的头像 发表于 09-19 15:29 348次阅读

    西门子博途SCL:REGION:构建程序代码的步骤

    可以使用指令“构建程序代码”,在 SCL 块中构建程序代码并将其分为几个不同区域。
    的头像 发表于 07-31 09:09 4177次阅读

    构建一个移动应用程序

    电子发烧友网站提供《构建一个移动应用程序.zip》资料免费下载
    发表于 07-04 14:33 0次下载
    <b class='flag-5'>构建</b>一个移动应用<b class='flag-5'>程序</b>

    C语言编程中main函数退出后程序去哪儿了?

    PIC 单片机语言程序进行跟踪,发现main() 函数最后一条语句为 reset,也就是单片机直接复位,这是 MAPLAB编译器根据 PIC 单片机特点增加的复位语句。
    发表于 05-31 15:28 230次阅读
    C语言编程中main<b class='flag-5'>函数</b>退出后<b class='flag-5'>程序</b>去哪儿了?