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

栏目: Go · 发布时间: 5年前

内容简介:这段代码,我们构建了一个实现了F()函数的结构体。然后使用反射机制,通过遍历和名称查找方式,找到函数并调用它。调用reflect.TypeOf之前的逻辑,我们已经在上节中讲解了。本文不再赘述。这段逻辑对应于上面go代码中的第19行for循环逻辑。

方法

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位置。

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

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字段表示的代码块起始地址加上偏移量即可。

在我们的例子中,只有一个代码块。所以使用下面的方式。

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

之前我们通过内存分析的偏移量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起始地址,使用上面偏移量获取方法信息组。
  • 通过方法信息中的偏移量和模块信息中记录的代码块起始地址,确定方法的地址。
  • 通过反射调用方法比直接调用方法要复杂很多

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

微信民族志、自媒体时代的知识生产与文化实践

微信民族志、自媒体时代的知识生产与文化实践

赵旭东 / 中国社会科学出版社 / 2017-9 / 98.00元

进入二十一世纪以来,随着网络技术的发展,自媒体的悄然登场深度影响着我们的日常生活。中国社会中自媒体通讯方式的普及以及随之而有的一种文化书写的新形式——微信民族志的出现使原有文化秩序中时空意义发生转变的同时,也在重新塑造着以研究异文化为己任的人类学学科自身的成长、转型与发展。在此种情境之下,由中国人民大学人类学研究所、中国人民大学国家发展与战略研究院、中国人民大学社会学理论与方法研究中心、《探索与争......一起来看看 《微信民族志、自媒体时代的知识生产与文化实践》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

随机密码生成器
随机密码生成器

多种字符组合密码

MD5 加密
MD5 加密

MD5 加密工具