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

    文章

    318

    浏览量

    23051
  • 函数参数
    +关注

    关注

    0

    文章

    6

    浏览量

    6141
  • BPF
    BPF
    +关注

    关注

    0

    文章

    26

    浏览量

    4633

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

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

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    房产数据平台安家go获取地区列表数据的API接口

    如何使用安家go提供的API接口获取地区列表数据,包括API端点、请求参数、响应格式以及代码实现。我们将逐步引导您完成整个过程,确保您能轻松集成到自己的项目中。 1. API概述 安家go
    的头像 发表于 11-21 14:38 172次阅读
    房产数据平台安家<b class='flag-5'>go</b>获取地区列表数据的API接口

    闪灯程序构建与调试

    在上一篇文章中我们成功了搭建了环境,那么接下来就用闪灯程序认识一下编译和调试工具吧!
    的头像 发表于 11-05 14:52 4013次阅读
    闪灯<b class='flag-5'>程序</b>的<b class='flag-5'>构建</b>与调试

    FreeRTOS增加新的设备驱动程序

    如果你正在使用FreeRTOS构建嵌入式系统,并且考虑添加新的设备驱动程序,那么这篇文章很适合你。高效的设备集成不仅仅是让设备功能正常运行——更关乎模块化、可靠性和安全性。
    的头像 发表于 08-06 15:44 745次阅读
    <b class='flag-5'>为</b>FreeRTOS增加新的设备驱动<b class='flag-5'>程序</b>

    zephyr设备驱动程序模型

        1:Zephyr 内核支持多种设备驱动程序。驱动程序是否可用取决于board 和驱动程序。 Zephyr 设备模型配置作为系统一部分的驱动
    的头像 发表于 07-29 10:34 463次阅读
    zephyr设备驱动<b class='flag-5'>程序</b>模型

    详解hal_entry入口函数

    当使用RTOS时,程序从main函数开始进行线程调度;当没有使用RTOS时,C语言程序的入口函数main函数调用了hal_entry
    的头像 发表于 07-25 15:34 1662次阅读

    请问是否可以在通用Windows平台中构建OpenVINO™ GenAI C++ 应用程序

    无法在通用 Windows 平台中构建OpenVINO™ GenAI C++ 应用程序
    发表于 06-24 07:35

    构建fx2lp程序时出现问题求解决

    我已经遇到这个问题好几天了,我需要大家的帮助。 我有 fx3sdkSetup_1.3.4 和 sdcc-4.1.0-x64 设置 已安装。 我正在使用 2 种方法构建我的程序但遇到了一个问题: i
    发表于 05-29 06:35

    使用Percepio View免费跟踪工具分析Zephyr应用

    Percepio View免费跟踪工具现在可以针对Zephyr应用程序进行跟踪和可视化分析了。Percepio View可以帮助开发人员理解和调试Zephyr中的固定优先级的多线程行为及复杂的线程交互。
    的头像 发表于 05-27 15:08 583次阅读
    使用Percepio View免费<b class='flag-5'>跟踪</b>工具分析Zephyr应用

    如何使用window程序对控制参数进行调整?

    目前我已经把程序刷写到控制器中,我想在不给对方控制程序代码的基础上,让对方可以实现对参数进行调整。请问如何做到
    发表于 04-23 07:51

    部署计算机上运行 LabVIEW 应用程序时出现以下错误: “缺少外部函数 dll...”解决办法

    如果你既有 DLL 文件,也有头 (.h) 文件,那么可以使用共享库批量生成VI,不用再一个一个使用“调用库函数节点”调用DLL,源代码运行是没有问题,一旦生成应用程序报错缺失外部函数
    发表于 04-01 19:10

    使用Mickledore生成BSP,移动到最新版本的Scarthgap v6.6.52时,驱动程序构建,怎么解决?

    当我使用 Mickledore 生成 BSP 时,moal.ko WIFI 驱动程序是自动构建的,并且位于 /lib/modules/ 中,并且 WIFI 可以正常工作。 当我移动到最新版
    发表于 03-27 06:49

    如何在 Raspberry Pi AI Camera 上构建开发人员提供实时的智能应用程序

    在这篇特邀文章中,我们的合作伙伴索尼公司的雷蒙娜-雷纳(RamonaRayner)将向您展示如何快速探索不同的模型和人工智能功能,以及如何在RaspberryPi人工智能相机的基础上轻松构建应用程序
    的头像 发表于 03-25 09:37 614次阅读
    如何在 Raspberry Pi AI Camera 上<b class='flag-5'>构建</b><b class='flag-5'>为</b>开发人员提供实时的智能应用<b class='flag-5'>程序</b>!

    DevEco Studio构建分析工具Build Analyzer 原生鸿蒙应用开发提速

    至Build Analyzer页签。 打开Build Analyzer,Overview窗格中,展示了构建历史、构建参数、工具链版本及构建整体耗时等信息;切换到Tasks视图,即可进一
    发表于 02-17 18:06

    TMETRIC:简单步骤将工作区连接到时间跟踪应用程序

    将计时器按钮添加到组合门票 完成这三个简单步骤以在 Assembla 中启用时间跟踪。设置时间不超过 3 分钟。 注册 TMetric 具有高级计费和报告功能的时间跟踪应用程序 安装浏览器扩展
    的头像 发表于 01-07 09:23 657次阅读
    TMETRIC:简单步骤将工作区连接到时间<b class='flag-5'>跟踪</b>应用<b class='flag-5'>程序</b>

    Todoist一键时间跟踪

    过三个简单的步骤将您的Todoist工作区连接到TMetric时间跟踪应用。通过单击鼠标跟踪执行任务所花费的时间。项目获取广泛而精确的报告。 添加计时器按钮到Todoist任务中 完成这三个简单
    的头像 发表于 01-03 11:08 750次阅读
    Todoist一键时间<b class='flag-5'>跟踪</b>