本文主要是通过GDB对go语言程序进行调试,探究go程序的启动过程。
总览
go语言具有内存管理、并发调度、垃圾回收等特性,go语言在编译阶段会在用户的主函数中插入引导程序,即go语言的入口函数不是用户的main.main()
,而是runtime/rt0_linux_amd64.s的_rt0_amd64_linux(SB)
。go语言通过引导程序来实现对命令行参数的读取、对环境变量的读取、运行环境的检查、内存管理的初始化、调度器的初始化、垃圾回收器的初始化等操作,其具体流程如图所示。
go程序的启动,首先进行了运行时类型的检查;然后读取了两个常用的运行时常量,即处理器核心数和内存物理页大小;后续,进行了调度器、内存空间、命令行参数、系统环境变量、垃圾收集器的初始化,其中调度器根据处理器核心数进行了处理器process数量的选取;随后,通过创建了新的G并对其进行调度,来执行runtime.main函数;在runtime.main函数中,进行了runtime包init方法的执行、垃圾收集器的启动、用户包init方法的执行、用户主函数的执行。
准备
- 相关软件
linux
gdb
go -> go1.7.4 linux/amd64
- 示例程序
// test.go
package main
func main() {
println("Hello World!")
}
- 编译程序
# -o 指定输出文件路径
# -gcflags "-N" 关闭编译器代码优化 disable optimizations
# -gcflags "-l" 关闭函数内联 disable inlining
go build -gcflags "-N -l" -o test test.go
- 使用GDB调试
gdb -tui ./test
# gdb常用命令如下:
# b 100 在第100行设置断点
# run 运行程序
# c 运行到下一个断点
# step 步进,进入函数
# next 运行下一行代码
启动过程
通过gdb调试,发现go语言的程序入口在runtime/rt0_linux_amd64.s _rt0_amd64_linux(SB)
,之后会跳转到runtime/asm_amd64.s runtime·rt0_go(SB)
。
- runtime/asm_amd64.s
TEXT runtime·rt0_go(SB),NOSPLIT,$0
// 复制参数数量argc和参数值argv到栈上
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)
// 初始化 g0 执行栈
MOVQ $runtime·g0(SB), DI // DI = g0
LEAQ (-64*1024+104)(SP), BX
MOVQ BX, g_stackguard0(DI) // g0.stackguard0 = SP + (-64*1024+104)
MOVQ BX, g_stackguard1(DI) // g0.stackguard1 = SP + (-64*1024+104)
MOVQ BX, (g_stack+stack_lo)(DI) // g0.stackguard1 = SP + (-64*1024+104)
MOVQ SP, (g_stack+stack_hi)(DI) // g0.stackguard1 = SP + (-64*1024+104)
...
// 该函数在 runtime/runtime1.go/check(),进行各种检查,包括类型的长度Sizeof、结构体字段的偏移量、
// CAS操作、指针操作、原子操作、汇编指令、栈大小检查等
CALL runtime·check(SB)
...
// 该函数在runtime/runtime1.go/args(c int32, v **byte) 进行命令行参数的初始化
CALL runtime·args(SB)
// 该函数在runtime/os_linux.go/osinit() 读取操作系统的CPU核数
CALL runtime·osinit(SB)
// 该函数在runtime/proc.go/schedinit() 调度器的初始化,涉及内存空间的初始化、命令行参数的初始化、
// 垃圾收集器参数的初始化、调度器process的设置等
CALL runtime·schedinit(SB)
// create a new goroutine to start program
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
PUSHQ $0 // arg size
// 该函数在runtime/proc.go/newproc(siz int32, fn *funcval)
// 创建一个新的G,并将G放到runtime的队列中,这个G用于执行runtime.main函数
CALL runtime·newproc(SB)
POPQ AX
POPQ AX
// start this M
// runtime/proc.go/mstart() 开始调度,从队列里面取G进行执行,并执行runtime.main函数
// 在runtime.main的执行中,会依次执行runtime中的init函数、启动GC收集器、
// 执行用户包的init函数、执行用户的main函数
CALL runtime·mstart(SB)
MOVL $0xf1, 0xf1 // crash
RET
参数初始化runtime.args
- 这里主要是对命令行中的参数进行处理,将参数数量赋值给
argc int32
,将参数值复制给argv **byte
供相关函数使用。 - mac采用mach-o格式的可执行文件,mac系统的
args(c int32, v **byte)
函数处理比较简单,只需要将命令的执行路径赋值给os_darwin.go
的executablePath
变量,executablePath
变量通过go:linkname
将值赋给os.executablePath
。 - linux系统的相对复杂,涉及到ELF格式,可以参考golang.design/under-the-h…
// runtime/runtime1.go
var (
argc int32
argv **byte
)
func args(c int32, v **byte) {
argc = c
argv = v
sysargs(c, v) // runtime/os_linux.go:sysargs(c, v)
}
系统初始化runtime.osinit
- 这里主要是通过
sched_getaffinity
获取当前进程绑定的CPU个数,这里涉及CPU的亲和性,可以参考:zhuanlan.zhihu.com/p/57470627
// runtime/os_linux.go/osinit()
var ncpu int32
func osinit() {
// 获取CPU核数
ncpu = getproccount()
}
func getproccount() int32 {
const maxCPUs = 64 * 1024
var buf [maxCPUs / (sys.PtrSize * 8)]uintptr
// 通过sched_getaffinity获取当前进程的CPU亲和力
r := sched_getaffinity(0, unsafe.Sizeof(buf), &buf[0])
n := int32(0)
for _, v := range buf[:r/sys.PtrSize] {
for v != 0 {
n += int32(v & 1)
v >>= 1
}
}
if n == 0 {
n = 1
}
return n
}
调度器初始化runtime.schedinit
- 这里进行调取器的初始化,主要涉及到执行栈空间、堆内存空间、hash算法、命令行参数、环境变量、GC垃圾收集器参数、调度器processor数量的初始化。值得注意的是,这里仅设置垃圾收集器的参数,并没有启动垃圾收集器。垃圾收集器的启动是在runtime.main函数中进行的。
// runtime/proc.go/schedinit()
func schedinit() {
_g_ := getg()
if raceenabled {
// 启动go的并发竞争探测器,具体用法见https://blog.golang.org/race-detector
_g_.racectx, raceprocctx0 = raceinit()
}
sched.maxmcount = 10000 // 最大系统线程数量
tracebackinit()
moduledataverify()
/**
执行栈初始化, runtime/stack.go/stackinit(),涉及到go的栈空间管理
主要将runtime/stack.go的stackpool和stackLarge进行初始化
这两个变量采用MSpanList进行内存管理
*/
stackinit()
/**
进行堆内存的初始化(内存分配器)的初始化
同时给当前G分配一个mcache
runtime/malloc.go/mallocinit()
*/
mallocinit()
/**
初始化g的系统线程
https://www.cnblogs.com/abozhang/p/10813229.html
*/
mcommoninit(_g_.m)
/**
根据CPU设置各种hash摘要算法,主要用于map的hash
参考https://studygolang.com/articles/7211
*/
alginit() // maps must not be used before this call
/**
涉及到外部包的映射
*/
typelinksinit() // uses maps
/**
应该类型相关的初始化
*/
itabsinit()
/**
设置信号屏蔽
*/
msigsave(_g_.m)
initSigmask = _g_.m.sigmask
/**
通过go:linkname加载命令行参数到os.Args
*/
goargs()
/**
通过go:linkname加载操作系统env变量os.Environ()
*/
goenvs()
/**
解析go环境变量中的“GODEBUG”和“GOTRACEBACK”,并设置runtime的debug相关变量
*/
parsedebugvars()
/**
垃圾回收器的初始化,这里仅设置参数,并没有启动垃圾收集器
垃圾收集器的启动在runtime.main函数中进行
*/
gcinit()
/**
设置MPG模型里面的procs数量,依据_MaxGomaxprocs、GOMAXPROCS环境变量、当前进程绑定的ncpu数量
*/
sched.lastpoll = uint64(nanotime())
procs := int(ncpu)
if procs > _MaxGomaxprocs {
procs = _MaxGomaxprocs
}
if n := atoi(gogetenv("GOMAXPROCS")); n > 0 {
if n > _MaxGomaxprocs {
n = _MaxGomaxprocs
}
procs = n
}
/**
设置调度器的proce的数量, STW操作
*/
if procresize(int32(procs)) != nil {
throw("unknown runnable goroutine during bootstrap")
}
if buildVersion == "" {
// Condition should never trigger. This code just serves
// to ensure runtime·buildVersion is kept in the resulting binary.
buildVersion = "unknown"
}
}
主函数调度runtime·newproc和runtime.mstart
- 这里主要是通过
runtime/proc.go/newproc
创建一个G,用于执行runtime.main,并将其加入到队列中。 - 然后调用
runtime/proc.go/mstart
开始进行调度,执行队列里面的runtime.main的G
/**
runtime/proc.go/newproc(siz int32, fn *funcval)
siz表示fn函数参数的bytes大小
fn表示需要G执行的函数
在启动场景中,fn是runtime/proc.go:runtime.main(),所以siz为0
这里的操作主要是,创建一个新的g,用于执行runtime.main;创建完成后,将新的g放到队列中,等待被调度执行。
*/
func newproc(siz int32, fn *funcval) {
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
pc := getcallerpc(unsafe.Pointer(&siz))
systemstack(func() {
// runtime/proc.go/newproc1(fn *funcval, argp *uint8, narg int32, nret int32, callerpc uintptr) *g
newproc1(fn, (*uint8)(argp), siz, 0, pc)
})
}
/**
runtime/proc.go/mstart()
进行调度
*/
func mstart() {
_g_ := getg()
...
mstart1()
}
func mstart1() {
_g_ := getg()
...
asminit()
minit() //runtime/os_linux.go/minit() 初始化m的sigal的栈和mask
...
schedule() // 开始调度执行,最终会执行runtime/proc.go/main()
}
// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule() {
_g_ := getg()
...
execute(gp, inheritTime) // 在这里会执行runtime.main
}
主函数执行runtime.main
runtime.main的G被调度之后,依次进行了runtime包内init方法的执行、GC垃圾收集器的启动、用户包init方法的执行、用户main主函数的执行。
// The main goroutine.
func main() {
g := getg()
...
// 执行栈最大限制:1GB(64位系统)或者 250MB(32位系统)
if sys.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}
...
// 启动系统后台监控(定期垃圾回收、抢占调度等等)
systemstack(func() {
newm(sysmon, nil)
})
...
// 让goroute独占当前线程,
// runtime.lockOSThread的用法详见http://xiaorui.cc/archives/5320
lockOSThread()
...
// runtime包内部的init函数执行
runtime_init() // must be before defer
// Defer unlock so that runtime.Goexit during init does the unlock too.
needUnlock := true
defer func() {
if needUnlock {
unlockOSThread()
}
}()
// 启动GC
gcenable()
...
// 用户包的init执行
main_init()
...
needUnlock = false
unlockOSThread()
...
// 执行用户的main主函数
main_main()
...
// 退出
exit(0)
for {
var x *int32
*x = 0
}
}
结论
通过对go程序启动的分析,Go语言并不是直接执行用户的main函数,而是在执行用户的main函数之前进行了go程序运行时的初始化与启动,包括调度器、栈空间、内存分配器、垃圾收集器等组件的初始化和启动,最后通过newproc
和mstart
调度执行runtime.main
,并执行用户的主函数。
由此可见,内存分配器、垃圾回收器、调度器是go运行时的主要组成部分。
参考
- go的栈内存管理:blog.csdn.net/kevin_tech/…
- go的栈内存管理:blog.csdn.net/weixin_3413…
- go的内存管理:studygolang.com/articles/72…
- go的内存管理:segmentfault.com/a/119000002…
- 欧长坤go语言原本之go程序启动:golang.design/under-the-h…
- go关键字 调度:www.cnblogs.com/33debug/p/1…
- go运行时启动过程: studygolang.com/articles/72…
- go语言学习笔记:github.com/qyuhen/book