問題
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 の一つです