这次写一点稍微冷门的知识,关于go语言的插件系统(plugin)。插件相关的相关资料其实不多,大多都是讲一下怎么用,关于插件的原理和潜藏的坑没有更多的解释,所以我希望能试着从原理的角度分析一下。

golang插件加载机制解析

在我们的实际业务中,使用到了go语言的plugin机制,go语言的plugin机制其实很不成熟,在社区也只是一个比较边缘化的特性,社区对于plugin的演进也没有给出明确的计划。但是由于业务选型使用了这个特性,短时间无法消除使用,那不如把这里的坑分析清楚,避免总是在同一个地方踩坑。

基本原理

Go语言的plugin包提供了以下几个方法和函数:

OpenPlugin
PluginLookup
Symbolinterface{}Lookup
Open${GOROOT}\src\plugin\plugin_dlopen.goopenC.pluginOpen
C.pluginOpendlopen方法的封装

也就是说,其实插件机制的基础就是linux下的动态库。 linux的动态库原生提供了init机制,可以在dlopen时执行一段动态库里的方法,所以如果希望搞清楚动态库的原理,首先就需要看一下在调用dlopen时,与插件有关的执行了什么代码。

go.link.addmoduledatainit0x18cdc0
0x18cdc0
go.link.addmoduledatalocal.moduledatardiruntime.addmoduledatalocal.moduledataruntime.addmoduledata
rdilastmoduledatapnextlastmoduledataplocal.moduledata
firstmoduledatalastmoduledatapmoduledatalastmoduledatapaddmoduledatalocal.moduledata
moduledatamoduledata
C.pluginOpenmoduledatamoduledataplugin.open
lastmoduleinitsrc\runtime\plugin.goplugin_lastmoduleinitplugin.lastmoduleinit

这个方法大体可以分为两部分,以上是第一部分,这部分是对于插件合法性的校验,我们具体看一看校验了哪些信息。

moduledatadlopenmoduledatathrowfatalmdtypemapmoduledataactiveModulespkghashesmodulehashlinktimehashruntimehash
moduledatapkghasheslocal.moduledata.noptrdataobjdump -dzj .noptrdata plugin1.so
moduledata
1963264(0x1DF500)

同样,按内存排布解析后得到:

modulenamelinktimehashruntimehashmodulenamelinktimehash
.rodatamodulename1769904(0x1b01b0)runtime/cgomodulename
linktimehashlinktimehash1769152(0x1afec0)
pkghashes
src\cmd\link\internal\ld\symtab.go


Fingerprintgo.link.pkglinkhashFingerprint
Fingerprint
FingerprintFingerprint
hdrp.stringsp.data0p.data0package
data0
p.strings
hdrp.stringsp.data0
Fingerprint

这里可以稍微总结一下,源代码的哪些变化会导致一个package的md5值变化,其实已经比较明确了:

1. 文件名及文件路径的修改

2. 新增或删除任何可导出符号(函数,类型,常量,变量)

3. 对于已有的可导出符号,修改其定义

4. 从通用的角度看,修改包含了修改其名称,在文件中的位置(行号),所属文件等行为

5. 对于函数类型,参数和返回值名称和类型的变化属于修改

6. 对于结构体类型, 新增,删除,修改它的任何成员(导出和不可导出)都属于修改

7. 对于interface类型,新增,删除,修改它的任何方法,都属于修改

………………

总之,在这种实现下,代码的修改很容易引起md5的变化。

moduledataPkghasheslinkimehashruntimehashgo.link.pkghashesruntimehash
Pkghashesruntimehashgo.link.pkghash.{pkgname}
go.link.pkghashbytes.{pkgname}
go.link.pkghashbytes.{pkgname}
Fingerprint
go.link.pkghashbytes.{pkgname}go.link.pkghash.{pkgname}dlopen

换个说法,就是: 如果插件和应用的二进制使用了同样的package,runtimehash就是应用中记录的值; 如果插件中有相对于应用新增的package,runtimehash就是插件中记录的值。

pkghash.linktimehash != *pkghash.runtimehash
plugin was built with a different version of package这种情况一般出现在编译过应用程序的代码后,修改了一些共用的包,包括自研的包以及开源软件的包
go不同版本之间,runtime的包一般都会有变化,而runtime的包一般一定是插件和应用程序共用的包,所以go版本的不一致一般都会导致出现这个问题
如上文所说,构成pkghash的信息中包含了源代码的文件路径,所以如果公共包的源码路径不一致,也是这个现象;另外,由于go编译器的trimpath参数也会影响GOPATH,所以如果即使路径相同,trimpath参数不同也会影响

在校验完成后,就真正到了加载插件中的符号信息到全局符号的过程:

modulesinitmoduledataactivemoduletypelinksinit
pluginftabverify
moduledataverify1
itabAdd

这些做完后,runtime需要的元数据信息其实已经准备好了,下来的操作就是将插件中的所有可导出符号放入一个map中,供插件的使用者在查找插件里的符号时使用,不过这里的在填充map时,对于value只填充了类型信息,还不知道具体的值。

lastmoduleinitplugin.open
..inittask


C.pluginLookup

到这里,插件的加载就全部完成了。

插件提供的api,也就是Lookup方法,实际只是从加载时保存的syms表中直接查找。