前言
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()也是在用户协程中运行的