pkujhd / goloader

load and run golang code at runtime.
Apache License 2.0
506 stars 58 forks source link

fatal error: unreachable method called. linker bug? #55

Closed devanwang closed 2 years ago

devanwang commented 2 years ago

hi, 大佬,下面是复现步骤: 在example下新建两个目录p和t,在p下新建p.go,在t下新建t.go p.go:

package p

import "fmt"

type Intf interface {
    Print(string)
}

type Stru struct {
}

func (Stru *Stru) Print(s string) {
    fmt.Println(s)
}

t.go:

package t

import (
    "fmt"

    "github.com/pkujhd/goloader/examples/p"
)

func Test(param p.Intf) p.Intf {
    param.Print("Intf")
    fmt.Println("done!")
    return param
}

修改loader.go函数声明和调用:

runFunc := *(*func(p.Intf) p.Intf)(unsafe.Pointer(&funcPtrContainer))
r := runFunc(&p.Stru{})
fmt.Println("r:", r)

在t目录中执行go build -x -n -v t.go 2>&1 | sed -n "/^# import config/,/EOF$/p" |grep -v EOF > importcfg && go tool compile -importcfg importcfg t.go,生成t.o文件 在loader目录中执行go build loader.go && ./loader -o ../t/t.o -run main.Test,报错fatal error: unreachable method called. linker bug?

pkujhd commented 2 years ago

@devanwang 因为你编译的t.go不是一个main包,所以go build -x -n -v t.go 2>&1 | sed -n "/^# import config/,/EOF$/p" |grep -v EOF > importcfg 这个得到的信息是错的.

pkujhd commented 2 years ago

正确的信息是类似这样的

# import config
packagefile fmt=/Users/pkujhd/programs/go/pkg/darwin_amd64/fmt.a
packagefile github.com/pkujhd/goloader/examples/p=/Users/pkujhd/Library/Caches/go-build/90/903b1feecaa67440b2c6c3b2edee39fffec4ad91db2874c894b6213cc3975991-d
packagefile runtime=/Users/pkujhd/programs/go/pkg/darwin_amd64/runtime.a
devanwang commented 2 years ago

@pkujhd 大佬,我把t.go的package改了main,cat importcfg结果如下:

# import config
packagefile fmt=/usr/local/go/pkg/linux_amd64/fmt.a
packagefile github.com/pkujhd/goloader/examples/p=/root/.cache/go-build/7c/7ca25758e4aed09361504e8dfac5576615f44fefc3d1960788eab0500ed48243-d
packagefile runtime=/usr/local/go/pkg/linux_amd64/runtime.a

然后重新编译t.o,执行loader还是同样的报错

pkujhd commented 2 years ago

给下你的运行环境和golang的版本

devanwang commented 2 years ago

Linux version 5.4.119-1-tlinux4-0005 (root@VM_197_173_centos) (gcc version 8.3.1 20191121 (Red Hat 8.3.1-5) go version go1.18.3 linux/amd64

devanwang commented 2 years ago

如果在t.go里调用reflect

package main

import (
    "fmt"
    "reflect"

    "github.com/pkujhd/goloader/examples/p"
)

func Test(param p.Intf) p.Intf {
    fmt.Println(reflect.TypeOf(param))
    return param
}

会报错 Load error: unresolve external:reflect.(*rtype).FieldByIndex

pkujhd commented 2 years ago

如果在t.go里调用reflect

package main

import (
  "fmt"
  "reflect"

  "github.com/pkujhd/goloader/examples/p"
)

func Test(param p.Intf) p.Intf {
  fmt.Println(reflect.TypeOf(param))
  return param
}

会报错 Load error: unresolve external:reflect.(*rtype).FieldByIndex

你的case可以在loader中注册掉这个interface暂时避免

var x p.Intf = &p.Stru{}
goloader.RegTypes(symPtr, x, x.Print)
devanwang commented 2 years ago
var x p.Intf = &p.Stru{}
goloader.RegTypes(symPtr, x, x.Print)

加了这个不会报linker bug了,unresolve external:reflect.xxx还是会有

pkujhd commented 2 years ago

reflect

var x p.Intf = &p.Stru{}
goloader.RegTypes(symPtr, x, x.Print)

加了这个不会报linker bug了,unresolve external:reflect.xxx还是会有

这个reflect的函数loader里没有使用,你需要注册

devanwang commented 2 years ago

在t.go里只加了一行reflect.TypeOf(param),每次都报不同的错,没法加啊 Load error: unresolve external:reflect.(rtype).Implements Load error: unresolve external:reflect.(rtype).FieldByNameFunc Load error: unresolve external:reflect.(rtype).ConvertibleTo Load error: unresolve external:reflect.(rtype).FieldAlign Load error: unresolve external:reflect.(rtype).MethodByName Load error: unresolve external:reflect.(rtype).FieldByIndex

我看原理会解析loader bin的symbols, 我在loader里同样加了reflect.TypeOf(param)代码,还是不行

pkujhd commented 2 years ago

Refelect 包会用到一系列的函数,都要注册,或者就在你的加载器里有类似的代码,会移动处理加载器里的符号

devanwang commented 2 years ago

不是很懂,大佬能不能给个具体例子,针对t.go里的reflect.TypeOf函数,我要在loader写什么?

pkujhd commented 2 years ago

fmt.Println(reflect.TypeOf(param))

就是你在loader里也调用过相应的函数,就会自动注册 就是在loader里加一行 fmt.Println(reflect.TypeOf(param))

devanwang commented 2 years ago

这个不行,在loader里加了同样的reflect.TypeOf(param)还是会报如下错误: Load error: unresolve external:reflect.(rtype).Implements Load error: unresolve external:reflect.(rtype).FieldByNameFunc Load error: unresolve external:reflect.(rtype).ConvertibleTo Load error: unresolve external:reflect.(rtype).FieldAlign Load error: unresolve external:reflect.(rtype).MethodByName Load error: unresolve external:reflect.(rtype).FieldByIndex

大佬你那边可以试下

devanwang commented 2 years ago

@pkujhd 查nm找到原因了,loader里的reflect.TypeOf被内联优化了,t.o的没有被内联,加上-gcflags -l就可以了,但这样编译器就禁止内联了,不知道有啥好办法没有

pkujhd commented 2 years ago

@pkujhd 查nm找到原因了,loader里的reflect.TypeOf被内联优化了,t.o的没有被内联,加上-gcflags -l就可以了,但这样编译器就禁止内联了,不知道有啥好办法没有

这个没啥办法,内联了要么你loader的时候重新提供一份,要么loader就是禁止inline的

pkujhd commented 2 years ago

@devanwang ,这个问题查了下,是因为loader中产生的type,由于连接优化,剔除了Intf.Print, 因此上在加载一个扩展的时候,两边的类型虽然名字相同但是实际类型却不一样,无法正确运行,如果你需要在runtime和dynamic library之间传递interface,那么需要使得两边的类型一致(即需要手动注册相应的函数,保证runtime里边产生的type是一样的). 故此问题只能和inline一样. 编写时规避

pkujhd commented 2 years ago

记录下这个case的调试信息:

relocateType symbolName relocateSymbolName address
5 main.Test.stkobj runtime.gcbits.02 1094664412
1 main..stmp_0 go.string."done!" 1107140612
24 main.Test type.github.com/pkujhd/goloader/examples/p.Intf 7043456
23 main.Test type.string 6948512
23 main.Test type.*os.File 7288448
14 main.Test go.string."Intf" 1107140608
10 main.Test  1094664360
14 main.Test type.string 6948512
14 main.Test main..stmp_0 1094664448
14 main.Test os.Stdout 9954648
14 main.Test go.itab.*os.File,io.Writer 7911456
7 main.Test fmt.Fprintln 4919424
7 main.Test runtime.morestack_noctxt 4594432
1 go.itab.*os.File,io.Writer type.io.Writer 7044224
1 go.itab.*os.File,io.Writer type.*os.File 7288448
32769 go.itab.*os.File,io.Writer os.(*File).Write 4893696

以上是这个case所有需要relocate的列表, main.Test需要重定向type.github.com/pkujhd/goloader/examples/p.Intf,这个type已经在load中存在,但是对应的stuc.Print不存在,就会导致运行错误

记录信息:

可以采用-dynlink作为编译参数,这个参数是golang为plugin的模式添加的编译参数,会产生一个go.plugin.tabs的symbol来储存需要导出的函数,plugin模式用它来初始化moduledata的ptabEntry, 然后plugin模块读取这个array来产生导出的符号列表, 这是一个interface{]的array,所以当把interface转成func()时,会导入相应的符号,就不会产生上述才问题.

但是这个问题在goversion>=1.12的schedule这个case的时候会导致找不到符号runtime.gosched_m·f, 这个符号在runtime.a里,但是生成的loader里边没有,生成的.o里也没有,暂时无法处理;而不包含-dynlink的编译成的.o,runtime.gosched_m·f,这个符号就在当前的.o里.

now blocking...

eh-steve commented 2 years ago

I think this is an artefact of the main loader's go compiler/linker setting some of the method offsets in the uncommon type of the *p.Stru to -1 as part of its reachability analysis (since no code in loader.go explicitly calls (*p.Stru).Print()). These "unreachable" method offsets are then baked into the firstmodule's itabs (go.itab.*github.com/pkujhd/goloader/examples/p.Stru,github.com/pkujhd/goloader/examples/p.Intf) during the first itab init and the method text pointers are set to point at the address of runtime.unreachableMethod, and when the method is later called from a dynamic module, it is indeed unreachable (from the first module's perspective).

PR #66 adds a functionality to patch the first module's itabs to point at newly included method offsets if they're now available in a dynamic module, which prevents these fatal error: unreachable method called. linker bug? errors.

I've made a separate branch here with this commit https://github.com/eh-steve/goloader/commit/78fc98a4d32a92ce1f67ab347242611936086761 demonstrating the solution.

If you check out that branch and run either:

cd ./examples/jit
go build .
./jit

or (using old loader)

cd ./examples/issue55/t/
go build -o t.o .
cd ../p/
go build -o p.o .
cd ../../loader
go build .
 ./loader -o ../issue55/t/t.o -o ../issue55/p/p.o -run "github.com/pkujhd/goloader/examples/issue55/t.Test"

It should be working (without the main binary having registered any extra types!)