在用Go编写应用程序的时候,可以认为main.main是整个应用程序的入口,但站在整个Go程序的角度来看,却并非如此。在main.main函数之前,Go底层已经做了大量的初始化工作,下面开始从程序真正的入口开始了解下初始化前的工作。

入口

通过GDB可以找到程序入口:

1
go build -gcflags "-N -l" -o test test.go

info files会列出程序的入口地址:

1
2
3
4
5
(gdb) info files
Symbols from "/home/sandydu/program/golang/test".
Local exec file:
        `/home/sandydu/program/golang/test', file type elf64-x86-64.
        Entry point: 0x452890

Entry point: 0x452890这个就是我们要打的入口地址,直接对这个地址打断点,GDB就会自动列出断点的具体位置:

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

rt0_linux_amd64.s文件的第8行就是真正的入口地址,看下详细代码:

@(src/runtime/rt0_linux_amd64.s:8)

1
2
3
4
TEXT _rt0_arm64_linux(SB),NOSPLIT|NOFRAME,$0
    MOVD    0(RSP), R0  // argc
    ADD     $8, RSP, R1 // argv
    BL  main(SB)

_rt0_arm64_linux的工作就是把命令行参数argc、argv放到寄存器,然后跳转到本文件的main继续执行,代码如下:

@(src/runtime/rt0_linux_amd64.s:98)

1
2
3
4
5
6
7
8
TEXT main(SB),NOSPLIT|NOFRAME,$0
    MOVD    $runtime·rt0_go(SB), R2
    BL  (R2)
exit:
    MOVD $0, R0
    MOVD    $94, R8 // sys_exit
    SVC
    B   exit

再继续跳转到runtime·rt0_go执行(没搞懂为啥要这样跳来跳去,直接点不好嘛。。。)

runtime·rt0_go开始进入初始化的流程,这个方法主要工作如下:

  • 处理命令行参数,将argc,argv放入栈。
  • 获取CPU相关信息
  • 将g0(0号goroutine)存入TLS,将m0(0号线程)存入TLS
  • 调用runtime.args,去 stack 里读取参数和环境变量
  • 调用runtime.osinit,获取CPU核数
  • 调用runtime.schedinit,这个函数功能很多,主要初始化调度相关的信息,后面详细介绍
  • 调用runtime.newproc,创建一个新的协程,并执行runtime.main,这个函数会调用我们熟悉的main.init和main.main
  • 创建 一个m,并调用schedule开始进入调度状态,整个程序开始run起来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
TEXT runtime·rt0_go(SB),NOSPLIT,$0
    // 将参数argc、argv放到堆栈上
    MOVQ    DI, AX      // argc
    MOVQ    SI, BX      // argv
    SUBQ    $(4*8+7), SP        // 2args 2auto
    ANDQ    $~15, SP
    MOVQ    AX, 16(SP)
    MOVQ    BX, 24(SP)

    // 初始化g0的stackguard和stack
    // g0.stackguard0 = (-64*1024+104)(SP)
    // g0.stackguard1 = (-64*1024+104)(SP)
    // g0.stack.l0 = (-64*1024+104)(SP)
    // g0.stack.hi = SP
    MOVQ    $runtime·g0(SB), DI
    LEAQ    (-64*1024+104)(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)

    // 通过CPUID指令获取CPU信息
    // 详见https://c9x.me/x86/html/file_module_x86_id_45.html
    MOVL    $0, AX
    CPUID
    MOVL    AX, SI
    CMPL    AX, $0
    JE  nocpuinfo

    // Figure out how to serialize RDTSC.
    // On Intel processors LFENCE is enough. AMD requires MFENCE.
    // Don't know about the rest, so let's do MFENCE.
    CMPL    BX, $0x756E6547  // "Genu"
    JNE notintel
    CMPL    DX, $0x49656E69  // "ineI"
    JNE notintel
    CMPL    CX, $0x6C65746E  // "ntel"
    JNE notintel
    MOVB    $1, runtime·isIntel(SB)
    MOVB    $1, runtime·lfenceBeforeRdtsc(SB)
notintel:

    // Load EAX=1 cpuid flags
    MOVL    $1, AX
    CPUID
    MOVL    AX, runtime·processorVersionInfo(SB)

nocpuinfo:
    // 如果启用了cgo,就执行_cgo_init函数
    MOVQ    _cgo_init(SB), AX
    TESTQ   AX, AX
    JZ  needtls
    // g0 already in DI
    MOVQ    DI, CX  // Win64 uses CX for first parameter
    MOVQ    $setg_gcc<>(SB), SI
    CALL    AX

    // 重新更新g0的g0的stackguard和stack
    MOVQ    $runtime·g0(SB), CX
    MOVQ    (g_stack+stack_lo)(CX), AX
    ADDQ    $const__StackGuard, AX
    MOVQ    AX, g_stackguard0(CX)
    MOVQ    AX, g_stackguard1(CX)
   
needtls:
    // 初始化TLS
    LEAQ    runtime·m0+m_tls(SB), DI
    CALL    runtime·settls(SB)

    // 验证TLS是否初始化成功
    get_tls(BX)
    MOVQ    $0x123, g(BX)
    MOVQ    runtime·m0+m_tls(SB), AX
    CMPQ    AX, $0x123
    JEQ 2(PC)
    CALL    runtime·abort(SB)
ok:
    // 把g0放入TLS,后面可以通过getg函数找到了
    get_tls(BX)
    LEAQ    runtime·g0(SB), CX
    MOVQ    CX, g(BX)
    LEAQ    runtime·m0(SB), AX

    // 将g0存到m0中,m->g0 = g0
    MOVQ    CX, m_g0(AX)
    // 将m0存到g0中,g0->m = m0
    MOVQ    AX, g_m(CX)

    CLD             // convention is D is always left cleared
    CALL    runtime·check(SB)

    // 处理参数
    MOVL    16(SP), AX      // copy argc
    MOVL    AX, 0(SP)
    MOVQ    24(SP), AX      // copy argv
    MOVQ    AX, 8(SP)
    CALL    runtime·args(SB)

    // 初始化OS:获取CPU数量
    CALL    runtime·osinit(SB)
   
    // 初始化调度,非常重要的函数,后续详解
    CALL    runtime·schedinit(SB)

    // 创建一个goruntine并执行,执行函数为runtime.main
    // 即: go runtime.main()
    MOVQ    $runtime·mainPC(SB), AX        // entry
    PUSHQ   AX
    PUSHQ   $0          // arg size
    CALL    runtime·newproc(SB)
    POPQ    AX
    POPQ    AX

    // 启动这个m,即m0,里面会调用schedule函数进入调度状态
    CALL    runtime·mstart(SB)

    CALL    runtime·abort(SB)  // mstart should never return
    RET

    MOVQ    $runtime·debugCallV1(SB), AX
    RET

DATA    runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOBL   runtime·mainPC(SB),RODATA,$8