Go Plugin的一个bug

  • Post author:
  • Post category:IT
  • Post comments:0评论

Go 1.8中增加了pluginpackage,但是仅支持Linux操作系统,并且还有一些已知的bug。可以说,这个插件系统的实现还未达到"产品级"的水平。

The plugin support is currently incomplete, only supports Linux, and has known bugs.

一些已知的bug已经推到 Go1.10甚至以后的版本中修复了。

今天在测试Go 1.9中的功能的时候就遇到了plugin的一个bug。

按照官方的文档, 开发一个插件很简单:

plugin1/main.go

1234567
package mainimport "fmt"var V intfunc F() { fmt.Printf("Hello, number %d\n", V) }

插件中定义了变量V和方法F,可以通过下面的命令生成一个so文件:

1
go build -buildmode=plugin -o ../p1.so main.go

然后通过plugin包可以加载插件:

main.go

1234567891011121314
p, err := plugin.Open("p1.so")if err != nil {	panic(err)}v, err := p.Lookup("V")if err != nil {	panic(err)}f, err := p.Lookup("F")if err != nil {	panic(err)}*v.(*int) = 7f.(func())() // prints "Hello, number 7"

当然作为插件系统,我们希望可以加载新的插件,来替换已有的插件, 如果你复制p1.so为p2.so,然后上上面的测试代码中再加载p2.so会报错:

main.go

123456789101112
p, err := plugin.Open("p1.so")if err != nil {	panic(err)}......p, err = plugin.Open("p2.so")if err != nil {	panic(err)}

错误信息如下:

12345
go run main.goHello, number 7plugin: plugin plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58 already loadedfatal error: plugin: plugin already loaded......

这一步我们还能理解,相同的plugin即使文件名更改了,加载进去还是一样的,所以会报plugin already loaded错误。

我们将plugin1/main.go中的代码稍微改一下

plugin1/main.go

1234567
package mainimport "fmt"var V intfunc F() { fmt.Printf("Hello world,  %d\n", V) }

然后生成插件p2.so:

1
go build -buildmode=plugin -o ../p2.so main.go

按说这次我们修改了代码,生成了一个新的插件,如果代码同时加载这两个插件,因为没什么问题,但是运行上面的加载两个插件的测试代码,发现还是出错:

12345
go run main.goHello, number 7plugin: plugin plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58 already loadedfatal error: plugin: plugin already loaded......

怪异吧,两个不同代码的生成插件,居然被认为是同一个插件(plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58)。

使用nm查看两个插件的符号表:

123456789101112131415161718192021
[root@colobu t]# nm p1.so |grep unname00000000001acfc0 R go.link.pkghashbytes.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d5800000000003f9620 D go.link.pkghash.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d580000000000199080 t local.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.F0000000000199130 t local.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.init0000000000199080 T plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.F0000000000199130 T plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.init000000000048e027 B plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.initdone·000000000048e088 B plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.V[root@colobu t]#[root@colobu t]#[root@colobu t]# nm p2.so |grep unname00000000001acfc0 R go.link.pkghashbytes.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d5800000000003f9620 D go.link.pkghash.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d580000000000199080 t local.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.F0000000000199130 t local.plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.init0000000000199080 T plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.F0000000000199130 T plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.init000000000048e027 B plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.initdone·000000000048e088 B plugin/unnamed-f0c47a2a99a0d8e8fb40defabb50f238c78f5d58.V[root@colobu t]#

可以看到两个插件中生成的符号表符号表是相同的,所以被误认为了同一个插件。

这种情况是在特殊情况下产生的,如果两个插件的文件名不同,或者引用包不同,或者引用的cgo不同,则会生成不同的插件,同时加载不会有问题。但是如果文件名相同,相关的引用也相同,则可能生成相同的插件,尽管插件内包含的方法和变量不同,实现也不同。

这是Go plugin生成的时候一个bug:issue#19358, 期望在Go 1.10中解决,目前的解决办法就是插件的go文件使用不同的名字,或者编译的时候指定pluginpath:

12
go build -ldflags "-pluginpath=p1"-buildmode=plugin -o ../p1.so main.gogo build -ldflags "-pluginpath=p2"-buildmode=plugin -o ../p2.so main.go

导致问题的原因正如 LionNatsu 在bug中指出的, Go 判断两个插件是否相同是通过比较pluginpath实现的,如果你在编译的时候指定了不同的pluginpath,则编译出来的插件是不同的,但是如果没有指定pluginpath,则由内部的算法生成,生成的格式为plugin/unnamed-" + root.Package.Internal.BuildID。

func computeBuildID(p *Package)生成一个SHA-1的哈希值作为BuildID。

go/src/cmd/go/internal/load/pkg.go

12345678910111213141516171819202122232425262728293031323334353637383940414243444546
func computeBuildID(p *Package) {	h := sha1.New()	// Include the list of files compiled as part of the package.	// This lets us detect removed files. See issue 3895.	inputFiles := str.StringList(		p.GoFiles,		p.CgoFiles,		p.CFiles,		p.CXXFiles,		p.MFiles,		p.HFiles,		p.SFiles,		p.SysoFiles,		p.SwigFiles,		p.SwigCXXFiles,	)	for _, file := range inputFiles {		fmt.Fprintf(h, "file %s\n", file)	}	// Include the content of runtime/internal/sys/zversion.go in the hash	// for package runtime. This will give package runtime a	// different build ID in each Go release.	if p.Standard && p.ImportPath == "runtime/internal/sys" && cfg.BuildContext.Compiler != "gccgo" {		data, err := ioutil.ReadFile(filepath.Join(p.Dir, "zversion.go"))		if err != nil {			base.Fatalf("go: %s", err)		}		fmt.Fprintf(h, "zversion %q\n", string(data))	}	// Include the build IDs of any dependencies in the hash.	// This, combined with the runtime/zversion content,	// will cause packages to have different build IDs when	// compiled with different Go releases.	// This helps the go command know to recompile when	// people use the same GOPATH but switch between	// different Go releases. See issue 10702.	// This is also a better fix for issue 8290.	for _, p1 := range p.Internal.Deps {		fmt.Fprintf(h, "dep %s %s\n", p1.ImportPath, p1.Internal.BuildID)	}	p.Internal.BuildID = fmt.Sprintf("%x", h.Sum(nil))}

函数的后半部分为Go不同的版本生成不同的哈希,避免用户使用不同的Go版本生成相同的ID。重点看前半部分,可以发现计算哈希的时候只依赖文件名,并不关心文件的内容,这也是我们前面稍微修改一下插件的代码会生成相同的原因, 如果你在代码中import _ "fmt"也会产生不同的插件。

总之,在Go 1.10之前,为了避免插件冲突, 最好是在编译的时候指定pluginpath, 比如:

1
go build -ldflags "-pluginpath=plugin/hot-$(date +%s)" -buildmode=plugin -o hotload.so hotload.go

发表回复