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)
}
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。