【Go】内存中的整数

876 阅读6分钟

上一篇 内存中的空接口 研究了 int 类型数据转变为 interface{} 类型数据的过程。

本文主要研究 int 类型 在内存中真实的样子。

环境

OS : Ubuntu 20.04.2 LTS; x86_64
Go : go version go1.16.2 linux/amd64

约定

  1. Go中描述数据类型的数据结构(reflect.rtype)称为Go类型结构(体)。

  2. Go中指向描述数据类型的数据结构(reflect.rtype)的指针,称为Go类型指针。

源码阅读

内存中的空接口 中,我们知道了 interface{} 类型变量实际包含两个字段(指针变量)。

其中,第一个字段(typ)是类型指针,第二个字段(word)是数据指针。

Go类型结构体

在Golang源代码中,有三处定义了Go类型结构体:

  • reflect/type.go中的rtype结构体

  • internal/reflectlite/type.go中的rtype结构体

  • runtime/type.go中的_type结构体

它们虽然是不同的类型,但是它们是等价的,在内存中是可以互换的、相互引用的。

我们来看reflect/type.go中的rtype结构体:

在64位程序和操作系统中,其各个字段的分布结构如下:

代码清单

仍然使用 内存中的空接口 中的代码。

package main

import "fmt"

func main() {
    var a = 123
    PrintInterface(a)
}

//go:noinline
func PrintInterface(v interface{}) {
    fmt.Println("it =", v)
}

深入内存

动态调试

编译以上代码,使用gdb动态调试,在调用PrintInterface函数的指令处设置断点,查看 int 类型的类型数据(48个字节)。 

上图中,小红框标记的就是栈顶保存的指针值(typ),大红框标记的就是reflect.rtype结构体在内存中表示 int 类型的数据对象。

字段详解

备注:Intel系列芯片采用小端模式。所以查看内存时注意字节顺序。

rtype.size

地址 0x4a2140 处的8个字节存储了size的值。 图片

int 类型数据在内存中占8个字节长度。

rtype.ptrdata

地址 0x4a2148 处的8个字节存储了ptrdata的值。 图片

int 类型数据不包含指针,所以值为0。

rtype.hash

地址 0x4a2150 处的4个字节存储了类型的hash值。 图片

类型的哈希值,用于哈希计算。

Ubuntu操作系统和Windows操作系统,编译的程序,int 类型 hash 相同。

Go编译器具体如何生成hash值尚未研究。

rtype.tflag

地址 0x4a2154 处的1个字节存储了额外的类型信息标记,值为0xF(0b00001111)。

在Go语言源代码中,定义了4个标记常量,用于位与运算:

const tflagUncommon     = 1 << 0 // 0b00000001
const tflagExtraStar    = 1 << 1 // 0b00000010
const tflagNamed        = 1 << 2 // 0b00000100
const tflagRegularMemory= 1 << 3 // 0b00001000

  1. tflagUncommon表示该rtype对象后是否紧跟着额外的包导入、方法信息。

  2. tflagExtraStar表示rtype.str字段指向的字符串是否包含"*"前缀。稍后将会详细介绍。

  3. tflagNamed表示该数据类型是否有名称。

  4. tflagRegularMemory means that equal and hash functions can treat this type as a single region of t.size bytes.。

很明显,int 类型符合以下特征

  • 包含uncommonType信息(所以实际上 int 类型结构体包含64个字节)

  • 有类型名字

  • rtype.str字段有*前缀

  • 数据连续存储

rtype.align

int 类型数据在内存中8字节对齐。

rtype.fieldAlign

int 类型数据作为结构体字段时,在内存中8字节对齐。

rtype.kind

Golang对数据类型(Type)进行了分类(Kind),一共有26种。

int 类型的分类Int,值是2。图片

rtype.equal

地址 0x4a2158 处的8个字节存储了用于该类型数据 equal 比较的函数指针。图片

从图上可以看出,int 类型数据使用runtime.memequal64函数进行相等比较。 图片

rtype.gcdata

垃圾收集数据。

rtype.str

地址 0x4a2168 处的4个字节存储了该类型的文字描述信息(string form)。实际上,这4个字节保存的只是该类型文字描述信息的相对偏移量。int 类型的文字描述信息偏移量是0x34f。但,它是相对哪个值进行偏移的?

通过查看源代码和猜测推断,很快定位到,偏移量的基址是.rodata section的起始地址。

Golang源代码地址:reflect.resolveNameOffruntime.resolveNameOff 图片

0x0000000000498000 + 0x34f = 0x49834F

地址 0x49834F 指向的是一个结构体数据,其定义如下,注释非常清晰的解释了其结构组成。

// name is an encoded type name with optional extra data.
//
// The first byte is a bit field containing:
//
// 1<<0 the name is exported
// 1<<1 tag data follows the name
// 1<<2 pkgPath nameOff follows the name and tag
//
// The next two bytes are the data length:
//
//  l := uint16(data[1])<<8 | uint16(data[2])
//
// Bytes [3:3+l] are the string data.
//
// If tag data follows then bytes 3+l and 3+l+1 are the tag length,
// with the data following.
//
// If the import path follows, then 4 bytes at the end of
// the data form a nameOff. The import path is only set for concrete
// methods that are defined in a different package than their type.
//
// If a name starts with "*", then the exported bit represents
// whether the pointed to type is exported.
type name struct {
   bytes *byte
}

把注释画成图,就非常清晰了: 图片

虚线框选的部分,可能存在,也可能不存在,具体存不存在由第一个字节 flag 决定。 

通过查看内存,int 类型的文字描述(string form)由4个字符组成:*int

int 类型的名称就是从这个字符串解析出来的。因为 int 类型的 rtype.tflag 包含 tflagExtraStar 标记,所以在获取类型名称的时候会把 * 号去掉,最终是 int。

rtype.ptrToThis

地址 0x4a216c 处的4个字节存储了该字段的值。

它也是一个偏移量,值可能为0。

表示该类型的指针类型结构体存储位置偏移量(令人读过之后一脸懵逼)。

以int类型为例,ptrToThis是 *int 类型结构体相对.rodata section起始地址的偏移量。

具体数据含义不再赘述,可按照本文的分析过程再来分析一次即可。

总结发现

  1. Golang中描述数据类型的数据结构包含48个字节,了解了各个字段的含义。

  2. int 类型数据占8个字节。

  3. *int 类型数据也占8个字节。

  4. int 类型文字描述信息(string form)为 *int,类型名称为 int。

  5. *int 类型文字描述信息(string form)为 *int,没有类型名称。

  6. int 类型和 *int 类型具有相同的 equal 函数。