go版本:go1.19
操作系统:linux
系统架构:amd64
go version go1.19 linux/amd64
本文主要分析在go程序中,编写的main函数是如何被执行的。
流程总览图
从程序执行入口开始
利用gdb确定程序执行入口
编写一个简单的go程序
//main.go
package main
import "fmt"
func main() {
fmt.Println("start")
}将程序编译成可执行文件
go build -gcflags "-N -l" main.go
gdb调试
gdb main
输入info files可以看到main文件的执行入口是0x45bfa0
在0x45bfa0处打一个断点(注意有个*号),可以看到程序从rt0_linux_amd64.s文件的第8行开始执行
扔掉gdb,从汇编函数开始分析
从汇编函数开始
go的汇编代码是通过plan9汇编写的
rt0_linux_amd64.s文件
这里可以看到执行了一个JMP指令,执行到_rt0_amd64函数
_rt0_amd64函数
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // DI = argc,argc表示命令行参数的个数
LEAQ 8(SP), SI // SI = argv,argv表示命令行的值
JMP runtime·rt0_go(SB) //跳转到rt0_go
保存两个命令行参数到寄存器中,相当于c++中的main函数int main(int argc,char **argv)
在go程序中,可以用os.argv获取argv的值,如go run xxx.go a b c d
,argc = 5,argv=[/{{file_root}}/xxx.go,a,b,c,d]
rt0_go函数
逐行分析这个函数
栈相关
- 保存命令行参数到栈中
- 设置g0的堆栈保护
TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
// copy arguments forward on an even stack
MOVQ DI, AX // AX = argc
MOVQ SI, BX // BX = argv
SUBQ $(5*8), SP // SP -= 40 栈顶向下扩展40字节
ANDQ $~15, SP //SP = SP & 10000,将SP的后四位置为0,内存地址16字节对齐,后4位必须为0
MOVQ AX, 24(SP) // SP + 24 = argc = AX(表示距SP地址24字节处,保存AX的值)
MOVQ BX, 32(SP) // SP + 32 = argv = BX 将命令行参数保存到线程栈中
// create istack out of the given (operating system) stack.
// _cgo_init may update stackguard.
MOVQ $runtime·g0(SB), DI //DI = g0的地址,DI = &g0,g0是个全局变量,定义在/src/runtime/proc.go文件中
LEAQ (-64*1024+104)(SP), BX //BX = SP -64*1024 + 104, BX = SP - 63KB
MOVQ BX, g_stackguard0(DI) //g0.stackguard0 = BX,用于检查栈溢出
MOVQ BX, g_stackguard1(DI) //g0.stackguard1 = BX,和g0.stackguard0一样
MOVQ BX, (g_stack+stack_lo)(DI) //g0.stack.lo = BX
MOVQ SP, (g_stack+stack_hi)(DI) //g0.stack.hi = SP,g0.stack,g0的执行堆栈
检查处理器
- 获取CPU信息检查是不是intel处理器
- 将CPU的相关信息保存到全局变量
MOVL $0, AX //COUID参数
CPUID //返回CPU的信息,如果获取到CPU信息,AX大于0
CMPL AX, $0 //比较
JE nocpuinfo //获取不到CPU信息,跳转到nocpuinfo
//获取到CPU信息,判断是不是intel处理器,如果是BX = "Genu",DX = "ineI",CX = "ntel"
CMPL BX, $0x756E6547 // "Genu"
JNE notintel
CMPL DX, $0x49656E69 // "ineI"
JNE notintel
CMPL CX, $0x6C65746E // "ntel"
JNE notintel
MOVB $1, runtime·isIntel(SB) //标记是intel,全局变量,同g0
notintel:
// Load EAX=1 cpuid flags
MOVL $1, AX //获取CPU功能的信息
CPUID
MOVL AX, runtime·processorVersionInfo(SB)//保存到processorVersionInfo变量
nocpuinfo:
// if there is an _cgo_init, call it.
MOVQ _cgo_init(SB), AX //检查_cgo_init是否初始化
TESTQ AX, AX //判断AX是否等于0
JZ needtls //等于跳转到needtls
// arg 1: g0, already in DI
MOVQ $setg_gcc<>(SB), SI // arg 2: setg_gcc
…… //nocpuinfo的其他信息省略掉,直接看needtls
设置TLS(本地线程存储)
- 将TLS关联到m0(全局变量)的tls属性中
- 检查设置是否有误
needtls:
…… //ifdef条件都不满足,省略掉这部分代码
LEAQ runtime·m0+m_tls(SB), DI //DI = &m0.tls,m0全局变量
CALL runtime·settls(SB) //设置tls为&m0.tls[0]
// store through it, to make sure it works
get_tls(BX) //BX = TLS
MOVQ $0x123, g(BX) //m0.tls[0] = 0x123
MOVQ runtime·m0+m_tls(SB), AX //AX = m0.tls[0]
CMPQ AX, $0x123
JEQ 2(PC) //相等,跳过runtime·abort(SB)
CALL runtime·abort(SB)
m0和g0的相互绑定
- 将当前工作的协程g0设置到TLS中
- 将g0和m0相互绑定
ok:
get_tls(BX)
LEAQ runtime·g0(SB), CX //CX = &runtime.g0
MOVQ CX, g(BX) //runtime.m0.tls[0] = &runtime.g0
LEAQ runtime·m0(SB), AX //AX = &runtime.m0
// save m->g0 = g0
MOVQ CX, m_g0(AX) //runtime.m0.g0 = runtime.g0
// save m0 to g0->m
MOVQ AX, g_m(CX) //runtime.g0.m = runtime.m0
CLD
初始化操作
- 初始化命令行参数相关数据
- 初始化操作系统相关数据
- 初始化调度器相关数据
…… //一堆ifdef条件,不满足,省略掉
CALL runtime·check(SB) //检查各种基本函数调用有没有问题,unsafe.sizeOf,atomic操作等
MOVL 24(SP), AX // AX = argc,在第一步操作已经在参数保存在栈位置
MOVL AX, 0(SP) // SP = argc
MOVQ 32(SP), AX // BX = argv
MOVQ AX, 8(SP) // SP + 8 = argv
CALL runtime·args(SB) //初始化命令行参数相关数据
CALL runtime·osinit(SB) //初始化操作系统相关数据
CALL runtime·schedinit(SB) //初始化调度器相关数据
创建goroutine调用runtime.main函数
- 将
runtime.main
函数加入到调度队列中 - 创建一个内核线程
// create a new goroutine to start program
MOVQ $runtime·mainPC(SB), AX // 这里就是我们runtime.main函数的调用地址,这个runtime.main函数会调用我们写的main函数,下面给出是如何关联的
PUSHQ AX //将AX压入到栈中
CALL runtime·newproc(SB) //调用newproc函数,创建一个gorountine,绑定到GMP中的p中,我们在代码中执行 go func()开启一个协程,调用的就是这个函数,可以理解为执行go runtime.main(),这里加入的协程执行队列,还没执行
POPQ AX //AX出栈
// start this M
CALL runtime·mstart(SB) //m开始执行协程任务
CALL runtime·abort(SB) //这里也就不会执行 mstart should never return
RET
mstart函数
TEXT runtime·mstart(SB),NOSPLIT|TOPFRAME,$0
CALL runtime·mstart0(SB) //调用mstart0函数
RET // not reached
mstart0函数
设置堆栈保护边界
func mstart0() {
_g_ := getg() //从TLS获取g,这里是g0
osStack := _g_.stack.lo == 0//在第1步已经设置了g0.stack.lo的值了,这里为false
if osStack {
…… //不执行省略掉
}
_g_.stackguard0 = _g_.stack.lo + _StackGuard //留出一定空间保护防止栈溢出
_g_.stackguard1 = _g_.stackguard0 //留出一定空间保护防止栈溢出
mstart1() //不会退出
if mStackIsSystemAllocated() {
osStack = true
}
mexit(osStack)
}
mstart1函数
保存g0的父调度信息
func mstart1() {
_g_ := getg() //g0
if _g_ != _g_.m.g0 {
throw("bad runtime·mstart")
}
_g_.sched.g = guintptr(unsafe.Pointer(_g_)) //g0.sched.g = g0
_g_.sched.pc = getcallerpc() //调用者PC值
_g_.sched.sp = getcallersp() //调用者SP值
asminit() //空实现
minit() //初始化信号相关操作,并设置线程id
if _g_.m == &m0 {
mstartm0() //初始化信号
}
if fn := _g_.m.mstartfn; fn != nil { //对于启动流程来说,不执行
fn()
}
if _g_.m != &m0 { //不满足条件,不执行
acquirep(_g_.m.nextp.ptr())
_g_.m.nextp = 0
}
schedule() //调度
}
schedule调度函数
寻找一个可执行的goroutine执行
func schedule() {
_g_ := getg() //获取当前g
if _g_.m.locks != 0 { //不满足条件
throw("schedule: holding locks")
}
if _g_.m.lockedg != 0 { //不满足条件
stoplockedm()
execute(_g_.m.lockedg.ptr(), false) // Never returns.
}
if _g_.m.incgo { //不满足条件
throw("schedule: in cgo")
}
top:
pp := _g_.m.p.ptr() //GMP模型中的p
pp.preempt = false //当前p抢占标记为false
if _g_.m.spinning && (pp.runnext != 0 || pp.runqhead != pp.runqtail) {
throw("schedule: spinning with local work")
}
gp, inheritTime, tryWakeP := findRunnable() //找到一个可运行的g,这里只有一个g,那就是runtime.main函数(第6步,CALL runtime·newproc(SB)设置的),先不展开
if _g_.m.spinning { //启动流程不会执行
resetspinning()
}
if sched.disable.user && !schedEnabled(gp) { //启动流程不会执行
…… //不执行省略掉
}
if tryWakeP { //启动流程不会执行
wakep()
}
if gp.lockedm != 0 { //启动流程不会执行
startlockedm(gp)
goto top
}
execute(gp, inheritTime) //运行g
}
execute函数
将goroutine与m0绑定,g在m0上运行
func execute(gp *g, inheritTime bool) {
//gp:执行runtime.main函数的g,我们描述为 main_g
_g_ := getg() //_g_:g0
if goroutineProfile.active { //false,不管
tryRecordGoroutineProfile(gp, osyield)
}
//将main_g与m相互绑定关联
_g_.m.curg = gp //设置当前运行g为main_g
gp.m = _g_.m //设置main_g与m绑定
casgstatus(gp, _Grunnable, _Grunning) //将main_g的状态从可运行的设置为运行中
gp.waitsince = 0 //main_g的阻塞时间
gp.preempt = false //标记为可抢占
gp.stackguard0 = gp.stack.lo + _StackGuard //设置栈溢出保护
if !inheritTime { //inheritTime为true
_g_.m.p.ptr().schedtick++ //没有继承的时间,p的调度次数加1
}
//确认cpu分析器是否需要打开
hz := sched.profilehz
if _g_.m.profilehz != hz { //不管
setThreadCPUProfiler(hz)
}
if trace.enabled { //不管
// GoSysExit has to happen when we have a P, but before GoStart.
// So we emit it here.
if gp.syscallsp != 0 && gp.sysblocktraced {
traceGoSysExit(gp.sysexitticks)
}
traceGoStart()
}
gogo(&gp.sched) //执行runtime.main函数
}
gogo函数
获取gorountine的上下文信息,执行runtime.main函数
// func gogo(buf *gobuf) //buf = &gp.sched
TEXT runtime·gogo(SB), NOSPLIT, $0-8
MOVQ buf+0(FP), BX // BX = buf, FP:是一个寄存器,它指向当前函数的栈帧
MOVQ gobuf_g(BX), DX // DX = buf.g
MOVQ 0(DX), CX //CX = DX = buf.g,make sure g != nil
JMP gogo<>(SB) //调到gogo<>执行,也就是下面的函数
TEXT gogo<>(SB), NOSPLIT, $0
get_tls(CX) // CX = tls
MOVQ DX, g(CX) // 将当前运行的g,保存在tls中,getg()才能获取到当前运行的g
MOVQ DX, R14 // R14 = g set the g register
MOVQ gobuf_sp(BX), SP //设置g的栈顶, SP = buf.sp restore SP
MOVQ gobuf_ret(BX), AX // AX = buf.ret
MOVQ gobuf_ctxt(BX), DX // DX = buf.ctxt
MOVQ gobuf_bp(BX), BP //设置g的栈底 BP = buf.bp
MOVQ $0, gobuf_sp(BX) // 清空gobuf大部分信息 clear to help garbage collector
MOVQ $0, gobuf_ret(BX)
MOVQ $0, gobuf_ctxt(BX)
MOVQ $0, gobuf_bp(BX)
MOVQ gobuf_pc(BX), BX // BX = buf.pc,buf.pc就是runtime.main函数的地址,下面runtime.newproc的执行简单分析节,分析这个为什么是runtime.main函数
JMP BX // 执行runtime.main函数
runtime.newproc的执行简单分析
这里只先简单分析一下runtime.main是如何被保存到goroutine上的
newproc函数
func newproc(fn *funcval) {
gp := getg() //获取当前g
pc := getcallerpc() //获取caller pc
systemstack(func() { //systemstack:切换到系统(线程)栈,如果是g0的话,就在g0的栈上执行,不切换,简单了解一下,可以不关注
newg := newproc1(fn, gp, pc)
_p_ := getg().m.p.ptr()
runqput(_p_, newg, true) //将新开的goroutine保存到本地的p,p是GMP模型中的p
if mainStarted { //这个时候还是false,runtime.main函数执行时,会置为true
wakep()
}
})
}newproc1函数
func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
_g_ := getg() //g0
if fn == nil {
fatal("go of nil func value")
}
acquirem() // 禁止抢占,m上不会再切换其他g进来
_p_ := _g_.m.p.ptr() //拿到p
newg := gfget(_p_) //拿一个dead G,当前协程执行结束之后,goroutine不会马上回收,而是将goroutine的状态设置为dead G,复用goroutine
if newg == nil { //拿不到G,就分配一个
newg = malg(_StackMin) //创建一个g对象
casgstatus(newg, _Gidle, _Gdead) //置为Dead状态
allgadd(newg) // 加到全局g对象中
}
if newg.stack.hi == 0 {
throw("newproc1: newg missing stack")
}
if readgstatus(newg) != _Gdead {
throw("newproc1: new g is not Gdead")
}
totalSize := uintptr(4*goarch.PtrSize + sys.MinFrameSize) //额外空间,读取时稍微超出
totalSize = alignUp(totalSize, sys.StackAlign) //调整大小
sp := newg.stack.hi - totalSize //goroutine的栈顶
spArg := sp
if usesLR { //false
// caller's LR
*(*uintptr)(unsafe.Pointer(sp)) = 0
prepGoExitFrame(sp)
spArg += sys.MinFrameSize
}
//清楚上一个goroutine执行的上下文信息
memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
//调度相关数据设置
newg.sched.sp = sp //栈顶
newg.stktopsp = sp //检查traceback,不管
newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum //函数的退出地址
newg.sched.g = guintptr(unsafe.Pointer(newg)) //g本身
gostartcallfn(&newg.sched, fn) //设置newg.sched.buf.pc = fn.fn,fn.fn就是runtime.main函数,还有newg.sched.buf的其他信息
newg.gopc = callerpc //设置goroutine的调用者pc
newg.ancestors = saveAncestors(callergp) //设置goroutine的祖先
newg.startpc = fn.fn //设置goroutine的startpc
if isSystemGoroutine(newg, false) { //false
atomic.Xadd(&sched.ngsys, +1)
} else {
if _g_.m.curg != nil {
newg.labels = _g_.m.curg.labels //不关注,设置goroutines的labels
}
if goroutineProfile.active { //false
newg.goroutineProfiled.Store(goroutineProfileSatisfied)
}
}
newg.trackingSeq = uint8(fastrand()) //初始化一个随机数
if newg.trackingSeq%gTrackingPeriod == 0 { //false
newg.tracking = true
}
casgstatus(newg, _Gdead, _Grunnable) //将goroutine设置为可运行的
gcController.addScannableStack(_p_, int64(newg.stack.hi-newg.stack.lo)) //GC相关,不管
if _p_.goidcache == _p_.goidcacheend { //这里就是给goroutine设置一个编号,在同一个p下,从1开始,2、3、4 ...
_p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)
_p_.goidcache -= _GoidCacheBatch - 1
_p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
}
newg.goid = int64(_p_.goidcache)设置进去
_p_.goidcache++
if raceenabled {
…… //不会执行,省略掉
}
if trace.enabled { //忽略
traceGoCreate(newg, newg.startpc)
}
releasem(_g_.m) //释放,运行抢占
return newg //返回g对象
}gostartcallfn函数(将runtime.main执行地址设置到g.sched.buf.pc上)
func gostartcallfn(gobuf *gobuf, fv *funcval) {
var fn unsafe.Pointer
if fv != nil {
fn = unsafe.Pointer(fv.fn) //取出runtime.main
} else {
fn = unsafe.Pointer(abi.FuncPCABIInternal(nilfunc))
}
gostartcall(gobuf, fn, unsafe.Pointer(fv)) //在这个函数上设置
}
func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) {
sp := buf.sp
sp -= goarch.PtrSize //扩展8字节,用来保存buf.pc
*(*uintptr)(unsafe.Pointer(sp)) = buf.pc //栈上保存的buf.pc的值
buf.sp = sp //重新设置buf.sp
buf.pc = uintptr(fn) //设置buf.pc = runtime.main,也是将runtime.main执行地址设置到g.sched.buf.pc上
buf.ctxt = ctxt
}g.sched.buf.pc = runtime.main,所以gogo<>函数最终会调到runtime.main函数执行
runtime.main函数
执行init函数,执行main函数
func main() {
g := getg() //获取当前g
g.m.g0.racectx = 0 //g0只在main goroutine使用
if goarch.PtrSize == 8 { //判断是不是64位机器
maxstacksize = 1000000000 //最大栈1GB
} else {
maxstacksize = 250000000
}
maxstackceiling = 2 * maxstacksize //2倍最大栈
mainStarted = true
if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
systemstack(func() {
newm(sysmon, nil, -1) //新开一个线程运行sysmon
})
}
lockOSThread() //将main goroutine锁定到这个主操作系统线程上
if g.m != &m0 {
throw("runtime.main not on m0")
}
runtimeInitTime = nanotime() //获取当前时间戳
if runtimeInitTime == 0 { //不满足
throw("nanotime returning zero")
}
if debug.inittrace != 0 { //不满足
inittrace.id = getg().goid
inittrace.active = true
}
//运行runtime包下的init函数
doInit(&runtime_inittask) // Must be before defer.
needUnlock := true
defer func() {
if needUnlock {
unlockOSThread()
}
}()
gcenable() //启用GC
main_init_done = make(chan bool)
if iscgo { //不开启cgo模式
…… //不执行省略掉
}
//执行main包下的初始化
doInit(&main_inittask)
inittrace.active = false //不管,关闭inittrace,避免在malloc和newproc中收集统计信息的开销
close(main_init_done) //不管
needUnlock = false //不需要defer函数来解锁
unlockOSThread()
if isarchive || islibrary { //不满足
return
}
fn := main_main // 这个就是我们的main函数,后面将是如何关联的
fn() //执行main函数,到这里,我们的main函数就被执行了
if raceenabled {
racefini()
}
//main函数退出后,程序就该关闭了
//defer panic相关
if atomic.Load(&runningPanicDefers) != 0 {
// Running deferred functions should not take long.
for c := 0; c < 1000; c++ {
if atomic.Load(&runningPanicDefers) == 0 {
break
}
Gosched()
}
}
if atomic.Load(&panicking) != 0 {
gopark(nil, nil, waitReasonPanicWait, traceEvGoStop, 1)
}
exit(0) //退出程序
for {
var x *int32
*x = 0
}
}
至此,main函数的执行流程就结束了
main函数是如何关联上的
runtime·mainPC与runtime·main关联
在创建goroutine调用main函数一节中,我们看到有这样一条指令MOVQ $runtime·mainPC(SB), AX
,我们当时提到这个$runtime·mainPC是runtime.main函数的调用地址,这个变量定义在/src/runtime/asm_amd64.s文件中,可以看到有两条汇编指令
// mainPC is a function value for runtime.main, to be passed to newproc.
// The reference to runtime.main is made via ABIInternal, since the
// actual function (not the ABI0 wrapper) is needed by newproc.
DATA runtime·mainPC+0(SB)/8,$runtime·main<ABIInternal>(SB)
GLOBL runtime·mainPC(SB),RODATA,$8
GlOBL声明runtime·mainPC是一个全局变量,所以可以直接被引用
DATA 定义变量runtime·mainPC是一个只读数据变量,大小为8字节,变量值是runtime·main
main_main与main.main关联
在runtime.main函数一节中,我们分析了这一段代码
func main() {
……
fn := main_main
fn() //main函数的调用
……
}
在main函数上方,有这样一个函数声明
//go:linkname main_main main.main
func main_main()
起作用的就是这个注释//go:linkname(官网对于go:linkname的说明),表示main_main函数由main.main来实现
main.main:第一个main,表示的是package,也就是包名,第二个main,就是我们的函数名
所以main_main就是我们自己写的main函数
总结
最后,总结一下启动程序到执行main函数,有哪些步骤
- 分配栈空间,保存命令行参数到栈中,初始化g0的栈信息
- 检查处理器是不是intel
- 设置本地线程tls,值为&m0.tls[0]
- 将m0和g0相互绑定,g0在m0上运行
- 创建一个执行runtime.main的goroutine到p的本地队列中
- 设置堆栈保护边界
- 保存g0的父调度信息,初始化信号相关操作,调度
- 取出执行runtime.main的goroutine,并运行gorountine
- 执行到runtime.main函数,执行init函数,然后再调用我们编写main函数