0%

【golang源码分析】之启动追踪

  很多人也许对Go代码是怎么启动的比较感兴趣, 我也不例外。 因此在这里调试下代码, 看看到底是怎么启动的, 并在此做下记录, 暂时不会逐行分析,只是了解下Go的启动流程。(关于环境,在第一篇【golang源码分析】之源码结构中已经提到过, 如无特殊说明后续相关的都是基于此环境,不再提及。)

安装go1.12.9

  应为需要调试源代码,所以这里源码安装go, 并禁止优化和内联。我是ubuntu18.04环境,其他环境类似。

  这里采用了docker来安装, github地址go-src-debug-docker, 并且镜像已经推到hub上面了go-src-debug

1
2
3
4
5
6
# 启动docker
# 如果需要映射卷的, 请加上-v选项
# 这里需要给--privileged, 不然gdb有问题
docker run -it -d --privileged --name go-src-debug veezhanggo-src-debug:1.12.9
# 进入docker
docker exec -it go-src-debug bash

  注: 如无特殊说明,后续的调试都是在docker下执行。

调试代码样例

  这里写了一个简单的代码(main.go)来进行启动追踪, 如下:

1
2
3
4
5
6
7
8
9
package main

import (
"fmt"
)

func main(){
fmt.Println("Hello, I'm Vee Zhang.")
}

编译

  Go打包的程序和C打包的程序一样在linux系统上是 elf 格式的。编译参数我们添加了 -gcflags 编译参数, 编译命令如下:

1
2
3
4
5
6
# -N    disable optimizations   禁止优化
# -l disable inlining 禁止内联
go build -gcflags "-N -l" -o main main.go
# 执行下,看看是否正常
./main
Hello, I'm Vee Zhang.

调试

  使用 gdb 进行调试, 由于添加了部分注释,可能会导致gdb行号对不上。

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
gdb ./main
GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-git
Copyright (C) 2018 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:
<http://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 ./main...done.
warning: File "go/src/runtime/runtime-gdb.py" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
To enable execution of this file add
add-auto-load-safe-path go/src/runtime/runtime-gdb.py
line to your configuration file "/root/.gdbinit".
To completely disable this security protection add
set auto-load safe-path /
line to your configuration file "/root/.gdbinit".
For more information about this security protection see the
"Auto-loading safe path" section in the GDB manual. E.g., run from the shell:
info "(gdb)Auto-loading safe path"

  在gdb命令行输入: info files, 可以看到main的Entry point

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) info files
Symbols from "/main".
Local exec file:
`/main', file type elf64-x86-64.
Entry point: 0x452890
0x0000000000401000 - 0x0000000000487304 is .text
0x0000000000488000 - 0x00000000004d2ac0 is .rodata
0x00000000004d2c60 - 0x00000000004d3834 is .typelink
0x00000000004d3838 - 0x00000000004d3880 is .itablink
0x00000000004d3880 - 0x00000000004d3880 is .gosymtab
0x00000000004d3880 - 0x00000000005433e6 is .gopclntab
0x0000000000544000 - 0x0000000000550a9c is .noptrdata
0x0000000000550aa0 - 0x0000000000557790 is .data
0x00000000005577a0 - 0x0000000000572ef0 is .bss
0x0000000000572f00 - 0x0000000000575658 is .noptrbss
0x0000000000400f9c - 0x0000000000401000 is .note.go.buildid

  在Entry point的指针上打算断点b *0x452890, 并启动。

1
2
3
4
5
6
7
(gdb) b *0x452890
Breakpoint 1 at 0x452890: file go/src/runtime/rt0_linux_amd64.s, line 8.
(gdb) r
Starting program: /main

Breakpoint 1, _rt0_amd64_linux () at go/src/runtime/rt0_linux_amd64.s:8
8 JMP _rt0_amd64(SB)

  可以看到,该断点在文件go/src/runtime/rt0_linux_amd64.s的第8行, 我们打开看看

1
2
3
4
5
6
7
8
9
10
11
12
// 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"

// linux amd64 系统的启动函数
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB) // 跳转到_rt0_amd64函数, 在 asm_amd64.s 中。

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

  注: 不同的平台有不同的程序入口, 如下:

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
go/src/runtime/rt0_aix_ppc64.s
go/src/runtime/rt0_android_386.s
go/src/runtime/rt0_android_amd64.s
go/src/runtime/rt0_android_arm.s
go/src/runtime/rt0_android_arm64.s
go/src/runtime/rt0_darwin_386.s
go/src/runtime/rt0_darwin_amd64.s
go/src/runtime/rt0_darwin_arm.s
go/src/runtime/rt0_darwin_arm64.s
go/src/runtime/rt0_dragonfly_amd64.s
go/src/runtime/rt0_freebsd_386.s
go/src/runtime/rt0_freebsd_amd64.s
go/src/runtime/rt0_freebsd_arm.s
go/src/runtime/rt0_js_wasm.s
go/src/runtime/rt0_linux_386.s
go/src/runtime/rt0_linux_amd64.s
go/src/runtime/rt0_linux_arm.s
go/src/runtime/rt0_linux_arm64.s
go/src/runtime/rt0_linux_mips64x.s
go/src/runtime/rt0_linux_mipsx.s
go/src/runtime/rt0_linux_ppc64.s
go/src/runtime/rt0_linux_ppc64le.s
go/src/runtime/rt0_linux_s390x.s
go/src/runtime/rt0_nacl_386.s
go/src/runtime/rt0_nacl_amd64p32.s
go/src/runtime/rt0_nacl_arm.s
go/src/runtime/rt0_netbsd_386.s
go/src/runtime/rt0_netbsd_amd64.s
go/src/runtime/rt0_netbsd_arm.s
go/src/runtime/rt0_openbsd_386.s
go/src/runtime/rt0_openbsd_amd64.s
go/src/runtime/rt0_openbsd_arm.s
go/src/runtime/rt0_plan9_386.s
go/src/runtime/rt0_plan9_amd64.s
go/src/runtime/rt0_plan9_arm.s
go/src/runtime/rt0_solaris_amd64.s
go/src/runtime/rt0_windows_386.s
go/src/runtime/rt0_windows_amd64.s
go/src/runtime/rt0_windows_arm.s

  从go/src/runtime/rt0_linux_amd64.s文件我们可以知道,跳转到_rt0_amd64了, 再设置断点, 并继续调试:

1
2
3
4
5
6
7
(gdb) b _rt0_amd64
Breakpoint 2 at 0x44ef70: file go/src/runtime/asm_amd64.s, line 15.
(gdb) c
Continuing.

Breakpoint 2, _rt0_amd64 () at go/src/runtime/asm_amd64.s:15
15 MOVQ 0(SP), DI // argc

  接着打开go/src/runtime/asm_amd64.s文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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 "go_asm.h"
#include "go_tls.h"
#include "funcdata.h"
#include "textflag.h"

// _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.
// _rt0_amd64 是使用内部链接时大多数amd64系统的通用启动代码。 这是内核中普通 -buildmode=exe 程序的入口点。
// 栈保存了参数的数量以及 C 风格的 argv
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc // 设置参数argc
LEAQ 8(SP), SI // argv // 设置参数argv
JMP runtime·rt0_go(SB) // 跳转到runtime·rt0_go

  从go/src/runtime/asm_amd64.s文件我们可以知道,设置参数后跳转到runtime·rt0_go了, 再设置断点:

  注意: 断点设置为runtime.rt0_go, 中间的 · 换成下面的 ., 下同 。

1
2
3
4
5
6
7
(gdb) b runtime.rt0_go
Breakpoint 3 at 0x44ef80: file go/src/runtime/asm_amd64.s, line 89.
(gdb) c
Continuing.

Breakpoint 3, runtime.rt0_go () at go/src/runtime/asm_amd64.s:89
89 MOVQ DI, AX // argc

  可以看到,还在文件go/src/runtime/asm_amd64.s中, 定义在87行, 如下:

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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
TEXT runtime·rt0_go(SB),NOSPLIT,$0
// copy arguments forward on an even stack
// 将参数向前复制到一个偶数栈上
MOVQ DI, AX // argc // 获取之前设置的argc参数
MOVQ SI, BX // argv // 获取之前设置的argv参数
SUBQ $(4*8+7), SP // 2args 2auto
ANDQ $~15, SP // 字节对齐
MOVQ AX, 16(SP)
MOVQ BX, 24(SP)

// create istack out of the given (operating system) stack.
// _cgo_init may update stackguard.
// 从给定(操作系统)栈中创建 istack 。 _cgo_init 可能更新 stackguard
// runtime.g0 位于 runtime/proc.go
// 初始化 g0,g0 的栈实际上就是 linux 分配的栈,大约 64k。
MOVQ $runtime·g0(SB), DI // DI = runtime·g0
LEAQ (-64*1024+104)(SP), BX // BX = SP-64*1024+104
MOVQ BX, g_stackguard0(DI) // g0.stackguard0 = SP-64*1024+104
MOVQ BX, g_stackguard1(DI) // g0.stackguard1 = g0.stackguard0
MOVQ BX, (g_stack+stack_lo)(DI) // g0.stack.lo = g0.stackguard0
MOVQ SP, (g_stack+stack_hi)(DI) // g0.stack.hi = SP

// find out information about the processor we're on
// 寻找正在运行的处理器信息
MOVL $0, AX // AX = 0 ,CPUID 参数?
CPUID // CPUID 会设置 AX , BX , CX , DX 的值
MOVL AX, SI // SI = AX , 保存 CPU 信息
CMPL AX, $0 // 如果没有获取到
JE nocpuinfo // 跳转到 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.
// 处理如何序列化 RDTSC 。在 intel 处理器上, LFENCE 足够了。 AMD 则需要 MFENCE。 其他处理器的情况不清楚,所以让用 MFENCE。
// 判断是否是 intel cpu
CMPL BX, $0x756E6547 // "Genu"
JNE notintel
CMPL DX, $0x49656E69 // "ineI"
JNE notintel
CMPL CX, $0x6C65746E // "ntel"
JNE notintel
MOVB $1, runtime·isIntel(SB) // 设置是否是 intel cpu ,在 proc.go 中。
MOVB $1, runtime·lfenceBeforeRdtsc(SB) // 设置是否在 RDTSC 指令之前是否需要 LFENCE 指令。否则是 MFENCE 指令。
notintel:

// Load EAX=1 cpuid flags
MOVL $1, AX // AX = 1 ,CPUID 参数?
CPUID // 获取 cpu flags
MOVL AX, runtime·processorVersionInfo(SB)// 设置 processorVersionInfo

nocpuinfo:
// if there is an _cgo_init, call it.
// 如果有 _cgo_init ,就执行
MOVQ _cgo_init(SB), AX
// TEST 对两个参数(目标,源)执行 AND 逻辑操作,并根据结果设置标志寄存器 (ZF),结果本身不会保存。
// ZF(Zero Flag) 零标志,运算结果为0时置1,否则置0。
TESTQ AX, AX
JZ needtls // jump if zero,也就是 AX AND AX == 0 ( _cgo_init 返回 0 ),则跳转到 needtls
// g0 already in DI
// 这里的 DI 就是上面初始化 g0 时设置的 g0 的地址
MOVQ DI, CX // Win64 uses CX for first parameter
MOVQ $setg_gcc<>(SB), SI
CALL AX

// update stackguard after _cgo_init
// _cgo_init 后更新 stackguard
MOVQ $runtime·g0(SB), CX // CX = g0
MOVQ (g_stack+stack_lo)(CX), AX // AX = g0.stack.lo
ADDQ $const__StackGuard, AX // AX += const__StackGuard , stack.go 中定义
MOVQ AX, g_stackguard0(CX) // g0.stackguard0 = AX = g0.stack.lo + const__StackGuard
MOVQ AX, g_stackguard1(CX) // g0.stackguard1 = AX = g0.stack.lo + const__StackGuard

#ifndef GOOS_windows
JMP ok // Windows 跳转到 ok
#endif
needtls:
#ifdef GOOS_plan9
// skip TLS setup on Plan 9
JMP ok // Plan 9 跳转到 ok
#endif
#ifdef GOOS_solaris
// skip TLS setup on Solaris
JMP ok // GOOS_solaris 跳转到 ok
#endif
#ifdef GOOS_darwin
// skip TLS setup on Darwin
JMP ok // GOOS_darwin 跳转到 ok
#endif

// 设置tls, Thread Local Storage
LEAQ runtime·m0+m_tls(SB), DI // DI = m0.tls ,这个会在 runtime·settls 中使用
CALL runtime·settls(SB) // 调用 runtime·settls, settls 函数的参数在DI寄存器中

// store through it, to make sure it works
// get_tls 和 g 是宏,位于 runtime/go_tls.h
// #define get_tls(r) MOVQ TLS, r
// #define g(r) 0(r)(TLS*1)
// 此处对 tls 进行了一次测试,确保值正确写入了 m0.tls
get_tls(BX) // 等价于 MOVQ TLS, BX 。 从 TLS 起始移动 8 byte 值到 BX 寄存器,获取 fs 段基地址并放入 BX 寄存器,其实就是 m0.tls[1] 的地址
MOVQ $0x123, g(BX) // 0(BX)(TLS*1) = $0x123 ,0x123拷贝到fs段基地址偏移-8的内存位置,也就是m0.tls[0] =0x123
MOVQ runtime·m0+m_tls(SB), AX // AX = m0.tls[0]
CMPQ AX, $0x123 // 比较 AX == $0x123
JEQ 2(PC) // 如果相等,跳转下面 2 条指令,也就是 ok 后
CALL runtime·abort(SB) // 检测失败
ok:
// set the per-goroutine and per-mach "registers"
// 将 g0 放到 tls 里,这里实际上就是 m0.tls
get_tls(BX) // 等价于 MOVQ TLS, BX 。 从 TLS 起始移动 8 byte 值到 BX 寄存器,获取 fs 段基地址并放入 BX 寄存器,其实就是 m0.tls[1] 的地址
LEAQ runtime·g0(SB), CX // CX=g0
MOVQ CX, g(BX) // 等价于 MOVQ CX, 0(BX)(TLS*1), 把g0存到TLS
LEAQ runtime·m0(SB), AX // AX=m0

// save m->g0 = g0
MOVQ CX, m_g0(AX) // m0.g0 = g0
// save m0 to g0->m
MOVQ AX, g_m(CX) // g0.m = m0

CLD // convention is D is always left cleared
// 这个函数检查了各种类型以及类型转换是否有问题
CALL runtime·check(SB)

MOVL 16(SP), AX // copy argc // AX = argc
MOVL AX, 0(SP) // 设置后面 runtime·args 调用的第一个参数
MOVQ 24(SP), AX // copy argv // AX = argv
MOVQ AX, 8(SP) // 设置后面 runtime·args 调用的第二个参数
CALL runtime·args(SB) // 设置参数 , 函数原型: func args(c int32, v **byte) , 在 runtime1.go
CALL runtime·osinit(SB) // 初始化 os ,在 os_linux.go
CALL runtime·schedinit(SB) // 初始化 sched ,在 proc.go

// create a new goroutine to start program
// 创建 goroutine 并加入到等待队列,该 goroutine 执行 runtime.mainPC 所指向的函数
MOVQ $runtime·mainPC(SB), AX // entry// 入口函数 在 proc.go 中
PUSHQ AX // 压栈,设置参数 runtime·newproc 的 fn
PUSHQ $0 // arg size // 压栈,设置参数 runtime·newproc 的 siz
CALL runtime·newproc(SB) // 调用 runtime·newproc ,在 proc.go 函数原型: func newproc(siz int32, fn *funcval)
POPQ AX // 弹出 PUSHQ $0
POPQ AX // 弹出 PUSHQ AX

// start this M
CALL runtime·mstart(SB) // 启动调度程序,调度到刚刚创建的 goroutine 执行,在 proc.go 函数原型: func mstart()

CALL runtime·abort(SB) // mstart should never return // mstart 永远不会返回
RET

// Prevent dead-code elimination of debugCallV1, which is
// intended to be called by debuggers.
// 防止调试程序要调用的 debugCallV1 消除死代码。
MOVQ $runtime·debugCallV1(SB), AX // AX = runtime·debugCallV1
RET

// 声明全局的变量 mainPC 为 runtime.main 函数的地址,该变量为 read only
DATA runtime·mainPC+0(SB)/8,$runtime·main(SB)
GLOBL runtime·mainPC(SB),RODATA,$8

   在上面我们看到会调用runtime·check, runtime·args, runtime·osinit, runtime·schedinit, runtime·mainPC, runtime·newproc, runtime·mstart, runtime·abort 等等几个函数, 这些就是Go启动的重要函数。

   从上面可以看到会调用到runtime.mainPC, 而runtime.mainPC即是runtime·main的地址, 那么最终就会调用到runtime·main, 我们查看runtime.main(在go/src/runtime/proc.go中)的实现:

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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// The main goroutine.
// 主 goroutine,也就是runtime·mainPC
func main() {
// 获取当前的G, G为TLS(Thread Local Storage)
g := getg()

// Racectx of m0->g0 is used only as the parent of the main goroutine.
// It must not be used for anything else.
// m0->g0 的racectx仅用作主 goroutine的父代。不得将其用于其他任何用途。
g.m.g0.racectx = 0

// Max stack size is 1 GB on 64-bit, 250 MB on 32-bit.
// Using decimal instead of binary GB and MB because
// they look nicer in the stack overflow failure message.
// 执行栈的最大限制: 1GB on 64-bit, 250 MB on 32-bit。使用十进制而不是二进制GB和MB,因为它们在堆栈溢出失败消息中好看些。
if sys.PtrSize == 8 {
maxstacksize = 1000000000
} else {
maxstacksize = 250000000
}

// Allow newproc to start new Ms.
// 标示main goroutine启动了,接下来允许 newproc 启动新的 m
mainStarted = true

if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon // 1.11 新引入的 web assembly, 目前 wasm 不支持线程,无系统监控
// 启动系统后台监控 (定期 GC,并发任务调度)
systemstack(func() {
newm(sysmon, nil)
})
}

// Lock the main goroutine onto this, the main OS thread,
// during initialization. Most programs won't care, but a few
// do require certain calls to be made by the main thread.
// Those can arrange for main.main to run in the main thread
// by calling runtime.LockOSThread during initialization
// to preserve the lock.
// 将主 goroutine 锁在主 OS 线程下进行初始化工作。大部分程序并不关心这一点,但是有一些图形库(基本上属于 cgo 调用)会要求在主线程下进行初始化工作。
// 即便是在 main.main 下仍然可以通过公共方法 runtime.LockOSThread 来强制将一些特殊的需要主 OS 线程的调用锁在主 OS 线程下执行初始化
lockOSThread()

// 执行 runtime.main 函数的 G 必须是绑定在 m0 上的
if g.m != &m0 {
throw("runtime.main not on m0")
}

// 执行初始化运行时
runtime_init() // must be before defer // defer 必须在此调用结束后才能使用
if nanotime() == 0 {
throw("nanotime returning zero")
}

// Defer unlock so that runtime.Goexit during init does the unlock too.
// 延迟解锁,以便init期间的runtime.Goexit也会执行解锁。
needUnlock := true
defer func() {
if needUnlock {
unlockOSThread()
}
}()

// Record when the world started.
// 记录程序的启动时间
runtimeInitTime = nanotime()

// 启动垃圾回收器后台操作
gcenable()

main_init_done = make(chan bool)
if iscgo {
if _cgo_thread_start == nil {
throw("_cgo_thread_start missing")
}
if GOOS != "windows" {
if _cgo_setenv == nil {
throw("_cgo_setenv missing")
}
if _cgo_unsetenv == nil {
throw("_cgo_unsetenv missing")
}
}
if _cgo_notify_runtime_init_done == nil {
throw("_cgo_notify_runtime_init_done missing")
}
// Start the template thread in case we enter Go from
// a C-created thread and need to create a new thread.
// 启动模板线程来处理从 C 创建的线程进入 Go 时需要创建一个新的线程的情况。
startTemplateThread()
cgocall(_cgo_notify_runtime_init_done, nil)
}

// 执行 main_init,进行间接调用,因为链接器在设定运行时的时候不知道 main 包的地址
fn := main_init // make an indirect call, as the linker doesn't know the address of the main package when laying down the runtime
fn()
close(main_init_done)

needUnlock = false
unlockOSThread()

// 如果是基础库则不需要执行 main 函数了
if isarchive || islibrary {
// A program compiled with -buildmode=c-archive or c-shared
// has a main, but it is not executed.
// 使用-buildmode=c-archive或c-shared编译的程序具有main函数,但不会执行。
return
}
// 执行用户 main 包中的 main 函数,处理为非间接调用,因为链接器在设定运行时不知道 main 包的地址
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()

// race 相关
if raceenabled {
racefini()
}

// Make racy client program work: if panicking on
// another goroutine at the same time as main returns,
// let the other goroutine finish printing the panic trace.
// Once it does, it will exit. See issues 3934 and 20018.\
// 使客户端程序正常工作:如果在其他 goroutine 上 panic 、与此同时 main 返回,也让其他 goroutine 能够完成 panic trace 的打印。打印完成后,立即退出。
// 见 issue 3934 和 20018
if atomic.Load(&runningPanicDefers) != 0 {
// Running deferred functions should not take long.
// 运行 defer 函数应该不会花太长时间。
for c := 0; c < 1000; c++ {
if atomic.Load(&runningPanicDefers) == 0 {
break
}
Gosched()
}
}
if atomic.Load(&panicking) != 0 {
gopark(nil, nil, waitReasonPanicWait, traceEvGoStop, 1)
}

// 退出执行,返回退出状态码
exit(0)

// 如果 exit 没有被正确实现,则下面的代码能够强制退出程序,因为 *nil (nil deref) 会崩溃。
for {
var x *int32
*x = 0
}
}

  接下来把其它几个函数看看:

  • runtime·check(go/src/runtime/runtime1.go)
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
// 做一些数据检测
func check() {
var (
a int8
b uint8
c int16
d uint16
e int32
f uint32
g int64
h uint64
i, i1 float32
j, j1 float64
k unsafe.Pointer
l *uint16
m [4]byte
)
type x1t struct {
x uint8
}
type y1t struct {
x1 x1t
y uint8
}
var x1 x1t
var y1 y1t

if unsafe.Sizeof(a) != 1 {
throw("bad a")
}
if unsafe.Sizeof(b) != 1 {
throw("bad b")
}
if unsafe.Sizeof(c) != 2 {
throw("bad c")
}
if unsafe.Sizeof(d) != 2 {
throw("bad d")
}
if unsafe.Sizeof(e) != 4 {
throw("bad e")
}
if unsafe.Sizeof(f) != 4 {
throw("bad f")
}
if unsafe.Sizeof(g) != 8 {
throw("bad g")
}
if unsafe.Sizeof(h) != 8 {
throw("bad h")
}
if unsafe.Sizeof(i) != 4 {
throw("bad i")
}
if unsafe.Sizeof(j) != 8 {
throw("bad j")
}
if unsafe.Sizeof(k) != sys.PtrSize {
throw("bad k")
}
if unsafe.Sizeof(l) != sys.PtrSize {
throw("bad l")
}
if unsafe.Sizeof(x1) != 1 {
throw("bad unsafe.Sizeof x1")
}
if unsafe.Offsetof(y1.y) != 1 {
throw("bad offsetof y1.y")
}
if unsafe.Sizeof(y1) != 2 {
throw("bad unsafe.Sizeof y1")
}

if timediv(12345*1000000000+54321, 1000000000, &e) != 12345 || e != 54321 {
throw("bad timediv")
}

var z uint32
z = 1
if !atomic.Cas(&z, 1, 2) {
throw("cas1")
}
if z != 2 {
throw("cas2")
}

z = 4
if atomic.Cas(&z, 5, 6) {
throw("cas3")
}
if z != 4 {
throw("cas4")
}

z = 0xffffffff
if !atomic.Cas(&z, 0xffffffff, 0xfffffffe) {
throw("cas5")
}
if z != 0xfffffffe {
throw("cas6")
}

m = [4]byte{1, 1, 1, 1}
atomic.Or8(&m[1], 0xf0)
if m[0] != 1 || m[1] != 0xf1 || m[2] != 1 || m[3] != 1 {
throw("atomicor8")
}

m = [4]byte{0xff, 0xff, 0xff, 0xff}
atomic.And8(&m[1], 0x1)
if m[0] != 0xff || m[1] != 0x1 || m[2] != 0xff || m[3] != 0xff {
throw("atomicand8")
}

*(*uint64)(unsafe.Pointer(&j)) = ^uint64(0)
if j == j {
throw("float64nan")
}
if !(j != j) {
throw("float64nan1")
}

*(*uint64)(unsafe.Pointer(&j1)) = ^uint64(1)
if j == j1 {
throw("float64nan2")
}
if !(j != j1) {
throw("float64nan3")
}

*(*uint32)(unsafe.Pointer(&i)) = ^uint32(0)
if i == i {
throw("float32nan")
}
if i == i {
throw("float32nan1")
}

*(*uint32)(unsafe.Pointer(&i1)) = ^uint32(1)
if i == i1 {
throw("float32nan2")
}
if i == i1 {
throw("float32nan3")
}

testAtomic64()

if _FixedStack != round2(_FixedStack) {
throw("FixedStack is not power-of-2")
}

if !checkASM() {
throw("assembly checks failed")
}
}
  • runtime·args(go/src/runtime/runtime1.go)
1
2
3
4
5
6
// 设置参数
func args(c int32, v **byte) {
argc = c
argv = v
sysargs(c, v)
}
  • runtime·osinit(go/src/runtime/os_linux.go)
1
2
3
4
// 初始化os, 根据不同的平台也不一样
func osinit() {
ncpu = getproccount()
}
  • runtime.schedinit(go/src/runtime/proc.go)
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
// The bootstrap sequence is:
//
// call osinit
// call schedinit
// make & queue new G
// call runtime·mstart
//
// The new G calls runtime·main.
// 启动顺序
// 调用 osinit
// 调用 schedinit
// make & queue new G
// 调用 runtime·mstart
// 创建 G 的调用 runtime·main.
//
// 初始化sched, 核心部分
func schedinit() {
// raceinit must be the first call to race detector.
// In particular, it must be done before mallocinit below calls racemapshadow.
// raceinit 是作为 race detector(探测器) ,必须是的首个调用,特别是:必须在 调用 mallocinit 函数之前,在 racemapshadow函数之后调用
// 获取当前 G
_g_ := getg()
if raceenabled {
_g_.racectx, raceprocctx0 = raceinit()
}

// 最大系统线程数量(即 M),参考标准库 runtime/debug.SetMaxThreads
sched.maxmcount = 10000

tracebackinit() // 初始化 traceback
moduledataverify() // 模块数据验证,负责检查链接器符号,以确保所有结构体的正确性
stackinit() // 栈初始化,复用管理链表
mallocinit() // 内存分配器初始化
mcommoninit(_g_.m) // 初始化当前 M
cpuinit() // must run before alginit // 必须在 alginit 之前运行
alginit() // maps must not be used before this call // maps 不能在此调用之前使用,从 CPU 指令集初始化散列算法
modulesinit() // provides activeModules // 模块链接,提供 activeModules
typelinksinit() // uses maps, activeModules // 使用 maps, activeModules
itabsinit() // uses activeModules // 初始化 interface table,使用 activeModules

msigsave(_g_.m) // 设置signal mask
initSigmask = _g_.m.sigmask

goargs() // 初始化命令行用户参数
goenvs() // 初始化环境变量
parsedebugvars() // 初始化debug参数,处理 GODEBUG、GOTRACEBACK 调试相关的环境变量设置
gcinit() // gc初始化

// 网络的上次轮询时间
sched.lastpoll = uint64(nanotime())
// 设置procs, 根据cpu核数和环境变量GOMAXPROCS, 优先环境变量
procs := ncpu
if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
procs = n
}
// 调整 P 的数量,这时所有 P 均为新建的 P,因此不能返回有本地任务的 P
if procresize(procs) != nil {
throw("unknown runnable goroutine during bootstrap")
}

// For cgocheck > 1, we turn on the write barrier at all times
// and check all pointer writes. We can't do this until after
// procresize because the write barrier needs a P.
// 对于 cgocheck>1 ,我们始终打开 write barrier 并检查所有指针写。 我们要等到 procresize 后才能执行此操作,因为写障碍需要一个P。
if debug.cgocheck > 1 {
writeBarrier.cgo = true
writeBarrier.enabled = true
for _, p := range allp {
p.wbBuf.reset()
}
}

if buildVersion == "" {
// Condition should never trigger. This code just serves
// to ensure runtime·buildVersion is kept in the resulting binary.
// 该条件永远不会被触发,此处只是为了防止 buildVersion 被编译器优化移除掉。
buildVersion = "unknown"
}
}
  • runtime.newproc(go/src/runtime/proc.go)
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// 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.
// Cannot split the stack because it assumes that the arguments
// are available sequentially after &fn; they would not be
// copied if a stack split occurred.
/go:nosplit
// 创建 G 运行 fn , 参数大小为 siz 。把 G 放到等待队列。编译器会将 go 语句转化为该调用。
// 这时不能将栈进行分段,因为它假设了参数在 &fn 之后顺序有效;如果 stack 进行了分段则他们不无法被拷贝。
func newproc(siz int32, fn *funcval) {
// add 是一个指针运算,跳过函数指针,把栈上的参数起始地址找到,见 runtime2.go 中的 funcval 类型
argp := add(unsafe.Pointer(&fn), sys.PtrSize)
gp := getg()
// 获取调用方 PC 寄存器值
pc := getcallerpc()
// 用 g0 系统栈创建 goroutine 对象。传递的参数包括 fn 函数入口地址, argp 参数起始地址, siz 参数长度, gp(g0),调用方 pc(goroutine)
systemstack(func() {
newproc1(fn, (*uint8)(argp), siz, gp, pc)
})
}

// Create a new g running fn with narg bytes of arguments starting
// at argp. callerpc is the address of the go statement that created
// this. The new g is put on the queue of g's waiting to run.
// 创建一个运行 fn 的新 g,具有 narg 字节大小的参数,从 argp 开始。callerps 是 go 语句的起始地址。新创建的 g 会被放入 g 的队列中等待运行。
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
// 因为是在系统栈运行所以此时的 g 为 g0
_g_ := getg()

// 判断下 func 的实现是否为空
if fn == nil {
_g_.m.throwing = -1 // do not dump full stacks
throw("go of nil func value")
}
// 设置 g 对应的 m 的 locks++, 禁止抢占,因为它可以在一个局部变量中保存 p
_g_.m.locks++ // disable preemption because it can be holding p in a local var
siz := narg
siz = (siz + 7) &^ 7 // 字节对齐

// We could allocate a larger initial stack if necessary.
// Not worth it: this is almost always an error.
// 4*sizeof(uintreg): extra space added below
// sizeof(uintreg): caller's LR (arm) or return address (x86, in gostartcall).
// 必要时,可以分配并初始化一个更大的栈。
// 不值得:这几乎总是一个错误。
// 4*sizeof(uintreg): 在下方增加的额外空间
// sizeof(uintreg): 调用者 LR (arm) 返回的地址 (x86 在 gostartcall 中)
if siz >= _StackMin-4*sys.RegSize-sys.RegSize {
throw("newproc: function arguments too large for new goroutine")
}
// 获取 p
_p_ := _g_.m.p.ptr()
// 从 g 空闲列表中,根据 p 获得一个新的 g
newg := gfget(_p_)

// 初始化阶段,gfget 是不可能找到 g 的,也可能运行中本来就已经耗尽了
if newg == nil {
// 创建一个拥有 _StackMin 大小的栈的 g
newg = malg(_StackMin)
// 将新创建的 g 从 _Gidle 更新为 _Gdead 状态
casgstatus(newg, _Gidle, _Gdead)
// 将 Gdead 状态的 g 添加到 allg,这样 GC 不会扫描未初始化的栈
allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
}
// 检查新 g 的执行栈
if newg.stack.hi == 0 {
throw("newproc1: newg missing stack")
}

// 无论是取到的 g 还是新创建的 g,都应该是 _Gdead 状态
if readgstatus(newg) != _Gdead {
throw("newproc1: new g is not Gdead")
}

// 计算运行空间大小,与 spAlign 对齐
totalSize := 4*sys.RegSize + uintptr(siz) + sys.MinFrameSize // extra space in case of reads slightly beyond frame
totalSize += -totalSize & (sys.SpAlign - 1) // align to spAlign
// 确定 sp 和参数入栈位置
sp := newg.stack.hi - totalSize
spArg := sp
// arm
if usesLR {
// caller's LR
// 调用方的 LR 寄存器
*(*uintptr)(unsafe.Pointer(sp)) = 0
prepGoExitFrame(sp)
spArg += sys.MinFrameSize
}
// 处理参数,当有参数时,将参数拷贝到 goroutine 的执行栈中
if narg > 0 {
// 从 argp 参数开始的位置,复制 narg 个字节到 spArg(参数拷贝)
memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg))
// This is a stack-to-stack copy. If write barriers
// are enabled and the source stack is grey (the
// destination is always black), then perform a
// barrier copy. We do this *after* the memmove
// because the destination stack may have garbage on
// it.
// 栈到栈的拷贝。如果启用了 write barrier 并且 源栈为灰色(目标始终为黑色),则执行 barrier 拷贝。因为目标栈上可能有垃圾,我们在 memmove 之后执行此操作。
// 如果需要 write barrier 并且 gc scan 未结束,
if writeBarrier.needed && !_g_.m.curg.gcscandone {
f := findfunc(fn.fn)
stkmap := (*stackmap)(funcdata(f, _FUNCDATA_ArgsPointerMaps))
if stkmap.nbit > 0 {
// We're in the prologue, so it's always stack map index 0.
// 我们正位于 prologue (序言) 部分,因此栈 map 索引总是 0
bv := stackmapdata(stkmap, 0)
// bulkBarrierBitmap执行写入障碍
bulkBarrierBitmap(spArg, spArg, uintptr(bv.n)*sys.PtrSize, 0, bv.bytedata)
}
}
}

// 清理、创建并初始化的 g 的运行现场
memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))
newg.sched.sp = sp
newg.stktopsp = sp
newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function // +PCQuantum 从而前一个指令还在相同的函数内
newg.sched.g = guintptr(unsafe.Pointer(newg))
gostartcallfn(&newg.sched, fn)

// 初始化 g 的基本状态
newg.gopc = callerpc
newg.ancestors = saveAncestors(callergp) // 调试相关,追踪调用方
newg.startpc = fn.fn // 如果 PC
if _g_.m.curg != nil {
// 设置 profiler 标签
newg.labels = _g_.m.curg.labels
}
// 统计 sched.ngsys
if isSystemGoroutine(newg, false) {
atomic.Xadd(&sched.ngsys, +1)
}
newg.gcscanvalid = false
// 将 g 更换为 _Grunnable 状态
casgstatus(newg, _Gdead, _Grunnable)

// 分配 goid
if _p_.goidcache == _p_.goidcacheend {
// Sched.goidgen is the last allocated id,
// this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].
// At startup sched.goidgen=0, so main goroutine receives goid=1.
// Sched.goidgen 为最后一个分配的 id,这一批必须为 [sched.goidgen+1, sched.goidgen+GoidCacheBatch]。启动时 sched.goidgen=0, 因此主 goroutine 的 goid 为 1
// 一次分配多个 _GoidCacheBatch(16) 个ID
_p_.goidcache = atomic.Xadd64(&sched.goidgen, _GoidCacheBatch)
_p_.goidcache -= _GoidCacheBatch - 1
_p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
}
newg.goid = int64(_p_.goidcache)
_p_.goidcache++
if raceenabled {
newg.racectx = racegostart(callerpc)
}
// trace 相关
if trace.enabled {
traceGoCreate(newg, newg.startpc)
}
// 将这里新创建的 g 放入 p 的本地队列或直接放入全局队列,true 表示放入执行队列的下一个,false 表示放入队尾
runqput(_p_, newg, true)

// 如果有空闲的 P、且 spinning 的 M 数量为 0,且主 goroutine 已经开始运行,则进行唤醒 p 。初始化阶段 mainStarted 为 false,所以 p 不会被唤醒
if atomic.Load(&sched.npidle) != 0 && atomic.Load(&sched.nmspinning) == 0 && mainStarted {
wakep()
}
_g_.m.locks--
if _g_.m.locks == 0 && _g_.preempt { // restore the preemption request in case we've cleared it in newstack // 在 newstack 中清除了抢占请求的情况下恢复抢占请求
_g_.stackguard0 = stackPreempt
}
}
  • runtime.mstart(go/src/runtime/proc.go)
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
// Called to start an M.
//
// This must not split the stack because we may not even have stack
// bounds set up yet.
//
// May run during STW (because it doesn't have a P yet), so write
// barriers are not allowed.
// 启动 M , M 的入口函数
// 该函数不允许分段栈,因为我们甚至还没有设置栈的边界。它可能会在 STW 阶段运行(因为它还没有 P),所以 write barrier 也是不允许的
//
/go:nosplit
/go:nowritebarrierrec
func mstart() {
_g_ := getg()

// 确定执行栈的边界。通过检查 g 执行占的边界来确定是否为系统栈
osStack := _g_.stack.lo == 0
if osStack {
// Initialize stack bounds from system stack.
// Cgo may have left stack size in stack.hi.
// minit may update the stack bounds.
// 根据系统栈初始化执行栈的边界。cgo 可能会离开 stack.hi 。minit 可能会更新栈的边界
size := _g_.stack.hi
if size == 0 {
size = 8192 * sys.StackGuardMultiplier
}
_g_.stack.hi = uintptr(noescape(unsafe.Pointer(&size)))
_g_.stack.lo = _g_.stack.hi - size + 1024
}
// Initialize stack guards so that we can start calling
// both Go and C functions with stack growth prologues.
// 初始化堆栈守卫,以便我们可以使用堆栈增长 prologue (序言) 开始调用Go和C函数。
_g_.stackguard0 = _g_.stack.lo + _StackGuard
_g_.stackguard1 = _g_.stackguard0
// 启动 M
mstart1()

// Exit this thread.
// 退出线程
if GOOS == "windows" || GOOS == "solaris" || GOOS == "plan9" || GOOS == "darwin" || GOOS == "aix" {
// Window, Solaris, Darwin, AIX and Plan 9 always system-allocate
// the stack, but put it in _g_.stack before mstart,
// so the logic above hasn't set osStack yet.
// Window,Solaris,Darwin,AIX和Plan 9始终对栈进行系统分配,但将其放在mstart之前的_g_.stack中,因此上述逻辑尚未设置osStack。
osStack = true
}
// 退出线程
mexit(osStack)
}

func mstart1() {
_g_ := getg()

// 检查当前执行的 g 是不是 g0
if _g_ != _g_.m.g0 {
throw("bad runtime·mstart")
}

// Record the caller for use as the top of stack in mcall and
// for terminating the thread.
// We're never coming back to mstart1 after we call schedule,
// so other calls can reuse the current frame.
// 这里会记录前一个调用者的状态, 包含 PC , SP 以及其他信息。这份记录会当作最初栈 (top stack),给之后的 mcall 调用,也用来结束那个线程。
// 接下來在 mstart1 调用到 schedule 之后就再也不会回到这个地方了,所以其他调用可以重用当前帧。

// 借助编译器的帮助获取 PC 和 SP , 然后在 save 中更新当前 G 的 sched (type gobuf) 的一些成员, 保存调用者的 pc 和 sp ,让日后其他执行者执行 gogo 函数的时候使用。
save(getcallerpc(), getcallersp())
asminit() // 初始化汇编,但是 amd64 架构下不需要执行任何代码就立刻返回,其他像是 arm、386 才有一些需在这里设定一些 CPU 相关的內容。
minit() // 初始化m 包括信号栈和信号掩码,procid

// Install signal handlers; after minit so that minit can
// prepare the thread to be able to handle the signals.
// 设置信号 handler ;在 minit 之后,因为 minit 可以准备处理信号的的线程
if _g_.m == &m0 {
// 在当前的 goroutine 的所属执行者是 m0 的情況下进入 mstartm0 函数,正式启动在此之前的 signal 处理设定,其中最关键的是 initsig 函数。
mstartm0()
}

// 执行启动函数
if fn := _g_.m.mstartfn; fn != nil {
fn()
}

// 如果当前 m 并非 m0,则要求绑定 p
if _g_.m != &m0 {
acquirep(_g_.m.nextp.ptr())
_g_.m.nextp = 0
}

// 彻底准备好,开始调度,永不返回
schedule()
}
  • runtime·abort(go/src/runtime/stubs.go)
1
2
3
4
5
6
7
8
// abort crashes the runtime in situations where even throw might not
// work. In general it should do something a debugger will recognize
// (e.g., an INT3 on x86). A crash in abort is recognized by the
// signal handler, which will attempt to tear down the runtime
// immediately.
// 在抛出异常甚至都不起作用的情况下,abort会使运行时崩溃。通常,它应该执行调试程序可以识别的操作(例如,x86上的INT3)。
// 信号处理程序会识别中止中的崩溃,这将尝试立即中断运行时。 INT 3
func abort()

在汇编中实现(go/src/runtime/asm_amd64.s)

1
2
3
4
TEXT runtime·abort(SB),NOSPLIT,$0-0
INT $3
loop:
JMP loop

查看断点情况

  等运行完后, 查看断点情况如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000452890 in _rt0_amd64_linux at go/src/runtime/rt0_linux_amd64.s:8
breakpoint already hit 1 time
2 breakpoint keep y 0x000000000044ef70 in _rt0_amd64 at go/src/runtime/asm_amd64.s:15
breakpoint already hit 1 time
3 breakpoint keep y 0x000000000044ef80 in runtime.rt0_go at go/src/runtime/asm_amd64.s:89
breakpoint already hit 1 time
4 breakpoint keep y 0x0000000000436750 in runtime.check at go/src/runtime/runtime1.go:136
breakpoint already hit 1 time
5 breakpoint keep y 0x00000000004361f0 in runtime.args at go/src/runtime/runtime1.go:60
breakpoint already hit 1 time
6 breakpoint keep y 0x00000000004264a0 in runtime.osinit at go/src/runtime/os_linux.go:277
breakpoint already hit 1 time
7 breakpoint keep y 0x000000000042a8b0 in runtime.schedinit at go/src/runtime/proc.go:526
breakpoint already hit 1 time
8 breakpoint keep y 0x00000000004310b0 in runtime.newproc at go/src/runtime/proc.go:3239
breakpoint already hit 4 times
9 breakpoint keep y 0x000000000042c4a0 in runtime.mstart at go/src/runtime/proc.go:1153
breakpoint already hit 5 times
10 breakpoint keep y 0x0000000000450a60 in runtime.abort at go/src/runtime/asm_amd64.s:837
-------------本文结束感谢您的阅读-------------