Golang反射机制的实现分析——reflect.Type方法查找和调用

breaksoftware · · 854 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/breaksoftware/article/details/86068788

        在《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


有疑问加站长微信联系(非本文作者)

本文来自:CSDN博客

感谢作者:breaksoftware

查看原文:Golang反射机制的实现分析——reflect.Type方法查找和调用

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

854 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传