go 二进制启动流程分析
14 min read

go 二进制启动流程分析

借助 gdb 调试信息观测 golang 二进制启动的过程,从侧面理解 go runtime 的实现细节以及 go 二进制的内存布局设计。
go 二进制启动流程分析
Photo by Andy Hermawan / Unsplash

gdb 分析 go 二进制的启动流程

通过简单的 go 文件 start.go 分析 go 二进制启动流程。

package main

import "fmt"

func main() {
    fmt.Print("test go binary")
}

gdb 分析步骤:

  1. 关闭内联函数和编译优化配置后,对 go 源码进行编译
  2. 通过 info files 找到 go 二进制进程的入口
  3. 在入口地址打上断点
  4. 单步运行

具体调试过程如下所示。

root@ubuntu-hirsute:~/go/src/go-tracing/gobinary#
>> go build -a -gcflags "-N -l" -o start start.go
root@ubuntu-hirsute:~/go/src/go-tracing/gobinary#
>> gdb start
GNU gdb (Ubuntu 10.1-2ubuntu2) 10.1.90.20210411-git
Copyright (C) 2021 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from start...
Loading Go Runtime support.
(gdb) info files
Symbols from "/root/go/src/go-tracing/gobinary/start".
Local exec file:
        `/root/go/src/go-tracing/gobinary/start', file type elf64-x86-64.
        Entry point: 0x454dc0
        0x0000000000401000 - 0x000000000048cfa3 is .text
        0x000000000048d000 - 0x00000000004dc5f0 is .rodata
        0x00000000004dc7c0 - 0x00000000004dd42c is .typelink
        0x00000000004dd430 - 0x00000000004dd480 is .itablink
        0x00000000004dd480 - 0x00000000004dd480 is .gosymtab
        0x00000000004dd480 - 0x0000000000548999 is .gopclntab
        0x0000000000549000 - 0x0000000000549020 is .go.buildinfo
        0x0000000000549020 - 0x00000000005560f8 is .noptrdata
        0x0000000000556100 - 0x000000000055d150 is .data
        0x000000000055d160 - 0x00000000005789d0 is .bss
        0x00000000005789e0 - 0x000000000057b148 is .noptrbss
        0x0000000000400f9c - 0x0000000000401000 is .note.go.buildid
(gdb) b *(0x454dc0)
Breakpoint 1 at 0x454dc0: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.
(gdb) r
Starting program: /root/go/src/go-tracing/gobinary/start 

Breakpoint 1, _rt0_amd64_linux () at /usr/local/go/src/runtime/rt0_linux_amd64.s:8
8               JMP     _rt0_amd64(SB)
(gdb) 
_rt0_amd64 () at /usr/local/go/src/runtime/asm_amd64.s:15
15              MOVQ    0(SP), DI       // argc
(gdb) 
16              LEAQ    8(SP), SI       // argv
(gdb) 
17              JMP     runtime·rt0_go(SB)
(gdb) 
runtime.rt0_go () at /usr/local/go/src/runtime/asm_amd64.s:89
89              MOVQ    DI, AX          // argc
(gdb) 
90              MOVQ    SI, BX          // argv
(gdb) 
91              SUBQ    $(4*8+7), SP            // 2args 2auto
(gdb) 
runtime.rt0_go () at /usr/local/go/src/runtime/asm_amd64.s:92
92              ANDQ    $~15, SP
(gdb) 
runtime.rt0_go () at /usr/local/go/src/runtime/asm_amd64.s:93
93              MOVQ    AX, 16(SP)
(gdb) 
94              MOVQ    BX, 24(SP)
(gdb) 
98              MOVQ    $runtime·g0(SB), DI
(gdb) 
99              LEAQ    (-64*1024+104)(SP), BX
(gdb) 
100             MOVQ    BX, g_stackguard0(DI)
(gdb) 
101             MOVQ    BX, g_stackguard1(DI)
(gdb) 
102             MOVQ    BX, (g_stack+stack_lo)(DI)
(gdb) 
103             MOVQ    SP, (g_stack+stack_hi)(DI)
(gdb) 
106             MOVL    $0, AX
(gdb) 
107             CPUID
(gdb) 
109             CMPL    AX, $0
(gdb) 
110             JE      nocpuinfo
(gdb) 
115             CMPL    BX, $0x756E6547  // "Genu"
(gdb) 
116             JNE     notintel
(gdb) 
117             CMPL    DX, $0x49656E69  // "ineI"
(gdb) 
118             JNE     notintel
(gdb) 
119             CMPL    CX, $0x6C65746E  // "ntel"
(gdb) 
120             JNE     notintel
(gdb) 
121             MOVB    $1, runtime·isIntel(SB)
(gdb) 
122             MOVB    $1, runtime·lfenceBeforeRdtsc(SB)
(gdb) 
126             MOVL    $1, AX
(gdb) 
127             CPUID
(gdb) 
132             MOVQ    _cgo_init(SB), AX
(gdb) 
133             TESTQ   AX, AX
(gdb) 
134             JZ      needtls
(gdb) 
183             LEAQ    runtime·m0+m_tls(SB), DI
(gdb) 
184             CALL    runtime·settls(SB)
(gdb) 
188             MOVQ    $0x123, g(BX)
(gdb) 
189             MOVQ    runtime·m0+m_tls(SB), AX
(gdb) 
190             CMPQ    AX, $0x123
(gdb) 
191             JEQ 2(PC)
(gdb) 
196             LEAQ    runtime·g0(SB), CX
(gdb) 
197             MOVQ    CX, g(BX)
(gdb) 
198             LEAQ    runtime·m0(SB), AX
(gdb) 
201             MOVQ    CX, m_g0(AX)
(gdb) 
203             MOVQ    AX, g_m(CX)
(gdb) 
205             CLD                             // convention is D is always left cleared
(gdb) 
206             CALL    runtime·check(SB)
(gdb) 
208             MOVL    16(SP), AX              // copy argc
(gdb) 
209             MOVL    AX, 0(SP)
(gdb) 
210             MOVQ    24(SP), AX              // copy argv
(gdb) 
211             MOVQ    AX, 8(SP)
(gdb) 
212             CALL    runtime·args(SB)
(gdb) 
213             CALL    runtime·osinit(SB)
(gdb) 
214             CALL    runtime·schedinit(SB)
(gdb) 
217             MOVQ    $runtime·mainPC(SB), AX         // entry
(gdb) 
218             PUSHQ   AX
(gdb) 
219             PUSHQ   $0                      // arg size
(gdb) 
220             CALL    runtime·newproc(SB)
(gdb) 
221             POPQ    AX
(gdb) 
222             POPQ    AX
(gdb) 
225             CALL    runtime·mstart(SB)
(gdb) 
[New LWP 1239316]
[New LWP 1239317]
[New LWP 1239319]
[New LWP 1239318]
test go binary[LWP 1239319 exited]
[LWP 1239317 exited]
[LWP 1239316 exited]
[LWP 1238518 exited]
[Inferior 1 (process 1238518) exited normally]
(gdb) 

通过上述的初步调试可以发现,go 的二进制进程启动的过程大致是:

_rt0_amd64        -->
rt0_go            -->
runtime·settls    -->
runtime·check     -->
runtime·args      -->
runtime·osinit    -->
runtime·schedinit -->
runtime·newproc   -->
runtime·mstart    -->
main.main(fmt.Print("test go binary"))

go 二进制启动流程详细分析

以 go 1.17 版本的源码进行进程启动的详细分析,golang 源码参考链接:Github golang 1.17

启动步骤1

gdb 调试信息:

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

源码信息:

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

#include "textflag.h"

TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
    JMP _rt0_amd64(SB)

TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0
    JMP _rt0_amd64_lib(SB)

由入口点跳转到对应平台的执行函数例如 amd64 平台就是 _rt0_amd64

启动步骤2

gdb 调试信息:

(gdb) 
_rt0_amd64 () at /usr/local/go/src/runtime/asm_amd64.s:15
15              MOVQ    0(SP), DI       // argc
(gdb) 
16              LEAQ    8(SP), SI       // argv
(gdb) 
17              JMP     runtime·rt0_go(SB)

源码信息:

// _rt0_amd64 is common startup code for most amd64 systems when using
// internal linking. This is the entry point for the program from the
// kernel for an ordinary -buildmode=exe program. The stack holds the
// number of arguments and the C-style argv.
TEXT _rt0_amd64(SB),NOSPLIT,$-8
    MOVQ    0(SP), DI   // argc
    LEAQ    8(SP), SI   // argv
    JMP runtime·rt0_go(SB)

执行 _rt0_amd64 函数主要就是做了两件事情:

  1. 处理命令行参数 argc(进程启动的参数个数) 和 argv(进程启动的参数,指针类型)
  • argc 参数被保存到寄存器 DI
  • argv 参数被保存到寄存器 SI
  1. 执行函数 rt0_go 函数

启动步骤3

go 二进程主要的进程准备逻辑都是在 runtime.rt0_go 函数中完成的,下面我打算将 rt0_go 函数按照逻辑拆分来分析。

启动步骤3.1

gdb 调试信息:

(gdb) 
runtime.rt0_go () at /usr/local/go/src/runtime/asm_amd64.s:89
89              MOVQ    DI, AX          // argc
(gdb) 
90              MOVQ    SI, BX          // argv
(gdb) 
91              SUBQ    $(4*8+7), SP            // 2args 2auto
(gdb) 
runtime.rt0_go () at /usr/local/go/src/runtime/asm_amd64.s:92
92              ANDQ    $~15, SP
(gdb) 
runtime.rt0_go () at /usr/local/go/src/runtime/asm_amd64.s:93
93              MOVQ    AX, 16(SP)
(gdb) 
94              MOVQ    BX, 24(SP)

源码信息:

TEXT runtime·rt0_go(SB),NOSPLIT|TOPFRAME,$0
    // copy arguments forward on an even stack
    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)

这一步主要是处理命令行参数,即将命令行参数拷贝到主线程的栈上。主要包括:

  1. argc 拷贝到 AX 寄存器
  2. argv 拷贝到 BX 寄存器
  3. 将栈扩大为 39 字节(这里不是很理解为什么是 39 字节)
  4. 进行16字节对齐
  5. 将 argc 放到栈指针 SP + 16 字节处
  6. 将 argv 放到栈指针 SP + 24 字节处

启动步骤3.2

gdb 调试信息:

(gdb) 
98              MOVQ    $runtime·g0(SB), DI
(gdb) 
99              LEAQ    (-64*1024+104)(SP), BX
(gdb) 
100             MOVQ    BX, g_stackguard0(DI)
(gdb) 
101             MOVQ    BX, g_stackguard1(DI)
(gdb) 
102             MOVQ    BX, (g_stack+stack_lo)(DI)
(gdb) 
103             MOVQ    SP, (g_stack+stack_hi)(DI)

源码信息:

    // create istack out of the given (operating system) stack.
    // _cgo_init may update stackguard.
    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)

这一步骤主要是初始化全局变量 g0,为 g0 在主线程栈上分配大约 64K 栈空间,并设置 g0 的 stackguard0,stackguard1,stack 三个字段:

  1. g0 的地址放入 DI 寄存器
  2. 设置主线程栈空间 BX = SP - 64*1024 + 104
  3. 开始初始化 g0 对象的 stackguard0,stackguard1,stack 这三个字段
  • g0.stackguard0 = SP - 64*1024 + 104
  • g0.stackguard1 = SP - 64*1024 + 104
  • g0.stack.lo = SP - 64*1024 + 104
  • g0.stack.hi = SP

启动步骤3.3

gdb 调试信息:

(gdb) 
106             MOVL    $0, AX
(gdb) 
107             CPUID
(gdb) 
109             CMPL    AX, $0
(gdb) 
110             JE      nocpuinfo
(gdb) 
115             CMPL    BX, $0x756E6547  // "Genu"
(gdb) 
116             JNE     notintel
(gdb) 
117             CMPL    DX, $0x49656E69  // "ineI"
(gdb) 
118             JNE     notintel
(gdb) 
119             CMPL    CX, $0x6C65746E  // "ntel"
(gdb) 
120             JNE     notintel
(gdb) 
121             MOVB    $1, runtime·isIntel(SB)
(gdb) 
122             MOVB    $1, runtime·lfenceBeforeRdtsc(SB)
(gdb) 
126             MOVL    $1, AX
(gdb) 
127             CPUID
(gdb) 
132             MOVQ    _cgo_init(SB), AX
(gdb) 
133             TESTQ   AX, AX

源码信息:

    // find out information about the processor we're on
    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:
    // if there is an _cgo_init, call it.
    MOVQ    _cgo_init(SB), AX
    TESTQ   AX, AX
    JZ  needtls
    // arg 1: g0, already in DI
    MOVQ    $setg_gcc<>(SB), SI // arg 2: setg_gcc
#ifdef GOOS_android
    MOVQ    $runtime·tls_g(SB), DX     // arg 3: &tls_g
    // arg 4: TLS base, stored in slot 0 (Android's TLS_SLOT_SELF).
    // Compensate for tls_g (+16).
    MOVQ    -16(TLS), CX
#else
    MOVQ    $0, DX  // arg 3, 4: not used when using platform's TLS
    MOVQ    $0, CX
#endif
#ifdef GOOS_windows
    // Adjust for the Win64 calling convention.
    MOVQ    CX, R9 // arg 4
    MOVQ    DX, R8 // arg 3
    MOVQ    SI, DX // arg 2
    MOVQ    DI, CX // arg 1
#endif
    CALL    AX

    // update stackguard after _cgo_init
    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)

#ifndef GOOS_windows
    JMP ok
#endif

这一步骤主要是执行 CPUID 指令,探测 CPU 信息和指令集代码,然后再执行 nocpuinfo 代码块判断是否需要初始化 cgo,如果开启了 cgo 特性,则会修改 g0 的部分字段。

启动步骤3.4

gdb 调试信息:

(gdb) 
133             TESTQ   AX, AX
(gdb) 
134             JZ      needtls
(gdb) 
183             LEAQ    runtime·m0+m_tls(SB), DI
(gdb) 
184             CALL    runtime·settls(SB)
(gdb) 
188             MOVQ    $0x123, g(BX)
(gdb) 
189             MOVQ    runtime·m0+m_tls(SB), AX
(gdb) 
190             CMPQ    AX, $0x123
(gdb) 
191             JEQ 2(PC)

源码信息:

needtls:
#ifdef GOOS_plan9
    // skip TLS setup on Plan 9
    JMP ok
#endif
#ifdef GOOS_solaris
    // skip TLS setup on Solaris
    JMP ok
#endif
#ifdef GOOS_illumos
    // skip TLS setup on illumos
    JMP ok
#endif
#ifdef GOOS_darwin
    // skip TLS setup on Darwin
    JMP ok
#endif
#ifdef GOOS_openbsd
    // skip TLS setup on OpenBSD
    JMP ok
#endif

    LEAQ    runtime·m0+m_tls(SB), DI
    CALL    runtime·settls(SB)

    // store through it, to make sure it works
    get_tls(BX)
    MOVQ    $0x123, g(BX)
    MOVQ    runtime·m0+m_tls(SB), AX
    CMPQ    AX, $0x123
    JEQ 2(PC)
    CALL    runtime·abort(SB)

这一步骤执行 needtls 代码块,初始化 tls 和 m0,tls 为线程本地存储,在 golang 程序运行过程中,每个 m 都需要和一个工作线程关联,那么工作线程如何知道其关联的 m,此时就会用到线程本地存储,线程本地存储就是线程私有的全局变量,通过线程本地存储可以为每个线程初始化一个私有的全局变量 m,然后就可以在每个工作线程中都使用相同的全局变量名来访问不同的 m 结构体对象。后面会分析到其实每个工作线程 m 在刚刚被创建出来进入调度循环之前就利用线程本地存储机制为该工作线程实现了一个指向 m 结构体实例对象的私有全局变量。tls 地址会写到 m0 中,而 m0 会和 g0 绑定,所以可以直接从 tls 中获取到 g0。

在后面代码分析中,会经常看到调用 getg 函数,getg 函数会从线程本地存储中获取当前正在运行的 g,这里获取出来的 m 关联的 g0。

NOTE:由于示例代码中没有用到本地变量,所以 tls 代码部分调试信息比较少,这里查阅资料做一下补充,如下所示。

// 下面开始初始化tls(thread local storage,线程本地存储),设置 m0 为线程私有变量,将 m0 绑定到主线程
needtls:
    LEAQ    runtime·m0+m_tls(SB), DI    // DI = &m0.tls,取m0的tls成员的地址到DI寄存器

    // 调用 runtime·settls 函数设置线程本地存储,runtime·settls 函数的参数在 DI 寄存器中
    // 在 runtime·settls 函数中将 m0.tls[1] 的地址设置为 tls 的地址
    // runtime·settls 函数在 runtime/sys_linux_amd64.s#599
    CALL    runtime·settls(SB)

    // 此处是在验证本地存储是否可以正常工作,确保值正确写入了 m0.tls,
    // 如果有问题则 abort 退出程序
    // get_tls 是宏,位于 runtime/go_tls.h
    get_tls(BX)                         // 将 tls 的地址放入 BX 中,即 BX = &m0.tls[1]
    MOVQ    $0x123, g(BX)               // BX = 0x123,即 m0.tls[0] = 0x123
    MOVQ    runtime·m0+m_tls(SB), AX    // AX = m0.tls[0]
    CMPQ    AX, $0x123
    JEQ 2(PC)                           // 如果相等则向后跳转两条指令即到 ok 代码块
    CALL    runtime·abort(SB)           // 使用 INT 指令执行中断

启动步骤3.5

gdb 调试信息:

(gdb) 
196             LEAQ    runtime·g0(SB), CX
(gdb) 
197             MOVQ    CX, g(BX)
(gdb) 
198             LEAQ    runtime·m0(SB), AX
(gdb) 
201             MOVQ    CX, m_g0(AX)
(gdb) 
203             MOVQ    AX, g_m(CX)
(gdb) 
205             CLD                             // convention is D is always left cleared
(gdb) 
206             CALL    runtime·check(SB)
(gdb) 
208             MOVL    16(SP), AX              // copy argc
(gdb) 
209             MOVL    AX, 0(SP)
(gdb) 
210             MOVQ    24(SP), AX              // copy argv
(gdb) 
211             MOVQ    AX, 8(SP)
(gdb) 
212             CALL    runtime·args(SB)
(gdb) 
213             CALL    runtime·osinit(SB)
(gdb) 
214             CALL    runtime·schedinit(SB)
(gdb) 
217             MOVQ    $runtime·mainPC(SB), AX         // entry
(gdb) 
218             PUSHQ   AX
(gdb) 
219             PUSHQ   $0                      // arg size
(gdb) 
220             CALL    runtime·newproc(SB)
(gdb) 
221             POPQ    AX
(gdb) 
222             POPQ    AX
(gdb) 
225             CALL    runtime·mstart(SB)

源码信息:

ok:
    // set the per-goroutine and per-mach "registers"
    get_tls(BX)
    LEAQ    runtime·g0(SB), CX
    MOVQ    CX, g(BX)
    LEAQ    runtime·m0(SB), AX

    // save m->g0 = g0
    MOVQ    CX, m_g0(AX)
    // save m0 to g0->m
    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)
    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)

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

    // Prevent dead-code elimination of debugCallV2, which is
    // intended to be called by debuggers.
    MOVQ    $runtime·debugCallV2<ABIInternal>(SB), AX
    RET

// 下面是摘取的 Reference 中的注释版本,帮助理解细节:
// 首先将 g0 地址保存在 tls 中,即 m0.tls[0] = &g0,然后将 m0 和 g0 绑定
// 即 m0.g0 = g0, g0.m = m0
ok:
    get_tls(BX)                 // 获取tls地址到BX寄存器,即 BX = m0.tls[0]
    LEAQ    runtime·g0(SB), CX  // CX = &g0
    MOVQ    CX, g(BX)           // m0.tls[0]=&g0
    LEAQ    runtime·m0(SB), AX  // AX = &m0

    MOVQ    CX, m_g0(AX)        // m0.g0 = g0
    MOVQ    AX, g_m(CX)         // g0.m = m0

    CLD                         // convention is D is always left cleared
    // check 函数检查了各种类型以及类型转换是否有问题,位于 runtime/runtime1.go#137 中
    CALL    runtime·check(SB)

    // 将 argc 和 argv 移动到 SP+0 和 SP+8 的位置
    // 此处是为了将 argc 和 argv 作为 runtime·args 函数的参数
    MOVL    16(SP), AX
    MOVL    AX, 0(SP)
    MOVQ    24(SP), AX
    MOVQ    AX, 8(SP)

    // args 函数会从栈中读取参数和环境变量等进行处理
    // args 函数位于 runtime/runtime1.go#61
    CALL    runtime·args(SB)

    // osinit 函数用来初始化 cpu 数量,函数位于 runtime/os_linux.go#301
    CALL    runtime·osinit(SB)
    // schedinit 函数用来初始化调度器,函数位于 runtime/proc.go#654
    CALL    runtime·schedinit(SB)

    // 创建第一个 goroutine 执行 runtime.main 函数。获取 runtime.main 的地址,调用 newproc 创建 g
    MOVQ    $runtime·mainPC(SB), AX
    PUSHQ   AX            // runtime.main 作为 newproc 的第二个参数入栈
    PUSHQ   $0            // newproc 的第一个参数入栈,该参数表示runtime.main函数需要的参数大小,runtime.main没有参数,所以这里是0

    // newproc 创建一个新的 goroutine 并放置到等待队列里,该 goroutine 会执行runtime.main 函数, 函数位于 runtime/proc.go#4250
    CALL    runtime·newproc(SB)
    // 弹出栈顶的数据
    POPQ    AX
    POPQ    AX

    // mstart 函数会启动主线程进入调度循环,然后运行刚刚创建的 goroutine,mstart 会阻塞住,除非函数退出,mstart 函数位于 runtime/proc.go#1328
    CALL    runtime·mstart(SB)

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

    // Prevent dead-code elimination of debugCallV2, which is
    // intended to be called by debuggers.
    MOVQ    $runtime·debugCallV2<ABIInternal>(SB), AX
    RET

执行 ok 代码块,主要逻辑为:

  • 将 m0 和 g0 进行绑定,启动主线程;
  • 调用 runtime·osinit 函数用来初始化 cpu 数量,调度器初始化时需要知道当前系统有多少个 CPU 核;
  • 调用 runtime·schedinit 函数会初始化 m0 和 p 对象,还设置了全局变量 sched 的 maxmcount 成员为10000,限制最多可以创建 10000 个操作系统线程出来工作;
  • 调用 runtime·newproc 为 main 函数创建 goroutine;
  • 调用 runtime·mstart 启动主线程,执行 main 函数;

至此 go 二进制就启动完成了,下面就开始运行 main 函数的逻辑了。最后为了帮助更好的理解,我引用了 Reference 中的 go 二进程的内存空间布局图:

References

  1. Golang 程序启动流程分析
  2. A Record of Debugging Golang Runtime - Polar9527’ Blog
  3. go/src/runtime at release-branch.go1.17 · golang/go

Public discussion