Go 程序启动流程(go开发windows程序)
 南窗  分类:IT技术  人气:195  回帖:0  发布于1年前 收藏

1:前言

通过之前《Go语言编译链接过程》我们知道Go程序需要经过编译链接成可执行程序才能到指定平台上运行,经过 ‘go build main.go’ 会在比如在windows下是.exe可执行程序,在 linux 平台上是 ELF 格式的可执行文件。

编辑切换为居中

添加图片注释,不超过 140 字(可选)

编译完后就行执行可执行程序,执行的时候可执行二进制文件会被操作系统加载起来运行,通常分为以下几个阶段:

  1. 从磁盘上把可执行程序读入内存;
  2. 创建进程和主线程;
  3. 为主线程分配栈空间;
  4. 把由用户在命令行输入的参数拷贝到主线程的栈;
  5. 把主线程放入操作系统的运行队列等待被调度执起来运行

那么可执行程序实际的启动流程是怎么样的呢?

2:Go程序启动流程

1:如何获得Go汇编

很多 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
...

2:程序入口

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 之间,后半部分才是核心启动部分,核心部分包括下面这些点:

  1. schedinit:进行各种运行时组件初始化工作,这包括我们的调度器与内存分配器、回收器的初始化
  2. newproc:负责根据主 goroutine(即 main)入口地址创建可被运行时调度的执行单元,这里的main还不是用户的main函数,是 runtime.main mstart
  3. 开始启动调度器的调度循环, 执行队列中 入口方法是 runtime.main 的 G

这些启动的顺序其实在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)
 (...)
}

3:总结

这里从网上盗一张图吧,它把整个调用脉络讲的很清楚。

讨论这个帖子(0)垃圾回帖将一律封号处理……