Golang反射机制的实现分析——reflect.Type方法查找和调用
在《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方法查找和调用相关推荐
- Golang反射机制的实现分析——reflect.Type类型名称
现在越来越多的java.php或者python程序员转向了Golang.其中一个比较重要的原因是,它和C/C++一样,可以编译成机器码运行,这保证了执行的效率.在上述解释型语言中,它们都支持了&quo ...
- Java 数据交换格式反射机制SpringIOC原理分析
数据交换格式&反射机制&SpringIOC原理分析 什么是数据交换格式? 数据交换格式使用场景 JSON简单使用 什么是JSON? JSON格式的分类 常用JSON解析框架 使用fas ...
- 依赖注入底层反射原理_PHP基于反射机制实现自动依赖注入的方法详解_php技巧...
这篇文章主要介绍了PHP基于反射机制实现自动依赖注入的方法,结合实例形式分析了php使用反射实现自动依赖注入的步骤.原理与相关操作技巧,本文实例讲述了PHP基于反射机制实现自动依赖注入的方法.分享给大 ...
- 【反射机制】Java中的反射机制,使用反射机制创建对象、访问属性、方法、构造方法等
这篇文章主要是整理了Java中的反射机制,包括:反射机制概念.反射机制访问构造方法.反射机制访问普通方法.反射机制访问属性,反射机制访问修饰符. 目录 一.反射机制概念 二.反射机制使用 (1)加载C ...
- java 获取类方法_Java之反射机制三:获取类的方法
一.实体类BigDog.java package reflex; public class BigDog extends Dog { private Integer age; public Strin ...
- Java学习之二-Java反射机制
问题: 在运行时,对一个JAVA类,能否知道属性和方法:能否调用它的任意方法? 答案是可以的,JAVA提供一种反射机制可以实现. 目录 什么是JAVA的反射机制 JDK中提供的Reflection A ...
- 粗浅看 java反射机制
Java 反射是 Java 被视为动态(或准动态)语言的一个关键性质.这个机制允许程序在运 行时透过 Reflection APIs 取得任何一个已知名称的class 的内部信息,包括其 modifi ...
- Java反射机制介绍
2019独角兽企业重金招聘Python工程师标准>>> 1. 文档概述 Java反射是Java被视为动态(或准动态)语言的一个关键性质,Java反射机制容许程序在运行时加载.探知.使 ...
- JAVA的反射机制==用反射分析类的实现
反射的概念:能够分析具体类的能力的程序称为反射 (JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法:这种动态获取的信息以及 ...
最新文章
- Java网络编程之简单UDP通信
- 中国博士开发可交互全球疫情地图,登上柳叶刀,GitHub已有4500星成为热榜第四...
- 分布式Session一致性概述
- 数据导出生成word附件使用POI的XWPFTemplate对象
- YBTOJ:单词频率(AC自动机)
- Sentinel流控规则持久化
- php二维数组 xml,xml 怎样通过php解析到二维数组里面
- 计算机师范类算师范教育类吗,师范教育类专业和计算机类专业,两者相比,哪个更适合自考生报读...
- 大寨鸿蒙系统的电器,华为传来两个好消息,鸿蒙OS大时代将于6月2日正式开启...
- 程序在发布前就应该发现的一些错误
- hihocoder第218周:AC自动机
- Android之改变控件的背景及形态
- AI 领域一大进展:“分布式”和“深度学习”真正深度融合
- Java面试题-多线程
- 在线文本字符串转十六进制工具
- 单片机与一般微型计算机相比具有哪些特点,单片机原理与接口技术习题答案
- Java将byte流转换成zip文件_java zip文件的压缩与解压
- python爬虫记录
- 基本面因子投资的三点思考
- 灰度、rgb之间的概念
热门文章
- 自然科学期刊能发表计算机论文吗,自然科学专业论文好发表吗?
- C++ 三五法则,看看你能不能理解
- postgresql Insert插入的几个报错
- POJ - 1986 Distance Queries 倍增求LCA
- Linux常用20条命令
- c语言arr什么意思6,初识C语言(六)
- 腐蚀rust电脑分辨率调多少_腐蚀Rust怎么设置画面 腐蚀Rust提高帧数画面设置方法...
- Learn OpenGL (四):纹理
- Windows DOS窗口查看历史执行过的命令的三种方式
- ATS插件开发中内存泄露问题的解决方法探讨