utotmp_1+48(SP)
0x007c 00124 (main.go:8)MOVQ $0, ""..autotmp_1+56(SP)
0x0085 00133 (main.go:8)LEAQ type.[]int(SB), AX
0x008c 00140 (main.go:8)MOVQ AX, (SP)
0x0090 00144 (main.go:8)LEAQ ""..autotmp_2+64(SP), AX
0x0095 00149 (main.go:8)MOVQ AX, 8(SP)
0x009a 00154 (main.go:8)PCDATA $0, $1
0x009a 00154 (main.go:8)CALL runtime.convT2Eslice(SB)
0x009f 00159 (main.go:8)MOVQ 16(SP), AX
0x00a4 00164 (main.go:8)MOVQ 24(SP), CX
0x00a9 00169 (main.go:8)MOVQ AX, ""..autotmp_1+48(SP)
0x00ae 00174 (main.go:8)MOVQ CX, ""..autotmp_1+56(SP)
0x00b3 00179 (main.go:8)LEAQ ""..autotmp_1+48(SP), AX
0x00b8 00184 (main.go:8)MOVQ AX, (SP)
0x00bc 00188 (main.go:8)MOVQ $1, 8(SP)
0x00c5 00197 (main.go:8)MOVQ $1, 16(SP)
0x00ce 00206 (main.go:8)PCDATA $0, $1
0x00ce 00206 (main.go:8)CALL fmt.Println(SB)
0x00d3 00211 (main.go:9)MOVQ 88(SP), BP
0x00d8 00216 (main.go:9)ADDQ $96, SP
0x00dc 00220 (main.go:9)RET
0x00dd 00221 (main.go:7)PCDATA $0, $0
0x00dd 00221 (main.go:7)CALL runtime.panicindex(SB)
0x00e2 00226 (main.go:7)UNDEF
0x00e4 00228 (main.go:7)NOP
0x00e4 00228 (main.go:5)PCDATA $0, $-1
0x00e4 00228 (main.go:5)CALL runtime.morestack_noctxt(SB)
0x00e9 00233 (main.go:5)JMP 0
先说明一下,Go 语言汇编 FUNCDATA
和 PCDATA
是编译器产生的,用于保存一些和垃圾收集相关的信息,我们先不用 care。
以上汇编代码行数比较多,没关系,因为命令都比较简单,而且我们的 Go 源码也足够简单,没有理由看不明白。
我们先从上到下扫一眼,看到几个关键函数:
CALL runtime.makeslice(SB)
CALL runtime.convT2Eslice(SB)
CALL fmt.Println(SB)
CALL runtime.morestack_noctxt(SB)
1 |
创建slice |
2 |
类型转换 |
3 |
打印函数 |
4 |
栈空间扩容 |
1
是创建 slice 相关的;2
是类型转换;调用 fmt.Println
需要将 slice 作一个转换; 3
是打印语句;4
是栈空间扩容函数,在函数开始处,会检查当前栈空间是否足够,不够的话需要调用它来进行扩容。暂时可以忽略。
调用了函数就会涉及到参数传递,Go 的参数传递都是通过 栈空间完成的。接下来,我们详细分析这整个过程。
1 |
main 函数定义,栈帧大小为 96B |
2-4 |
判断栈是否需要进行扩容,如果需要则跳到 228 ,这里会调用 runtime.morestack_noctxt(SB) 进行栈扩容操作。具体细节后续还会有文章来讲 |
5-9 |
将 caller BP 压栈,具体细节后面会讲到 |
10-15 |
调用 runtime.makeslice(SB) 函数及准备工作。*_type表示的是 int ,也就是 slice 元素的类型。这里对应的源码是第6行,也就是调用 make 创建 slice 的那一行。5 和 10 分别代表长度和容量,函数参数会在栈顶准备好,之后执行函数调用命令 CALL ,进入到被调用函数的栈帧,就会按顺序从 caller 的栈顶取函数参数 |
16-18 |
接收 makeslice 的返回值,通过 move 移动到寄存器中 |
19-21 |
给数组索引值为 2 的元素赋上值 2 ,因为是 int 型的 slice ,元素大小为8字节,所以 MOVQ $2, 16(AX) 此命令就是将 2 搬到索引为 2 的位置。这里还会对索引值的大小进行检查,如果越界,则会跳转到 221 ,执行 panic 函数 |
22-26 |
分别通过寄存器 AX,CX,DX 将 makeslice 的返回值 move 到内存的其他位置,也称为局部变量,这样就构造出了 slice |
左边是栈上的数据,右边是堆上的数据。array
指向 slice
的底层数据,被分配到堆上了。注意,栈上的地址是从高向低增长;堆则从低向高增长。栈左边的数字表示对应的汇编代码的行数,栈右边箭头则表示栈地址。(48)SP、(56)SP 表示的内容接着往下看。
注意,在图中,栈地址是从下往上增长,所以 SP 表示的是图中 *_type
所在的位置,其它的依此类推。
27-32 |
准备调用 runtime.convT2Eslice(SB) 的函数参数 |
33-36 |
接收返回值,通过AX,CX寄存器 move 到(48)SP、(56)SP |
convT2Eslice
的函数声明如下:
func convT2Eslice(t *_type, elem unsafe.Pointer) (e eface)
第一个参数是指针 *_type
,_type
是一个表示类型的结构体,这里传入的就是 slice
的类型 []int
;第二个参数则是元素的指针,这里传入的就是 slice
底层数组的首地址。
返回值 eface
的结构体定义如下:
type eface struct {
_type *_type
data unsafe.Pointer
}
由于我们会调用 fmt.Println(slice)
,看下函数原型:
func Println(a ...interface{}) (n int, err error)
Println
接收 interface 类型,因此我们需要将 slice
转换成 interface 类型。由于 slice
没有方法,是个“空 interface
”。因此会调用 convT2Eslice
完成这一转换过程。
convT2Eslice
函数返回的是类型指针和数据地址。源码就不贴了,大体流程是:调用 mallocgc
分配一块内存,把数据 copy
进到新的内存,然后返回这块内存的地址,*_type
则直接返回传入的参数。
32(SP)
和 40(SP)
其实是 makeslice
函数的返回值,这里可以忽略。
还剩 fmt.Println(slice)
最后一个函数调用了,我们继续。