chikaku

且听风吟

永远是深夜有多好。
github
email

Go 运行时之程序的启动

编译以下代码

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.sruntime/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)
}
加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。