以下のコードをコンパイルします。
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.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 // 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)
}