当前位置:   article > 正文

xgo: golang基于-toolexec实现猴子补丁

xgo: golang基于-toolexec实现猴子补丁

注: 转载请注明出处, 原文链接

概述

在这篇博客中,我将详细介绍 xgo 的实现细节。

如果你不知道,xgo 项目位于 https://github.com/xhd2015/xgo

它的作用很简单,就是在每个 Go 函数的开头添加拦截器,从而引入了所谓的 Trap 概念,然后在此基础上引入了其他功能,比如 MockPatchTrace

什么是 Trap

Trap 是插入到函数体开头的一段代码。以一个名为 greet 的函数为例:

func greet(s string) string {
    return "hello " + s
}
  • 1
  • 2
  • 3

经过xgo的处理后,编译器看到的代码将变为:

import "runtime"

func greet(s string) (r0 string){
    stop, post := runtime.__xgo_trap(greet, &s, &r0)
    if stop {
        return
    }
    defer post()
    return "hello " +s
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这两者的差异可以通过下面的图表来可视化:
在这里插入图片描述

如图所示,一旦函数被调用,它的控制流首先转移到 Trap,然后一系列拦截器将根据其目的检查当前调用是否应该被Mock、修改、记录或停止。

这个想法很简单,但也引发了一些问题:

  • 编译器如何看到插桩后的代码?
  • import runtime 是什么?

这两个问题反映了 xgo 的两个基本部分:编译器插桩和运行时插桩。

让我们先看看第一个问题。

编译器如何看到插桩后的代码?

为了让编译器看到与其原始源代码不同的代码,中间必须发生某种事情。

有趣的是,go build 有一个名为-toolexec的标志:

$ go help build
...
-toolexec 'cmd args'
        a program to use to invoke toolchain programs like vet and asm.
        For example, instead of running asm, the go command will run
        'cmd args /path/to/asm <arguments for asm>'.
        The TOOLEXEC_IMPORTPATH environment variable will be set,
        matching 'go list -f {{.ImportPath}}' for the package being built.
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

如果你搜索 go toolexec,甚至有一个示例:https://go.dev/src/cmd/go/testdata/script/toolexec.txt。

简而言之,-toolexec 标志允许用户拦截 go 调用的每个 compilelink 命令,并根据需要执行某种插桩,如下图所示:
在这里插入图片描述
请注意,当你在 go build 中添加 -toolexec=my_tool 标志时,它不会直接调用 compile argslink args,而是将这些调用转发给 my_tool <cmd> args

因此,xgo 利用这个标志来拦截 compile 命令,将所有的编译转发到增强后的编译器。

然后,增强后的编译器将在每个函数中插入这些Trap调用,为运行时在实际调用之前捕获函数调用提供机会。

import runtime 是什么?

现在,编译器已经为我们添加了Trap调用,我们如何知道需要进行什么样的检查?

我们不能让每个包都依赖于 xgo,因为它们可能并不需要它。

好在 runtime 也被插桩了,将调用转发给 xgo。因为在 Go 中,每个包都隐式依赖于 runtime 包。控制流程如下图所示:
在这里插入图片描述

这实际上是一种依赖注入。这样一来,用户的代码就不必显式依赖xgo。

上述代码可以在 runtime/trap_runtime/xgo_trap.goruntime/trap/trap.go 中找到。

为了使Trap可扩展,xgo 引入了一个名为 interceptor 的概念。它具有以下签名:

type Interceptor struct {
	Pre  func(ctx context.Context, f *core.FuncInfo, args core.Object, result core.Object) (data interface{}, err error)
	Post func(ctx context.Context, f *core.FuncInfo, args core.Object, result core.Object, data interface{}) error
}
  • 1
  • 2
  • 3
  • 4

一个 interceptor 由两个子函数组成,称为 PrePost

  • Pre 在函数逻辑之前调用,
  • Post 在函数逻辑之后使用 defer 语句调用。

总结

让我们总结一下我们所讨论的内容。

当你运行 xgo test ./ 时,它会执行以下操作:

  1. 找到 GOROOT,
  2. 将 GOROOT 复制到 ~/.xgo/go-instruments/GOROOT 以准备进行插桩,
  3. 对 ~/.xgo/go-instruments/GOROOT 进行补丁,包括编译器和运行时,
  4. 构建插桩的编译器,
  5. 使用额外的标志调用 go build:go build -toolexec=exec_tool ./
  6. exec_tool 然后将所有编译命令转发给插桩的编译器,
  7. 一旦所有编译完成,go 调用 link 生成可执行文件,你就得到了一个插桩的二进制文件!

优点和缺点

因此,xgo 从上述机制中获得了优点和缺点。
优点:

  • 并发安全:它不会替换需要修改全局地址的函数,因此每个 goroutine 可以设置自己的拦截器并单独删除它们,
  • 兼容性:它重写源代码而不是架构指令,因此与操作系统和架构无关,
  • 可扩展性:它提供了通用的拦截器,因此它的用途不仅限于模拟,你可以借鉴 GRPC 拦截器的所有用途,比如已经实现的追踪、缓存、日志记录等…

缺点:

  • 用户需要安装 xgo 才能启用陷阱功能。

感谢阅读,xgo的核心实现已经在上面全部介绍了。你对此有什么看法?请在这里留下评论,让我们一起讨论吧!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/395084
推荐阅读
相关标签
  

闽ICP备14008679号