前言
本系列基于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)))
}
}