Go程序是如何运行的
# Go 程序是如何运行的
# Go 程序的入口?
main
方法?
我这里是mac m1
是arm64
架构的,真正的入口是这几个文件
rt0
:runtime 的入口linux
:哪个操作系统amd64/arm64
:什么架构的芯片
如果是 linux-amd64 的,入口文件则为
#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)
1
2
3
4
5
6
7
2
3
4
5
6
7
都会进入_rt0_amd64
这个方法去启动程序,这个是一个汇编语言。
所以 go 程序的入口是以runtime/rt0_xxx.s
这样的文件为入口的
接下来,执行_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)
1
2
3
4
2
3
4
这个方法就两行有意义的代码,就是将argc
和argv
放到寄存器里,就是一些命令行的参数;
下面就调用runtime.rt0_go
方法,这个方法里有一个地方:
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)
// 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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
它会初始化g0
协程的执行栈,go 程序的第一个协程,它是为了调度协程而产生的协程。
# 运行时检测
中间会有一堆的检查,直接跳过,我们走到
CLD // convention is D is always left cleared
CALL runtime·check(SB)
1
2
2
这里真正执行了 go 的一个方法,称呼为“运行时检测”:
- 检查各种类型的长度
- 检查指针操作
- 检查结构体字段的偏移量
- 检查
atomic
原子操作 - 检查
CAS
操作 - 检查栈大小是否是 2 的幂次
# 参数初始化runtime.args
# 参数初始化
CALL runtime·args(SB)
CALL runtime·osinit(SB)
1
2
3
4
2
3
4
- 对命令行中的参数进行处理
- 参数数量赋值给
argc int32
- 参数值赋值给
argv **byte
# 调度器初始化
# 调度器初始化
CALL runtime·schedinit(SB)
1
2
2
- 全局栈空间内存分配
- 加载命令行参数到
os.Args
- 堆内存空间的初始化
- 加载操作系统环境变量
- 初始化当前系统线程
- 垃圾回收器的参数初始化
- 算法初始化(map、hash)
- 设置
process
数量,p 结构体的数量
# 创建主协程
// 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
1
2
3
4
5
6
7
2
3
4
5
6
7
mainPC
就是runtime.main
方法的地址,调用了newproc
就是go
启动协程的关键字,也就是说来启动一个新协程,用来运行runtime.manPC
方法,但是没有进行调度。
初始化一个M
,用来调度主协程
// start this M
CALL runtime·mstart(SB)
CALL runtime·abort(SB) // mstart should never return
RET
1
2
3
4
5
2
3
4
5
接下来就是主协程来执行runtime.main
函数
// mainPC is a function value for runtime.main, to be passed to newproc.
// The reference to runtime.main is made via ABIInternal, since the
// actual function (not the ABI0 wrapper) is needed by newproc.
DATA runtime·mainPC+0(SB)/8,$runtime·main<ABIInternal>(SB)
GLOBL runtime·mainPC(SB),RODATA,$8
1
2
3
4
5
2
3
4
5
会遇到一个比较重要的 go 代码
//go:linkname main_main main.main
func main_main()
1
2
2
go:linkname
:在编译的第二个阶段,链接器就会把它链接到 main.main
就是我们用户自己写的main
包下的main
方法,这样用户的程序就跑起来了。
主协程执行主函数的操作内容
- 执行 runtime 包的
init
方法 - 启动
gc
垃圾回收器 - 执行用户包依赖的
init
方法 - 执行用户主函数
main.main()
# 总结
- Go 启动时经历了检查、各种初始化、初始化协程调度的过程
main.main()
也是在协程中运行的,还是在主协程中运行的,是第二个协程,第一个协程时g0
# 问题
- 调度器是什么
- 为什么初始化 M
- 为什么不是直接执行
main.main()
,而是将其放入调度器
# 体会
Go 程序启动过程像不像一个虚拟机,或者框架,围绕着用户的 main 方法去启动,加载一堆组件和初始化一堆内容。
编辑 (opens new window)
上次更新: 2022/05/13, 22:45:18