内容简介:这段代码,我们构建了一个实现了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语言反射之类型反射
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
网站开发案例课堂:HTML5+CSS3+JavaScript网页设计案例课堂
刘玉红 / 2015-1-1 / 68
《网站开发案例课堂:HTML5+CSS3+JavaScript网页设计案例课堂》作者根据在长期教学中积累的网页设计教学经验,完整、详尽地介绍HTML 5 + CSS 3 + JavaScript网页设计技术。 《网站开发案例课堂:HTML5+CSS3+JavaScript网页设计案例课堂》共分24章,分别介绍HTML 5概述、HTML 5网页文档结构、HTML 5网页中的文本和图像、HTML......一起来看看 《网站开发案例课堂:HTML5+CSS3+JavaScript网页设计案例课堂》 这本书的介绍吧!