Skip to content

Latest commit

 

History

History
706 lines (664 loc) · 22.3 KB

2019-08-07-go程序启动源码分析.md

File metadata and controls

706 lines (664 loc) · 22.3 KB
layout title subtitle date categories cover tags
post
Go程序启动源码分析
从第一行源码开始阅读runtime,也包含一些pmg调度知识
2019-08-07
技术
Golang

测试环境:

$ go version
go version go1.12.7 linux/amd64
$ uname -a
Linux wu-insparition 4.18.0-25-generic #26~18.04.1-Ubuntu SMP Thu Jun 27 07:28:31 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux

测试范例:

package main
import "fmt"
func main() {
	fmt.Println("hello,world")
}

程序的入口处为:

// usr/local/go/src/runtime/rt0_linux_amd64.s
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
	JMP	_rt0_amd64(SB)

将断点打在_rt0_amd64处:

TEXT _rt0_amd64(SB),NOSPLIT,$-8
	MOVQ	0(SP), DI	// argc
	LEAQ	8(SP), SI	// argv
	JMP	runtime·rt0_go(SB)

由于我们将断点打在用户空间的入口处,在这之前在内核中涉及到进程的do_fork(),这里的argc和argv应该与这个函数的参数有关,由于不影响整个流程,这里分析暂先跳过。下面执行到rt0_go

TEXT runtime·rt0_go(SB),NOSPLIT,$0
	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)

这里做了一下参数的拷贝,栈的变化大致如下:

+---------+
|         |
|         |
+---------+ <--+ 0x00007fffdafd5120
|         |
|         |
+---------+ <--+ 0x00007fffdafd50f9
|         |
|         |
+---------+ <--+ 0x00007fffdafd50f0 sp
|         |
|   ...   |
|         |
+---------+
MOVQ	$runtime·g0(SB), DI    // 这里将全局的g0放进DI
LEAQ	(-64*1024+104)(SP), BX
MOVQ	BX, g_stackguard0(DI)
MOVQ	BX, g_stackguard1(DI)  // 设置g0的stackguard0和stackguard1
MOVQ	BX, (g_stack+stack_lo)(DI)
MOVQ	SP, (g_stack+stack_hi)(DI) // 设置g0的stack.hi和stack.lo

可以看出g0的栈大约为64K(system stack)。省略一些读取CPU与cgo的代码,接着看:

LEAQ	runtime·m0+m_tls(SB), DI // 将m0.tls的地址放进DI
CALL	runtime·settls(SB)
TEXT runtime·settls(SB),NOSPLIT,$32
	...
	ADDQ	$8, DI	// ELF wants to use -8(FS),这里暂时没明白为什么会这样设计TODO
	...
	MOVQ	DI, SI  // 将m0.tls的地址作为系统调用的参数之一
	MOVQ	$0x1002, DI	// ARCH_SET_FS
	MOVQ	$SYS_arch_prctl, AX // 存入系统调用号
	SYSCALL
	CMPQ	AX, $0xfffffffffffff001 // 是否成功
	JLS	2(PC) // 如果成功直接调到RET处返回
	MOVL	$0xf1, 0xf1  // crash
	RET

这里涉及到TLS的相关知识。总的来说做了两件事:(1)告诉kernel,用户空间以后会用FS这个段寄存器来访问TLS段(glibc使用了GS段寄存器)。(2)告诉kernel,当前线程的TSL是m0.tls。

get_tls(BX)
MOVQ	$0x123, g(BX)
MOVQ	runtime·m0+m_tls(SB), AX
CMPQ	AX, $0x123
JEQ 2(PC)
CALL	runtime·abort(SB)

这段代码是为了测试m0的TLS是否设置成功。通过查看m0.tls[0]处的值,发现0x123确实以十进制数291的形式存入了。这里令我疑惑的是代码中的一个宏:

#define g(r)    0(r)(TLS*1) 

从表面上,没看出这个宏的作用。通过向get_tls(BX)后添加一句MOVQ g(BX), AX,查看rax寄存器中的值居然是0。但是不影响进行,有空再深究TODO。

ok:
	// set the per-goroutine and per-mach "registers"
	get_tls(BX)
	LEAQ	runtime·g0(SB), CX  
	MOVQ	CX, g(BX) // 将g0的地址放进当前线程(m0)的TLS
	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)
(dlv) p m0.tls
[6]uintptr [5746784,0,0,0,0,0]
(dlv) p &g0
(*runtime.g)(0x57b060)

可以看到g0的地址以十进制的形式存入了m0.tls。

MOVL	16(SP), AX		// copy argc
MOVL	AX, 0(SP)
MOVQ	24(SP), AX		// copy argv
MOVQ	AX, 8(SP)

这里拷贝参数到esp的正上方(注意中间有ret addr),这两个参数分别是下面3个初始化函数的参数:

CALL	runtime·args(SB)
CALL	runtime·osinit(SB)  // 获取core个数,本机上为4
CALL	runtime·schedinit(SB)
func schedinit() {
	// 这是一个特殊的函数,它的函数体由编译器填写
	// 这个函数的返回值有3种可能:
	// 1. m.g0
	// 2. m.gsinnal
	// 3. m.curg
	// 更多见HACKING.md
	_g_ := getg()  // 这里返回的是当前m的g0,即全局的g0
	if raceenabled {
		_g_.racectx, raceprocctx0 = raceinit()
	}
	sched.maxmcount = 10000  // 最大m数量
	tracebackinit() // skipPC=4546432
	moduledataverify() // symbol table
	stackinit()  // 初始化栈池
	mallocinit()  // 内存初始化,留着看allocator的时候再来分析TODO
	// 初始化m的一些内容,包括m的gsignal(栈大小为32KB)等
	// 在给gsignal分配栈的时候,走的是allocManual() osStackAlloc(s)
	mcommoninit(_g_.m) 
	cpuinit()       // must run before alginit
	alginit()       // maps must not be used before this call
	modulesinit()   // provides activeModules
	typelinksinit() // uses maps, activeModules
	itabsinit()     // uses activeModules
	msigsave(_g_.m)
	initSigmask = _g_.m.sigmask
	// 这里将g0栈上的两个fork参数拷贝到此程序的全局变量中
	goargs()
	goenvs()
	parsedebugvars()
	gcinit() // gc相关的初始化TODO

	sched.lastpoll = uint64(nanotime())
	procs := ncpu
	if n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {
		procs = n
	}
	// 初始化procs个p,并将当前m与第一个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.
	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 = "unknown"
	}
}
// 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

注意这里mainPC是:

DATA	runtime·mainPC+0(SB)/8,$runtime·main(SB)

下面进入newproc函数:

func newproc(siz int32, fn *funcval) {
	argp := add(unsafe.Pointer(&fn), sys.PtrSize) // argp指向第一个参数,下面做了一个小验证
	// 前面讲过,这里从当前线程的tls中取得当前正在运行的g
	// 有3中可能,这里是gp=g0
	gp := getg()  // 获得当前的g
	// getcallerpc returns the program counter (PC) of its caller's caller. 见stubs.go
	// 参考后面的一个小验证范例
	pc := getcallerpc()
	systemstack(func() {
		newproc1(fn, (*uint8)(argp), siz, gp, pc)
	})
}

验证代码:

package main
import "fmt"
import "time"
func main() {
	i := 69
	j := 100
	go func(arg1, arg2 int){
		fmt.Println(arg1+arg2)
	}(i, j)
	time.Sleep(1*time.Second)
}

这里将程序build后,使用objdump -d查看main()newproc()的汇编代码,画出函数的栈帧大致如下:

        newproc              main
      +--------+           +--------+
      |  rbp   |           |  ...   |
      +--------+           +--------+ <-+
      |  ret   |           |  rbp   |
      +--------+           +--------+ +-+
      |  TLS   |           |  69    |   |
      +--------+           +--------+   | local var
      |  16    |           |  100   |   |
      +--------+           +--------+ +-+
argp  |   |----------+     |  100   |   |
      +--------|     |     +--------+   | argument
      |   |-------+  +---> |  69    |   |
      +--------+  |        +--------+ +-+
      |  func1 |  +------> |  fun_  |
      +--------+           +--------+
      |  func1 |           |  16    |
      +--------+ <-+RSP    +--------+
                           |  ret   |
                           +--------+

通过栈帧结构也能看出,在newproc的栈帧中,argp指向了main函数中第一个参数的位置。值得注意的是,newproc中拷贝了上一个栈帧的ret addr到自己的本地变量中,这正是执行pc := getcallerpc()的效果(caller's caller)。

验证完这句代码,接下来来到systemstack

// func systemstack(fn func())
TEXT runtime·systemstack(SB), NOSPLIT, $0-8
	// mov    0x8(%rsp),%rdi 结合上面的栈帧结构可以知道,这里的fn指的是传进来的匿名函数
	MOVQ	fn+0(FP), DI	// DI = fn
	get_tls(CX)
	MOVQ	g(CX), AX	// AX = g  mov %fs:0xfffffffffffffff8,%rax
	MOVQ	g_m(AX), BX	// BX = m

	CMPQ	AX, m_gsignal(BX) // 判断当前执行的g是否是gsignal
	JEQ	noswitch

	MOVQ	m_g0(BX), DX	// DX = g0
	CMPQ	AX, DX   // 判断当前执行的g是否是g0,成立,跳转到noswitch,
	JEQ	noswitch
	...
noswitch:
	MOVQ	DI, DX
	MOVQ	0(DI), DI
	// 跳转到匿名函数
	// 调到注意这里使用的是JMP指令,而不是CALL指令
	// 二者的区别在于前者不会压入ret addr,后者会
	JMP	DI  
// 这里再回忆一下这5个参数的作用:
// fn: 从asm_amd64.s中传入,为proc.go中的main函数
// argp: 参数指针,这里没有参数
// narg: 参数总大小,这里为0
// callerpg: 有3种可能,这里是g0
// callerpc: 在asm_amd64.s中执行newproc的前一条指令的地址
// 下面的分析中,只摘取了主线内容
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
	_g_ := getg()  // go
	...
	// 这里与内核中禁止抢占的方式有点类似,有待考究TODO
	_g_.m.locks++ // disable preemption because it can be holding p in a local var
	...
	_p_ := _g_.m.p.ptr() // 拿到当前m的p(id为0),它们的关系在前面的procresize()函数中已经建立
	newg := gfget(_p_) // 因为目前只有一个g0,这里newg为空
	if newg == nil {
		newg = malg(_StackMin)
		casgstatus(newg, _Gidle, _Gdead) // 改变g的状态
		// 将新分配的newg加入allgs数组中
		allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.
	}

注意gfget()函数,优先从本地队列(p.gFree)中复用(reuse),如果本地队列没有了,就从全局队列(sched.gFree)中取一些(优先取有栈的g)放到本地队列中。注意这里别和下面会讲到的schedule()函数里获取g的方式弄混了。

func malg(stacksize int32) *g {
	newg := new(g)  // 分配一个g,new会进入runtime调用newObject()
	if stacksize >= 0 {
		stacksize = round2(_StackSystem + stacksize) // 2048
		systemstack(func() { // 依然是切换到g0的栈上操作
			newg.stack = stackalloc(uint32(stacksize))
		})
		// 这两句就很明显了
		newg.stackguard0 = newg.stack.lo + _StackGuard
		newg.stackguard1 = ^uintptr(0) // stackguard1的值为什么要设置成-1?TODO
	}
	return newg
}

下面在g0栈上执行stackalloc()函数:

// stackalloc allocates an n byte stack.
//
// stackalloc must run on the system stack because it uses per-P
// resources and must not split the stack.
//
//同上,此函数只摘取主线内容分析
//go:systemstack
func stackalloc(n uint32) stack {
	// Stackalloc must be called on scheduler stack, so that we
	// never try to grow the stack during the code that stackalloc runs.
	// Doing so would cause a deadlock (issue 1547).
	// 上面注释也说了,为了避免grow stack,选择在g0的栈上进行函数调用
	// 在asm_amd64.s中也提到过,g0的栈大约是64KB,是完全够用的
	thisg := getg()
	if thisg != thisg.m.g0 {
		// 在runtime中,g0的栈有两个别名,system stack和scheduler stack
		throw("stackalloc not on scheduler stack")
	}
	...
	// Small stacks are allocated with a fixed-size free-list allocator.
	// If we need a stack of a bigger size, we fall back on allocating
	// a dedicated span.
	var v unsafe.Pointer
	if n < _FixedStack<<_NumStackOrders && n < _StackCacheSize {
		order := uint8(0)
		n2 := n
		for n2 > _FixedStack { // n2=_FixedStack,略过循环
			order++
			n2 >>= 1
		}
		var x gclinkptr
		c := thisg.m.mcache
		if stackNoCache != 0 || c == nil || thisg.m.preemptoff != "" {
			...
		} else {
			x = c.stackcache[order].list
			if x.ptr() == nil {
				// m.mcache.stackcache为空,需要进这个函数填充
				// 这里函数会调用stackpoolalloc() allocManual()
				// 最后使用osStackAlloc()->mmap()完成映射TODO
				stackcacherefill(c, order)  // 这里没太看懂order的作用TODO
				x = c.stackcache[order].list
			}
			c.stackcache[order].list = x.ptr().next
			c.stackcache[order].size -= uintptr(n)
		}
		v = unsafe.Pointer(x)
	} else {
	...
	return stack{uintptr(v), uintptr(v) + uintptr(n)}
}
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
	...
	// totalSize = 4*8 + 0 + 0
	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 := newg.stack.hi - totalSize
	spArg := sp
	// 此时newg的栈大致如下:
	/*+-------+ <-+
	  |  32B  |
	  +-------+ <-+ sp/spArg
	  |  ...  |
	  |  ...  |
	  |  ...  |
	  +-------+
	*/
	...

	// 这里本来有一段mmmove()拷贝参数的操作
	// 由于这个goroutine创建时没有参数,所以省略了

	// memclr_amd64.s:14 TODO
	memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))

	// 下面就是对newg的初始化
	newg.sched.sp = sp
	newg.stktopsp = sp
	newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
	newg.sched.g = guintptr(unsafe.Pointer(newg))
	gostartcallfn(&newg.sched, fn)
	newg.gopc = callerpc
	newg.ancestors = saveAncestors(callergp)
	newg.startpc = fn.fn
	if _g_.m.curg != nil {
		newg.labels = _g_.m.curg.labels
	}
	if isSystemGoroutine(newg, false) {
		atomic.Xadd(&sched.ngsys, +1)
	}
	newg.gcscanvalid = false
	// 执行完这条语句后,在delve中发现有1个goroutine在运行,因为g的状态变成了_Grunnable
	// 后面可以看到在execute()函数里面由_Grunnable变成_Grunning
	casgstatus(newg, _Gdead, _Grunnable)
	...
	// 将newg放入runnable queue,按照p.runq -> p.runnext -> global queue的顺序尝试
	runqput(_p_, newg, true) 
	...
}

这里注意gostartcallfn()这个函数。这个函数会调用gostartcall()

func gostartcall(buf *gobuf, fn, ctxt unsafe.Pointer) {
	sp := buf.sp
	if sys.RegSize > sys.PtrSize {
		sp -= sys.PtrSize
		*(*uintptr)(unsafe.Pointer(sp)) = 0
	}
	sp -= sys.PtrSize // 预留一个指针大小的位置,存放ret addr
	*(*uintptr)(unsafe.Pointer(sp)) = buf.pc // 在ret addr中存入goexit()函数的地址
	buf.sp = sp
	buf.pc = uintptr(fn) // 将runtime.main设置为goroutine的任务函数
	buf.ctxt = ctxt // TODO
}

下面对比描述了执行gostartcall()前后goroutine栈桢结构的变化:

+-------+            +-------+
|  32B  |            |  32B  |
+-------+            +-------+
|  ...  |            |  ...  |
|  arg  |            |  arg  |
+-------+ <-+ rsp    +-------+
|       |            | goexit| ret addr
|       |            +-------+ <-+ rsp
|       |            |       |
|       |            |       |
+-------+            +-------+

很明显,这是在构造一个栈桢结构。为了让goroutine上的任务函数执行完后,执行return,将ret addr处的goexit地址放入rip寄存器。在后面会看到执行goroutine任务函数的时候,使用的是JMP指令,而不是CALL指令。这就避免了将不正确的ret addr压栈。

至此,已经完成了newg相关的操作,这个goroutine将执行runtime.main。接下来回到asm_amd64.s:206开始执行mstart()函数:

func mstart() {
	_g_ := getg() // 这里_g_为g0
	...	
	// 这里有点奇怪,通过单步调试,执行_g_ := getg(),得到的_g_是g0
	// 而当执行到下面这句的时候,通过delve查看_g_就发生了变化
	// 相当费解TODO
	_g_.stackguard0 = _g_.stack.lo + _StackGuard
	_g_.stackguard1 = _g_.stackguard0
	mstart1()
	...
}
func mstart1() {
	_g_ := getg() // 得到g0

	if _g_ != _g_.m.g0 {
		throw("bad runtime·mstart") // 说明mstart应该在m的systemstack/scheduler stack上执行
	}

	// 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.
	// TODO
	save(getcallerpc(), getcallersp())
	asminit() // 这是个空函数
	minit()

	// Install signal handlers; after minit so that minit can
	// prepare the thread to be able to handle the signals.
	if _g_.m == &m0 {
		mstartm0()
	}
	// 此时m0的fn为空,注意区别m.mstartfn与前面在newg.startpc中绑定的fn
	if fn := _g_.m.mstartfn; fn != nil { 
		fn() 
	}

	if _g_.m != &m0 {
		acquirep(_g_.m.nextp.ptr()) // m执行前需要绑定一个p
		_g_.m.nextp = 0
	}
	schedule() // 进入协程调度,参考xv6内核调度
}
// One round of scheduler: find a runnable goroutine and execute it.
// Never returns.
func schedule() {
	_g_ := getg() // _g_为g0
	...
top:
	...
	var gp *g
	var inheritTime bool
	...
	if gp == nil {
		// Check the global runnable queue once in a while to ensure fairness.
		// Otherwise two goroutines can completely occupy the local runqueue
		// by constantly respawning each other.
		// 
		// 每隔61次调度就去sched.runq中照顾一次全局goroutine,以保证公平性,防止starvation
		// 至于为什么会选择61这个数,还挺有讲究,这里就不展开说了,更多可以去参考Dmitry Vyukov的liveblog
		if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
			lock(&sched.lock)
			gp = globrunqget(_g_.m.p.ptr(), 1)
			unlock(&sched.lock)
		}
	}
	if gp == nil {
		// 从本地队列中得到了前面放到p中的newg
		// 回忆前面,newg是在newproc1()中放到p中的
		// inheritTime返回为true,说明应该将g0的剩余时间片给gp
		gp, inheritTime = runqget(_g_.m.p.ptr()) 
		if gp != nil && _g_.m.spinning {
			throw("schedule: spinning with local work")
		}
	}
	if gp == nil {
		// 这个函数很复杂,Tries to steal from other P's, get g from global queue, poll network.
		// TODO
		gp, inheritTime = findrunnable() // blocks until work is available
	}
	...
	// 执行到这里,离目标越来越近了 :)
	execute(gp, inheritTime)
}

schedule()函数里可以看到获取g的三种途径:

  • globrunqget()
  • runqget()
  • findrunnable()

回忆前面的gfget()函数,注意二者获取g的区别。

// Schedules gp to run on the current M.
// If inheritTime is true, gp inherits the remaining time in the
// current time slice. Otherwise, it starts a new time slice.
// Never returns.
//
// Write barriers are allowed because this is called immediately after
// acquiring a P in several places.
//
//go:yeswritebarrierrec
func execute(gp *g, inheritTime bool) {
	// 当前的g依旧是g0,整个newproc1函数都是在g0的栈上执行的
	// 为什么许多函数执行前都要执行一次?TODO
	_g_ := getg() 
	casgstatus(gp, _Grunnable, _Grunning) // 改变g的状态
	gp.waitsince = 0
	gp.preempt = false
	gp.stackguard0 = gp.stack.lo + _StackGuard
	if !inheritTime {
		_g_.m.p.ptr().schedtick++
	}
	/* 执行前栈帧
	(dlv) bt
	0  0x000000000043258e in runtime.execute
	   at /usr/local/go/src/runtime/proc.go:2155
	1  0x00000000004335ad in runtime.schedule
	   at /usr/local/go/src/runtime/proc.go:2559
	2  0x000000000043061e in runtime.mstart1
	   at /usr/local/go/src/runtime/proc.go:1213
	3  0x0000000000430564 in runtime.mstart
	   at /usr/local/go/src/runtime/proc.go:1156
	4  0x00000000004569e0 in runtime.rt0_go
	   at /usr/local/go/src/runtime/asm_amd64.s:206
	*/
	_g_.m.curg = gp // 把当前m.curg指明为我们要调度的goroutine
	/* 执行后栈帧
	(dlv) bt
	0  0x00000000004325ad in runtime.execute
	   at /usr/local/go/src/runtime/proc.go:2156
	1  0x000000000042dbf0 in runtime.main
	   at /usr/local/go/src/runtime/proc.go:110
	2  0x0000000000458a21 in runtime.goexit
	   at /usr/local/go/src/runtime/asm_amd64.s:1337
	*/
	gp.m = _g_.m  // 指明要调度的goroutine归属于当前m
	...
	// 注意这里传入的参数,是在newproc1()中设置的
	gogo(&gp.sched)
}
// func gogo(buf *gobuf)
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $16-8
	MOVQ	buf+0(FP), BX		// gobuf
	MOVQ	gobuf_g(BX), DX // 这是我们要执行的goroutine
	MOVQ	0(DX), CX		// make sure g != nil
	get_tls(CX)
	MOVQ	DX, g(CX) // 切换,把将要执行的goroutine放到m的tls中
	/*
	(dlv) bt
	0  0x0000000000456a23 in runtime.gogo
	   at /usr/local/go/src/runtime/asm_amd64.s:259
	1  0x0000000000432600 in runtime.execute
	   at /usr/local/go/src/runtime/proc.go:2173
	2  0x00000000004335ad in runtime.schedule
	   at /usr/local/go/src/runtime/proc.go:2559
	3  0x000000000043061e in runtime.mstart1
	   at /usr/local/go/src/runtime/proc.go:1213
	4  0x0000000000430564 in runtime.mstart
	   at /usr/local/go/src/runtime/proc.go:1156
	5  0x00000000004569e0 in runtime.rt0_go
	   at /usr/local/go/src/runtime/asm_amd64.s:206
	*/
	MOVQ	gobuf_sp(BX), SP	// restore SP,切换到将要执行的goroutine的栈上去
	/* 执行前栈帧,
	0  0x0000000000456a26 in runtime.gogo
       at /usr/local/go/src/runtime/asm_amd64.s:260
	*/
	MOVQ	gobuf_ret(BX), AX
	MOVQ	gobuf_ctxt(BX), DX
	MOVQ	gobuf_bp(BX), BP
	MOVQ	$0, gobuf_sp(BX)	// clear to help garbage collector
	MOVQ	$0, gobuf_ret(BX)
	MOVQ	$0, gobuf_ctxt(BX)
	MOVQ	$0, gobuf_bp(BX)
	MOVQ	gobuf_pc(BX), BX
	/* 执行前栈帧
	0  0x0000000000456a55 in runtime.gogo
	   at /usr/local/go/src/runtime/asm_amd64.s:268
	*/
	JMP	BX // 跳转到main goroutine

为什么要用JMP BX,而不是CALL指令?原因前面在gostartcallfn()提到过。

真不容易啊,就要大功告成了~

/* 执行前栈帧
0  0x000000000042dc11 in runtime.main
   at /usr/local/go/src/runtime/proc.go:111
1  0x0000000000458a21 in runtime.goexit
   at /usr/local/go/src/runtime/asm_amd64.s:1337
*/
// The main goroutine.
func main() {
	...
	if GOARCH != "wasm" { // no threads on wasm yet, so no sysmon
		systemstack(func() {
			newm(sysmon, nil) // 创建一个监控线程执行sysmon()函数
		})
	}
	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() // 执行我们编写的main函数
	...
}

整个过程跑下来,发现函数启动流程远比我想象中的复杂。文中许多TODO标识,是当下我还没弄懂的一些地方。随着对runtime包的阅读,会持续的回过头来解决这些疑惑。

over! :)