go是如何运行的?

发布于:2024-04-29 ⋅ 阅读:(30) ⋅ 点赞:(0)

前言

go程序的入口是main函数吗?诚然很多程序的入口都是main,比如java,C++,C等,但是go由于他的运行时环境是代码,而不是像Java那样有自己的虚拟机,所以程序在运行main函数之前,需要做很多的准备工作,
该文章就来研究一下go程序是如何运行的!

起步

我们下载好go的sdk以后,我们可以看到一个命名为runtime的包:

紧接着我们可以看到该包下有”rt0_xxx.s“文件,这是一个汇编文件!

这就是程序入口(/runtime/rt0_xxx.s),假如你使用的是windows-amd64的系统(其他系统也类似),那么就有个文件 runtime/rt0_windows_amd64.s

里面有个方法:

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

这是程序启动的引导方法

我们可以看到调用了一个 _rt0_amd64(SB)的汇编方法该方法位于:runtime/asm_amd64.s

// _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(SB)调用了runtime·rt0_go方法,我们对以下代码进行解读

// Defined as ABIInternal since it does not use the stack-based Go ABI (and
// in addition there are no calls to this entry point from Go code).
TEXT runtime·rt0_go<ABIInternal>(SB),NOSPLIT,$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. --------初始化g0的携程
	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)

	// 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
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)
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) ---------->调用go语言的check方法

	MOVL	16(SP), AX		// copy argc
	MOVL	AX, 0(SP)
	MOVQ	24(SP), AX		// copy argv
	MOVQ	AX, 8(SP)
	CALL	runtime·args(SB)------->向go语言传参
	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)--------->创建一个go携程 go语言启动一个新的携程:newproc
	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 debugCallV1, which is
	// intended to be called by debuggers.
	MOVQ	$runtime·debugCallV1<ABIInternal>(SB), AX
	RET

这些写代码描述了go的启动过程

启动步骤大致可以概括如下:
1.读取命令行参数:复制参数数量 argc 和参数值 argv 到栈上

2.初始化g0执行栈资源:g0是为了调度协程而产生的协程(g0是万物之母 用来启动其他所有携程)

3.运行时检查
检查各种类型的长度
检查结构体字段的偏移量
检查 CAS 操作
检查指针操作
检查atomic原子操作
检查栈大小是否是2的幂次

4.参数初始化runtime.args
对命令行中的参数进行处理
参数数量赋值给argcint32
参数值复制给 argv **byte

5.判断系统字长和核数

6.调度器初始化runtime.schedinit
全局栈空间内存分配
加载命令行参数到 os.Args
堆内存空间的初始化
加载操作系统环境变量
初始化当前系统线程
垃圾回收器的参数初始化
算法初始化 (map、hash)
设置 process 数量

7.创建主携程
创建一个新的协程,执行runtime.main,并将runtime.main放在调度器等待调度

8.初始化M
初始化一个M,用来调度主协程

9.主协程执行主函数
执行runtime包中的init
方法启动GC垃圾收集器
执行用户包依赖的init方法
执行用户主函数main.main0

该文件在runtime/proc.go中

// The main goroutine.
func main() {
     .....
   //---------------------//
   gcenable()
   //---------------------//
   ......
   //------------//
   doInit(&main_inittask)
   //------------//
     .....
   //----------------用户程序的入口,也就是我们自己写的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()
   //-------------------------------------------------------//
     ...
}

总结

Go启动时经历了检查、各种初始化、初始化协程调度的过程,main.main()也是在用户协程中运行的