前言

本系列基于golang 1.20.4版本 我们先查找下启动入口,编写一个最简单的demo查找下启动入口,执行 go build demo.go后,得到执行文件,然后我们使用readelf查找到启动的内存地址(0x456c40),使用dlv寻找到启动代码在rt0_linux_amd64.s里。

//demo.go
package main
func main(){
    println("hello")
}
root@node1:~/work/go/demo# readelf -h demo
ELF 头:
  Magic:  7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              EXEC (可执行文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:              0x456c40
  程序头起点:              64 (bytes into file)
  Start of section headers:          456 (bytes into file)
  标志:             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         7
  Size of section headers:           64 (bytes)
  Number of section headers:         23
  Section header string table index: 3
  
root@node1:~/work/go/demo# dlv exec demo
Type 'help' for list of commands.
(dlv) b *0x456c40
Breakpoint 1 set at 0x456c40 for _rt0_amd64_linux() /usr/local/go/src/runtime/rt0_linux_amd64.s:8

以linux amd为例,启动文件如下如下

// ~go/src/runtime/rt0_linux_amd64.s
#include "textflag.h"
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP	_rt0_amd64(SB)

TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0
JMP	_rt0_amd64_lib(SB)

调用了我们的_rt0_amd64,我们继续看_rt0_amd64做了哪些事

// ~go/src/runtime/asan_amd64.s
//为了方便读者,多余的都删除,感兴趣可以找源文件阅读
TEXT _rt0_amd64(SB),NOSPLIT,$-8
	MOVQ	0(SP), DI	// argc
	LEAQ	8(SP), SI	// argv
	JMP	runtime·rt0_go(SB)
	
TEXT runtime·rt0_go(SB),NOSPLIT|NOFRAME|TOPFRAME,$0
    {......}
    //argc,argv 作为操作系统的参数传递给args函数
    MOVQ	DI, AX			// argc
    MOVQ	SI, BX			// argv
    // create istack out of the given (operating system) stack.
    // _cgo_init may update stackguard.
    MOVQ	$runtime·g0(SB), DI  //// 初始化 g0 执行栈
   {......}
    //注意着下面四个调用,我们后文基于这4个
    //类型检查 在src/runtime/runtime1.go->check()方法,感兴趣可以自己查看
    CALL	runtime·check(SB) 
    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
    // start this M
    CALL	runtime·newproc(SB)
    CALL	runtime·mstart(SB)
    CALL	runtime·abort(SB)	// mstart should never return

上面_rt0_amd64就是我们启动的主流程汇编调用,下面我们来分析下每个调用具体干了啥

1.1 runtime.args(SB)

设置argv,auxv

// src/runtime/runtime1.go
func args(c int32, v **byte) {
	argc = c
	argv = v
	sysargs(c, v)
}

func sysargs(argc int32, argv **byte) {
	// skip over argv, envp to get to auxv
	for argv_index(argv, n) != nil {
		n++
	}
	if pairs := sysauxv(auxvp[:]); pairs != 0 {
		auxv = auxvp[: pairs*2 : pairs*2]
		return
	}
	{......}
    pairs := sysauxv(auxvreadbuf[:])
}

//我们主要看sysauxv 方法
func sysauxv(auxv []uintptr) (pairs int) {
	var i int
	for ; auxv[i] != _AT_NULL; i += 2 {
		tag, val := auxv[i], auxv[i+1]
		switch tag {
		case _AT_RANDOM:
			// The kernel provides a pointer to 16-bytes
			// worth of random data.
			startupRandomData = (*[16]byte)(unsafe.Pointer(val))[:]

		case _AT_PAGESZ:
			//读取内存也大小,在第三讲go内存管理我们会再讲该变量
			physPageSize = val

		case _AT_SECURE:
			secureMode = val == 1
		}

		archauxv(tag, val)
		vdsoauxv(tag, val)
	}
	return i / 2
}

1.2 runtime·osinit(SB)

完成对 CPU 核心数的获取,以及设置内存页大小,特别注意!!!
在Container里获取到的runtime.NumCPU()=ncpu是主机的, 可通过https://github.com/uber-go/automaxprocs获取容器cpu,
有兴趣的了解的读者可以可以看看容器cgroup相关docker技术详解

func osinit() {
	//核心的逻辑sched_getaffinit获取一个数据一堆计算后最终得到n cpu个数
	//在sys_linux_amd64.s用汇编产生系统调用SYS_sched_getaffinity
	//有兴趣可以搜索下SYS_sched_getaffinity
	ncpu = getproccount()
	//getHugePageSize() 提高内存管理的性能透明大页
	//root@node1:~# cat /sys/kernel/mm/transparent_hugepage/hpage_pmd_size
	//2097152
	physHugePageSize = getHugePageSize()
	{......}
	osArchInit()
}

1.3 runtime·schedinit(SB)

主要负责各种运行时组件初始化工作

// src/runtime/proc.go
// The bootstrap sequence is:
//
//	call osinit
//	call schedinit
//	make & queue new G
//	call runtime·mstart
//
// The new G calls runtime·main.
func schedinit() {
	// raceinit must be the first call to race detector.
	// In particular, it must be done before mallocinit below calls racemapshadow.
	gp := getg()
	if raceenabled {
		//race检测初始化
		gp.racectx, raceprocctx0 = raceinit()
	}
    //最大m数量,包含状态未die的m
	sched.maxmcount = 10000

	//gc里的stw
	// The world starts stopped.
	worldStopped()

	//栈初始化
	stackinit()
	//内存初始化
	mallocinit()
	fastrandinit()         // must run before mcommoninit
	//初始化当前m
	mcommoninit(gp.m, -1)
	//gc初始化,及三色标记法
	gcinit()


	//上次网络轮训的时间,网络部分讲
	sched.lastpoll.Store(nanotime())

    // 通过CPU核心数和 GOMAXPROCS 环境变量确定 P 的数量
	procs := ncpu
	if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
		procs = n
	}
	
	//关闭stw
	// World is effectively started now, as P's can run.
	worldStarted()
}

1.4 runtime·mstart(SB)

主goroutine的启动,及g0

main() {
	mp := getg().m

	
	// 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.
	// 执行栈最大限制:1GB(64位系统)或者 250MB(32位系统)
	if goarch.PtrSize == 8 {
		maxstacksize = 1000000000
	} else {
		maxstacksize = 250000000
	}

	// An upper limit for max stack size. Used to avoid random crashes
	// after calling SetMaxStack and trying to allocate a stack that is too big,
	// since stackalloc works with 32-bit sizes.
	maxstackceiling = 2 * maxstacksize

	if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
		//启动监控用于垃圾回收,抢占调度
		systemstack(func() {
			newm(sysmon, nil, -1)
		})
	}

	//锁死主线程,例如我们在调用c代码时候,goroutine需要独占线程,可用该方法独占m
	// 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()

	
    //执行init函数,编译器把包中所有的init函数存在runtime_inittasks里
	doInit(runtime_inittasks) // Must be before defer.
	
    // 启动垃圾回收器后台操作
	gcenable()

	

	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
	}
    // 执行用户main包中的 main函数
	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()
	if raceenabled {
        //开启data -race的退出
		runExitHooks(0) // run hooks now, since racefini does not return
		racefini()
	}
	//退出并运行退出hook
	runExitHooks(0)
	exit(0)
	
}

1.5 总结

通过上文分析我们得知了go程序从runtime.rt0_amd64*(各个平台汇编文件不一样)启动,然后转到runtime.rt0_go调用,主要检查参数,以及设置cpu核心和内存
页大小,随后在schedinit中,对整个程序进行初始化,最后通过newproc和mstart由点读起转换g0执行
在g0中,systemstack负责后台监控,runtime_init运行初始化函数,main_main运行用户态main函数。
下面我们再来看看init的执行顺序

2 init执行顺序

由上文我们doInit(runtime_inittasks)得知,函数init会被linker存储到runtime_inittasks里,具体逻辑为
计算出执行它们的良好顺序,并发出该顺序供运行时使用,其次根据调用链,A倒入B包则会先初始化B包,
最后此函数计算所有 inittask 记录的排序,以便该顺序尊重所有依赖项,并在给定该限制的情况下,按字典顺序对 inittask 进行排序。 对比之前版本,现在按字典序排序,完整代码在如下两个文件中

// src/cmd/link/internal/ld/inittask.go
// cmd/compile/internal/pkginit/init.go
// Inittasks finds inittask records, figures out a good
// order to execute them in, and emits that order for the
// runtime to use.
//
// An inittask represents the initialization code that needs
// to be run for a package. For package p, the p..inittask
// symbol contains a list of init functions to run, both
// explicit user init functions and implicit compiler-generated
// init functions for initializing global variables like maps.
//
// In addition, inittask records have dependencies between each
// other, mirroring the import dependencies. So if package p
// imports package q, then there will be a dependency p -> q.
// We can't initialize package p until after package q has
// already been initialized.
//
// Package dependencies are encoded with relocations. If package
// p imports package q, then package p's inittask record will
// have a R_INITORDER relocation pointing to package q's inittask
// record. See cmd/compile/internal/pkginit/init.go.
//
// This function computes an ordering of all of the inittask
// records so that the order respects all the dependencies,
// and given that restriction, orders the inittasks in
// lexicographic order.
func (ctxt *Link) inittasks() {
	switch ctxt.BuildMode {
	case BuildModeExe, BuildModePIE, BuildModeCArchive, BuildModeCShared:
		// Normally the inittask list will be run on program startup.
		ctxt.mainInittasks = ctxt.inittaskSym("main..inittask", "go:main.inittasks")
	case BuildModePlugin:
		// For plugins, the list will be run on plugin load.
		ctxt.mainInittasks = ctxt.inittaskSym(fmt.Sprintf("%s..inittask", objabi.PathToPrefix(*flagPluginPath)), "go:plugin.inittasks")
		// Make symbol local so multiple plugins don't clobber each other's inittask list.
		ctxt.loader.SetAttrLocal(ctxt.mainInittasks, true)
	case BuildModeShared:
		// Nothing to do. The inittask list will be built by
		// the final build (with the -linkshared option).
	default:
		Exitf("unhandled build mode %d", ctxt.BuildMode)
	}

	// If the runtime is one of the packages we are building,
	// initialize the runtime_inittasks variable.
	ldr := ctxt.loader
	if ldr.Lookup("runtime.runtime_inittasks", 0) != 0 {
		t := ctxt.inittaskSym("runtime..inittask", "go:runtime.inittasks")

		// This slice header is already defined in runtime/proc.go, so we update it here with new contents.
		sh := ldr.Lookup("runtime.runtime_inittasks", 0)
		sb := ldr.MakeSymbolUpdater(sh)
		sb.SetSize(0)
		sb.SetType(sym.SNOPTRDATA) // Could be SRODATA, but see issue 58857.
		sb.AddAddr(ctxt.Arch, t)
		sb.AddUint(ctxt.Arch, uint64(ldr.SymSize(t)/int64(ctxt.Arch.PtrSize)))
		sb.AddUint(ctxt.Arch, uint64(ldr.SymSize(t)/int64(ctxt.Arch.PtrSize)))
	}
}