通常来说,程序执行的入口点都不是我们所写的 main
函数,编译器会在之前插入部分代码,用来执行相关的初始化工作。本文从整体流程上分析一下 Go 编译器在我们写的代码之前究竟做了什么。
本次测试在我的电脑主机上完成,Golang 版本为 1.5.1:
$ go version
go version go1.5.1 linux/amd64
先编写一个简单的程序,如下:
test.go
package main
import "fmt"
func main(){
fmt.Println("hello world")
}
编译为可执行程序,需要注意的是:最好使用 -gcflags -N -l
禁止编译器进行优化和内联,否则实际生成代码可能会不一样:
go build -gcflags "-N -l"
然后使用 gdb 进行调试:
(gdb) info files
Symbols from "/home/smart/code/go/src/test/test".
Local exec file:
`/home/smart/code/go/src/test/test', file type elf64-x86-64.
Entry point: 0x4561d0
0x0000000000401000 - 0x00000000004b2240 is .text
0x00000000004b3000 - 0x000000000053b4db is .rodata
0x000000000053b4e0 - 0x000000000053d588 is .typelink
0x000000000053d588 - 0x000000000053d588 is .gosymtab
0x000000000053d5a0 - 0x000000000058ece5 is .gopclntab
0x000000000058f000 - 0x0000000000590bc8 is .noptrdata
0x0000000000590be0 - 0x0000000000593150 is .data
0x0000000000593160 - 0x00000000005b6d38 is .bss
0x00000000005b6d40 - 0x00000000005bbb80 is .noptrbss
0x0000000000400fc8 - 0x0000000000401000 is .note.go.buildid
可以看到,程序的入口点在 0x4561d0
处。需要注意的是,入口点不一定是 .text
段开始的位置(比如这里 0x4561d0
处就是 main.main
),该段各模块代码的顺序和链接器有关。在 Linux 下 ELF 格式中,有个 e_entry
字段指定程序的入口,然后在代码中会进行跳转。ELF 格式参考这里,可用 readelf
或 objdump
工具查看。
在此处打个 breakpoint:
(gdb) b *0x4561d0
Breakpoint 1 at 0x4561d0: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.
打开该文件,跳转到第 8 行:
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
LEAQ 8(SP), SI // argv
MOVQ 0(SP), DI // argc
MOVQ $main(SB), AX
JMP AX
这里将 argc
和 argv
压栈,并跳转到 main
代码段。不过我暂时还不清楚是在哪里将 argc
和 argv
放到 DI 和SI 寄存器中,以及参数 SB 代表的意义。
TEXT main(SB),NOSPLIT,$-8
MOVQ $runtime·rt0_go(SB), AX
JMP AX
这里跳转到 $runtime.rt0_go
,通过 breakpoint 查看:
(gdb) b runtime.rt0_go
Breakpoint 2 at 0x452b00: file /usr/local/go/src/runtime/asm_amd64.s, line 12.
在 runtime.rt0_go
中,才真正执行一系列初始化工作,关键代码如下:
TEXT runtime·rt0_go(SB),NOSPLIT,$0
......
CALL runtime·args(SB)
CALL runtime·osinit(SB)
CALL runtime·schedinit(SB)
// create a new goroutine to start program
MOVQ $runtime·mainPC(SB), AX // entry
......
CALL runtime·newproc(SB)
......
// start this M
CALL runtime·mstart(SB)
这里分别调用 runtime.args()
、runtime.osinit()
、runtime.schedinit()
、runtime.newproc()
、runtime.mstart()
几个函数,再用 gdb 查看:
(gdb) b runtime.args
Breakpoint 4 at 0x434ec0: file /usr/local/go/src/runtime/runtime1.go, line 48.
(gdb) b runtime.osinit
Breakpoint 5 at 0x424290: file /usr/local/go/src/runtime/os1_linux.go, line 172.
(gdb) b runtime.schedinit
Breakpoint 6 at 0x429eb0: file /usr/local/go/src/runtime/proc1.go, line 40.
(gdb) b runtime.newproc
Breakpoint 3 at 0x42fce0: file /usr/local/go/src/runtime/proc1.go, line 2208.
(gdb) b runtime.mstart
Breakpoint 4 at 0x42bb60: file /usr/local/go/src/runtime/proc1.go, line 674.
runtime.args()
做的事情比较简单,完成了 argc
和 argv
的初始化工作。
runtime.osinit()
获取 CPU 数目。
runtime.schedinit()
比较复杂,做了一些初始化工作,具体放到后面再看。
runtime.newproc()
则创建一个 goroutine,用来运行我们编写的 main.main()
函数。
runtime.mstart()
执行这个 M,M、P、G是什么以及他们的关系同样放到后面。
至此,程序启动前的工作就已经做完了。