通过之前《Go语言编译链接过程》我们知道Go程序需要经过编译链接成可执行程序才能到指定平台上运行,经过 ‘go build main.go’ 会在比如在windows下是.exe可执行程序,在 linux 平台上是 ELF 格式的可执行文件。
编辑切换为居中
添加图片注释,不超过 140 字(可选)
编译完后就行执行可执行程序,执行的时候可执行二进制文件会被操作系统加载起来运行,通常分为以下几个阶段:
那么可执行程序实际的启动流程是怎么样的呢?
很多 Go 语言的开发者都知道我们可以使用下面的命令将 Go 语言的源代码编译成汇编语言,然后通过汇编语言分析程序具体的执行过程。
// 源代码
package main
import "fmt"
func main() {
go Nice()
}
func Nice() {
fmt.Println("hello xiaoxu code")
}
通过命令 go build -gcflags -S main.go 或者 go tool compile -N -l -S main.go 获得go程序汇编
//得到如下汇编代码
...
"".Nice STEXT size=144 args=0x0 locals=0x58
0x0000 00000 (D:\project\src\Arnold\src\demo\main.go:9) TEXT "".Nice(SB), ABIInternal, $88-0
0x0000 00000 (D:\project\src\Arnold\src\demo\main.go:9) MOVQ TLS, CX
0x0009 00009 (D:\project\src\Arnold\src\demo\main.go:9) PCDATA $0, $-2
0x0009 00009 (D:\project\src\Arnold\src\demo\main.go:9) MOVQ (CX)(TLS*2), CX
0x0010 00016 (D:\project\src\Arnold\src\demo\main.go:9) PCDATA $0, $-1
0x0010 00016 (D:\project\src\Arnold\src\demo\main.go:9) CMPQ SP, 16(CX)
0x0014 00020 (D:\project\src\Arnold\src\demo\main.go:9) PCDATA
...
Go 程序启动后需要对自身运行时进行初始化,其真正的程序入口由 runtime 包控制,同时针对不同的系统平台,在src/runtime目录下游ret0开头的汇编文件,比如windows下的ret0*_*windows_amd64.s,linux系统下是在src/runtime/rt0_linuxamd64.s*。*他们都是指向ret_amd64(),程序编译为机器码之后, 依赖特定 CPU 架构的指令集,而操作系统的差异则是直接反应在运行时进行不同的系统级操作上。
TEXT _rt0_amd64_windows(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
而_rt0amd64()在src/runtime/asm_amd64.s,真实的入口就是runtime·rt0_go。
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB)
// go程序启动时进行初始化工作
TEXT runtime·rt0_go(SB),NOSPLIT,$0
// copy arguments forward on an even stack
MOVQ DI, AX // argc
MOVQ SI, BX // argv
SUBQ $(4*8+7), SP // 2args 2auto
ANDQ $~15, SP
MOVQ AX, 16(SP)
MOVQ BX, 24(SP)
#ifdef GOOS_android
MOVQ $runtime·tls_g(SB), DX // arg 3: &tls_g
// arg 4: TLS base, stored in slot 0 (Android's TLS_SLOT_SELF).
// Compensate for tls_g (+16).
MOVQ -16(TLS), CX
#else
MOVQ $0, DX // arg 3, 4: not used when using platform's TLS
MOVQ $0, CX
#endif
#ifdef GOOS_windows
....
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
PUSHQ AX
PUSHQ $0 // arg size
CALL runtime·newproc(SB) //创建第一个goroutine,习惯成为main goroutine
// start this M
CALL runtime·mstart(SB) //启动 M 主线程调度上面创建的main goroutine
...
这里只截取了一部分ret_go的代码,其中前面部分包括了系统相关检查,在 #ifdef 和 #endif 之间,后半部分才是核心启动部分,核心部分包括下面这些点:
这些启动的顺序其实在schedinit()函数有注释的,这里就很清楚的说明了启动的调用序列。
// src/runtime/proc.go schedinit函数所在文件
// The bootstrap sequence is:
//
// call osinit
// call schedinit
// make & queue new G (其实就是newproc,go关键字创建goroutine就是newproc函数)
// call runtime·mstart
这些runtime的核心函数其实都是调用src/runtime下对应的函数,比如schedinit就是在 src/runtime/proc.go,接着就是各种初始化。
// 这里的注释就更加清楚的说明了启动的顺序
func schedinit() {
_g_ := getg()
(...)
// 栈、内存分配器、调度器相关初始化
sched.maxmcount = 10000 // 限制最大系统线程数量
stackinit() // 初始化执行栈
mallocinit() // 初始化内存分配器
mcommoninit(_g_.m) // 初始化当前系统线程
(...)
gcinit() // 垃圾回收器初始化
(...)
// 创建 P
// 通过 CPU 核心数和 GOMAXPROCS 环境变量确定 P 的数量
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
procresize(procs)
(...)
}
这里从网上盗一张图吧,它把整个调用脉络讲的很清楚。