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 が存在するファイルを見つけることができ、プログラム起動時の作業は対応するプラットフォームのアセンブリコードに書かれています。主に 2 つのファイル 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        // argc をコピー
    MOVL    AX, 0(SP)
    MOVQ    32(SP), AX        // 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        // エントリ
    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 // 到達しない

runtime.args#

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

この関数は主に argc と argv の 2 つのグローバル変数を設定し、sysargs 内で argv を読み取ることで取得 / 設定します:

  • startupRandomData カーネル (ld-linux.so) によって設定された 16 バイトのランダムデータバッファ
  • physPageSize システムの物理ページサイズ
  • ELF ファイルヘッダーを読み取って文字列テーブル、シンボルテーブル、動的リンクおよび vdso 情報を取得
  • Linux ではシンボルテーブルを読み取り、2 つの特別な 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()      // モジュールデータの検証
    stackinit()             // コルーチンスタックメモリプールの初期化
    mallocinit()            // メモリアロケータの初期化
    alginit()               // AES アルゴリズムハードウェアサポートの初期化
    fastrandinit()          // ランダムシードの初期化、以前の startupRandomData を使用
    mcommoninit(gp.m, -1)   // スレッドの増分 ID シグナル処理コルーチンと高速ランダムシードの初期化
    modulesinit()           // 各モジュールの初期化 GC スキャンのグローバル変数のサイズを読み取る
    typelinksinit()         // 各モジュールのタイプリンクリスト情報を収集
    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("ブートストラップ中の未知の実行可能なゴルーチン")
    }
}

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 マスク、各ビットは対応するインデックス allp 上の P が空いているかどうかを示します
    // 3. timerpMask 定期的な P のマスク、各ビットは対応するインデックス allp 上の P にタイマーがあるかどうかを示します

    // プロセス数の拡張時に新しく作成された 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)
    }

    // プロセス数の縮小時に古い 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 {
        // もしプロセス数が変わった場合、GC が占有するプロセス容量を変更する必要があります(デフォルトでは 25% のプロセス数を取ります)
        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) // フレームを少し超えて読み取る場合のための追加スペース
    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 以前の命令が同じ関数にあるようにするため
    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()

    // m.g0.sched を mstart0 の mstart1 呼び出しの直後に戻るラベルとして設定し、goexit0 と mcall に使用します。
    // スケジュールを呼び出した後は mstart1 に戻ることはありません。
    // したがって、他の呼び出しは現在のフレームを再利用できます。
    // goexit0 は gogo を行い、mstart1 から戻る必要があり、mstart0 をスレッドから終了させます。
    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 が1つだけです
    // すべてが 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 または 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()
        }
    }

    // プロセス終了前に登録されたフックを実行します
    // 例えば compile -cover モード下のコードカバレッジデータ出力
    runExitHooks(0)
    exit(0)
}
読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。