编译时注入原理概述
编译时注入原理概述
在正常情况下,go build 命令通过以下主要步骤编译 Golang 应用程序:
- 源代码解析: Golang 编译器首先会解析源代码文件,并将其转换为抽象语法树(AST)。
- 类型检查: 解析之后,类型检查确保代码符合 Golang 的类型系统。
- 语义分析: 这包括分析程序的语义,包括变量定义和用法以及包导入。
- 编译优化: 将语法树转换为中间表示,并进行各种优化,以提高代码执行效率。
- 代码生成: 生成目标平台的机器代码。
- 链接: 将不同的软件包和库链接在一起,形成一个可执行文件。
使用我们的编译时插桩工具时,在上述步骤之前会增加两个额外的阶段: 预处理和代码插桩。
- 预处理: 分析依赖关系,选择以后应使用的规则。
- 代码插桩: 根据规则生成代码,并将新生成的代码注入源代码。
预处理
在这一阶段,该工具会分析用户项目代码中的第三方库依赖关系,并将其与现有的插桩规则进行匹配,以找到合适的规则。它还会预先配置这些规则所需的额外依赖关系。
工具规则精确定义了哪些代码需要注入到哪个版本的框架或标准库中。不同类型的工具规则有不同的作用。目前可用的工具规则类型包括
- InstFuncRule:在方法的入口和出口点注入代码。
- InstStructRule: 通过添加新字段修改结构体。
- InstFileRule: 添加新文件以参与原始编译过程。
完成所有预处理后,调用 go build -toolexec otel cmd/app
进行编译。-toolexec
参数是我们自动工具的核心,用于拦截传统的编译过程,并用用户定义的工具取而代之,让开发人员可以更灵活地定制编译过程。在这里,otel 就是自动工具,也就是我们所说的 “工具 ”阶段。
插桩
在这一阶段,根据规则在目标函数中插入蹦床代码。蹦床代码本质上是一种复杂的 If 语句,允许在目标函数的入口和出口点插入监控代码,从而实现监控数据的收集。此外,还在 AST 层进行了若干优化,以尽量减少蹦床代码的额外性能开销,并优化代码执行效率。
完成这些步骤后,工具会修改编译参数,然后调用 go build cmd/app
进行正常编译,如前所述。
net/http插件示例
首先,我们将函数分为以下三类: RawFunc、TrampolineFunc 和 HookFunc。RawFunc 是需要注入的原始函数。TrampolineFunc 是蹦床函数。HookFunc 是 onEnter/onExit 函数,需要作为探测代码插入到原始函数的入口和出口点。RawFunc 通过插入的蹦床代码跳转到 TrampolineFunc,然后 TrampolineFunc 构建上下文,准备错误恢复处理,最后跳转到 HookFunc 执行探测代码。
接下来,我们以 net/http
为例,演示编译时自动仪器如何在目标函数 (*Transport).RoundTrip()
中插入监控代码。框架将在该函数的入口处生成蹦床代码,即跳转到 TrampolineFunc.RoundTrip()
的 if 语句(实际上是一行,为了演示而写成多行):
这里,OtelOnEnterTrampoline_RoundTrip37639
是 TrampolineFunc。它准备错误处理和调用上下文,然后跳转到 ClientOnEnterImpl
:
ClientOnEnterImpl
是 HookFunc,它是我们的探测代码,用于执行跟踪、指标报告等。ClientOnEnterImpl
是一个函数指针,在预处理阶段自动生成的 otel_setup_inst.go 中预先配置,它实际上指向 clientOnEnter
:
clientOnEnter
函数执行实际监控任务:
通过上述步骤,我们不仅在 (*Transport).RoundTrip()
函数中插入了监控代码,还确保了监控数据和上下文的准确性和传播性。在编译时自动插桩过程中,这些操作都是自动完成的,为开发人员节省了大量时间,并降低了手动探测的错误率。