1. 什么是反射?
In computer science, reflection programming is the ability of a process to examine, introspect, and modify its own structure and behavior.[7]
通俗上说,反射就是在语言提供的程序跑的时候,查询对象的类型和数据结构的能力。通过这种能力,可以在运行阶段自举,在运行阶段实现更动态的功能。最典型的就是Python的元编程。
反射标定的是对象的数据结构以及类型,一般上是和对象模型强相关,Python的对象的内存分布:
- class:类型对象。
- dict:类对应的成员变量。
- 其他Python对象头。
go是一种静态类型的语言,拥有现代语言的GC,现代语言强大的反射能力。
笔者在学习和使用的过程中常常在思考,go作为一种静态语言,究竟是如何承实现运行时的类型系统的?
go的反射能力都是通过reflect
包向外暴露的,要理解go的反射能力,必须先深入了解reflect
的能力。reflect
提供了两种类型来获取变量的类型和值:
-
reflect.ValueOf
:获取输入对象的值对象reflect.Value
(结构体类型)。 -
reflect.TypeOf
:获取输入对象的类型对象reflect.Type
(接口类型)。
var r float64 = 9.336
fmt.Println("type: ", reflect.TypeOf(r)) // type: float64
fmt.Println("value: ", reflect.ValueOf(r)) //value: 9.336
上述两种获取对象值的方法,它们的参数都是interface{}
。笔者在初次接触到时,曾有这样的疑问,为什么所有数据都能够转换为interface{}
?为什么interface{}
能够提取类型对象和数值对象?
在下一节,笔者将揭开这个面纱……
2. interface:多态魔法
go的类型系统非常丰富,除了丰富的基本类型(int
、byte
、string
等),还有自定义的interface
类型和自定义的struct
类型。
go的自定义struct
类型和C语言的结构体类型相比,除了相近的内存排布方式和访问方式,增加了类C++方法定义能力,和类似限定符的隐藏能力和类似继承的embed
。
interface
类型则是go的一大特色,是go语言实现多态的核心特性。Russ Cox曾经说过[1]:
Go's interfaces—static, checked at compile time, dynamic when asked for—are, for me, the most exciting part of Go from a language design point of view. If I could export one feature of Go into other languages, it would be interfaces.
interface
允许开发人员像使用Python那样的动态语言一样使用duck typing,但仍然让编译器捕捉到明显的错误。
2.1. 接口在go中的应用
go的interface
一般有两种用法:
- 使用
interface{}
声明变量类型:interface{}
类型类似于Java的Object和Python2的Object,可以存储任何对象,并在随后转换为其他类型对象。 - 使用
interface
作为关键字定义接口类型:接口类型限定一组行为(如代码段2),此类型接口只能存储实现这一组行为的对象。
type TestA interface{
A() string
}
interface
的魔力隐藏于其数据模型之中(见代码段3):
……
// runtime/runtime2.go
// 空接口:例如 var i interface{}
type eface struct {
_type *_type // 类型信息
data unsafe.Pointer //数据信息,指向数据指针
}
// 非空接口:例如 var i ReaderWriter
type iface struct {
tab *itab
data unsafe.Pointer //指向原始数据指针
}
- 空接口(
eface
):对应interface{}
存储的对象,是一个(类型对象指针, 数据对象指针)
的二元模型。 - 非空接口(
iface
):对应interface
定义的特定接口类型,是一个(接口方法表指针, 数据对象指针)
的二元模型。
从上面的描述,任意接口对象对象(空接口或者非空接口),其内存模型都是一个包含两个指针的数据结构,一个类型相关,一个数据相关。因此,reflect
包可以通过interface{}
存储任意对象,go语言可以使用interface
实现多态。
Russ Cox在博客[1]中给出了一个样例,笔者总结后得到图1:
type Stringer interface{
A() int
}
type Binary struct{
a int
}
func(b*Binary) A() int{
return b.a
}
……
b := Binary{a: 123}
s := Stringer(&b) // 计算itable
i := interface{}(&b)
fmt.Println(s.A())
fmt.Println(i.(Stringer).A())
// 输出
// 123
// 123
……
2.2. 特定类型的接口
在上文中,笔者已经说明了interface{}
的二元结构,从而说明了为什么interface{}
变量可以存储任意类型并读取其类型对象和数据对象。
笔者下面将详细说明特定接口类型相关的itable
(见代码段5),这个特定是go多态的基础,也是go相对于主流编程语言多态实现的一种创新性做法,编程语言实现多态,一般有以下两种做法:
- 编译阶段静态计算所有的方法映射表
itable
((类型,方法名)->函数
),并将所有调用点在编译阶段替换为obj.itable[xxx]()
,代表语言为C++和Java。 - 运行阶段动态查找对象是否包含对应方法(
(对象,方法名)->(类型,方法名)->函数
),并返回对应方法对象(可以必要的时候缓存调用关系以进行优化,提高执行效率),代表语言:JavaScript和Python。
go的多态则借鉴了两种方式,它既有itable
,但也有运行态方法的动态计算,具体做法如下如下:
- 编译阶段,为每个内置类型和开发人员定义的类型,构建对应的类型对象,对应源码中的
_type
。 - 编译阶段,为每个类型对象(
struct
定义)中构建一个方法列表,存储类型所实现的所有方法指针。 - 编译阶段,为每个开发人员定义的
接口类型
,构建对应的接口类型对象
,接口类型对象
包含一个方法列表,这些方法是实现接口必须实现的方法。 - 执行阶段,每当开发者通过接口对象使用方法时,go会查找此时
(接口,类型)
对应的itable
:- 如果
itable
已经生成,则直接读取go的缓存并使用。 - 如果
itable
未生成,根据接口和类型各自的方法列表,计算itable
,go会缓存结果并返回给调用方。(通过对类型方法列表进行排序(编译期),这其实是一个O(ni + nt)
,见代码段5)
- 如果
……
// runtime/type.go
type nameOff int32
type typeOff int32
type imethod struct {
name nameOff // 方法名
ityp typeOff // 描述方法参数返回值等细节
}
// 接口类型对应的实际类型
type interfacetype struct {
typ _type // 类型
pkgpath name // 报名
mhdr []imethod // 方法列表,是func 的声明抽象,而 itab 中的 fun 字段才是存储 func 的真实切片。
}
……
// runtime/runtime2.go
type itab struct {
inter *interfacetype // 接口对应type
_type *_type // 结构体对应type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
// runtime/iface.go
……
const itabInitSize = 512 // 默认itab容器大小
var (
itabLock mutex // 全局锁
itabTable = &itabTableInit // itable容器
itabTableInit = itabTableType{size: itabInitSize} // 起始itable容器,go语言运行时容器
)
// itable容器类型
type itabTableType struct {
size uintptr // length of entries array. Always a power of 2.
count uintptr // current number of filled entries.
entries [itabInitSize]*itab // really [size] large
}
……
// 动态计算itab
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
if len(inter.mhdr) == 0 {
throw("internal error - misuse of itab")
}
……
var m *itab
// 这里采用了一种全量复制的手段,来尽可能不影响已经存在的业务
// 1. 原子获取当前itable容器地址,在原容器中查找,未找到。这只是为了优化访问效率。
t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
if m = t.find(inter, typ); m != nil {
goto finish
}
// 2. 锁上(等待插入,等待容器地址替换(扩容导致))
lock(&itabLock)
if m = itabTable.find(inter, typ); m != nil {
unlock(&itabLock)
goto finish
}
// 3. 在抢到更改itable之后,如果数据还不存在,则开始新建itable
m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
m.inter = inter
m._type = typ
m.hash = 0
m.init() // 初始化
itabAdd(m) // 加入到itable中,散列冲突后,直接往后移动一位
unlock(&itabLock)
finish:
if m.fun[0] != 0 {
return m
}
if canfail {
return nil
}
panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
}
综合上述的计算代码和内存结构,可以得到发开人员自定义的接口内存模型如下:
3. 反射:类型的潘多拉魔盒
在本节中,笔者将详细介绍go提供给开发人员的工具包reflect
。在第一节中,笔者已经介绍了reflect
包中几个核心的概念,下面将进行逐一说明。
3.1. reflect.Type
接口
reflect
返回的运行时类型对象Type
是一个接口类型,规范了反射获得的运行时类型对象的所有行为。
笔者认为,任何在运行时动态获取类型对象都会有一定的风险,开发人员任意不留神的改动可能会引起巨大的bug(甚至导致程序crush)。因此,将类型对象封装成一个接口对象返回,是go提供的一层安全协议(类比Linux内核态)。通过这个协议屏蔽类型对象的数据结构访问权限,进而限定开发者在一个安全的环境下使用go提供的一个反射能力。
在介绍reflect.Type
的实现前,笔者必须在这里科普一个小的C语言特性(见代码段6),C语言的任意变量都可以对应到一块内存,一个内存地址。开发者可以将这块地址解释为任何的类型(最好保持转换前后数据模型一致),这个特性常常被用来定义一类相似内存模型的数据模型。
struct B{
int type;
};
struct BA{
int type; // 为1
char a[8];
};
struct BB{
int type; // 为2
int b;
};
void *p = struct BB();
B* pb = (B*)p;
if(pb.type == 1){
((*BA)(pb)).a();
}else{
((*BB)pb).b();
}
go语言被认为是21世纪的C语言,原因在于go的很多的特性源自于C语言,代码段6所展示的特性正是它实现反射没能力的重要一环。
在reflect
中,go定义了结构体类型rtype
,符合reflect.Type
接口协议。
深入go的源码,笔者发现,go的运行时定义了结构体类型_type
,这个类型是运行时,go实际存储的各个类型的类型对象实例。在源码注释中可以发现,rtype
必须要和_type
保持内存模型(内存排布)上的一致(见代码段7)。
go为什么要在reflect
中再定义一个rtype
?
- go在语言层面提供了一个
export
机制,必须首字母大写的结构体或者结构体属性才能在包外访问。 - go必须保持
_type
的独立性,因此必须使用代码段6中的技术,以内存镜像映射的方式映射到rtype
。新增reflect.Type
所需要的行为,约束开发人员可以操作类型对象的界限。
_type
可以认为是所有类型的基类,采用了go的embeded技术嵌入在其他类型中。rtype
自然也映射了这个特性,笔者读reflect
源码时发现,rtype
embeded了几乎所有类型对象(不确定是否所有,毕竟没逐条过)。笔者在这里节选了代码段7介绍下这些特征。
……
// reflect/type.go
const (
Invalid Kind = iota
Bool
Int
Int8
Int16
Int32
Int64
Uint
Uint8
Uint16
Uint32
Uint64
Uintptr
Float32
Float64
Complex64
Complex128
Array // 数组类型
Chan // 通道对象
Func // 函数对象
Interface // 类型是interface定义的接口
Map // Map类型
Ptr // 指针类型
Slice // 切片类型
String // 字符串类型
Struct // 结构体
UnsafePointer // unsafepointer
)
// rtype相当于reflect返回的大多数类型对象的基类,rtypeh必须和 runtime包内的_type保持一致,相当于暴露出来的反射类型对象。
type rtype struct {
size uintptr
ptrdata uintptr // number of bytes in the type that can contain pointers
hash uint32 // hash of type; avoids computation in hash tables
tflag tflag // extra type information flags
align uint8 // alignment of variable with this type
fieldAlign uint8 // alignment of struct field with this type
kind uint8 // enumeration for C
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
gcdata *byte // garbage collection data
str nameOff // string form
ptrToThis typeOff // type for pointer to this type, may be zero
}
// 根据反射的Kind获取其内容的Type
func (t *rtype) Elem() Type {
switch t.Kind() {
case Array:
tt := (*arrayType)(unsafe.Pointer(t))
return toType(tt.elem)
case Chan:
tt := (*chanType)(unsafe.Pointer(t))
return toType(tt.elem)
case Map:
tt := (*mapType)(unsafe.Pointer(t))
return toType(tt.elem)
case Ptr:
tt := (*ptrType)(unsafe.Pointer(t))
return toType(tt.elem)
case Slice:
tt := (*sliceType)(unsafe.Pointer(t))
return toType(tt.elem)
}
panic("reflect: Elem of invalid type " + t.String())
}
func (t *rtype) Field(i int) StructField {
if t.Kind() != Struct {
panic("reflect: Field of non-struct type " + t.String())
}
tt := (*structType)(unsafe.Pointer(t))
return tt.Field(i)
}
……
func TypeOf(i interface{}) Type {
eface := *(*emptyInterface)(unsafe.Pointer(&i))
return toType(eface.typ)
}
func toType(t *rtype) Type {
if t == nil {
return nil
}
return t
}
……
3.2. reflect.Value
结构类型
3.1节中笔者详细介绍了reflect
在运行时获取类型对象(reflect.Type
)的原理和方法。在本节,笔者将介绍第一节中提到过的go的值对象reflect.Value
。
reflect
提供了Value
作为对象值的用户态反射数据。Value
不仅仅只是对数据内容的简单返回,它将对象的值的类型对象、数据内容和数据标记封装后返回给调用者(如代码段8):
……
// reflect/value.go
func ValueOf(i interface{}) Value {
if i == nil {
return Value{}
}
escapes(i)
return unpackEface(i)
}
……
// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
typ *rtype
word unsafe.Pointer
}
// nonEmptyInterface is the header for an interface value with methods.
type nonEmptyInterface struct {
// see ../runtime/iface.go:/Itab
itab *struct {
ityp *rtype // static interface type
typ *rtype // dynamic concrete type
hash uint32 // copy of typ.hash
_ [4]byte
fun [100000]unsafe.Pointer // method table
}
word unsafe.Pointer
}
……
type Value struct {
typ *rtype
ptr unsafe.Pointer
flag
}
// 提取Value的内容
func (v Value) Elem() Value {
k := v.kind()
switch k {
case Interface:
var eface interface{}
if v.typ.NumMethod() == 0 {
eface = *(*interface{})(v.ptr)
} else {
eface = (interface{})(*(*interface {
M()
})(v.ptr))
}
x := unpackEface(eface)
if x.flag != 0 {
x.flag |= v.flag.ro()
}
return x
case Ptr:
ptr := v.ptr
if v.flag&flagIndir != 0 {
ptr = *(*unsafe.Pointer)(ptr)
}
// The returned value's address is v's value.
if ptr == nil {
return Value{}
}
tt := (*ptrType)(unsafe.Pointer(v.typ))
typ := tt.elem
fl := v.flag&flagRO | flagIndir | flagAddr
fl |= flag(typ.Kind())
return Value{typ, ptr, fl}
}
panic(&ValueError{"reflect.Value.Elem", v.kind()})
}
// unpackEface converts the empty interface i to a Value.
func unpackEface(i interface{}) Value {
e := (*emptyInterface)(unsafe.Pointer(&i))
// NOTE: don't read e.word until we know whether it is really a pointer or not.
t := e.typ
if t == nil {
return Value{}
}
f := flag(t.Kind())
if ifaceIndir(t) {
f |= flagIndir
}
return Value{t, e.word, f}
}
……
代码段8中截取的代码非常清晰,读者可以很明确的知道reflect.Value
是如何从interface{}
的二元数据模型获取值对象的类型和数据,从对象类型中提取flag,并打包返回。
其中唯一需要读者注意的是go中的地址转换相关技术,对应了在代码段8中出现了了两个新的变量类型:unsafe.Pointer
和uintptr
。这个技术将在下一节中详细说明。
3.3. 地址转换技术
go语言*有三大类指针(见代码段9):
-
*T
:普通类型指针类型。用于传递对象地址,操作对象内容,不能进行指针运算,引用对象可以被go识别(不会被GC)。 -
unsafe.poniter
:通用指针类型。可以被转换为不同类型的指针,被转换为*T
才能操作对象,不能进行指针运算,引用对象会被go识别(不会被GC)。 -
uintptr
:指针地址的数值。可以用于指针运算,引用对象不被go识别(指向对象无法使其不被GC回收)。
众所周知,go是强类型语言,且包含了一个健壮的runtime。go的编译器保证了指针、interface
之间的转换必须满足类型约束,满足里氏替换原则。然而这种强烈的约束会导致在某些情况下程序非常死板,性能差劲,代码垃圾,甚至就完全无法做到一些事情(例如reflect
包)。
go在这时候提供了unsafe.poniter
,作为一种折中方案。它提供了灵活性,同时也保证使用者明确知道使用unsafe.poniter
的地方是需要注意的,可能存在潜在不安全性。在这之上,go提供了近于C语言的内存操作手段,它就是uintptr
,它仅仅代表一块内存的地址,一个数字,开发者可以随意操作,然而go不对它的生命周期负责,不对它的有效性负责。
在go中,unsafe.poniter
和uintptr
可以相互转换,它们打破了Go的类型和内存安全机制,提供了非常灵活的内存地址操作和内存块内容的读写,大大提高了运行效率和操作灵活性(例子见代码段10)。
package unsafe
type ArbitraryType int
type IntegerType int
type Pointer *ArbitraryType
// 返回任意类型占用的内存空间,unsafe.Sizeof(1) 返回8,类似于C语言的sizeof
func Sizeof(x ArbitraryType) uintptr
// 返回结构体属性相对于结构体地址的偏移量
func Offsetof(x ArbitraryType) uintptr
// 等同于reflect.TypeOf(x).Align(),对齐字符数
func Alignof(x ArbitraryType) uintptr
// 例子1:取地址修改局部变量
i1 := 1000
i2 := 2000
i1Addr := uintptr(unsafe.Pointer(&i1))
i2Addr := uintptr(unsafe.Pointer(&i2))
i3Addr := uintptr(0)
if i1Addr < i2Addr {
i3Addr = i1Addr + unsafe.Sizeof(i1)
} else {
i3Addr = i1Addr - unsafe.Sizeof(i2)
}
pANext := (*int)(unsafe.Pointer(i3Addr))
*pANext += 1000
fmt.Printf("0x%x, 0x%x, 0x%x, %d\n", i1Addr, i2Addr, i3Addr, i2)
// 输出:0xc000034530, 0xc000034528, 0xc000034528, 3000
// 例子二:获取Value值
res := []int{1}
fmt.Printf("slice地址:%p 0x%x\n", &res, unsafe.Pointer(&res)) // slice对象地址
fmt.Printf("slice地址:0x%x 0x%x\n", reflect.ValueOf(&res).Elem().UnsafeAddr(), reflect.ValueOf(&res).Pointer()) // slice对象地址
// slice对象包含三部分:len、cap和data
fmt.Printf("data地址 :0x%x 0x%x\n", &res[0], reflect.ValueOf(res).Pointer()) // slice的data地址
// 输出:
// slice地址:0xc00000c100 0xc00000c100
// slice地址:0xc00000c100 0xc00000c100
// data地址:0xc0000161a8 0xc0000161a8
4. reflect
包的应用
4.1. example
通过Interface
获取Value对象的值。
var val float64 = 1.2345
pointer := reflect.ValueOf(&val).Interface().(*float64)
value := reflect.ValueOf(val).Interface().(float64)
fmt.Println(pointer) // 地址
fmt.Println(value) // 1.2345
reflect.ValueOf(&val).Elem().SetFloat(2.2)
fmt.Println(val) // 2.2
通过Field
遍历获取函数和变量。
user := User{Name: "user"}
t := reflect.TypeOf(user)
fmt.Println("TypeName:", t.Name()) // TypeName: User
val := reflect.ValueOf(user)
fmt.Println("Fields:", val) // {user}
field := t.Field(0)
value := val.Field(0).Interface()
fmt.Printf("%s(%v) = %v\n", field.Name, field.Type, value) // Name(string) = user
fmt.Printf("%s(%v)\n", t.Method(0).Name, t.Method(0).Type) // HelloWorld(func(main.User))
val.Method(0).Call(make([]reflect.Value, 0)) // Hello World
除上述样例外,笔者写了一些其他的reflect
操作的demo,有兴趣可以点击传送门。
4.2. dump数据结构地址
笔者在搜寻反射应用的时候,看到这样一个例子[5],通过反射可以拿到对象的地址以及各个成员变量的地址,笔者在repo提供了一个改造过的工具(节选代码段11)。
……
func dumpObject(path string, v reflect.Value, rootBaseAddr uintptr, localBaseAddr uintptr) string {
detail := dumpObjectDetail(path, v, rootBaseAddr, localBaseAddr)
switch v.Kind() {
case reflect.Struct:
childLocalBaseAddr := v.UnsafeAddr()
for i := 0; i < v.NumField(); i++ {
fieldPath := fmt.Sprintf("%s.%s", path, v.Type().Field(i).Name)
detail += dumpObject(fieldPath, v.Field(i), rootBaseAddr, childLocalBaseAddr)
}
}
return detail
}
func dumpObjectDetail(path string, v reflect.Value, rootBaseAddr uintptr, localBaseAddr uintptr) string {
addr := v.UnsafeAddr()
var val interface{} = "#unexported#"
if v.CanInterface() {
val = v.Interface()
}
return fmt.Sprintf("%-12s%-30s0x%018x%10v %11v %4v %10v\n", path, v.Type().String(), addr,
addr-rootBaseAddr, addr-localBaseAddr, v.Type().Size(), val)
}
4.3. 地址操作:斗转星移
C语言以其接近于计算机底层的能力而被广为使用,它能够非常轻易得获取内存地址,通过地址对应的内存快。go为了防止过于*的操作计算机底层会损害软件质量,将地址这种极底层的特性隐藏了起来,因此看似好像没有这种能力。
笔者在看猴子补丁[4]一文中受到启发,发现go语言也可以灵活操作内存数据,修改数据段乃至代码段内容。笔者在代码段12中提供了两个工具函数:
-
GetPage
:可以获得整地址所在的整个页的内容(这个页可以是任意的进程虚拟内存空间中的段(包括代码段))。 -
GetEntryCodeSegment
:可以获得本数据页内,地址开始的内存页片段。 -
LittleEndianBytesToXs
:根据小端[8]模式(笔者电脑是这样哈)读取比特流数据,存储到目标变量中。
// GetPageStartAddresss 获取地址的页面地址
func GetPageStartAddress(addr uintptr) uintptr {
return addr & ^(uintptr(syscall.Getpagesize()) - 1)
}
// 获取整个数据页内容
func GetPage(addr uintptr) []byte {
pageAddr := GetPageStartAddress(addr)
page := reflect.SliceHeader{
Data: pageAddr,
Len: syscall.Getpagesize(),
Cap: syscall.Getpagesize(),
}
return *(*[]byte)(unsafe.Pointer(&page))
}
// GetEntryCodeSegment 获取代码实际地址
func GetEntryCodeSegment(addr uintptr) []byte {
page := GetPage(addr)
startAddr := GetPageStartAddress(addr)
offset := addr - startAddr
return page[offset:]
}
// 字节流根据小端模式转换到x中
func LittleEndianBytesToXs(b []byte, x interface{}) {
bytesBuffer := bytes.NewBuffer(b)
if err := binary.Read(bytesBuffer, binary.LittleEndian, x); err != nil {
panic(err)
}
}
笔者在代码段13中,详细说明了如何通过代码段12中说明的工具方法,读取并修改内存页中的数据页。
type Tag struct {
s string `test:"B"`
n int64 `test:"A"`
}
tag := Tag{
s: "hello world",
n: 123,
}
d, err := greflect.DumpObjectWithTableHeader("tag", reflect.ValueOf(&tag))
if err != nil {
panic(err)
}
offset := uintptr(unsafe.Pointer(&tag)) - greflect.GetPageStartAddress(reflect.ValueOf(&tag).Elem().UnsafeAddr())
page := greflect.GetPage(uintptr(unsafe.Pointer(&tag)))
fmt.Println(d)
fmt.Printf("PageSize: %d\n", syscall.Getpagesize())
fmt.Printf("tag Addr: 0x%x, %p\n", unsafe.Pointer(&tag), &tag)
fmt.Printf("tag's Page Addr: 0x%x, 0x%x\n", greflect.GetPageStartAddress(reflect.ValueOf(&tag).Pointer()),
greflect.GetPageStartAddress(reflect.ValueOf(&tag).Elem().UnsafeAddr()))
fmt.Printf("tag Struct Size: %d\n", unsafe.Sizeof(tag))
fmt.Printf("tag Addr Offset: %d\n", offset)
fmt.Printf("tag.n Addr Offset: %d\n", unsafe.Offsetof(tag.n))
data := int64(0)
l := int64(0)
greflect.LittleEndianBytesToXs(page[offset:offset+8], &data)
greflect.LittleEndianBytesToXs(page[offset+8:offset+16], &l)
sh := reflect.StringHeader{
Data: uintptr(data),
Len: int(l),
}
fmt.Printf("tag.s result: %s\n", *(*string)(unsafe.Pointer(&sh)))
offset += unsafe.Offsetof(tag.n)
n, _ := binary.Uvarint(page[offset : offset+8])
fmt.Printf("tag.n result: %d\n", n)
……
// Var Type Address RootOffset LocalOffset Size Value
// tag greflect_test.Tag 0x00000000c00011e0a0 0 0 24 {hello world 123}
// tag.s string 0x00000000c00011e0a0 0 0 16 #unexported#
// tag.n int64 0x00000000c00011e0b0 16 16 8 #unexported#
// PageSize: 4096
// tag Addr: 0xc00011e0a0, 0xc00011e0a0
// tag's Page Addr: 0xc00011e000, 0xc00011e000
// tag Struct Size: 24
// tag Addr Offset: 160
// tag.n Addr Offset: 16
// Tag.s result: hello world
// Tag.n result: 123
4.4. 猴子补丁:暗渡陈仓
在本节,笔者将实现上节提到过的猴子补丁。
动态语言如Python,猴子补丁非常好做,直接修改class
对象或者对象本身就可以,因为Python的对象是实时计算的。但go是编译形的静态类型语言,要实现猴子补丁,不能够单单靠直接改变类型对象的函数表,go语言在整个运行阶段,函数指针将存在于多个itable
,是很难一棍子打死的。因此,实现猴子补丁最好的方式是直接将被补丁函数的函数体切换到补丁函数上。
笔者在阅读一片关于猴子补丁的博文时[4],作者给笔者提供了一个思路,直接将函数体内容改为汇编JMP [函数指针地址]
就行了(go函数指针地址见代码段14)。
……
// go/src/runtime/runtime2.go
// https://github.com/golang/go/blob/e9d9d0befc634f6e9f906b5ef7476fbd7ebd25e3/src/runtime/runtime2.go#L75-L78
type funcval struct { // 运行时函数值对象
fn uintptr
// variable-size, fn-specific data here
}
……
笔者结合着gomonkey库[6]给出了代码段15的跳转汇编代码(源码)。
s
……
// 笔者的工具方法
// reflect.Value的镜像
type valueMirror struct {
_ unsafe.Pointer // rtype
data unsafe.Pointer // 指向funcval,即函数指针
_ uintptr // flag
}
// 参考:https://github.com/agiledragon/gomonkey/blob/master/jmp_amd64.go
func buildJmpDirective(f interface{}) []byte {
// 保证f是一个函数对象
if reflect.TypeOf(f).Kind() != reflect.Func {
panic("f not func")
}
// 获取函数对象值
value := reflect.ValueOf(f)
// 获取函数指针
funcPtr := (*(*valueMirror)(unsafe.Pointer(&value))).data
d0 := byte(uintptr(funcPtr))
d1 := byte(uintptr(funcPtr) >> 8)
d2 := byte(uintptr(funcPtr) >> 16)
d3 := byte(uintptr(funcPtr) >> 24)
d4 := byte(uintptr(funcPtr) >> 32)
d5 := byte(uintptr(funcPtr) >> 40)
d6 := byte(uintptr(funcPtr) >> 48)
d7 := byte(uintptr(funcPtr) >> 56)
// 汇编语言,1. 将函数指针的地址转移到rdx寄存器 2. rdx存储函数指针的地址,跳转到函数指针的真正函数地址
return []byte{
0x48, 0xBA, d0, d1, d2, d3, d4, d5, d6, d7, // MOV rdx, double
0xFF, 0x22, // JMP [rdx]
}
}
在有了代码段15的代码后,现在笔者还需要做的就是,将代码改动到被补丁函数在代码段的地址中。这里必须明确,进程执行过程是二进制文件(可执行程序)的执行指令序列载入到进程的代码段后,CP按序执行的一个过程。出于保护应用程序的目的,代码段默认不可写。
要将代码段数据修改成可写,这是个系统级的操作,本文以MacOs为例,可以通过go的syscall.Mprotect
进行数据段权限更改,具体如代码段16。
func modifyBinary(dstAddr uintptr, jmpCode []byte) {
page := GetPage(dstAddr)
if err := syscall.Mprotect(page, syscall.PROT_READ|syscall.PROT_WRITE|syscall.PROT_EXEC); err != nil {
panic(err)
}
defer func() {
if err := syscall.Mprotect(page, syscall.PROT_READ|syscall.PROT_EXEC); err != nil {
panic(err)
}
}()
codeSegment := GetEntryCodeSegment(dstAddr)
copy(codeSegment, jmpCode)
}
在具备代码段16技术、代码段15和代码段12后,笔者最终得到了代码段17的猴子补丁技术(源码)。
特别要注意的一点是,go会将简短的函数inline。调用处代码直接展开,改动函数内容也就无效了。因此,如果遇到猴子补丁失效的情况,通过编译参数-gcflags=-l
取消inline优化。
// DoMonkeyPatch 猴子补丁
func DoMonkeyPatch(dst interface{}, src interface{}) {
if reflect.ValueOf(dst).Kind() != reflect.Func {
panic(fmt.Sprintf("src %s not func", reflect.ValueOf(src).Type().Name()))
}
if reflect.ValueOf(dst).Type() != reflect.ValueOf(src).Type() {
panic(fmt.Sprintf("src %s not target %s", reflect.ValueOf(src).Type().Name(),
reflect.ValueOf(dst).Type().Name()))
}
jmpCode := buildJmpDirective(src)
dstAddr := reflect.ValueOf(dst).Pointer()
nextPage := GetPageStartAddress(dstAddr) + uintptr(syscall.Getpagesize())
for len(jmpCode) > 0 {
// 页不够大,则截取代码段
codeLen := len(jmpCode)
if nextPage-dstAddr < uintptr(len(jmpCode)) {
codeLen = int(nextPage - dstAddr)
}
modifyBinary(dstAddr, jmpCode[:codeLen])
// 更新地址和下一页地址
jmpCode = jmpCode[codeLen:]
dstAddr = nextPage
nextPage = nextPage + uintptr(syscall.Getpagesize())
}
}
笔者基于代码段17的猴子补丁技术,最终完成了下面代码段18的demo。
func A() string {
res := "A"
for i := 0; i < 10; i++ {
res = fmt.Sprintf("%s%d", res, i)
}
return res
}
func B() string {
res := "B"
for i := 0; i < 10; i++ {
res = fmt.Sprintf("%s%d", res, i)
}
return res
}
a := A // 必须这样能拿到地址
b := B
c := []string{}
d := []int{}
fmt.Printf("局部变量a地址:%p\n", &a)
fmt.Printf("局部变量b地址:%p\n", &b)
fmt.Printf("局部变量c地址:%p\n", &c)
fmt.Printf("局部变量d地址:%p\n", &d)
fmt.Printf("变量a对应函数对应地址: 0x%x,函数A地址:0x%x\n", reflect.ValueOf(a).Pointer(), reflect.ValueOf(A).Pointer())
fmt.Printf("变量b对应函数对应地址: 0x%x,函数B地址:0x%x\n", reflect.ValueOf(b).Pointer(), reflect.ValueOf(B).Pointer())
fmt.Printf("变量a对应两层指针地址: 0x%x,a三层地址:0x%x\n", *(*uintptr)(unsafe.Pointer(&a)), **(**uintptr)(unsafe.Pointer(&a)))
fmt.Printf("变量b对应两层指针地址: 0x%x,b三层地址:0x%x\n", *(*uintptr)(unsafe.Pointer(&b)), **(**uintptr)(unsafe.Pointer(&b)))
greflect.DoMonkeyPatch(A, B)
fmt.Println(A())
// 局部变量a地址:0xc0000a0038
// 局部变量b地址:0xc0000a0040
// 局部变量c地址:0xc0000b60a0
// 局部变量d地址:0xc0000b60c0
// 变量a对应函数对应地址: 0x112e110,函数A地址:0x112e110
// 变量b对应函数对应地址: 0x112e140,函数B地址:0x112e140
// 变量a对应两层指针地址: 0x1187960,a三层地址:0x112e110
// 变量b对应两层指针地址: 0x1187968,b三层地址:0x112e140
// B0123456789
5. 反射问题
reflect包特别慢:
- 涉及到内存分配以及后续的GC;
- reflect实现里面有大量的枚举,也就是for循环,比如类型之类的。
6. 参考文献
[1] go interface:https://research.swtch.com/interfaces
[2] go interface源码分析:https://www.cnblogs.com/jiujuan/p/12653806.html
[3] go unsafe包:https://segmentfault.com/a/1190000039141774
[4] go monkey patch:https://bou.ke/blog/monkey-patching-in-go/
[5] 结构体内容dump:http://r12f.com/posts/learning-golang-object-model-inbox-data-type/#more
[6] gomonkey包:https://github.com/agiledragon/gomonkey
[7] 反射编程: https://en.wikipedia.org/wiki/Reflective_programming
[8] 大小端:https://baike.baidu.com/item/大小端模式/6750542