Golang 程序启动流程

通常来说,程序执行的入口点都不是我们所写的 main 函数,编译器会在之前插入部分代码,用来执行相关的初始化工作。本文从整体流程上分析一下 Go 编译器在我们写的代码之前究竟做了什么。

准备

本次测试在我的电脑主机上完成,Golang 版本为 1.5.1:

$ go version
go version go1.5.1 linux/amd64

先编写一个简单的程序,如下:

test.go

package main

import "fmt"

func main(){
    fmt.Println("hello world")
}

编译为可执行程序,需要注意的是:最好使用 -gcflags -N -l 禁止编译器进行优化和内联,否则实际生成代码可能会不一样:

go build -gcflags "-N -l"

然后使用 gdb 进行调试:

(gdb) info files
Symbols from "/home/smart/code/go/src/test/test".
Local exec file:
    `/home/smart/code/go/src/test/test', file type elf64-x86-64.
    Entry point: 0x4561d0
    0x0000000000401000 - 0x00000000004b2240 is .text
    0x00000000004b3000 - 0x000000000053b4db is .rodata
    0x000000000053b4e0 - 0x000000000053d588 is .typelink
    0x000000000053d588 - 0x000000000053d588 is .gosymtab
    0x000000000053d5a0 - 0x000000000058ece5 is .gopclntab
    0x000000000058f000 - 0x0000000000590bc8 is .noptrdata
    0x0000000000590be0 - 0x0000000000593150 is .data
    0x0000000000593160 - 0x00000000005b6d38 is .bss
    0x00000000005b6d40 - 0x00000000005bbb80 is .noptrbss
    0x0000000000400fc8 - 0x0000000000401000 is .note.go.buildid

可以看到,程序的入口点在 0x4561d0 处。需要注意的是,入口点不一定是 .text 段开始的位置(比如这里 0x4561d0 处就是 main.main),该段各模块代码的顺序和链接器有关。在 Linux 下 ELF 格式中,有个 e_entry 字段指定程序的入口,然后在代码中会进行跳转。ELF 格式参考这里,可用 readelfobjdump 工具查看。

在此处打个 breakpoint:

(gdb) b *0x4561d0
Breakpoint 1 at 0x4561d0: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.

打开该文件,跳转到第 8 行:

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
    LEAQ    8(SP), SI // argv
    MOVQ    0(SP), DI // argc
    MOVQ    $main(SB), AX
    JMP    AX

这里将 argcargv 压栈,并跳转到 main 代码段。不过我暂时还不清楚是在哪里将 argcargv 放到 DI 和SI 寄存器中,以及参数 SB 代表的意义。

TEXT main(SB),NOSPLIT,$-8
    MOVQ    $runtime·rt0_go(SB), AX
    JMP    AX

这里跳转到 $runtime.rt0_go,通过 breakpoint 查看:

(gdb) b runtime.rt0_go
Breakpoint 2 at 0x452b00: file /usr/local/go/src/runtime/asm_amd64.s, line 12.

runtime.rt0_go 中,才真正执行一系列初始化工作,关键代码如下:

TEXT runtime·rt0_go(SB),NOSPLIT,$0

    ......

    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

    ......

    CALL    runtime·newproc(SB)

    ......

    // start this M
    CALL    runtime·mstart(SB)

这里分别调用 runtime.args()runtime.osinit()runtime.schedinit()runtime.newproc()runtime.mstart() 几个函数,再用 gdb 查看:

(gdb) b runtime.args
Breakpoint 4 at 0x434ec0: file /usr/local/go/src/runtime/runtime1.go, line 48.
(gdb) b runtime.osinit
Breakpoint 5 at 0x424290: file /usr/local/go/src/runtime/os1_linux.go, line 172.
(gdb) b runtime.schedinit
Breakpoint 6 at 0x429eb0: file /usr/local/go/src/runtime/proc1.go, line 40.
(gdb) b runtime.newproc
Breakpoint 3 at 0x42fce0: file /usr/local/go/src/runtime/proc1.go, line 2208.
(gdb) b runtime.mstart
Breakpoint 4 at 0x42bb60: file /usr/local/go/src/runtime/proc1.go, line 674.

runtime.args() 做的事情比较简单,完成了 argcargv 的初始化工作。

runtime.osinit() 获取 CPU 数目。

runtime.schedinit() 比较复杂,做了一些初始化工作,具体放到后面再看。

runtime.newproc() 则创建一个 goroutine,用来运行我们编写的 main.main() 函数。

runtime.mstart() 执行这个 M,M、P、G是什么以及他们的关系同样放到后面。

至此,程序启动前的工作就已经做完了。