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

Posted breaksoftware

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang反射机制的实现分析——reflect.Type方法查找和调用相关的知识,希望对你有一定的参考价值。

        在《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 := t20190107678
	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.Valuereflect.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结构对应上就是methodnameOff=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 = Valuemt.(*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起始地址,使用上面偏移量获取方法信息组。
  • 通过方法信息中的偏移量和模块信息中记录的代码块起始地址,确定方法的地址。
  • 通过反射调用方法比直接调用方法要复杂很多

以上是关于Golang反射机制的实现分析——reflect.Type方法查找和调用的主要内容,如果未能解决你的问题,请参考以下文章

Golang反射机制的实现分析——reflect.Type类型名称

Golang反射机制的实现分析——reflect.Type类型名称

Golang的反射reflect深入理解和示例

Golang的反射reflect深入理解和示例

Golang的反射reflect深入理解和示例

Golang 反射机制 reflect.TypeOf reflect.ValueOf字符串处理