Go的汇编
要看懂Go的内存模型,需要对Go runtime的源码和Go的汇编指令有一定的了解。Go的汇编是基于 Pan9 汇编的风格。Go的实现有大量的汇编代码,比如:goroutine的上下文切换肯定是要汇编的,切换栈帧和寄存器,这些是无法通过简单的function call来完成的,操作系统的线程上下文切换同样类似。
在Linux平台,可以通过看Go runtime的源码结合GDB(v7.1+)调试来快速理解一些Go关键的内存模型和运行原理,从而更加深刻的理解Go的语言特性。
Go的接口
在Go 1.10版中,Go interface 的实现在 runtime2.go 中,定义如下:
// 可以利用Goland IDE双击shift快速搜索到
type iface struct {
tab *itab // 有类型信息、函数指针表等
data unsafe.Pointer // 类似C的void*
}
// layout of Itab known to compilers
// allocated in non-garbage-collected memory
// Needs to be in sync with
// ../cmd/compile/internal/gc/reflect.go:/^func.dumptypestructs.
type itab struct {
inter *interfacetype
_type *_type
link *itab
bad int32
unused int32
fun [1]uintptr // variable sized
}
通过上面的源码,我们可以看到Go的接口类型本质也是一个结构体,里面是两个指针,一个指向类型+函数等信息,一个指向数据信息。那么显而易见,简单来说,一个结构体实现了一个接口,把这个结构体变量赋值给这个接口变量,那么显然这个接口变量里的俩指针,就完成了数据和实现的绑定。
一个小例子:
// @file: hi.go
package main
import (
"fmt"
)
type Printer interface {
Print()
}
type User struct {
id int
name string
}
func (u *User) Print() {
fmt.Println(u.id, u.name)
}
func main() {
a := &User{
id: 1,
name: "Michel",
}
var m Printer = a
m.Print()
}
编译时带上-gcflags "-N -l"
忽略优化,然后用gdb分析如下:
go build -gcflags "-N -l" hi.go
gdb hi // gdb 拉起进程, 断main.main然后next到接口变量m赋值,或者直接断对应行
(gdb) set pr pr on // 优化输出
// 接口的数据域
(gdb) p m
$4 = {
tab = 0x4c3a20 <User,main.Printer>,
data = 0xc42007c020 // 结构数据指针 == 对应结构体指针a
}
(gdb) p a
$5 = (struct main.User *) 0xc42007c020
// 接口的函数表
(gdb) p m.tab
$6 = (runtime.itab *) 0x4c3a20 <User,main.Printer>
(gdb) p *m.tab
$7 = {
inter = 0x49b740,
_type = 0x4991c0,
hash = 1834220034,
_ = "\000\000\000",
fun = {4727024} // hex(4727024) = 0x00000000004820f0, 函数的入口地址
}
(gdb) s // step in 这个函数,看入口地址
main.(*User).Print (u=0xc42007c020) at /media/sf_VMshare/go_workspace/src/hi.go:16
16 func (u *User) Print() {
(gdb) disass
Dump of assembler code for function main.(*User).Print:
=> 0x00000000004820f0 <+0>: mov %fs:0xfffffffffffffff8,%rcx
// 接口赋值的地方的反汇编
25 var m Printer = a
=> 0x00000000004822d8 <+120>: mov %rax,0x20(%rsp)
0x00000000004822dd <+125>: lea 0x4173c(%rip),%rcx # 0x4c3a20 <go.itab.*main.User,main.Printer>
0x00000000004822e4 <+132>: mov %rcx,0x28(%rsp)
0x00000000004822e9 <+137>: mov %rax,0x30(%rsp)
这样,通过看Go runtime的源码,加上gdb调试,就很容易理解接口的底层实现原理了,进一步的Go进程内存布局也可以用同样方法深入查看。