Golang反射机制的实现分析——reflect.Type类型名称
? ? ? ? 現在越來越多的java、php或者python程序員轉向了Golang。其中一個比較重要的原因是,它和C/C++一樣,可以編譯成機器碼運行,這保證了執行的效率。在上述解釋型語言中,它們都支持了“反射”機制,讓程序員可以很方便的構建一些動態邏輯。這是C/C++相對薄弱的環節,而Golang卻有良好的支持。本系列,我們將通過反匯編Golang的編譯結果,探究其反射實現的機制。(轉載請指明出于breaksoftware的csdn博客)
? ? ? ? 為了防止編譯器做優化,例子中的源碼都通過下面的指令編譯
go build -gcflags "-N -l" [xxxxxx].go
類型名稱
基本類型
package mainimport ("fmt""reflect"
)func main() {t := reflect.TypeOf(1)s := t.Name()fmt.Println(s)
}
? ? ? ? 這段代碼最終將打印出1的類型——int。
? ? ? ? main函數的入口地址是main.main。我們使用gdb在這個位置下斷點,然后反匯編。略去一部分函數準備工作,我們看到
0x0000000000487c6f <+31>: mov %rbp,0xa0(%rsp)0x0000000000487c77 <+39>: lea 0xa0(%rsp),%rbp0x0000000000487c7f <+47>: lea 0xfb5a(%rip),%rax # 0x4977e00x0000000000487c86 <+54>: mov %rax,(%rsp)0x0000000000487c8a <+58>: lea 0x40097(%rip),%rax # 0x4c7d28 <main.statictmp_0>0x0000000000487c91 <+65>: mov %rax,0x8(%rsp)0x0000000000487c96 <+70>: callq 0x46f210 <reflect.TypeOf>
? ? ? ? 第3~4行,這段代碼將地址0x4977e0壓棧。之后在5~6行,又將0x4c7d28壓棧。64位系統下,程序的壓棧不像32位系統使用push指令,而是使用mov指令間接操作rsp寄存器指向的棧空間。
? ? ? ? 第7行,調用了reflect.TypeOf方法,在Golang的源碼中,該方法的相關定義位于\src\reflect\type.go中
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {eface := *(*emptyInterface)(unsafe.Pointer(&i))return toType(eface.typ)
}// toType converts from a *rtype to a Type that can be returned
// to the client of package reflect. In gc, the only concern is that
// a nil *rtype must be replaced by a nil Type, but in gccgo this
// function takes care of ensuring that multiple *rtype for the same
// type are coalesced into a single Type.
func toType(t *rtype) Type {if t == nil {return nil}return t
}
? ? ? ??reflect.emptyInterface是一個保存數據類型信息和裸指針的結構體,它位于\src\reflect\value.go
// emptyInterface is the header for an interface{} value.
type emptyInterface struct {typ *rtypeword unsafe.Pointer
}
? ? ? ? 之前壓棧的兩個地址0x4977e0和0x4c7d28分別對應于type和word。
(gdb) x/16xb $rsp
0xc42003fed0: 0xe0 0x77 0x49 0x00 0x00 0x00 0x00 0x00
0xc42003fed8: 0x28 0x7d 0x4c 0x00 0x00 0x00 0x00 0x00
? ? ? ? 這樣在內存上便構成了一個emptyInterface結構。下面我們查看它們的內存,0x4c7d28保存的值0x01即是我們傳入reflect.TypeOf的值。
0x4977e0: 0x08 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x4c7d28 <main.statictmp_0>: 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
? ? ? ? reflect.rtype定義位于src\reflect\type.go
// rtype is the common implementation of most values.
// It is embedded in other struct types.
//
// rtype must be kept in sync with ../runtime/type.go:/^type._type.
type rtype struct {size uintptr……str nameOff // string formptrToThis typeOff // type for pointer to this type, may be zero
}
? ? ? ? 在reflect.TypeOf方法中,我們看到reflect.toType隱式的將reflect.rtype轉換成了reflect.Type類型,而reflect.Type類型和它完全不一樣
type Type interface {Align() intFieldAlign() intMethod(int) Method……
}
? ? ? ? 從Golang的源碼的角度去解析似乎進入了死胡同,我們繼續轉向匯編層面,查看reflect.TypeOf的實現
0x000000000046f210 <+0>: mov 0x8(%rsp),%rax0x000000000046f215 <+5>: test %rax,%rax0x000000000046f218 <+8>: je 0x46f22c <reflect.TypeOf+28>0x000000000046f21a <+10>: lea 0xaddbf(%rip),%rcx # 0x51cfe0 <go.itab.*reflect.rtype,reflect.Type>0x000000000046f221 <+17>: mov %rcx,0x18(%rsp)0x000000000046f226 <+22>: mov %rax,0x20(%rsp)0x000000000046f22b <+27>: retq 0x000000000046f22c <+28>: xor %eax,%eax0x000000000046f22e <+30>: mov %rax,%rcx0x000000000046f231 <+33>: jmp 0x46f221 <reflect.TypeOf+17>
? ? ? ? 之前介紹過,在調用reflect.TypeOf前,已經在棧上構建了一個emptyInterface結構體。由于此函數只關注類型,而不關注值,所以此時只是使用了typ字段——rsp+0x08地址的值。
? ? ? ? 比較有意思的是這個過程獲取了一個內存地址0x51cfe0,目前我們尚不知它是干什么的。之后我們會再次關注它。
0x0000000000487c9b <+75>: mov 0x10(%rsp),%rax0x0000000000487ca0 <+80>: mov 0x18(%rsp),%rcx0x0000000000487ca5 <+85>: mov %rax,0x38(%rsp)0x0000000000487caa <+90>: mov %rcx,0x40(%rsp)0x0000000000487caf <+95>: mov 0xc0(%rax),%rax0x0000000000487cb6 <+102>: mov %rcx,(%rsp)0x0000000000487cba <+106>: callq *%rax
? ? ? ? 從reflect.TypeOf調用中返回后,rax寄存器保存的是0x51cfe0,然后在第5行計算了該地址偏移0xC0的地址中保存的值。最后在第7行調用了該地址所指向的函數。
(gdb) x/64bx 0x51cfe0+0xc0
0x51d0a0 <go.itab.*reflect.rtype,reflect.Type+192>: 0x80 0xcc 0x46 0x00 0x00 0x00 0x00 0x00
0x51d0a8 <go.itab.*reflect.rtype,reflect.Type+200>: 0xf0 0xd6 0x46 0x00 0x00 0x00 0x00 0x00
0x51d0b0 <go.itab.*reflect.rtype,reflect.Type+208>: 0x60 0xd7 0x46 0x00 0x00 0x00 0x00 0x00
0x51d0b8 <go.itab.*reflect.rtype,reflect.Type+216>: 0xe0 0xbe 0x46 0x00 0x00 0x00 0x00 0x00
0x51d0c0 <go.itab.*reflect.rtype,reflect.Type+224>: 0xd0 0xd7 0x46 0x00 0x00 0x00 0x00 0x00
0x51d0c8 <go.itab.*reflect.rtype,reflect.Type+232>: 0x80 0xd8 0x46 0x00 0x00 0x00 0x00 0x00
0x51d0d0 <go.itab.*reflect.rtype,reflect.Type+240>: 0x90 0xcb 0x46 0x00 0x00 0x00 0x00 0x00
0x51d0d8 <go.itab.*reflect.rtype,reflect.Type+248>: 0x60 0xb9 0x46 0x00 0x00 0x00 0x00 0x00
? ? ? ? 使用反匯編指令看下0x46cc80處的函數,可以看到它是reflect.(*rtype).Name()
(gdb) disassemble 0x46cc80
Dump of assembler code for function reflect.(*rtype).Name:
? ? ? ? 我們再看0x51d0a0附近的內存中的值,發現其很有規律。其實它們都是reflect.(*rtype)下的函數地址。
(gdb) disassemble 0x46b960
Dump of assembler code for function reflect.(*rtype).Size:(gdb) disassemble 0x46cb90
Dump of assembler code for function reflect.(*rtype).PkgPath:
? ? ? ? 這些方法也是reflect.Type接口暴露的方法。當我們調用Type暴露的方法的時候,實際底層調用的rtype對應的同名方法。
type Type interface {Align() intFieldAlign() int……Name() stringPkgPath() stringSize() uintptr……
}
? ? ? ? 從reflect.TypeOf調用返回后,就調用reflect.(*rtype).Name()。它的相關實現是
func (t *rtype) Name() string {if t.tflag&tflagNamed == 0 {return ""}s := t.String()……return s[i+1:]
}func (t *rtype) String() string {s := t.nameOff(t.str).name()if t.tflag&tflagExtraStar != 0 {return s[1:]}return s
}type name struct {bytes *byte
}func (t *rtype) nameOff(off nameOff) name {return name{(*byte)(resolveNameOff(unsafe.Pointer(t), int32(off)))}
}
? ? ? ? 這段代碼表示,變量的類型值和rtype的地址和rtype.str字段有關。而這個rtype就是reflect.TypeOf調用前構建的emptyInterface的rtype。我們使用gdb查看該結構體
$4 = {size = 0x8, ptrdata = 0x0, hash = 0xf75371fa, tflag = 0x7, align = 0x8, fieldAlign = 0x8, kind = 0x82, alg = 0x529a70, gcdata = 0x4c6cd8, str = 0x3a3, ptrToThis = 0xac60
}
? ? ? ? 最后我們就要看相對復雜的resolveNameOff實現。
func resolveNameOff(ptrInModule unsafe.Pointer, off nameOff) name {if off == 0 {return name{}}base := uintptr(ptrInModule)for md := &firstmoduledata; md != nil; md = md.next {if base >= md.types && base < md.etypes {res := md.types + uintptr(off)if res > md.etypes {println("runtime: nameOff", hex(off), "out of range", hex(md.types), "-", hex(md.etypes))throw("runtime: name offset out of range")}return name{(*byte)(unsafe.Pointer(res))}}}// No module found. see if it is a run time name.reflectOffsLock()res, found := reflectOffs.m[int32(off)]reflectOffsUnlock()if !found {println("runtime: nameOff", 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: name offset base pointer out of range")}return name{(*byte)(res)}
}
? ? ? ? 我們先忽略17行之后的代碼。從6~15行,程序會遍歷模塊信息,并檢測rtype地址是否在該區間之內(base >= md.types && base < md.etypes)。如果在此區間,則返回相對于該區間起始地址的off偏移地址。
? ? ? ? 所以,rtype.str字段的偏移不是相對于rtype的起始地址。而是相對于rtype起始地址所在的區間的保存type信息區塊([md.types, md.etypes))起始地址。
? ? ? ? 和rtype信息一樣,firstmoduledata的信息也是全局初始化的。我們使用IDA協助查看它位置。
? ? ? ? 可以看到這些數據都存儲在elf的.noptrdata節中,該節中數據是Golang構建程序時保存全局數據的地方。所以這種“反射”是編譯器在編譯的過程中,暗中幫我們構建了和變量等有關的信息。
? ? ? ? 我們再看下模塊起始地址0x488000偏移rtype.str=0x3a3的地址空間。
? ? ? ? 這樣我們就看到int字段的來源了。
自定義結構類型
package mainimport ("fmt""reflect"
)type t20190107 struct {v string
}func main() {i2 := t20190107{"s20190107"}t2 := reflect.TypeOf(i2)s2 := t2.Name()fmt.Println(s2)
}
? ? ? ? 這段代碼故意構建一個名字很特殊的結構體,我們看下反匯編的結果。
0x0000000000487c6f <+31>: mov %rbp,0xc0(%rsp)0x0000000000487c77 <+39>: lea 0xc0(%rsp),%rbp0x0000000000487c7f <+47>: movq $0x0,0x58(%rsp)0x0000000000487c88 <+56>: movq $0x0,0x60(%rsp)0x0000000000487c91 <+65>: lea 0x2f868(%rip),%rax # 0x4b75000x0000000000487c98 <+72>: mov %rax,0x58(%rsp)0x0000000000487c9d <+77>: movq $0x9,0x60(%rsp)0x0000000000487ca6 <+86>: mov %rax,0x98(%rsp)0x0000000000487cae <+94>: movq $0x9,0xa0(%rsp)0x0000000000487cba <+106>: lea 0x196ff(%rip),%rax # 0x4a13c00x0000000000487cc1 <+113>: mov %rax,(%rsp)0x0000000000487cc5 <+117>: lea 0x98(%rsp),%rax0x0000000000487ccd <+125>: mov %rax,0x8(%rsp)0x0000000000487cd2 <+130>: callq 0x40c7e0 <runtime.convT2E>
? ? ? ? 第5行,我們獲取了0x4b7500空間地址,我們看下其值,就是我們初始化結構體的字面量“s20190107"
? ? ? ? 第10行,我們又獲取了0x4a13c0地址。依據之前的經驗,該地址保存的是reflect.rtype類型數據。但是由于之后調用的runtime.convT2E,所以其類型是runtime._type。
func convT2E(t *_type, elem unsafe.Pointer) (e eface) {if raceenabled {raceReadObjectPC(t, elem, getcallerpc(unsafe.Pointer(&t)), funcPC(convT2E))}if msanenabled {msanread(elem, t.size)}x := mallocgc(t.size, t, true)// TODO: We allocate a zeroed object only to overwrite it with actual data.// Figure out how to avoid zeroing. Also below in convT2Eslice, convT2I, convT2Islice.typedmemmove(t, x, elem)e._type = te.data = xreturn
}
? ? ? ? 其實runtime._type和reflect.rtype的定義是一樣的
type _type struct {size uintptr……str nameOffptrToThis typeOff
}type rtype struct {size uintptr…… str nameOff // string formptrToThis typeOff // type for pointer to this type, may be zero
}
? ? ? ? 而reflect.emptyInterface和runtime.eface也一樣
type eface struct {_type *_typedata unsafe.Pointer
}type emptyInterface struct {typ *rtypeword unsafe.Pointer
}
? ? ? ? 這讓我們對基本類型的分析結果和經驗在此處依然適用。
? ? ? ? 使用gdb把_type信息打印出來,可以發現這次類型名稱的偏移量0x6184比較大。
$3 = {size = 0x10, ptrdata = 0x8, hash = 0xe1c71878, tflag = 0x7, align = 0x8, fieldalign = 0x8, kind = 0x19, alg = 0x529a90, gcdata = 0x4c6dc4, str = 0x6184, ptrToThis = 0xae80
}
? ? ? ??runtime.convT2E第8行在垃圾回收器上構建了一段內存,并將裸指針指向的數據保存到該地址空間中。然后在第12~13行重新構建了eface結構體。
? ? ? ? 之后進入reflect.TypeOf邏輯,這和之前分析的流程一致。我們最后看下保存的類型數據的全局區域
總結
- 編譯器在編譯過程中,將變量對應的類型信息(runtime._type或reflect.rtype)保存在.rodata節中。
- 字面量直接使用reflect.TypeOf方法獲取rtype類型函數地址列表
- 變量使用runtime.convT2*類型轉換函數,使用垃圾回收器上分配的空間存儲變量值,然后調用reflect.TypeOf方法
- 遍歷保存在.noptrdata節中的模塊信息,確認類型信息的存儲地址位于的模塊區域。然后以該區塊中保存type信息的區塊起始地址為基準,使用rtype.str字段表示的偏移量計算出名稱在內存中的位置。
?
總結
以上是生活随笔為你收集整理的Golang反射机制的实现分析——reflect.Type类型名称的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++拾取——Linux下实测布隆过滤器
- 下一篇: Golang反射机制的实现分析——ref