問題
GO のプログラムはmainという package のmain()方法で始めるのがよくご存知でしたが、その前に何が起こったのかな?これをちょっと調べて行きます。
実行環境
- Ubuntu 18.04
- Go 1.14
もっともシンプルな GO プログラム
まずはこの何もしないプログラムを用意します
package main
func main() {}
$ go build main.go
main.mainを探す
ELFを調べてみる
まずはコンパイルされたバイナリをreadelfでみてみま
$ readelf -h main
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x4552c0
Start of program headers: 64 (bytes into file)
Start of section headers: 456 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 7
Size of section headers: 64 (bytes)
Number of section headers: 25
Section header string table index: 3
その中のEntry point addressは実行の入口です、そのアドレスは0x4552c0。じゃ次はこのアドレスを見てみよう。
$ objdump -d main | grep 4552c0
00000000004552c0 <_rt0_amd64_linux>:
4552c0: e9 4b c4 ff ff jmpq 451710 <_rt0_amd64>
0x4552c0のとこは_rt0_amd64_linuxという関数ですね、そしてすぐ_rt0_amd64に移行するようです。
_rt0_amd64はどこ?
ソースコードに_rt0_amd64を検索したら、asm_amd64.sに見つけました。
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB)
そしてすぐruntime·rt0_goにジャンプしました。もうちょっと追ってみれば、これを見つけた
...
MOVL 16(SP), AX // copy argc
MOVL AX, 0(SP)
MOVQ 24(SP), AX // copy argv
MOVQ AX, 8(SP)
CALL runtime·args(SB)
CALL runtime·osinit(SB)
CALL runtime·schedinit(SB)
// create a new goroutine to start program
MOVQ $runtime·mainPC(SB), AX // entry
PUSHQ AX
PUSHQ $0 // arg size
CALL runtime·newproc(SB)
POPQ AX
POPQ AX
// start this M
CALL runtime·mstart(SB)
...
DATA runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOBL runtime·mainPC(SB),RODATA,$8
...
runtime·args、runtime·osinitとruntime·schedinitを次々CALLしてから、runtime·mainPCを引数としてruntime·newprocをCALLしました。このentryで注釈したruntime·mainPC実はruntime·mainとして定義したものです。
runtime.mainとruntime.newproc
またソースにnewprocを検索したら、proc.goにたどり着いた
...
// Create a new g running fn with siz bytes of arguments.
// Put it on the queue of g's waiting to run.
// The compiler turns a go statement into a call to this.
//
// The stack layout of this call is unusual: it assumes that the
// arguments to pass to fn are on the stack sequentially immediately
// after &fn. Hence, they are logically part of newproc's argument
// frame, even though they don't appear in its signature (and can't
// because their types differ between call sites).
//
// This must be nosplit because this stack layout means there are
// untyped arguments in newproc's argument frame. Stack copies won't
// be able to adjust them and stack splits won't be able to copy them.
//
//go:nosplit
func newproc(siz int32, fn *funcval) {
...
コメントによると、runtime.newprocは一つの goroutine を起動してfnを実行するということですね。そしてここのfnは先のruntime.mainのようです。次はruntime.mainを見てみましょ。
...
// The main goroutine.
func main() {
g := getg()
...
fn := main_main // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn()
...
runtime.mainはいろんなことをやっていて、main_mainをコールした。このmain_mainはもしかして。。。
...
//go:linkname main_main main.main
func main_main()
...
そうか、ここはgo:linknameという Compiler Directive に通してmain_mainをmain.mainに変換しました。
まとめ
- GO プログラムのほんとのエントリーポイントは
_rt0_amd64_linuxです(Linuxの場合)。 - いろんな初期化は
xxxinitでやっています。特にruntime.schedinit。 -
main.mainの実行も goroutine の一つです