// func mcall(fn func(*g))
// Switch to m->g0's stack, call fn(g).
// Fn must never return. It should gogo(&g->sched)
// to keep running g.
// 上面的翻译:
// 切换到 m->g0 的栈上,然后调用 fn (g)函数。
// fn 必须不能返回。fn 应该 调用 gogo 函数 来保持运行 g。
TEXT runtime·mcall(SB), NOSPLIT, $0-8
    MOVQ    fn+0(FP), DI    // 很容易理解的,将fn这个函数指针存到DI寄存器。

    get_tls(CX)    // 宏,顾名思义,tls 是线程局部存储,将这个存储的开始位置存到 CX
    MOVQ    g(CX), AX    // save state in g->sched    // g(CX)还是宏,在tls上可以拿到g,也就是当前正在跑的g,把这个g的地址存到AX寄存器
    MOVQ    0(SP), BX    // caller's PC    // 这一句有点难懂,得明白整个golang的函数调用规则才能明白。简介:golang的函数栈的大小,不包含传入的参数和返回值,这两个部分由调用者管理,mcall例子中,fn是传入参数,这个fn是放在【调用mcall的函数】的函数栈里的。mcall除了一个传入参数fn之外,没有其他变量了,所以mcall的栈大小就是0。一个栈大小为0的函数,栈顶,也就是SP指向的位置,刚好就是存的调用mcall的函数的下一条指令,也就是这里所谓的 caller 的 PC。这个PC值为什么会出现在这里,那就是因为,这是CALL指令自动做的事情,也是golang编译器做的事情,调用某函数时,先把本函数的下一掉指令push进栈。话说回来了,既然mcall不需要额外的栈大小了,前面说了,栈大小是0,所以,自然而然的,SP,存的东西就是caller的PC。好,拿到了caller‘s PC,把他存到BX里。
    MOVQ    BX, (g_sched+gobuf_pc)(AX)    // 这句简单,AX存了g的地址,BX存了caller's PC,那这句就是把caller's PC 存到 g 结构体的 sched 的 gobuf 的 pc 字段。
    LEAQ    fn+0(FP), BX    // caller's SP    // 这句又难了。为什么栈上,存fn的地方,是caller的栈顶呢。根据上面的解释能猜到一点,因为栈上存fn这个事,就是由caller来处理的,caller的栈在往下生长的时候,最后一个数据项就是fn。再下面就是caller pc,等等等等。
    MOVQ    BX, (g_sched+gobuf_sp)(AX)    // BX存的是caller的SP,将BX存到g的相应位置 
    MOVQ    AX, (g_sched+gobuf_g)(AX)    // AX存的就是g本身,也存起来
    MOVQ    BP, (g_sched+gobuf_bp)(AX)    //  这一句不太重要,好像BP寄存器已经没啥用了。可能在debug的时候,有点用??不清楚,这个,但是肯定是跟切换上下文的主逻辑和函数调用的主逻辑没什么关系了。

    // switch to m->g0 & its stack, call fn // 切换到m->g0的栈,然后调用fn
    MOVQ    g(CX), BX    // 把当前g的地址存到BX里
    MOVQ    g_m(BX), BX    // 根据g,可以拿到m
    MOVQ    m_g0(BX), SI    // 根据m,又可以拿到 g0,目的就是拿到 g0,下面要切换了。
    CMPQ    SI, AX    // if g == m->g0 call badmcall    // 如果g0就是g,那不行,这种情况,我们不考虑先。
    JNE 3(PC)    // 假如g!= g0,就跳到红色标记,继续执行。 
    MOVQ    $runtime·badmcall(SB), AX
    JMP AX
红色标记:

    MOVQ    SI, g(CX)    // g = m->g0    // SI存的是g0,将g0变成当前g。
    MOVQ    (g_sched+gobuf_sp)(SI), SP    // sp = m->g0->sched.sp    // 将g0的SP值,拿出来,交给寄存器SP,看见没,开始切换了,或者说,已经切完了,下面就是调用 fn (g)
    PUSHQ   AX    // AX 是什么,存的就是刚才的g,不是现在的g0,将g放到栈上。这一步就是普通的,我要调用fn函数了,我要把参数g,先放到栈上。
    MOVQ    DI, DX    // 这一步把fn存到DX不知道要干嘛,可能后续调用fn的时候,会用到??不知道,等再接着看。
    MOVQ    0(DI), DI    // 这一步和下一步,就是调用 fn
    CALL    DI           // 调用fn
    POPQ    AX

go 1.18  runtime/asm_amd64.s 汇编文件里的mcall函数源码:

// func mcall(fn func(*g))
// Switch to m->g0's stack, call fn(g).
// Fn must never return. It should gogo(&g->sched)
// to keep running g.
TEXT runtime·mcall<ABIInternal>(SB), NOSPLIT, $0-8
	MOVQ	AX, DX	// DX = fn

	// save state in g->sched
	MOVQ	0(SP), BX	// caller's PC
	MOVQ	BX, (g_sched+gobuf_pc)(R14)
	LEAQ	fn+0(FP), BX	// caller's SP
	MOVQ	BX, (g_sched+gobuf_sp)(R14)
	MOVQ	BP, (g_sched+gobuf_bp)(R14)

	// switch to m->g0 & its stack, call fn
	MOVQ	g_m(R14), BX
	MOVQ	m_g0(BX), SI	// SI = g.m.g0
	CMPQ	SI, R14	// if g == m->g0 call badmcall
	JNE	goodm
	JMP	runtime·badmcall(SB)
goodm:
	MOVQ	R14, AX		// AX (and arg 0) = g
	MOVQ	SI, R14		// g = g.m.g0
	get_tls(CX)		// Set G in TLS
	MOVQ	R14, g(CX)
	MOVQ	(g_sched+gobuf_sp)(R14), SP	// sp = g0.sched.sp
	PUSHQ	AX	// open up space for fn's arg spill slot
	MOVQ	0(DX), R12
	CALL	R12		// fn(g)
	POPQ	AX
	JMP	runtime·badmcall2(SB)
	RET

其中,

    get_tls(CX)        // Set G in TLS
    MOVQ    R14, g(CX)

这两句的意思是,get_tls()是宏,获取TLS(thread local storage)的首地址放入CX寄存器;然后g(CX)也是宏,意思是根据CX(存储了TLS的首地址)获取TLS中g的地址,然后将R14专用寄存器中的内容赋值给TLS中的g,而根据之前的汇编代码可以看出R14中存放的是g0,也就是说mcall函数会替换TLS中g,也就是源代码注释中的 "// Set G in TLS"。所以,结论就是,mcall不仅会切换到g0的栈,并且会切换到g0执行,TLS中和R14专用寄存器都被替换成了g0,然后替换SP为g0的sp,再在g0的堆栈上调用函数fn。(之后通过mcall调用的函数,比如park_m(),其内部逻辑中的getg()获得的是g0。)