前言
借助gdb
来查看go的底层汇编,借此梳理和分析go程序的初始化流程,看看在初始化阶段go都做了哪些工作,对于理解go的工作机制很有帮助。目前是基于go 1.16.4
进行的。
gdb调试
在 搭建gdb调试go程序 中已经探究并介绍了gdb
的环境搭建、基本使用以及如何利用gdb
来调试断点查看函数调用次序。
流程调试
如上图,是go程序初始化流程的整理,由于整个流程调用方法非常多,这里挑选较为核心部分进行梳理和分析,这里仅梳理初始化过程的调用流程和次序,用来熟悉初始化的工作机制,不涉及原理性分析,结合这张图的执行次序来说明,如下:
阶段 |
次序 |
语言环境 |
执行文件 |
执行函数 |
核心逻辑 |
1 |
1.1 |
汇编(.s) |
$GOROOT/src/runtime/rt0_darwin_amd64.s |
_rt0_amd64_darwin |
汇编引导 |
1 |
1.2 |
汇编(.s) |
$GOROOT/src/runtime/asm_amd64.s |
_rt0_amd64 |
汇编引导 |
1 |
1.3 |
汇编(.s) |
$GOROOT/src/runtime/asm_amd64.s |
rt0_go |
汇编引导 |
2 |
2.1 |
golang(.go) |
$GOROOT/src/runtime/runtime1.go |
args |
整理命令行参数 |
2 |
2.2 |
golang(.go) |
$GOROOT/src/runtime/os_darwin.go |
osinit |
确定CPU数量 |
2 |
2.3 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
schedinit |
初始化、参数、环境处理 |
2 |
2.4 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
newproc |
创建主goroutine即runtime.main对应的g |
2 |
2.5 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
mstart |
启动调度循环 |
2 |
2.6 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
main |
调用main goroutine运行,但不是用户函数入口的main.main |
3 |
2.3 => 2.3.1 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
lockInit(xxx) |
各类Lock的初始化 |
3 |
2.3 => 2.3.2 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
sched.maxmcount = 10000 |
最大线程数 |
3 |
2.3 => 2.3.3 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
stackinit() |
栈初始化 |
3 |
2.3 => 2.3.4 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
mallocinit() |
内存管理器初始化 |
3 |
2.3 => 2.3.5 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
mcommoninit() |
调度器初始化 |
3 |
2.3 => 2.3.6 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
goargs() |
命令行参数处理 |
3 |
2.3 => 2.3.7 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
goenvs() |
环境变量处理 |
3 |
2.3 => 2.3.8 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
parsedebugvars() |
调试相关参数处理 |
3 |
2.3 => 2.3.9 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
gcinit() |
垃圾回收器初始化 |
3 |
2.6 => 2.6.1 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
(64bit-1G 32bit-250M) |
Stack栈的最大限制 |
3 |
2.6 => 2.6.2 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
systemstack() |
启动系统后台监控(垃圾回收,并发调度相关) |
3 |
2.6 => 2.6.3 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
runtime_init |
runtime包内所有init函数初始化 |
3 |
2.6 => 2.6.4 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
gcenable() |
启动垃圾回收 |
3 |
2.6 => 2.6.5 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
main_init |
用户包内所有init函数初始化 |
3 |
2.6 => 2.6.6 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
main_main |
调用用户程序入口执行,由编译器动态生成 |
3 |
2.6 => 2.6.7 |
golang(.go) |
$GOROOT/src/runtime/proc.go |
exit(0) |
执行结束 |
案例分析
我们创建一个简单的项目,在项目main
函数中import
一些依赖,查看下用户main_main
的方法由编译器动态生成了什么内容。
项目目录及文件结构,大致如下:
% tree
.
├── funcs
│ └── func.go
├── main.go
funcs/func.go
文件
package funcs
import "fmt"
func init(){
fmt.Println(" funcs init")
}
func Add(a int, b int) int {
fmt.Println("Add method called.")
return a + b
}
main.go
文件
package main
import (
//这里导入项目包
"program/funcs"
"fmt"
//这里导入外部包
_ "github.com/jinzhu/gorm/dialects/mysql"
)
func init() {
fmt.Println("main init",funcs.Add(4,5))
}
func main() {
a, b := 1, 2
fmt.Println("result => ", funcs.Add(a, b))
}
通过go tool objdump -s “\.init\.0\b” [program]
反编译查看该项目来查看编译情况,这里信息比较多,删减保留主要信息如下:
//===== 系统内部初始化开始 =====
TEXT internal/bytealg.init.0(SB) /usr/local/go/src/internal/bytealg/index_amd64.go
//省略...
TEXT runtime.init.0(SB) /usr/local/go/src/runtime/cpuflags_amd64.go
//省略...
TEXT os.init.0(SB) /usr/local/go/src/os/proc.go
//省略...
proc.go:18 0x10eb852 eb8c JMP os.init.0(SB)
//省略...
//===== 项目包内初始化开始 =====
TEXT program/funcs.init.0(SB) /Users/guanjian/workspace/go/program/funcs/func.go
//省略...
func.go:6 0x10facda e8617fffff CALL fmt.Println(SB)
//省略...
func.go:5 0x10facee e96dffffff JMP program/funcs.init.0(SB)
//省略...
//===== 外部包初始化开始 =====
TEXT github.com/go-sql-driver/mysql.init.0(SB) /Users/guanjian/go/pkg/mod/github.com/go-sql-driver/mysql@v1.5.0/driver.go
//省略...
driver.go:83 0x12707e7 eb97 JMP github.com/go-sql-driver/mysql.init.0(SB)
//省略...
TEXT main.init.0(SB) /Users/guanjian/workspace/go/program/main.go
//省略...
main.go:10 0x128cec0 e83bdee6ff CALL program/funcs.Add(SB)
//省略...
main.go:10 0x128cf75 e8c65ce6ff CALL fmt.Println(SB)
//省略...
main.go:9 0x128cf96 e9e5feffff JMP main.init.0(SB)
//省略...
//省略...
下面是将以上编译文件主要信息进行了整理,梳理了初始化顺序、包依赖关系、编译文件映射
初始化顺序
通过编译文件可以梳理得知,go程序的初始化大致可以分为两个部分,分别是Go环境初始化和用户环境初始化,Go环境初始化指的是SDK内部执行流程的OS环境识别读取、参数初始化、相关底层支持函数的初始化准备等;用户环境初始化指的是项目中编写的代码逻辑,这里可以再细分为当前项目代码和外部导入包代码。加载顺序是先初始化Go环境,再根据用户代码逻辑从main.main
作为入口按序进行初始化。
包依赖关系
包依赖关系的次序与真正的初始化顺序是相反的,在整个依赖链条上最被依赖的包及其init
方法是最先被执行初始化的,相反,依赖下游的程序入口main.main
的相关初始化操作是最靠后的。
编译文件映射
总结
- 通过编译文件和gdb的调试可以得知,所有
init
函数都在同⼀个goroutine
内执⾏的,感兴趣可以参考 https://github.com/golang/go/blob/master/src/runtime/proc.go中doInit
的实现 - 所有
init
函数结束后才会执⾏main.main
函数,也就是说先完成初始化再进入程序入口,这点可以帮助我们更好地理解初始化与程序执行入口两者的次序关系,在编码时避免出现问题 import
会产生对包的依赖,如果依赖包有init
函数则会先执行,而init
函数中的内容以及依赖也会有此效果,因此不控制使用init
会产生隐藏地、不易察觉的依赖链并产生初始化一系列操作,所以慎用该功能,推荐的是只做当前局部作用域的初始化工作,以免带来不必要的问题隐患
参考
《Go语言学习笔记》 雨痕
搭建gdb调试go程序
golang底层 引导、初始化