编译以下代码
package main
func main() {
println("Hello Go")
}
查找 Go 可执行程序的入口#
使用 readelf
查看可执行二进制的入口地址,使用 go 工具链的 nm 工具找到符号表中对应地址的函数名
$ readelf -h ./hello
ELF Header:
...
Entry point address: 0x454020
...
$ go tool nm ./hello| grep 454020
454020 T _rt0_amd64_linux
启动阶段#
在 Go 源码中可以查找到 _rt0_amd64_linux
所在的文件,程序启动之初的工作写在对应平台下的汇编代码里,主要有两个文件 runtime/rt0_linux_amd64.s
和 runtime/asm_amd64.s
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB)
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
JMP runtime·rt0_go(SB)
TEXT runtime·rt0_go(SB),NOSPLIT|NOFRAME|TOPFRAME,$0
// ...
// 设置 g0 的栈空间
MOVQ $runtime·g0(SB), DI
LEAQ (-64*1024)(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)
// 使用 arch_prctl(ARCH_SET_FS) 设置 m0 线程的 TLS 基地址为 m0.tls
LEAQ runtime·m0+m_tls(SB), DI
CALL runtime·settls(SB)
// 进行基础的正确性和安全性检查
// 包括: 基础类型的大小, 平台指针类型的大小 CAS 正确性检查
CALL runtime·check(SB)
MOVL 24(SP), AX // copy argc
MOVL AX, 0(SP)
MOVQ 32(SP), AX // copy argv
MOVQ AX, 8(SP)
CALL runtime·args(SB)
CALL runtime·osinit(SB)
CALL runtime·schedinit(SB)
// rnutime.main 函数的地址传入 newproc
// 即将 rnutime.main 加入到一个 p 的运行队列
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
CALL runtime·newproc(SB)
POPQ AX
// 主线程执行 schedule 调度循环
// runtime.main 会被调度执行
// runtime.main 内部会调用 main.main
CALL runtime·mstart(SB)
// mstart 不应该返回所以走到这里直接报错
CALL runtime·abort(SB)
RET
TEXT runtime·mstart(SB),NOSPLIT|TOPFRAME|NOFRAME,$0
CALL runtime·mstart0(SB)
RET // not reached
runtime.args#
// runtime/runtime1.go
func args(c int32, v **byte) {
argc = c
argv = v
sysargs(c, v)
}
此函数主要设置了 argc 和 argv 两个全局变量,在 sysargs
中通过读取 argv 获取 / 设置:
startupRandomData
一个由内核 (ld-linux.so) 设置的 16 字节随机随机数据缓冲区physPageSize
系统物理页大小- 读取 ELF 文件头获取字符串表,符号表,动态链接和 vdso 信息
- 在 Linux 下会读取符号表设置了两个特殊的 vdso 调用的指针
var vdsoSymbolKeys = []vdsoSymbolKey{
{"__vdso_gettimeofday", 0x315ca59, 0xb01bca00, &vdsoGettimeofdaySym},
{"__vdso_clock_gettime", 0xd35ec75, 0x6e43a318, &vdsoClockgettimeSym},
}
runtime.osinit#
// runtime/os_linux.go
func osinit() {
ncpu = getproccount()
physHugePageSize = getHugePageSize()
if iscgo {
// ... 处理 cgo 信号相关
}
osArchInit()
}
Linux 下的 osinit 比较简单:
- 通过
sched_getaffinity
获取处理器信息 - 通过
/sys/kernel/mm/transparent_hugepage/hpage_pmd_size
获取透明大页的物理大小
runtime.schedinit#
Go 协程调度器的初始化工作,这里只取关键部分的代码,对一些暂时没有实现的空函数调用也忽略掉了
// runtime/proc.go
func schedinit() {
gp := getg()
sched.maxmcount = 10000
moduledataverify() // module 数据校验
stackinit() // 协程栈内存池初始化
mallocinit() // 内存分配器的初始化
alginit() // AES 算法硬件支持初始化
fastrandinit() // 初始化随机种子, 用了之前的 startupRandomData
mcommoninit(gp.m, -1) // 初始化线程递增 ID 信号处理协程和快速随机种子
modulesinit() // 读取各个 module 初始化 GC 扫描全局变量的大小
typelinksinit() // 读取各个 module 收集类型链表信息
itabsinit() // 根据 typelink 初始化 itab 表
stkobjinit() // 栈初始化用于 GC 相关
sigsave(&gp.m.sigmask) // 保存线程信号掩码
goargs() // 设置全局变量 argslice
goenvs() // 设置全局变量 envs
parsedebugvars() // 读取 GODEBUG 环境变量设置 trace
gcinit() // GC 初始化
// 根据物理处理器数量或者 GOMAXPROCS 初始化 allp
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}
}
runtime.getg#
runtime.getg 函数会在编译过程中编译为一条单独的汇编,即取出 TLS 基地址,这个基址在协程启动的时候通过 runtime.gogo
设置为当前协程数据指针即 *g
// ir.OGETG -> ssa.OpGetG -> ssa.OpAMD64LoweredGetG -> MOVQ (TLS), r
TEXT runtime·gogo(SB), NOSPLIT, $0-8
// ...
JMP gogo<>(SB)
TEXT gogo<>(SB), NOSPLIT, $0
get_tls(CX)
MOVQ DX, g(CX)
runtime.procresize#
// runtime/proc.go
func procresize(nprocs int32) *p {
// 前半部分主要是修改:
// 1. allp []*p
// 2. idlepMask 空闲 P 掩码每个 bit 表示对应索引 allp 上的 P 是否空闲
// 3. timerpMask 可能有定时器的 P 的掩码每个 bit 表示对应索引 allp 上的 P 是否有定时器
// proc 数量扩张时初始化新创建的 P
// 程序刚开始时 allp 是空的这里会创建和初始化所有 P
for i := old; i < nprocs; i++ {
pp := allp[i]
if pp == nil {
pp = new(p)
}
pp.init(i)
atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))
}
// 后续 P 收缩的时候会销毁多余的 P
// 所以这里会先确保当前 P 不是要被销毁的那个
// 如果是就换成 allp[0]
gp := getg()
if gp.m.p != 0 && gp.m.p.ptr().id < nprocs {
// 继续用当前的 P
gp.m.p.ptr().status = _Prunning
gp.m.p.ptr().mcache.prepareForSweep()
} else {
if gp.m.p != 0 {
gp.m.p.ptr().m = 0
}
gp.m.p = 0
pp := allp[0]
pp.m = 0
pp.status = _Pidle
acquirep(pp)
}
// proc 数量收缩时清理旧的 P
for i := nprocs; i < old; i++ {
pp := allp[i]
pp.destroy()
}
// 返回可运行的 P 链表
var runnablePs *p
for i := nprocs - 1; i >= 0; i-- {
pp := allp[i]
if gp.m.p.ptr() == pp {
continue
}
pp.status = _Pidle
if runqempty(pp) {
// 如果 p 上的本地队列没有可运行的 g 放置到空闲 p 掩码上
pidleput(pp, now)
} else {
// 给 p 找到一个空闲的 m 这里 m 可能是空的
pp.m.set(mget())
pp.link.set(runnablePs)
runnablePs = pp
}
}
if old != nprocs {
// 如果 proc 数量改变需要修改 GC 占用的 proc 容量(默认会取 25% 的 proc 数量)
gcCPULimiter.resetCapacity(now, nprocs)
}
return runnablePs
}
runtime.newproc#
newproc 传入一个函数 (地址) 创建一个新的 g 放到当前 p 的本地队列上然后唤醒当前 p
在程序启动过程中这里传入的就是 runtime.main 的地址
func newproc(fn *funcval) {
gp := getg()
pc := getcallerpc()
systemstack(func() {
newg := newproc1(fn, gp, pc)
pp := getg().m.p.ptr()
runqput(pp, newg, true)
if mainStarted {
// mainStarted 在 runtime.main 中设置
wakep()
}
})
}
func newproc1(fn *funcval, callergp *g, callerpc uintptr) *g {
mp := acquirem()
pp := mp.p.ptr()
newg := gfget(pp) // 这里先尝试从 p 的 freeg 链表里取一个出来
if newg == nil {
// 如果找不到再用 malg 创建一个新的 g
// 新的 g 的栈还没有初始化为了避免被 GC 扫描到先将状态设置为 dead
// 添加到全局的 allg 里面
newg = malg(_StackMin)
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg)
}
// 计算栈指针位置
totalSize := uintptr(4*goarch.PtrSize + sys.MinFrameSize) // extra space in case of reads slightly beyond frame
totalSize = alignUp(totalSize, sys.StackAlign)
sp := newg.stack.hi - totalSize
// 设置 newg 的调度数据, 栈指针, 函数地址, 程序计数器, 上级 caller 信息等等数据
memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
newg.sched.sp = sp
newg.stktopsp = sp
newg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
newg.sched.g = guintptr(unsafe.Pointer(newg))
gostartcallfn(&newg.sched, fn)
newg.parentGoid = callergp.goid
newg.gopc = callerpc
newg.ancestors = saveAncestors(callergp)
newg.startpc = fn.fn
// 修改状态为 runnable 把栈添加到 GC 栈扫描
casgstatus(newg, _Gdead, _Grunnable)
gcController.addScannableStack(pp, int64(newg.stack.hi-newg.stack.lo))
newg.goid = pp.goidcache
pp.goidcache++
releasem(mp)
return newg
}
runtime.mstart0#
func mstart0() {
gp := getg()
// 初始化 g0 的 stackguard 用于栈溢出和栈扩容检查
gp.stackguard0 = gp.stack.lo + _StackGuard
gp.stackguard1 = gp.stackguard0
mstart1()
mexit(osStack)
}
func mstart1() {
gp := getg()
// Set up m.g0.sched as a label returning to just
// after the mstart1 call in mstart0 above, for use by goexit0 and mcall.
// We're never coming back to mstart1 after we call schedule,
// so other calls can reuse the current frame.
// And goexit0 does a gogo that needs to return from mstart1
// and let mstart0 exit the thread.
gp.sched.g = guintptr(unsafe.Pointer(gp))
gp.sched.pc = getcallerpc()
gp.sched.sp = getcallersp()
// 初始化线程的信号处理协程栈和信后掩码
minit()
if gp.m == &m0 {
// 设置线程的信号处理函数 sighandler
mstartm0()
}
// 一些内部线程比如 sysmon 直接在这里启动
if fn := gp.m.mstartfn; fn != nil {
fn()
}
if gp.m != &m0 {
acquirep(gp.m.nextp.ptr())
gp.m.nextp = 0
}
// 执行调度循环永不返回
// 这里当前只有一个 p 和一个 g
// 所有会跳转到 runtime.main
schedule()
}
func main() {
mp := getg().m
// 设置栈最大为 1G
if goarch.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}
// newproc 创建的协程可以通过 wakep 找到或创建一个线程执行
mainStarted = true
// 执行 runtime 下的 init 函数和全局变量的初始化
doInit(&runtime_inittask)
// 启用 GC
gcenable()
// 执行用户级的 init 函数和全局变量的初始化
doInit(&main_inittask)
// -buildmode=c-archive or c-shared 不执行 main
if isarchive || islibrary {
return
}
// 执行用户级的 main.main 函数
fn := main_main
fn()
// 主协程退出前如果当前有其他协程的 panic-defer 在处理
// 需要等其他协程先处理完(比如打印 panic 信息等)
if runningPanicDefers.Load() != 0 {
for c := 0; c < 1000; c++ {
if runningPanicDefers.Load() == 0 {
break
}
Gosched()
}
}
// 进程退出前执行注册过的 hook
// 比如 compile -cover 模式下的代码覆盖率数据输出
runExitHooks(0)
exit(0)
}