内容简介:这段代码,我们构建了一个实现了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位置。
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起始地址,使用上面偏移量获取方法信息组。
- 通过方法信息中的偏移量和模块信息中记录的代码块起始地址,确定方法的地址。
- 通过反射调用方法比直接调用方法要复杂很多
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Go语言反射之反射调用
- Golang利用反射reflect动态调用方法
- .NET/C# 反射的的性能数据,以及高性能开发建议(反射获取 Attribute 和反射调用方法)
- Golang 通过反射的方式调用结构体方法
- Java 反射调用与面向对象结合使用产生的惊艳
- Go语言反射之类型反射
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
微信民族志、自媒体时代的知识生产与文化实践
赵旭东 / 中国社会科学出版社 / 2017-9 / 98.00元
进入二十一世纪以来,随着网络技术的发展,自媒体的悄然登场深度影响着我们的日常生活。中国社会中自媒体通讯方式的普及以及随之而有的一种文化书写的新形式——微信民族志的出现使原有文化秩序中时空意义发生转变的同时,也在重新塑造着以研究异文化为己任的人类学学科自身的成长、转型与发展。在此种情境之下,由中国人民大学人类学研究所、中国人民大学国家发展与战略研究院、中国人民大学社会学理论与方法研究中心、《探索与争......一起来看看 《微信民族志、自媒体时代的知识生产与文化实践》 这本书的介绍吧!