Go Plugin
Go plugin是什么
Go 1.8版本开始提供了一个创建共享库的新工具,称为 Plugins.
A plugin is a Go main package with exported functions and variables that has been built with:
go build -buildmode=plugin
Plugin插件是包含可导出(可访问)的function和变量的***main package***编译(go build -buildmode=plugin)之后的文件.
同时官方文档也提示了:Currently plugins are only supported on Linux and macOS .它目前支持Linux和Mac操作系统(不支持windows)
Go plugin生命周期
When a plugin is first opened, the init functions of all packages not already part of the program are called. The main function is not run. A plugin is only initialized once, and cannot be closed.
plugin插件被打开加载 plugin.Open("***.so") ,插件的init初始化函数才开始执行. 也就是说main函数执行前plugin的init函数是不会执行的. 插件只被初始化一次,不能被关闭.
使用plugin的main.go生命周期
- main.go的init函数执行
- 开始执行main.go main函数
- 开始执行
plugin.Open("***.so")打开插件 - 插件开始执行内部的
init函数
Go plugin应用场景
- 通过plugin我们可以很方便的对于不同功能加载相应的模块并调用相关的模块;
- 针对不同语言(英文、汉语、德语……)加载不同的语言so文件,进行不同的输出;
- 编译出的文件给不同的编程语言用(如:c/java/python/lua等).
- 需要加密的核心算法,核心业务逻辑可以可以编译成plugin插件
- 黑客预留的后门backdoor可以使用plugin
- 函数集动态加载
Go plugin 示例
这个示例建展示一下两方面内容:
- 演示plugin插件的
init的执行顺序 - 演示怎么编写一个shell黑客后门
编写插件plugin代码
直接上代码libragen/felix/blob/master/plugin/plugin_bad_docter.go
|
|
编写plugin插件要点
- 包名称必须是main
- 没有main函数
- 必须有可以导出(访问)的变量或者方法
编写完成之后使用编译plugin
|
|
使用plugin插件
使用加载plugin基本流程
- 加载编译好的插件
plugin.Open("./plugin_doctor.so")(*.so文件路径相对与可执行文件的路径,可以是绝对路径) - 寻找插件可到变量
plug.Lookup("Doctor"), - TypeAssert: Symbol(interface{}) 转换成API的接口类型
- 执行API interface的方法
|
|
libragen/felix/blob/master/plugin/use_plugin_example.go
|
|
build plugin程序
|
|
go run
|
|
Go语言plugin局限和不足
Go plugin 还不是一个成熟的解决方案.它迫使您的插件实现与主应用程序产生高度耦合.即使您可以控制插件和主应用程序, 最终结果也非常脆弱且难以维护.如果插件的作者对主应用程序没有任何控制权,开销会更高.
Go版本兼容问题
插件实现和主应用程序都必须使用完全相同的Go工具链版本构建. 由于插件提供的代码将与主代码在相同的进程空间中运行, 因此编译的二进制文件应与主应用程序 100%兼容.
总结
我希望您记下的关键要点:
- Go插件从v1.8版本开始支持,它目前支持Linux和Mac操作系统(不支持windows)
- Go plugin包提供了一个简单的函数集动态加载,可以帮助开发人员编写可扩展的代码.
- Go插件是使用
go build -buildmode = plugin构建标志编译 - Go插件包中的导出函数和公开变量,可以使用插件包在运行时查找并绑定调用.
- Go runtime import插件的开发人员必须将插件视为黑盒子,做好各种最坏的假设*
熟悉 Go 语言的开发者一般都非常了解 Goroutine 和 Channel 的原理,包括如何设计基于 CSP 模型的应用程序,但是 Go 语言的插件系统是很少有人了解的模块,通过插件系统,我们可以在运行时加载动态库实现一些比较有趣的功能。
设计原理
Go 语言的插件系统基于 C 语言动态库实现的,所以它也继承了 C 语言动态库的优点和缺点,我们在本节中会对比 Linux 中的静态库和动态库,分析它们各自的特点和优势。
- 静态库或者静态链接库是由编译期决定的程序、外部函数和变量构成的,编译器或者链接器会将程序和变量等内容拷贝到目标的应用并生成一个独立的可执行对象文件1;
- 动态库或者共享对象可以在多个可执行文件之间共享,程序使用的模块会在运行时从共享对象中加载,而不是在编译程序时打包成独立的可执行文件2;
由于特性不同,静态库和动态库的优缺点也比较明显;只依赖静态库并且通过静态链接生成的二进制文件因为包含了全部的依赖,所以能够独立执行,但是编译的结果也比较大;而动态库可以在多个可执行文件之间共享,可以减少内存的占用,其链接的过程往往也都是在装载或者运行期间触发的,所以可以包含一些可以热插拔的模块并降低内存的占用。
使用静态链接编译二进制文件在部署上有非常明显的优势,最终的编译产物也可以直接运行在大多数的机器上,静态链接带来的部署优势远比更低的内存占用显得重要,所以很多编程语言包括 Go 都将静态链接作为默认的链接方式。
插件系统
在今天,动态链接带来的低内存占用优势虽然已经没有太多作用,但是动态链接的机制却可以为我们提供更多的灵活性,主程序可以在编译后动态加载共享库实现热插拔的插件系统。
通过在主程序和共享库直接定义一系列的约定或者接口,我们可以通过以下的代码动态加载其他人编译的 Go 语言共享对象,这样做的好处是主程序和共享库的开发者不需要共享代码,只要双方的约定不变,修改共享库后也不需要重新编译主程序。
|
|
上述代码定义了 Driver 接口并认为共享库中一定包含 func NewDriver() Driver 函数,当我们通过 plugin.Open 读取包含 Go 语言插件的共享库后,获取文件中的 NewDriver 符号并转换成正确的函数类型,可以通过该函数初始化新的 Driver 并获取它的名字了。
操作系统
不同的操作系统会实现不同的动态链接机制和共享库格式,Linux 中的共享对象会使用 ELF 格式3并提供了一组操作动态链接器的接口,在本节的实现中我们会看到以下的几个接口4:
|
|
dlopen 会根据传入的文件名加载对应的动态库并返回一个句柄(Handle);我们可以直接使用 dlsym 函数在该句柄中搜索特定的符号,也就是函数或者变量,它会返回该符号被加载到内存中的地址。因为待查找的符号可能不存在于目标动态库中,所以在每次查找后我们都应该调用 dlerror 查看当前查找的结果。
动态库
Go 语言插件系统的全部实现都包含在 plugin 中,这个包实现了符号系统的加载和决议。插件是一个带有公开函数和变量的包,我们需要使用下面的命令编译插件:
|
|
该命令会生成一个共享对象 .so 文件,当该文件被加载到 Go 语言程序时会使用下面的结构体 plugin.Plugin 表示,该结构体中包含文件的路径以及包含的符号等信息:
|
|
与插件系统相关的两个核心方法分别是用于加载共享文件的 plugin.Open 和在插件中查找符号的 plugin.Plugin.Lookup,本节将详细介绍它们的实现原理。
CGO
在具体分析 plugin 包中几个公有方法之前,我们需要先了解一下包中使用的两个 C 语言函数 plugin.pluginOpen 和 plugin.pluginLookup;plugin.pluginOpen 只是简单包装了一下标准库中的 dlopen 和 dlerror 函数并在加载成功后返回指向动态库的句柄:
|
|
plugin.pluginLookup 使用了标准库中的 dlsym 和 dlerror 获取动态库句柄中的特定符号:
|
|
这两个函数的实现原理都比较简单,它们的作用也只是简单封装标准库中的 C 语言函数,让它们的签名看起来更像是 Go 语言中的函数签名,方便在 Go 语言中调用。
加载过程
用于加载共享对象的函数 plugin.Open 会将共享对象文件的路径作为参数并返回 plugin.Plugin 结构:
|
|
上述函数会调用私有的函数 plugin.open 加载插件,它是插件加载过程的核心函数,我们可以将该函数拆分成以下几个步骤:
- 准备 C 语言函数
plugin.pluginOpen的参数; - 通过 cgo 调用
plugin.pluginOpen并初始化加载的模块; - 查找加载模块中的
init函数并调用该函数; - 通过插件的文件名和符号列表构建
plugin.Plugin结构;
首先是使用 cgo 提供的一些结构准备调用 plugin.pluginOpen 所需要的参数,下面的代码会将文件名转换成 *C.char 类型的变量,该类型的变量可以作为参数传入 C 函数:
|
|
当我们拿到了指向动态库的句柄之后会调用 plugin.lastmoduleinit,链接器会将它会链接到运行时的 runtime.plugin_lastmoduleinit 函数上,它会解析文件中的符号并返回共享文件的目录和其中包含的全部符号:
|
|
在该函数的最后,我们会构建一个新的 plugin.Plugin 结构体并遍历 plugin.lastmoduleinit 返回的全部符号,为每一个符号调用 plugin.pluginLookup:
|
|
上述函数在最后会返回一个包含符号名到函数或者变量映射的 plugin.Plugin 结构体,调用方可以将该结构体作为句柄查找其中的符号,需要注意的是,我们在这段代码中省略了查找 init 并初始化插件的过程。
符号查找 #
plugin.Plugin.Lookup 可以在 plugin.Open 返回的结构体中查找符号 plugin.Symbol,该符号是 interface{} 类型的一个别名,我们可以将它转换成变量或者函数真实的类型:
|
|
上述方法调用的私有函数 plugin.lookup 实现比较简单,它直接利用了结构体中的符号表,如果没有找到对应的符号会直接返回错误。
小结
Go 语言的插件系统利用了操作系统的动态库实现模块化的设计,它提供功能虽然比较有趣,但是在实际使用中会遇到比较多的限制,目前的插件系统也仅支持 Linux、Darwin 和 FreeBSD,在 Windows 上是没有办法使用的。因为插件系统的实现基于一些黑魔法,所以跨平台的编译也会遇到一些比较奇葩的问题,作者在使用插件系统时也踩过很多坑,如果对 Go 语言不是特别了解,还是不建议使用该模块的。
参考资料
-
No backlinks found.