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反射机制的实现分析——ref
- 下一篇: bug诞生记——不定长参数隐藏的类型问题