参考资料

分享到:

参考资料

我们已经看过了 ifaceeface 的源码,知道 iface 最重要的是 itab_type

为了研究清楚接口是如何构造的,接下来我会拿起汇编的武器,还原背后的真相。

来看一个示例代码:

 1package main
 2
 3import "fmt"
 4
 5type Person interface {
 6	growUp()
 7}
 8
 9type Student struct {
10	age int
11}
12
13func (p Student) growUp() {
14	p.age += 1
15	return
16}
17
18func main() {
19	var qcrao = Person(Student{age: 18})
20
21	fmt.Println(qcrao)
22}
23

执行命令:

1go tool compile -S main.go

得到 main 函数的汇编代码如下:

 10x0000 00000 (./src/main.go:30) TEXT    "".main(SB), $80-0
 20x0000 00000 (./src/main.go:30) MOVQ    (TLS), CX
 30x0009 00009 (./src/main.go:30) CMPQ    SP, 16(CX)
 40x000d 00013 (./src/main.go:30) JLS     157
 50x0013 00019 (./src/main.go:30) SUBQ    $80, SP
 60x0017 00023 (./src/main.go:30) MOVQ    BP, 72(SP)
 70x001c 00028 (./src/main.go:30) LEAQ    72(SP), BP
 80x0021 00033 (./src/main.go:30) FUNCDATA$0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
 90x0021 00033 (./src/main.go:30) FUNCDATA$1, gclocals·e226d4ae4a7cad8835311c6a4683c14f(SB)
100x0021 00033 (./src/main.go:31) MOVQ    $18, ""..autotmp_1+48(SP)
110x002a 00042 (./src/main.go:31) LEAQ    go.itab."".Student,"".Person(SB), AX
120x0031 00049 (./src/main.go:31) MOVQ    AX, (SP)
130x0035 00053 (./src/main.go:31) LEAQ    ""..autotmp_1+48(SP), AX
140x003a 00058 (./src/main.go:31) MOVQ    AX, 8(SP)
150x003f 00063 (./src/main.go:31) PCDATA  $0, $0
160x003f 00063 (./src/main.go:31) CALL    runtime.convT2I64(SB)
170x0044 00068 (./src/main.go:31) MOVQ    24(SP), AX
180x0049 00073 (./src/main.go:31) MOVQ    16(SP), CX
190x004e 00078 (./src/main.go:33) TESTQ   CX, CX
200x0051 00081 (./src/main.go:33) JEQ     87
210x0053 00083 (./src/main.go:33) MOVQ    8(CX), CX
220x0057 00087 (./src/main.go:33) MOVQ    $0, ""..autotmp_2+56(SP)
230x0060 00096 (./src/main.go:33) MOVQ    $0, ""..autotmp_2+64(SP)
240x0069 00105 (./src/main.go:33) MOVQ    CX, ""..autotmp_2+56(SP)
250x006e 00110 (./src/main.go:33) MOVQ    AX, ""..autotmp_2+64(SP)
260x0073 00115 (./src/main.go:33) LEAQ    ""..autotmp_2+56(SP), AX
270x0078 00120 (./src/main.go:33) MOVQ    AX, (SP)
280x007c 00124 (./src/main.go:33) MOVQ    $1, 8(SP)
290x0085 00133 (./src/main.go:33) MOVQ    $1, 16(SP)
300x008e 00142 (./src/main.go:33) PCDATA  $0, $1
310x008e 00142 (./src/main.go:33) CALL    fmt.Println(SB)
320x0093 00147 (./src/main.go:34) MOVQ    72(SP), BP
330x0098 00152 (./src/main.go:34) ADDQ    $80, SP
340x009c 00156 (./src/main.go:34) RET
350x009d 00157 (./src/main.go:34) NOP
360x009d 00157 (./src/main.go:30) PCDATA  $0, $-1
370x009d 00157 (./src/main.go:30) CALL    runtime.morestack_noctxt(SB)
380x00a2 00162 (./src/main.go:30) JMP     0

我们从第 10 行开始看,如果不理解前面几行汇编代码的话,可以回去看看公众号前面两篇文章,这里我就省略了。

汇编行数 操作
10-14 构造调用 runtime.convT2I64(SB) 的参数

我们来看下这个函数的参数形式:

1func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) {
2	// ……
3}

convT2I64 会构造出一个 inteface,也就是我们的 Person 接口。

第一个参数的位置是 (SP),这里被赋上了 go.itab."".Student,"".Person(SB) 的地址。

我们从生成的汇编找到:

1go.itab."".Student,"".Person SNOPTRDATA dupok size=40
2        0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  
3        0x0010 00 00 00 00 00 00 00 00 da 9f 20 d4              
4        rel 0+8 t=1 type."".Person+0
5        rel 8+8 t=1 type."".Student+0

size=40 大小为40字节,回顾一下:

 1type itab struct {
 2	inter  *interfacetype // 8字节
 3	_type  *_type // 8字节
 4	link   *itab // 8字节
 5	hash   uint32 // 4字节
 6	bad    bool   // 1字节
 7	inhash bool   // 1字节
 8	unused [2]byte // 2字节
 9	fun    [1]uintptr // variable sized // 8字节
10}

把每个字段的大小相加,itab 结构体的大小就是 40 字节。上面那一串数字实际上是 itab 序列化后的内容,注意到大部分数字是 0,从 24 字节开始的 4 个字节 da 9f 20 d4 实际上是 itabhash 值,这在判断两个类型是否相同的时候会用到。

下面两行是链接指令,简单说就是将所有源文件综合起来,给每个符号赋予一个全局的位置值。这里的意思也比较明确:前8个字节最终存储的是 type."".Person 的地址,对应 itab 里的 inter 字段,表示接口类型;8-16 字节最终存储的是 type."".Student 的地址,对应 itab_type 字段,表示具体类型。

第二个参数就比较简单了,它就是数字 18 的地址,这也是初始化 Student 结构体的时候会用到。

汇编行数 操作
15 调用 runtime.convT2I64(SB)

具体看下代码:

 1func convT2I64(tab *itab, elem unsafe.Pointer) (i iface) {
 2	t := tab._type
 3	
 4	//...
 5	
 6	var x unsafe.Pointer
 7	if *(*uint64)(elem) == 0 {
 8		x = unsafe.Pointer(&zeroVal[0])
 9	} else {
10		x = mallocgc(8, t, false)
11		*(*uint64)(x) = *(*uint64)(elem)
12	}
13	i.tab = tab
14	i.data = x
15	return
16}

这块代码比较简单,把 tab 赋给了 ifacetab 字段;data 部分则是在堆上申请了一块内存,然后将 elem 指向的 18 拷贝过去。这样 iface 就组装好了。

汇编行数 操作
17 i.tab 赋给 CX
18 i.data 赋给 AX
19-21 检测 i.tab 是否是 nil,如果不是的话,把 CX 移动 8 个字节,也就是把 itab_type 字段赋给了 CX,这也是接口的实体类型,最终要作为 fmt.Println 函数的参数

后面,就是调用 fmt.Println 函数及之前的参数准备工作了,不再赘述。

这样,我们就把一个 interface 的构造过程说完了。

【引申1】 如何打印出接口类型的 Hash 值?

这里参考曹大神翻译的一篇文章,参考资料里会写上。具体做法如下:

 1type iface struct {
 2	tab  *itab
 3	data unsafe.Pointer
 4}
 5type itab struct {
 6	inter uintptr
 7	_type uintptr
 8	link uintptr
 9	hash  uint32
10	_     [4]byte
11	fun   [1]uintptr
12}
13
14func main() {
15	var qcrao = Person(Student{age: 18})
16
17	iface := (*iface)(unsafe.Pointer(&qcrao))
18	fmt.Printf("iface.tab.hash = %#x\n", iface.tab.hash)
19}

定义了一个山寨版ifaceitab,说它山寨是因为 itab 里的一些关键数据结构都不具体展开了,比如 _type,对比一下正宗的定义就可以发现,但是山寨版依然能工作,因为 _type 就是一个指针而已嘛。

main 函数里,先构造出一个接口对象 qcrao,然后强制类型转换,最后读取出 hash 值,非常妙!你也可以自己动手试一下。

运行结果:

1iface.tab.hash = 0xd4209fda

值得一提的是,构造接口 qcrao 的时候,即使我把 age 写成其他值,得到的 hash 值依然不变的,这应该是可以预料的,hash 值只和他的字段、方法相关。

参考资料

【曹大神翻译的文章,非常硬核】http://xargin.com/go-and-interface/#reconstructing-an-itab-from-an-executable