从源代码和汇编层面解析golang结构体元数据

929 阅读11分钟

这两天正在看左大的《go语言设计与实现》,接触了很多汇编代码,网上很多资料,发现都是在解析函数执行代码的,对于其他的汇编代码没有提及,但是对结构体相关的汇编比较感兴趣,因此了解下结构体相关的汇编代码
本次的环境是go1.14.2 windows/amd64
样例代码:

type Cat struct {
	Name    string
	Age     int
	address string
}

func (c Cat) A() {

}

func (c Cat) b() {

}

以Cat结构体为例,通过go tool compile -N -S -l main.go编译成汇编代码之后得到相关Cat类型汇编代码如下。接下来就是对这些二进制代码做解析

type."".Cat SRODATA size=200
        0x0000 28 00 00 00 00 00 00 00 20 00 00 00 00 00 00 00  (....... .......
        0x0010 12 a8 f8 10 07 08 08 19 00 00 00 00 00 00 00 00  ................
        0x0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
        0x0030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
        0x0040 03 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00  ................
        0x0050 00 00 00 00 02 00 01 00 58 00 00 00 00 00 00 00  ........X.......
        0x0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
        0x0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
        0x0080 00 00 00 00 00 00 00 00 20 00 00 00 00 00 00 00  ........ .......
        0x0090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
        0x00a0 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  0...............
        0x00b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
        0x00c0 00 00 00 00 00 00 00 00                          ........
        rel 24+8 t=1 type..eqfunc."".Cat+0
        rel 32+8 t=1 runtime.gcbits.09+0
        rel 40+4 t=5 type..namedata.*main.Cat.+0
        rel 44+4 t=5 type.*"".Cat+0
        rel 48+8 t=1 type..importpath."".+0
        rel 56+8 t=1 type."".Cat+96
        rel 80+4 t=5 type..importpath."".+0
        rel 96+8 t=1 type..namedata.Name.+0
        rel 104+8 t=1 type.string+0
        rel 120+8 t=1 type..namedata.Age.+0
        rel 128+8 t=1 type.int+0
        rel 144+8 t=1 type..namedata.address-+0
        rel 152+8 t=1 type.string+0
        rel 168+4 t=5 type..namedata.A.+0
        rel 172+4 t=25 type.func()+0
        rel 176+4 t=25 "".(*Cat).A+0
        rel 180+4 t=25 "".Cat.A+0
        rel 184+4 t=5 type..namedata.b-+0
        rel 188+4 t=25 type.func()+0
        rel 192+4 t=25 "".(*Cat).b+0
        rel 196+4 t=25 "".Cat.b+0

go源代码层面的结构体类型信息

在网上查阅了相关博客得知,golang的类型系统以runtime._type结构体为基础,并从中扩展出了其他类型,这里主要关注结构体类型,源代码如下所示,可在_runtime/type.go_文件中找到下列信息

type nameOff int32
type typeOff int32
type tflag uint8
// name is an encoded type name with optional extra data.
// See reflect/type.go for details.
type name struct {
	bytes *byte
}

type _type struct {
	size       uintptr
	ptrdata    uintptr // size of memory prefix holding all pointers
	hash       uint32
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       uint8
	equal func(unsafe.Pointer, unsafe.Pointer) bool
	gcdata    *byte
	str       nameOff
	ptrToThis typeOff
}

type structtype struct {
	typ     _type
	pkgPath name
	fields  []structfield
}

type structfield struct {
	name       name
	typ        *_type
	offsetAnon uintptr
}

除了上面的信息之外,结构体类型信息还通过uncommontype结构体包含了一些方法相关的信息。从(*type).uncommon()方法中可以看到,uncommontype结构体就在每个structtype结构体之后

type uncommontype struct {
	pkgpath nameOff
	mcount  uint16 // 
	xcount  uint16 // number of exported methods
	moff    uint32 // offset from this uncommontype to [mcount]method
	_       uint32 // unused
}

// _type的uncommon方法会返回一个uncommontype结构体,依据代码可以知道
// 每个structtype相关的uncommontype信息就紧跟在structtype之后
func (t *_type) uncommon() *uncommontype {
	if t.tflag&tflagUncommon == 0 {
		return nil
	}
	switch t.kind & kindMask {
	case kindStruct:
		type u struct {
			structtype
			u uncommontype
		}
		return &(*u)(unsafe.Pointer(t)).u
        ......
    }
}

uncommontype中,moff字段表示方法表所在的位置距离uncommontype的偏移量,在_reflect/type.go_中也能看到,uncommontype有以下方法

func (t *uncommonType) methods() []method {
	if t.mcount == 0 {
		return nil
	}
	return (*[1 << 16]method)(add(unsafe.Pointer(t), uintptr(t.moff), "t.mcount > 0"))[:t.mcount:t.mcount]
}

type nameOff int32
type typeOff int32
type textOff int32
// method定义
type method struct {
	name nameOff // 方法名称
	mtyp typeOff // 方法的类型(不包含接收者)
	ifn  textOff // 接口调用的时候,使用的fn
	tfn  textOff // 正常调用的时候,使用的fn
}

因此,一个完整的结构体构成大致如下所示
未命名绘图-第 2 页.png

解析二进制

接下来就对开头的Cat二进制数据进行解析,与structtype定义相结合

typ _type

首先structtype中内嵌_type结构体,typ字段就是_type结构体类型,定义如下

type _type struct {
	size       uintptr
	ptrdata    uintptr // size of memory prefix holding all pointers
	hash       uint32
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       uint8
	equal 	   func(unsafe.Pointer, unsafe.Pointer) bool
	gcdata    *byte
	str       nameOff
	ptrToThis typeOff
}

type nameOff int32
type typeOff int32
type tflag uint8

通过计算可知,_type结构体所占内存空间大小为48字节,取出编译后的前48字节数据如下

0x0000 28 00 00 00 00 00 00 00 20 00 00 00 00 00 00 00  (....... .......
0x0010 12 a8 f8 10 07 08 08 19 00 00 00 00 00 00 00 00  ................
0x0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
rel 24+8 t=1 type..eqfunc."".Cat+0
rel 32+8 t=1 runtime.gcbits.09+0
rel 40+4 t=5 type..namedata.*main.Cat.+0
rel 44+4 t=5 type.*"".Cat+0                                                   

按照_type结构体定义的字段分析这里的二进制代码:

  1. 首先8个字节是size,它表示整个结构体所占大小,这里是0x28,即40字节,计算一下Cat内部Name和address是string类型,一个string占16个字节,加上Age字段,int类型占8字节,正好是40字节
  2. 然后8个字节是ptrdata,该字段表示结构体中可以包含指针的字节数,这个字段还不太确定是怎么计算出来,
  3. 接下来4个字节是hash,是这个类型的hash值,在类型断言的时候可以看到
  4. 然后4个字节分别是tflagalignfieldAlignkind
  5. 再往后8个字节表示equals函数,是一个符号引用,这里涉及到链接时候的重定向,可以参考《深入理解计算机系统》第7章了解链接相关知识,以及这篇博客,了解golang的链接
  6. 然后的8个字节是gcdata信息,也是一个符号引用
  7. 最后的8个字节,前4个字节表示该结构体的名称信息str,后4个字节表示该结构体对应的指针类型相关信息ptrToThis。golang会为每个结构体自动生成对应的指针类型的信息,感兴趣的可以了解一下golang的类型系统

至此,structtype中第一个字段typ就解析完成了,接下来继续分析其他字段

pkgPath name

name定义

type name struct {
	bytes *byte
}

接下来的pkgPath是一个name类型,它表示与类型名称相关的一些额外数据,其中只有一个byte指针,也就是8字节,对应到二进制代码中为下面的部分,也是一个引用值。但是在所有输出的代码中没有找到相关的实际内容

0x0030 00 00 00 00 00 00 00 00
rel 48+8 t=1 type..importpath."".+0

fields []structfield

接下来分析fields字段,它是一个切片类型,下面是切片的运行时表示

type sliceHeader struct {
	Data unsafe.Pointer
	Len  int
	Cap  int
}

相关汇编代码如下

0x0030                         00 00 00 00 00 00 00 00  ................
0x0040 03 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00  ................
rel 56+8 t=1 type."".Cat+96

可以看到,这24个字节中,前8个字节分别就是structfield真实数据的地址,后16个字节分别表示切片的len和cap,其中都为3,与定义的Cat结构体中有3个字段相符合
按照汇编代码中,字段相关的信息保存在type."".Cat中偏移量为96的位置,因此我们先分析0x0060处的代码

structfield

相关结构体定义如下

type structfield struct {
	name       name
	typ        *_type
	offsetAnon uintptr	// 表示该字段在结构体中的偏移量,同时还有<<1,最后一位表示是否为嵌入字段
}

从前面可以知道,name结构体中只有一个byte指针,因此计算可以知道一个structfield占24个字节的大小,Cat中有3个字段,因此有72个字节
我们把相关的二进制信息都取出来如下

0x0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x0070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x0080 00 00 00 00 00 00 00 00 20 00 00 00 00 00 00 00  ........ .......
0x0090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00a0 30 00 00 00 00 00 00
rel 96+8 t=1 type..namedata.Name.+0
rel 104+8 t=1 type.string+0
rel 120+8 t=1 type..namedata.Age.+0
rel 128+8 t=1 type.int+0
rel 144+8 t=1 type..namedata.address-+0
rel 152+8 t=1 type.string+0

按照顺序分析,首先是一个8字节的引用值,指向字段名称,然后是一个8字节的指针,指向该字段的类型,然后8字节表示该字段在结构体布局中的位置偏移量乘以2,最后一位用来表示该字段是否为嵌入字段
所以字段相关的信息都分析完成了,接下来分析定义在structtype之外的uncommontype信息

uncommontype

structtype之后,就是其相关的uncommontype,其中包含了方法相关的信息,golang定义如下

type nameOff int32
type uncommontype struct {
	pkgpath nameOff
	mcount  uint16 // 
	xcount  uint16 // number of exported methods
	moff    uint32 // offset from this uncommontype to [mcount]method
	_       uint32 // unused
}

其相关的二进制代码如下

0x0050 00 00 00 00 02 00 01 00 58 00 00 00 00 00 00 00  ........X.......
rel 80+4 t=5 type..importpath."".+0
  1. 首先四个字节即pkgpath,表示包路径相关信息,是一个符号引用
  2. 接下来2个字节表示方法数量mcount,包括导出和未导出方法,其值为2,即代码中定义的A()和b()方法。从这里也可以看出,一个结构体最多定义2^16-1个方法
  3. 然后2个字节表示导出的方法数量xcount,其值为1,表示A()方法
  4. 最后8个字节中,只有前4个字节有,即moff,即方法表和uncommontype的偏移量,这里是0x58,因此,从0x0050加上0x58就得到方法表的位置在0xa8。回顾前面fields的代码,这个位置就在字段信息之后

methods

下面是方法相关的结构体定义

type nameOff int32
type typeOff int32
type textOff int32
// method定义
type method struct {
	name nameOff // 方法名称
	mtyp typeOff // 方法的类型(不包含接收者)
	ifn  textOff // 接口调用的时候,使用的fn
	tfn  textOff // 正常调用的时候,使用的fn
}

相关二进制代码如下所示

0x00a0                         00 00 00 00 00 00 00 00  0...............
0x00b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0x00c0 00 00 00 00 00 00 00 00                          ........
rel 168+4 t=5 type..namedata.A.+0
rel 172+4 t=25 type.func()+0
rel 176+4 t=25 "".(*Cat).A+0
rel 180+4 t=25 "".Cat.A+0
rel 184+4 t=5 type..namedata.b-+0
rel 188+4 t=25 type.func()+0
rel 192+4 t=25 "".(*Cat).b+0
rel 196+4 t=25 "".Cat.b+0

每个method需要16个字节表示,其中全是偏移量相关的信息,首先4个字节表示方法名称相关的nameOff,然后4个字节是方法的类型typeOff,然后的4个字节是使用接口调用方法时的方法地址ifn,最后4个字节为正常调用方法时的方法地址tfn