在《Golang反射机制的实现分析——reflect.Type类型名称》一文中,我们分析了Golang获取类型基本信息的流程。本文将基于上述知识和经验,分析方法的查找和调用。(转载请指明出于breaksoftware的csdn博客)

方法

package mainimport ("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),%rax0x00000000004b0230 <+144>:   mov    0x10(%rsp),%rcx……0x00000000004b026a <+202>:   mov    0xe0(%rsp),%rax0x00000000004b0272 <+210>:   mov    0xd8(%rax),%rax0x00000000004b0279 <+217>:   mov    0xe8(%rsp),%rcx0x00000000004b0281 <+225>:   mov    %rcx,(%rsp)0x00000000004b0285 <+229>:   callq  *%rax0x00000000004b0287 <+231>:   mov    0x8(%rsp),%rax0x00000000004b028c <+236>:   mov    %rax,0x90(%rsp)0x00000000004b0294 <+244>:   mov    0x78(%rsp),%rcx0x00000000004b0299 <+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][]methodfunc (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)).ucase Ptr:……}
}

这段逻辑,我们只要看下汇编将该地址如何转换的

   0x000000000048d4df <+143>:   cmp    $0x19,%rcx0x000000000048d4e3 <+147>:   jne    0x48d481 <reflect.(*rtype).uncommon+49>0x000000000048d4e5 <+149>:   add    $0x50,%rax0x000000000048d4e9 <+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, stringmcount  uint16  // number of methods_       uint16  // unusedmoff    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 methodmtyp 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),%rax0x00000000004b02ab <+267>:   mov    0xb0(%rax),%rax0x00000000004b02b2 <+274>:   mov    0x78(%rsp),%rcx0x00000000004b02b7 <+279>:   mov    0xe8(%rsp),%rdx0x00000000004b02bf <+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 = mttfn := t.textOff(p.tfn)fn := unsafe.Pointer(&tfn)m.Func = Value{mt.(*rtype), fn, fl}m.Index = ireturn m
}

Method方法构建了一个Method结构体,其中方法名称、入参、出参等都不再分析。我们关注下函数地址的获取,即第27行。

textOff底层调用的是

func (t *_type) textOff(off textOff) unsafe.Pointer {base := uintptr(unsafe.Pointer(t))var md *moduledatafor next := &firstmoduledata; next != nil; next = next.next {if base >= next.types && base < next.etypes {md = nextbreak}}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].vaddrsectlen := md.textsectmap[i].lengthif uintptr(off) >= sectaddr && uintptr(off) <= sectaddr+sectlen {res = md.textsectmap[i].baseaddr + uintptr(off) - uintptr(md.textsectmap[i].vaddr)break}}} else {// single text sectionres = 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),%rax0x00000000004b018e <+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 = vrcvrtype, 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方法查找和调用相关推荐

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

    现在越来越多的java.php或者python程序员转向了Golang.其中一个比较重要的原因是,它和C/C++一样,可以编译成机器码运行,这保证了执行的效率.在上述解释型语言中,它们都支持了&quo ...

  2. Java 数据交换格式反射机制SpringIOC原理分析

    数据交换格式&反射机制&SpringIOC原理分析 什么是数据交换格式? 数据交换格式使用场景 JSON简单使用 什么是JSON? JSON格式的分类 常用JSON解析框架 使用fas ...

  3. 依赖注入底层反射原理_PHP基于反射机制实现自动依赖注入的方法详解_php技巧...

    这篇文章主要介绍了PHP基于反射机制实现自动依赖注入的方法,结合实例形式分析了php使用反射实现自动依赖注入的步骤.原理与相关操作技巧,本文实例讲述了PHP基于反射机制实现自动依赖注入的方法.分享给大 ...

  4. 【反射机制】Java中的反射机制,使用反射机制创建对象、访问属性、方法、构造方法等

    这篇文章主要是整理了Java中的反射机制,包括:反射机制概念.反射机制访问构造方法.反射机制访问普通方法.反射机制访问属性,反射机制访问修饰符. 目录 一.反射机制概念 二.反射机制使用 (1)加载C ...

  5. java 获取类方法_Java之反射机制三:获取类的方法

    一.实体类BigDog.java package reflex; public class BigDog extends Dog { private Integer age; public Strin ...

  6. Java学习之二-Java反射机制

    问题: 在运行时,对一个JAVA类,能否知道属性和方法:能否调用它的任意方法? 答案是可以的,JAVA提供一种反射机制可以实现. 目录 什么是JAVA的反射机制 JDK中提供的Reflection A ...

  7. 粗浅看 java反射机制

    Java 反射是 Java 被视为动态(或准动态)语言的一个关键性质.这个机制允许程序在运 行时透过 Reflection APIs 取得任何一个已知名称的class 的内部信息,包括其 modifi ...

  8. Java反射机制介绍

    2019独角兽企业重金招聘Python工程师标准>>> 1. 文档概述 Java反射是Java被视为动态(或准动态)语言的一个关键性质,Java反射机制容许程序在运行时加载.探知.使 ...

  9. JAVA的反射机制==用反射分析类的实现

    反射的概念:能够分析具体类的能力的程序称为反射 (JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法:这种动态获取的信息以及 ...

最新文章

  1. Java网络编程之简单UDP通信
  2. 中国博士开发可交互全球疫情地图,登上柳叶刀,GitHub已有4500星成为热榜第四...
  3. 分布式Session一致性概述
  4. 数据导出生成word附件使用POI的XWPFTemplate对象
  5. YBTOJ:单词频率(AC自动机)
  6. Sentinel流控规则持久化
  7. php二维数组 xml,xml 怎样通过php解析到二维数组里面
  8. 计算机师范类算师范教育类吗,师范教育类专业和计算机类专业,两者相比,哪个更适合自考生报读...
  9. 大寨鸿蒙系统的电器,华为传来两个好消息,鸿蒙OS大时代将于6月2日正式开启...
  10. 程序在发布前就应该发现的一些错误
  11. hihocoder第218周:AC自动机
  12. Android之改变控件的背景及形态
  13. AI 领域一大进展:“分布式”和“深度学习”真正深度融合
  14. Java面试题-多线程
  15. 在线文本字符串转十六进制工具
  16. 单片机与一般微型计算机相比具有哪些特点,单片机原理与接口技术习题答案
  17. Java将byte流转换成zip文件_java zip文件的压缩与解压
  18. python爬虫记录
  19. 基本面因子投资的三点思考
  20. 灰度、rgb之间的概念

热门文章

  1. 自然科学期刊能发表计算机论文吗,自然科学专业论文好发表吗?
  2. C++ 三五法则,看看你能不能理解
  3. postgresql Insert插入的几个报错
  4. POJ - 1986 Distance Queries 倍增求LCA
  5. Linux常用20条命令
  6. c语言arr什么意思6,初识C语言(六)
  7. 腐蚀rust电脑分辨率调多少_腐蚀Rust怎么设置画面 腐蚀Rust提高帧数画面设置方法...
  8. Learn OpenGL (四):纹理
  9. Windows DOS窗口查看历史执行过的命令的三种方式
  10. ATS插件开发中内存泄露问题的解决方法探讨