golang源码分析-启动过程概述
golang语言作为根据CSP模型实现的一种强类型的语言,本文主要就是通过简单的实例来分析一下golang语言的启动流程,为深入了解与学习做铺垫。
golang代码示例
package main
import "fmt"
func main(){
fmt.Println("hello,world")
}
编写完示例代码之后,进行编译;
go build test.go
调试程序的方式有多种方式,可以使用gdb或者golang调试推荐使用的Devle工具。本文采用gdb调试方式;
gdb ./test
(gdb) info files
Symbols from "/root/test/test".
Local exec file:
`/root/test/test', file type elf64-x86-64.
Entry point: 0x454ae0
0x0000000000401000 - 0x000000000048cba9 is .text
0x000000000048d000 - 0x00000000004dc24c is .rodata
0x00000000004dc420 - 0x00000000004dd084 is .typelink
0x00000000004dd088 - 0x00000000004dd0d8 is .itablink
0x00000000004dd0d8 - 0x00000000004dd0d8 is .gosymtab
0x00000000004dd0e0 - 0x0000000000548426 is .gopclntab
0x0000000000549000 - 0x0000000000549020 is .go.buildinfo
0x0000000000549020 - 0x00000000005560f8 is .noptrdata
0x0000000000556100 - 0x000000000055d0f0 is .data
0x000000000055d100 - 0x0000000000578950 is .bss
0x0000000000578960 - 0x000000000057b0b8 is .noptrbss
0x0000000000400f9c - 0x0000000000401000 is .note.go.buildid
(gdb) b *0x454ae0
Breakpoint 1 at 0x454ae0: file /usr/lib/golang/src/runtime/rt0_linux_amd64.s, line 8.
此时我们查看位于rt0_linux_amd64.s中的的内容查看;
#include "textflag.h"
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB) # 跳转到_rt0_amd64处执行
TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0
JMP _rt0_amd64_lib(SB)
此时_rt0_amd64的代码位于runtime/asm_amd64.s中执行。此时就进入了整个的启动与初始化过程。
runtime中的启动与初始化
在位于runtime/asm_amd64.s中;
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB) // 跳转到rt0_go处执行
真正的初始化与执行的流程都是包含在了rt0_go的流程中。
rt0_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)
// create istack out of the given (operating system) stack.
// _cgo_init may update stackguard.
MOVQ $runtime·g0(SB), DI // 设置g0信息 并设置栈信息
LEAQ (-64*1024+104)(SP), BX
MOVQ BX, g_stackguard0(DI)
MOVQ BX, g_stackguard1(DI)
MOVQ BX, (g_stack+stack_lo)(DI)
MOVQ SP, (g_stack+stack_hi)(DI)
// find out information about the processor we're on
MOVL $0, AX
CPUID
MOVL AX, SI
CMPL AX, $0
JE nocpuinfo
// Figure out how to serialize RDTSC.
// On Intel processors LFENCE is enough. AMD requires MFENCE.
// Don't know about the rest, so let's do MFENCE. 根据平台不同进行跳转
CMPL BX, $0x756E6547 // "Genu"
JNE notintel
CMPL DX, $0x49656E69 // "ineI"
JNE notintel
CMPL CX, $0x6C65746E // "ntel"
JNE notintel
MOVB $1, runtime·isIntel(SB)
MOVB $1, runtime·lfenceBeforeRdtsc(SB)
notintel:
// Load EAX=1 cpuid flags
MOVL $1, AX
CPUID
MOVL AX, runtime·processorVersionInfo(SB)
nocpuinfo:
// if there is an _cgo_init, call it.
MOVQ _cgo_init(SB), AX
TESTQ AX, AX
JZ needtls
// g0 already in DI
MOVQ DI, CX // Win64 uses CX for first parameter
MOVQ $setg_gcc<>(SB), SI
CALL AX
// update stackguard after _cgo_init
MOVQ $runtime·g0(SB), CX
MOVQ (g_stack+stack_lo)(CX), AX
ADDQ $const__StackGuard, AX
MOVQ AX, g_stackguard0(CX)
MOVQ AX, g_stackguard1(CX)
#ifndef GOOS_windows
JMP ok
#endif
needtls:
#ifdef GOOS_plan9
// skip TLS setup on Plan 9
JMP ok
#endif
#ifdef GOOS_solaris
// skip TLS setup on Solaris
JMP ok
#endif
#ifdef GOOS_darwin
// skip TLS setup on Darwin
JMP ok
#endif
LEAQ runtime·m0+m_tls(SB), DI
CALL runtime·settls(SB)
// store through it, to make sure it works
get_tls(BX)
MOVQ $0x123, g(BX)
MOVQ runtime·m0+m_tls(SB), AX
CMPQ AX, $0x123
JEQ 2(PC)
CALL runtime·abort(SB)
ok:
// set the per-goroutine and per-mach "registers"
get_tls(BX)
LEAQ runtime·g0(SB), CX // 设置g0信息
MOVQ CX, g(BX)
LEAQ runtime·m0(SB), AX // 设置m0信息
// save m->g0 = g0
MOVQ CX, m_g0(AX)
// save m0 to g0->m
MOVQ AX, g_m(CX)
CLD // convention is D is always left cleared
CALL runtime·check(SB) // 进行检查
MOVL 16(SP), AX // copy argc 拷贝标准输入数据
MOVL AX, 0(SP)
MOVQ 24(SP), AX // copy argv
MOVQ AX, 8(SP)
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) // 创建协程并绑定运行
POPQ AX
POPQ AX
// start this M
CALL runtime·mstart(SB) // 开始运行
CALL runtime·abort(SB) // mstart should never return
RET
// Prevent dead-code elimination of debugCallV1, which is
// intended to be called by debuggers.
MOVQ $runtime·debugCallV1(SB), AX
RET
DATA runtime·mainPC+0(SB)/8,$runtime·main(SB) // 设置mainPC为runtime.main的地址
GLOBL runtime·mainPC(SB),RODATA,$8
此时通过该流程可以看出主要的流程首先设置g0的相关环境,接着就初始化输入参数(args)、初始化运行核数与页大小(osinit)接着再初始化运行环境(schedinit),然后调用main函数进行绑定最后调用mstart方法开始执行。
schedinit调度相关初始化
func schedinit() {
// raceinit must be the first call to race detector.
// In particular, it must be done before mallocinit below calls racemapshadow.
_g_ := getg() // 获取g实例
if raceenabled {
_g_.racectx, raceprocctx0 = raceinit()
}
sched.maxmcount = 10000 // 设置系统线程M的最大数量
tracebackinit() // 初始化计数器等内容
moduledataverify()
stackinit() // 栈相关初始化
mallocinit() // 内存相关初始化
mcommoninit(_g_.m) // 初始化当前的m 即m0的初始化
cpuinit() // must run before alginit
alginit() // maps must not be used before this call
modulesinit() // provides activeModules
typelinksinit() // uses maps, activeModules
itabsinit() // uses activeModules
msigsave(_g_.m)
initSigmask = _g_.m.sigmask
goargs() // 获取命令行参数
goenvs() // 获取环境变量
parsedebugvars()
gcinit() // 内存回收Gc的初始化
sched.lastpoll = uint64(nanotime())
procs := ncpu // 运行p的个数检查
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n // 如果设置了最大p个数,检查p个数合法后就设置为该值
}
if procresize(procs) != nil { // 初始化对应procs个数的p
throw("unknown runnable goroutine during bootstrap")
}
// For cgocheck > 1, we turn on the write barrier at all times
// and check all pointer writes. We can't do this until after
// procresize because the write barrier needs a P.
if debug.cgocheck > 1 {
writeBarrier.cgo = true
writeBarrier.enabled = true
for _, p := range allp {
p.wbBuf.reset()
}
}
if buildVersion == "" {
// Condition should never trigger. This code just serves
// to ensure runtime·buildVersion is kept in the resulting binary.
buildVersion = "unknown"
}
}
该函数主要就是初始化了命令行参数,环境变量,gc和p的初始化过程等操作,都是为了后续执行做准备。
newproc函数
//go:nosplit
func newproc(siz int32, fn *funcval) {
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
gp := getg() // 获取g
pc := getcallerpc() // 获取当前pc
systemstack(func() {
newproc1(fn, (*uint8)(argp), siz, gp, pc) // 添加到栈中 此时的入口函数就是main函数
})
}
// Create a new g running fn with narg bytes of arguments starting
// at argp. callerpc is the address of the go statement that created
// this. The new g is put on the queue of g's waiting to run.
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
_g_ := getg() // 获取g
if fn == nil {
_g_.m.throwing = -1 // do not dump full stacks
throw("go of nil func value")
}
_g_.m.locks++ // disable preemption because it can be holding p in a local var
siz := narg // 设置大小
siz = (siz + 7) &^ 7
// We could allocate a larger initial stack if necessary.
// Not worth it: this is almost always an error.
// 4*sizeof(uintreg): extra space added below
// sizeof(uintreg): caller's LR (arm) or return address (x86, in gostartcall).
if siz >= _StackMin-4*sys.RegSize-sys.RegSize {
throw("newproc: function arguments too large for new goroutine")
}
_p_ := _g_.m.p.ptr() // 获取当前的m
newg := gfget(_p_) // 生成一个新的g
if newg == nil {
newg = malg(_StackMin)
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
}
if newg.stack.hi == 0 {
throw("newproc1: newg missing stack")
}
if readgstatus(newg) != _Gdead {
throw("newproc1: new g is not Gdead")
}
totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame 设置栈大小
totalSize += -totalSize & (sys.SpAlign - 1) // align to spAlign
sp := newg.stack.hi - totalSize // 设置可用的sp
spArg := sp
if usesLR {
// caller's LR
*(*uintptr)(unsafe.Pointer(sp)) = 0
prepGoExitFrame(sp)
spArg += sys.MinFrameSize
}
if narg > 0 { // 如果输入参数大于0
memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg))
// This is a stack-to-stack copy. If write barriers
// are enabled and the source stack is grey (the
// destination is always black), then perform a
// barrier copy. We do this *after* the memmove
// because the destination stack may have garbage on
// it.
if writeBarrier.needed && !_g_.m.curg.gcscandone {
f := findfunc(fn.fn) // 保存输入参数
stkmap := (*stackmap)(funcdata(f, _FUNCDATA_ArgsPointerMaps))
if stkmap.nbit > 0 {
// We're in the prologue, so it's always stack map index 0.
bv := stackmapdata(stkmap, 0)
bulkBarrierBitmap(spArg, spArg, uintptr(bv.n)*sys.PtrSize, 0, bv.bytedata)
}
}
}
memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
newg.sched.sp = sp // 设置当前的sp
newg.stktopsp = sp
newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function 设置g执行完成后退出的函数地址 指向了goexit
newg.sched.g = guintptr(unsafe.Pointer(newg)) // 设置当前的g的指针
gostartcallfn(&newg.sched, fn) // 设置当前g的入口函数即该g被调度时执行的入口
newg.gopc = callerpc
newg.ancestors = saveAncestors(callergp)
newg.startpc = fn.fn // 保存执行的func地址
if _g_.m.curg != nil {
newg.labels = _g_.m.curg.labels
}
if isSystemGoroutine(newg, false) {
atomic.Xadd(&sched.ngsys, +1)
}
newg.gcscanvalid = false // 设置该g不被gc收集回收
casgstatus(newg, _Gdead, _Grunnable) // 设置当前的g的状态为可运行状态
if _p_.goidcache == _p_.goidcacheend {
// Sched.goidgen is the last allocated id,
// this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].
// At startup sched.goidgen=0, so main goroutine receives goid=1.
_p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)
_p_.goidcache -= _GoidCacheBatch - 1
_p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
}
newg.goid = int64(_p_.goidcache) // 获取当前g的id
_p_.goidcache++
if raceenabled {
newg.racectx = racegostart(callerpc)
}
if trace.enabled {
traceGoCreate(newg, newg.startpc)
}
runqput(_p_, newg, true) // 把当前g加入队列中并设置下一个就可被唤起运行
if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted { // 将当前g加入到可调度的队列中去 如果是启动阶段不会调用wakeup 如果是运行中则会在队列中重新唤起可运行的
wakep()
}
_g_.m.locks--
if _g_.m.locks == 0 && _g_.preempt { // restore the preemption request in case we've cleared it in newstack
_g_.stackguard0 = stackPreempt
}
}
主要就是新生成一个g来运行,并将该g设置执行函数的入口,栈的初始化并设置g可运行状态,加入到队列中可被调用执行,在启动阶段的第一个g传入的函数其实就是main函数,接着就会调用mstart来调用该新生成的g来执行被包裹的函数main。
mstart函数
//go:nosplit
//go:nowritebarrierrec
func mstart() {
_g_ := getg() // 获取当前的g
osStack := _g_.stack.lo == 0
if osStack {
// Initialize stack bounds from system stack.
// Cgo may have left stack size in stack.hi.
// minit may update the stack bounds.
size := _g_.stack.hi
if size == 0 {
size = 8192 * sys.StackGuardMultiplier
}
_g_.stack.hi = uintptr(noescape(unsafe.Pointer(&size)))
_g_.stack.lo = _g_.stack.hi - size + 1024
}
// Initialize stack guards so that we can start calling
// both Go and C functions with stack growth prologues.
_g_.stackguard0 = _g_.stack.lo + _StackGuard
_g_.stackguard1 = _g_.stackguard0
mstart1() // 调用mastart1执行
// Exit this thread.
if GOOS == "windows" || GOOS == "solaris" || GOOS == "plan9" || GOOS == "darwin" || GOOS == "aix" {
// Window, Solaris, Darwin, AIX and Plan 9 always system-allocate
// the stack, but put it in _g_.stack before mstart,
// so the logic above hasn't set osStack yet.
osStack = true
}
mexit(osStack) // 退出
}
func mstart1() {
_g_ := getg() // 获取当前的g
if _g_ != _g_.m.g0 {
throw("bad runtime·mstart")
}
// Record the caller for use as the top of stack in mcall and
// for terminating the thread.
// We're never coming back to mstart1 after we call schedule,
// so other calls can reuse the current frame.
save(getcallerpc(), getcallersp())
asminit()
minit() // 初始化信号量
// Install signal handlers; after minit so that minit can
// prepare the thread to be able to handle the signals.
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() // 调度可执行的g 本文先不讨论该函数的流程
}
mstart函数主要就是开始调度可以运行的g来执行,在启动阶段可执行的g就是被包裹的main函数,此时继续了解main函数
main函数
func main() {
g := getg()
// Racectx of m0->g0 is used only as the parent of the main goroutine.
// It must not be used for anything else.
g.m.g0.racectx = 0
// Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
// Using decimal instead of binary GB and MB because
// they look nicer in the stack overflow failure message. 设置栈的大小
if sys.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}
// Allow newproc to start new Ms.
mainStarted = true // 设置标志位可以允许其他newporc开始生成新的m
if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
systemstack(func() { // 开启一个后台协程来执行垃圾回收等操作
newm(sysmon, nil)
})
}
// Lock the main goroutine onto this, the main OS thread,
// during initialization. Most programs won't care, but a few
// do require certain calls to be made by the main thread.
// Those can arrange for main.main to run in the main thread
// by calling runtime.LockOSThread during initialization
// to preserve the lock.
lockOSThread()
if g.m != &m0 { // 检查是否是m0协程执行
throw("runtime.main not on m0")
}
runtime_init() // must be before defer 各个包的init函数执行,即init的加载
if nanotime() == 0 {
throw("nanotime returning zero")
}
// Defer unlock so that runtime.Goexit during init does the unlock too.
needUnlock := true
defer func() {
if needUnlock {
unlockOSThread()
}
}()
// Record when the world started.
runtimeInitTime = nanotime() // 记录当前执行时间
gcenable() // 开启垃圾回收
main_init_done = make(chan bool)
if iscgo {
if _cgo_thread_start == nil {
throw("_cgo_thread_start missing")
}
if GOOS != "windows" {
if _cgo_setenv == nil {
throw("_cgo_setenv missing")
}
if _cgo_unsetenv == nil {
throw("_cgo_unsetenv missing")
}
}
if _cgo_notify_runtime_init_done == nil {
throw("_cgo_notify_runtime_init_done missing")
}
// Start the template thread in case we enter Go from
// a C-created thread and need to create a new thread.
startTemplateThread()
cgocall(_cgo_notify_runtime_init_done, nil)
}
fn := main_init // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn() // 执行main的init函数
close(main_init_done)
needUnlock = false
unlockOSThread()
if isarchive || islibrary {
// A program compiled with -buildmode=c-archive or c-shared
// has a main, but it is not executed.
return
}
fn = main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn() // 执行程序定义的main入口函数
if raceenabled {
racefini()
}
// Make racy client program work: if panicking on
// another goroutine at the same time as main returns,
// let the other goroutine finish printing the panic trace.
// Once it does, it will exit. See issues 3934 and 20018.
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重新调度让其他协程执行
gopark(nil, nil, waitReasonPanicWait, traceEvGoStop, 1)
}
exit(0)
for {
var x *int32
*x = 0
}
}
main函数主要就是最后对应于go程序中的main函数执行,在执行的过程中首先会先执行其他包中的init函数的执行,然后再执行main函数中的init函数,最后执行main函数,至此启动过程中的基本执行流程就完成。
总结
本文主要就是简单查看了一下go程序的启动过程,go中涉及到部分汇编知识,在汇编代码中一步步查找到runtime中的相关的go的源码的实现,本文也参考了大量网上已有的内容,大家有兴趣课自行查看。由于本人才疏学浅,如有错误请批评指正。