在《Golang反射机制的实现分析——reflect.Type类型名称》一文中,我们分析了Golang获取类型基本信息的流程。本文将基于上述知识和经验,分析方法的查找和调用。(转载请指明出于breaksoftware的csdn博客)
方法
package main
import (
"fmt"
"reflect"
)
type t20190107 struct {
v int
}
func (t t20190107) F() int {
return t.v
}
func main() {
i := t20190107{678}
t := reflect.TypeOf(i)
for it := 0; it < t.NumMethod(); it++ {
fmt.Println(t.Method(it).Name)
}
f, _ := t.MethodByName("F")
fmt.Println(f.Name)
r := f.Func.Call([]reflect.Value{reflect.ValueOf(i)})[0].Int()
fmt.Println(r)
}
这段代码,我们构建了一个实现了F()方法的结构体。然后使用反射机制,通过遍历和名称查找方式,找到方法并调用它。
调用reflect.TypeOf之前的逻辑,我们已经在上节中讲解了。本文不再赘述。
0x00000000004b0226 <+134>: callq 0x491150 <reflect.TypeOf>
0x00000000004b022b <+139>: mov 0x18(%rsp),%rax
0x00000000004b0230 <+144>: mov 0x10(%rsp),%rcx
……
0x00000000004b026a <+202>: mov 0xe0(%rsp),%rax
0x00000000004b0272 <+210>: mov 0xd8(%rax),%rax
0x00000000004b0279 <+217>: mov 0xe8(%rsp),%rcx
0x00000000004b0281 <+225>: mov %rcx,(%rsp)
0x00000000004b0285 <+229>: callq *%rax
0x00000000004b0287 <+231>: mov 0x8(%rsp),%rax
0x00000000004b028c <+236>: mov %rax,0x90(%rsp)
0x00000000004b0294 <+244>: mov 0x78(%rsp),%rcx
0x00000000004b0299 <+249>: cmp %rax,%rcx
这段逻辑对应于上面go代码中的第19行for循环逻辑。
汇编代码的第9行,调用了一个保存于寄存器中的地址。依据之前的分析经验,这个地址是rtype.NumMethod()方法地址。
(gdb) disassemble $rax
Dump of assembler code for function reflect.(*rtype).NumMethod:
看下Golang的代码,可以发现其区分了类型是否是“接口”。“接口”类型的计算比较特殊,而其他类型则调用rtype.exportedMethods()方法。
func (t *rtype) NumMethod() int {
if t.Kind() == Interface {
tt := (*interfaceType)(unsafe.Pointer(t))
return tt.NumMethod()
}
if t.tflag&tflagUncommon == 0 {
return 0 // avoid methodCache synchronization
}
return len(t.exportedMethods())
}
因为我们这个例子是struct类型,所以调用的是下面的方法
var methodCache sync.Map // map[*rtype][]method
func (t *rtype) exportedMethods() []method {
methodsi, found := methodCache.Load(t)
if found {
return methodsi.([]method)
}
methodCache是个全局变量,它以rtype为key,保存了其对应的方法信息。这个缓存在初始时没有数据,所以我们第一次对某rtype调用该方法,是找不到其对应的缓存的。
ut := t.uncommon()
if ut == nil {
return nil
}
rtype.uncommon()根据变量类型,在内存中寻找uncommonType信息。
func (t *rtype) uncommon() *uncommonType {
if t.tflag&tflagUncommon == 0 {
return nil
}
switch t.Kind() {
case Struct:
return &(*structTypeUncommon)(unsafe.Pointer(t)).u
case Ptr:
……
}
}
这段逻辑,我们只要看下汇编将该地址如何转换的
0x000000000048d4df <+143>: cmp $0x19,%rcx
0x000000000048d4e3 <+147>: jne 0x48d481 <reflect.(*rtype).uncommon+49>
0x000000000048d4e5 <+149>: add $0x50,%rax
0x000000000048d4e9 <+153>: mov %rax,0x10(%rsp)
0x000000000048d4ee <+158>: retq
rax寄存器之前保存的是rtype的地址0x4d1320,于是uncommonType的信息保存于0x4d1320+0x50位置。
type uncommonType struct {
pkgPath nameOff // import path; empty for built-in types like int, string
mcount uint16 // number of methods
_ uint16 // unused
moff uint32 // offset from this uncommontype to [mcount]method
_ uint32 // unused
}
依据其结构体,我们可以得出各个变量的值:mcount=0x1,moff=0x28。此处mcount的值正是测试结构体的方法个数1。
获取完uncommonType信息,我们需要通过其找到方法信息
allm := ut.methods()
func (t *uncommonType) methods() []method {
return (*[1 << 16]method)(add(unsafe.Pointer(t), uintptr(t.moff)))[:t.mcount:t.mcount]
}
这个计算比较简单,只是在uncommonType的地址0x4d1370基础上偏移t.moff=0x28即可。我们查看下其内存
(gdb) x/16xb 0x4d1370+0x28
0x4d1398: 0x07 0x00 0x00 0x00 0x40 0x18 0x01 0x00
0x4d13a0: 0x80 0xf8 0x0a 0x00 0x80 0xf1 0x0a 0x00
// Method on non-interface type
type method struct {
name nameOff // name of method
mtyp typeOff // method type (without receiver)
ifn textOff // fn used in interface call (one-word receiver)
tfn textOff // fn used for normal method call
}
和method结构对应上就是method{nameOff=0x07, typeOff=0x011840, ifn=0x0af880, tfn=0x0af180}。
获取方法信息后,exportedMethods筛选出可以对外访问的方法,然后将结果保存到methodCache中。这样下次就不用再找一遍了。
……
methodsi, _ = methodCache.LoadOrStore(t, methods)
return methodsi.([]method)
}
获取到方法个数后,我们就可以使用rtype.Method()方法获取方法信息了。和其他rtype方法一样,Method也是通过指针偏移算出来的。
0x00000000004b02a3 <+259>: mov 0xe0(%rsp),%rax
0x00000000004b02ab <+267>: mov 0xb0(%rax),%rax
0x00000000004b02b2 <+274>: mov 0x78(%rsp),%rcx
0x00000000004b02b7 <+279>: mov 0xe8(%rsp),%rdx
0x00000000004b02bf <+287>: mov %rcx,0x8(%rsp)
0x00000000004b02c4 <+292>: mov %rdx,(%rsp)
0x00000000004b02c8 <+296>: callq *%rax
func (t *rtype) Method(i int) (m Method) {
if t.Kind() == Interface {
tt := (*interfaceType)(unsafe.Pointer(t))
return tt.Method(i)
}
methods := t.exportedMethods()
if i < 0 || i >= len(methods) {
panic("reflect: Method index out of range")
}
p := methods[i]
pname := t.nameOff(p.name)
m.Name = pname.name()
fl := flag(Func)
mtyp := t.typeOff(p.mtyp)
ft := (*funcType)(unsafe.Pointer(mtyp))
in := make([]Type, 0, 1+len(ft.in()))
in = append(in, t)
for _, arg := range ft.in() {
in = append(in, arg)
}
out := make([]Type, 0, len(ft.out()))
for _, ret := range ft.out() {
out = append(out, ret)
}
mt := FuncOf(in, out, ft.IsVariadic())
m.Type = mt
tfn := t.textOff(p.tfn)
fn := unsafe.Pointer(&tfn)
m.Func = Value{mt.(*rtype), fn, fl}
m.Index = i
return m
}
Method方法构建了一个Method结构体,其中方法名称、入参、出参等都不再分析。我们关注下函数地址的获取,即第27行。
textOff底层调用的是
func (t *_type) textOff(off textOff) unsafe.Pointer {
base := uintptr(unsafe.Pointer(t))
var md *moduledata
for next := &firstmoduledata; next != nil; next = next.next {
if base >= next.types && base < next.etypes {
md = next
break
}
}
if md == nil {
reflectOffsLock()
res := reflectOffs.m[int32(off)]
reflectOffsUnlock()
if res == nil {
println("runtime: textOff", hex(off), "base", hex(base), "not in ranges:")
for next := &firstmoduledata; next != nil; next = next.next {
println("\ttypes", hex(next.types), "etypes", hex(next.etypes))
}
throw("runtime: text offset base pointer out of range")
}
return res
}
res := uintptr(0)
// The text, or instruction stream is generated as one large buffer. The off (offset) for a method is
// its offset within this buffer. If the total text size gets too large, there can be issues on platforms like ppc64 if
// the target of calls are too far for the call instruction. To resolve the large text issue, the text is split
// into multiple text sections to allow the linker to generate long calls when necessary. When this happens, the vaddr
// for each text section is set to its offset within the text. Each method's offset is compared against the section
// vaddrs and sizes to determine the containing section. Then the section relative offset is added to the section's
// relocated baseaddr to compute the method addess.
if len(md.textsectmap) > 1 {
for i := range md.textsectmap {
sectaddr := md.textsectmap[i].vaddr
sectlen := md.textsectmap[i].length
if uintptr(off) >= sectaddr && uintptr(off) <= sectaddr+sectlen {
res = md.textsectmap[i].baseaddr + uintptr(off) - uintptr(md.textsectmap[i].vaddr)
break
}
}
} else {
// single text section
res = md.text + uintptr(off)
}
if res > md.etext {
println("runtime: textOff", hex(off), "out of range", hex(md.text), "-", hex(md.etext))
throw("runtime: text offset out of range")
}
return unsafe.Pointer(res)
}
我们又看到模块信息了,这在《Golang反射机制的实现分析——reflect.Type类型名称》一文中也介绍过。
通过rtype的地址确定哪个模块,然后查看模块的代码块信息。
第33行显示,如果该模块中的代码块多于1个,则通过偏移量查找其所处的代码块,然后通过虚拟地址的偏移差算出代码的真实地址。
如果代码块只有一个,则只要把模块中text字段表示的代码块起始地址加上偏移量即可。
在我们的例子中,只有一个代码块。所以使用下面的方式。
之前我们通过内存分析的偏移量tfn=0x0af180,而此模块记录的代码块起始地址是0x401000。则反汇编这块地址
(gdb) disassemble 0x401000+0x0af180
Dump of assembler code for function main.t20190107.F:
0x00000000004b0180 <+0>: movq $0x0,0x10(%rsp)
0x00000000004b0189 <+9>: mov 0x8(%rsp),%rax
0x00000000004b018e <+14>: mov %rax,0x10(%rsp)
0x00000000004b0193 <+19>: retq
如此我们便取到了函数地址。
rtype.MethodByName方法实现比较简单,它只是遍历并通过函数名匹配方法信息,然后返回
func (t *rtype) MethodByName(name string) (m Method, ok bool) {
if t.Kind() == Interface {
tt := (*interfaceType)(unsafe.Pointer(t))
return tt.MethodByName(name)
}
ut := t.uncommon()
if ut == nil {
return Method{}, false
}
utmethods := ut.methods()
for i := 0; i < int(ut.mcount); i++ {
p := utmethods[i]
pname := t.nameOff(p.name)
if pname.isExported() && pname.name() == name {
return t.Method(i), true
}
}
return Method{}, false
}
反射出来的函数使用Call方法调用。其底层就是调用上面确定的函数地址。
func (v Value) Call(in []Value) []Value {
v.mustBe(Func)
v.mustBeExported()
return v.call("Call", in)
}
func (v Value) call(op string, in []Value) []Value {
// Get function pointer, type.
……
if v.flag&flagMethod != 0 {
rcvr = v
rcvrtype, t, fn = methodReceiver(op, v, int(v.flag)>>flagMethodShift)
} else if v.flag&flagIndir != 0 {
fn = *(*unsafe.Pointer)(v.ptr)
} else {
fn = v.ptr
}
……
// Call.
call(frametype, fn, args, uint32(frametype.size), uint32(retOffset))
……
}
总结
- 通过rtype中的kind信息确定保存方法信息的偏移量。
- 相对于rtype起始地址,使用上面偏移量获取方法信息组。
- 通过方法信息中的偏移量和模块信息中记录的代码块起始地址,确定方法的地址。
- 通过反射调用方法比直接调用方法要复杂很多
我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=1umyv4sqscw34
有疑问加站长微信联系(非本文作者)