1 前言
1.1 Go汇编
Go语言被定义为一门系统编程语言,与C语言一样通过编译器生成可直接运行的二进制文件。这一点与Java,PHP,Python等编程语言存在很大的不同,这些语言都是运行在基于C语言开发的虚拟机上,如果想深入了解运行原理只需要看懂对应的C语言开发的虚拟机(绝大部分程序员应该都对C语言有基本的了解)。但是如果想深入学习Go语言,就需要对基本的汇编指令和语法有一定的了解(通过汇编可以了解到编译器到底做了什么工作)。
通过下面的例子简单了解如何通过汇编来了解Go语言的运行原理。编辑一个go文本call_function.go,输入如下代码:
1 package main 2 3 func add(a, b int) int { 4 return a + b 5 } 6 7 func main() { 8 a := 10 9 b := 20 10 11 c := add(a, b) 12 _ = c 13 }
输入命令go build -gcflags '-l -N' call_function.go生成可执行文件,然后输入命令go tool objdump -s "main.main" call_function查看汇编代码如下:
1 TEXT main.main(SB) /Users/didi/Source/Go/src/ppt/call_function.go 2 call_function.go:7 0x104f380 65488b0c25a0080000 MOVQ GS:0x8a0, CX 3 call_function.go:7 0x104f389 483b6110 CMPQ 0x10(CX), SP 4 call_function.go:7 0x104f38d 764c JBE 0x104f3db 5 call_function.go:7 0x104f38f 4883ec38 SUBQ $0x38, SP 6 call_function.go:7 0x104f393 48896c2430 MOVQ BP, 0x30(SP) 7 call_function.go:7 0x104f398 488d6c2430 LEAQ 0x30(SP), BP 8 call_function.go:8 0x104f39d 48c74424280a000000 MOVQ $0xa, 0x28(SP) 9 call_function.go:9 0x104f3a6 48c744242014000000 MOVQ $0x14, 0x20(SP) 10 call_function.go:11 0x104f3af 488b442428 MOVQ 0x28(SP), AX 11 call_function.go:11 0x104f3b4 48890424 MOVQ AX, 0(SP) 12 call_function.go:11 0x104f3b8 488b442420 MOVQ 0x20(SP), AX 13 call_function.go:11 0x104f3bd 4889442408 MOVQ AX, 0x8(SP) 14 call_function.go:11 0x104f3c2 e899ffffff CALL main.add(SB) 15 call_function.go:11 0x104f3c7 488b442410 MOVQ 0x10(SP), AX 16 call_function.go:11 0x104f3cc 4889442418 MOVQ AX, 0x18(SP) 17 call_function.go:13 0x104f3d1 488b6c2430 MOVQ 0x30(SP), BP 18 call_function.go:13 0x104f3d6 4883c438 ADDQ $0x38, SP 19 call_function.go:13 0x104f3da c3 RET 20 call_function.go:7 0x104f3db e89083ffff CALL runtime.morestack_noctxt(SB) 21 call_function.go:7 0x104f3e0 eb9e JMP main.main(SB)
第8~9行汇编代码,分别将SP(栈寄存器)偏移0x28和0x20的地址赋值为0xa和0x14,对应Go代码的第8行和第9行中的对a,b变量赋值,也就是说a变量对应的内存地址是SP+0x28,b变量对应的内存地址是SP+0x20。
然后10~14行汇编代码表示对a,b变量进行拷贝,分别拷贝到SP+0x0和SP+0x8地址,然后调用add方法,这就是通常说到的函数调用时的“值传递”。
输入命令go tool objdump -s "main.add" call_function,可以看到如下的汇编代码:
1 TEXT main.add(SB) /Users/didi/Source/Go/src/ppt/call_function.go 2 call_function.go:3 0x104f360 48c744241800000000 MOVQ $0x0, 0x18(SP) 3 call_function.go:4 0x104f369 488b442408 MOVQ 0x8(SP), AX 4 call_function.go:4 0x104f36e 4803442410 ADDQ 0x10(SP), AX 5 call_function.go:4 0x104f373 4889442418 MOVQ AX, 0x18(SP) 6 call_function.go:4 0x104f378 c3 RET
第3~5行汇编代码表示,将SP+0x8和SP+0x10地址的值相加,并复制到SP+0x18地址。
为什么在main函数中,a和b变量分别复制到了SP+0x0和SP+0x8地址,但是在add函数中,却将SP+0x8和SP+0x10地址的值进行相加呢?
这是因为在main函数中的汇编代码14行中,调用call执行时CPU会执行一次压栈操作,将函数调用完成以后需要返回的地址存在SP-0x8的地址处,并执行一次SP=SP-0x8的操作(具体操作可以百度一下)。所以在add函数里面的SP+0x8和SP+0x10地址就对应着main函数中的SP+0x0和SP+0x8地址。
具体过程如下图:

1.2 Go指针
Go的库代码中大量使用了一些指针进行内存操作。但是在Go语言中指针变量是不能进行运算的,所以不能像C语言那样方便的对内存进行偏移寻址,但是Go中提供了unsafe包来对指针计算运算。
下面的例子可以说明使用方式:
1 package main 2 3 import ( 4 "fmt" 5 "unsafe" 6 ) 7 8 type Struct1 struct { 9 A int64 10 B int64 11 C int64 12 } 13 14 type Struct2 struct { 15 A int64 16 B int64 17 C int64 18 } 19 20 func main() { 21 struct1 := Struct1 { 22 A : 1, 23 B : 2, 24 C : 3, 25 } 26 27 struct2 := new(Struct2) 28 29 var src uintptr = uintptr(unsafe.Pointer(&struct1)) 30 var dst uintptr = uintptr(unsafe.Pointer(struct2)) 31 for i := 0; i < 24; i++ { 32 *(*uint8)(unsafe.Pointer(dst + uintptr(i))) = *(*uint8)(unsafe.Pointer(src + uintptr(i))) 33 } 34 35 fmt.Println("struct1=%v||struct2=%v", struct1, *struct2); 36 }
在上面的例子将struct1对应内存的值复制到struct2对应的内存中,从例子中可以看出可以看到Go语言中
- unsafe.Pointer类似于C中的void*,任何类型的指针都可以转换为unsafe.Pointer 类型,unsafe.Pointer 类型也可以转换为任何指针类型;
- uintptr可以存go中的任何变量,如果想对指针进行运算,必须先把指针转换为uintptr。
2 Go的interface的实现
在Go语言中interface是一个非常重要的概念,也是与其它语言相比存在很大特色的地方。interface也是一个Go语言中的一种类型,是一种比较特殊的类型,存在两种interface,一种是带有方法的interface,一种是不带方法的interface。Go语言中的所有变量都可以赋值给空interface变量,实现了interface中定义方法的变量可以赋值给带方法的interface变量,并且可以通过interface直接调用对应的方法,实现了其它面向对象语言的多态的概念。
2.1 内部定义
两种不同的interface在Go语言内部被定义成如下的两种结构体(源码基于Go的1.9.2版本):
// 没有方法的interface type eface struct { _type *_type data unsafe.Pointer } // 记录着Go语言中某个数据类型的基本特征 type _type struct { size uintptr ptrdata uintptr hash uint32 tflag tflag align uint8 fieldalign uint8 kind uint8 alg *typeAlg gcdata *byte str nameOff ptrToThis typeOff } // 有方法的interface type iface struct { tab *itab data unsafe.Pointer } type itab struct { inter *interfacetype _type *_type link *itab hash uint32 bad bool inhash bool unused [2]byte fun [1]uintptr } // interface数据类型对应的type type interfacetype struct { typ _type pkgpath name mhdr []imethod }
可以看到两种类型的interface在内部实现时都是定义成了一个2个字段的结构体,所以任何一个interface变量都是占用16个byte的内存空间。
在Go语言中_type这个结构体非常重要,记录着某种数据类型的一些基本特征,比如这个数据类型占用的内存大小(size字段),数据类型的名称(nameOff字段)等等。每种数据类型都存在一个与之对应的_type结构体(Go语言原生的各种数据类型,用户自定义的结构体,用户自定义的interface等等)。如果是一些比较特殊的数据类型,可能还会对_type结构体进行扩展,记录更多的信息,比如interface类型,就会存在一个interfacetype结构体,除了通用的_type外,还包含了另外两个字段pkgpath和mhdr,后文在对这两个字段的作用进行解析。除此之外还有其它类型的数据结构对应的结构体,比如structtype,chantype,slicetype,有兴趣的可以在$GOROOT/src/runtime/type.go文件中查看。

2.2 赋值
存在对没有方法的interface变量和有方法的interface变量赋值这两种不同的情况。分别详解这两种不同的赋值过程。
- 没有方法的interface变量赋值
对没有方法的interface变量赋值时编译器做了什么工作?创建一个eface.go文件,代码如下:
1 package main 2 3 type Struct1 struct { 4 A int64 5 B int64 6 } 7 8 func main() { 9 s := new(Struct1) 10 var i interface{} 11 i = a 12 13 _ = i 14 }
输入命令go build -gcflags '-l -N' eface.go,go tool objdump -s "main.main" eface,查看汇编代码。
1 TEXT main.main(SB) /Users/didi/Source/Go/src/ppt/eface.go 2 eface.go:8 0x104f360 4883ec38 SUBQ $0x38, SP 3 eface.go:8 0x104f364 48896c2430 MOVQ BP, 0x30(SP) 4 eface.go:8 0x104f369 488d6c2430 LEAQ 0x30(SP), BP 5 eface.go:9 0x104f36e 48c7042400000000 MOVQ $0x0, 0(SP) 6 eface.go:9 0x104f376 48c744240800000000 MOVQ $0x0, 0x8(SP) 7 eface.go:9 0x104f37f 488d0424 LEAQ 0(SP), AX 8 eface.go:9 0x104f383 4889442410 MOVQ AX, 0x10(SP) 9 eface.go:10 0x104f388 48c744242000000000 MOVQ $0x0, 0x20(SP) 10 eface.go:10 0x104f391 48c744242800000000 MOVQ $0x0, 0x28(SP) 11 eface.go:11 0x104f39a 488b442410 MOVQ 0x10(SP), AX 12 eface.go:11 0x104f39f 4889442418 MOVQ AX, 0x18(SP) 13 eface.go:11 0x104f3a4 488d0dd5670000 LEAQ 0x67d5(IP), CX 14 eface.go:11 0x104f3ab 48894c2420 MOVQ CX, 0x20(SP) 15 eface.go:11 0x104f3b0 4889442428 MOVQ AX, 0x28(SP) 16 eface.go:14 0x104f3b5 488b6c2430 MOVQ 0x30(SP), BP 17 eface.go:14 0x104f3ba 4883c438 ADDQ $0x38, SP
汇编代码第5~6行给结构体Struct1分配了空间SP+0x0和SP+0x8,第7~8行把这个结构体的地址放在存入了SP+0x10地址,这个地址就是变量s,第9~10行给interface类型的变量i分配了SP+0x20和SP+0x28,第13~14行把结构体A对应的_type的地址赋值到SP+0x20,然后把a变量赋值到了SP+0x28。这就是对没有方法的interface进行赋值的过程。赋值完以后的内存分配如下图:

- 有方法的interface变量赋值
如下一段代码在内存的分布
1 package main 2 3 type I interface { 4 Add() 5 Del() 6 } 7 8 type Struct1 struct { 9 A int64 10 B int64 11 } 12 13 func (a *Struct1) Add() { 14 a.A = a.A + 1 15 a.B = a.B + 1 16 } 17 18 func (a *Struct1) Del() { 19 a.A = a.A - 1 20 a.B = a.B - 1 21 } 22 23 func main() { 24 a := new(Struct1) 25 var i I 26 i = a 27 28 i.Add() 29 i.Del() 30 }

这些内存地址都可以使用gdb调试时得到
(gdb) p i
$11 = {tab = 0x10a70e0 <Struct1,main.I>, data = 0xc42001a0c0}
(gdb) p a
$12 = (struct main.Struct1 *) 0xc42001a0c0
(gdb) p i.tab
$13 = (runtime.itab *) 0x10a70e0 <Struct1,main.I>
(gdb) p i.tab.inter
$14 = (runtime.interfacetype *) 0x105dc60 <type.*+59232>
(gdb) p i.tab._type
$15 = (runtime._type *) 0x105d200 <type.*+56576>
通过对内存地址的打印,可以很清晰的看出在对有方法的interface变量进行赋值时的内存分布。Struct1类型和interface I类型都存在内存记录着各自的_type结构体信息,在将Struct1类型的变量赋值给interface I类型时,会有一个itab类型的结构体将Struct1类型和interface I类型关联起来。
上面的例子都是将一个指针赋值给interface变量,如果是将一个值赋值给interface变量。会先对分配一块空间保存该值的副本,然后将该interface变量的data字段指向这个新分配的空间。将一个值赋值给interface变量时,操作的都是该值的一个副本。
2.3 方法的调用
上面对有方法的interface进行赋值后,是如何实现通过接口变量实现了函数调用呢?参考下面的汇编代码
1 TEXT main.main(SB) /Users/didi/Source/Go/src/ppt/iface.go 2 iface.go:23 0x104f3e0 65488b0c25a0080000 MOVQ GS:0x8a0, CX 3 iface.go:23 0x104f3e9 483b6110 CMPQ 0x10(CX), SP 4 iface.go:23 0x104f3ed 0f8687000000 JBE 0x104f47a 5 iface.go:23 0x104f3f3 4883ec38 SUBQ $0x38, SP 6 iface.go:23 0x104f3f7 48896c2430 MOVQ BP, 0x30(SP) 7 iface.go:23 0x104f3fc 488d6c2430 LEAQ 0x30(SP), BP 8 iface.go:23 0x104f401 488d0578ff0000 LEAQ 0xff78(IP), AX 9 iface.go:24 0x104f408 48890424 MOVQ AX, 0(SP) 10 iface.go:24 0x104f40c e86fcefbff CALL runtime.newobject(SB) 11 iface.go:24 0x104f411 488b442408 MOVQ 0x8(SP), AX 12 iface.go:24 0x104f416 4889442410 MOVQ AX, 0x10(SP) 13 iface.go:25 0x104f41b 48c744242000000000 MOVQ $0x0, 0x20(SP) 14 iface.go:25 0x104f424 48c744242800000000 MOVQ $0x0, 0x28(SP) 15 iface.go:26 0x104f42d 488b442410 MOVQ 0x10(SP), AX 16 iface.go:26 0x104f432 4889442418 MOVQ AX, 0x18(SP) 17 iface.go:26 0x104f437 488d0da27c0500 LEAQ 0x57ca2(IP), CX 18 iface.go:26 0x104f43e 48894c2420 MOVQ CX, 0x20(SP) 19 iface.go:26 0x104f443 4889442428 MOVQ AX, 0x28(SP) 20 iface.go:28 0x104f448 488b442420 MOVQ 0x20(SP), AX 21 iface.go:28 0x104f44d 488b4020 MOVQ 0x20(AX), AX 22 iface.go:28 0x104f451 488b4c2428 MOVQ 0x28(SP), CX 23 iface.go: