乐趣区

关于golang:汇编函数mcall-systemstack-asmcgocall-syscall

转于文章: https://studygolang.com/artic…

申明

上面的剖析均基于 Golang1.14 版本。
不同硬件平台应用的汇编文件不同,本文剖析的函数 mcall, systemstack, asmcgocall 是基于 asm_arm64.s 汇编文件。
不必操作系统平台应用的零碎调用不同,本文剖析的函数 syscall 是基于 asm_linux_arm64.s 汇编文件。

CPU 的上下文

这些函数的实质都是为了切换 goroutine,goroutine 切换时须要切换 CPU 执行的上下文,次要有 2 个寄存器的值 SP(以后线程应用的栈的栈顶地址),PC(下一个要执行的指令的地址)。

mcall 函数

mcall 函数的定义如下,mcall 传入的是函数指针,传入函数的类型如下,只有一个参数 goroutine 的指针,无返回值。

func mcall(fn func(*g) 

mcall 函数的作用是在零碎栈中执行调度代码,并且调度代码不会返回,将在运行过程中又一次执行 mcall。mcall 的流程是保留以后的 g 的上下文,切换到 g0 的上下文,传入函数参数,跳转到函数代码执行。

// void 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(SB), NOSPLIT|NOFRAME, $0-8
    // Save caller state in g->sched
    // 此时线程以后的 sp pc bp 等上下文都存在寄存器中 须要将寄存器的值写回 g 上面就是写回 g 的过程
    MOVD    RSP, R0  // R0 = RSP
    MOVD    R0, (g_sched+gobuf_sp)(g)  // g_sp = RO 保留 sp 寄存器的值
    MOVD    R29, (g_sched+gobuf_bp)(g) // g_bp = R29 (R29 保留 bp 值)
    MOVD    LR, (g_sched+gobuf_pc)(g)  // g_pc = LR (LR 保留 pc 值)
    MOVD    $0, (g_sched+gobuf_lr)(g)  // g_lr = 0
    MOVD    g, (g_sched+gobuf_g)(g)    // ???

    // Switch to m->g0 & its stack, call fn.
    // 将以后的 g 切为 g0
    MOVD    g, R3  // R3 = g (g 示意以后调用 mcall 时的 goutine)
    MOVD    g_m(g), R8 // R8 = g.m (R8 示意 g 绑定的 m 即以后的 m)
    MOVD    m_g0(R8), g // g = m.g0 (将以后 g 切换为 g0)
    BL  runtime·save_g(SB) // ???
    CMP g, R3 // g == g0  R3 == 调用 mcall 的 g 必不相等
    BNE 2(PC) // 如果不想等则失常执行
    B   runtime·badmcall(SB) // 相等则阐明有 bug 调用 badmcall
    // fn 是要调用的函数 写入寄存器
    MOVD    fn+0(FP), R26           // context R26 存的是 fn 的 pc
    MOVD    0(R26), R4          // code pointer R4 也是 fn 的 pc 值
    MOVD    (g_sched+gobuf_sp)(g), R0  // g0 的 sp 值赋给寄存器
    MOVD    R0, RSP // sp = m->g0->sched.sp
    MOVD    (g_sched+gobuf_bp)(g), R29 // g0 的 bp 值赋给对应的寄存器
    MOVD    R3, -8(RSP) // R3 在之前被赋值为调用 mcall 的 g 当初写入 g0 的栈中 作为 fn 的函数参数
    MOVD    $0, -16(RSP) // 此处的空值不太了解 只有一个参数且无返回值 为何要在栈中预留 8 字节
    SUB $16, RSP // 对栈进行偏移 16byte(下面 g $0 各占 8byte)BL  (R4) // R4 此时是 fn 的 pc 值 跳到该 PC 执行 fn
    B   runtime·badmcall2(SB) // 该函数永远不会返回 因而这一步实践上永远执行不到 

常见的调用 mcall 执行的函数有:

mcall(gosched_m)
mcall(park_m)
mcall(goexit0)
mcall(exitsyscall0)
mcall(preemptPark)
mcall(gopreempt_m) 

systemstack 函数

systemstack 函数的定义如下,传入的函数无参数,无返回值。

func systemstack(fn func()) 

systemstack 函数的作用是在零碎栈中执行只能由 g0(或 gsignal?) 执行的调度代码,和 mcall 不同的是,在执行完调度代码后会切回到当初正在执行的代码。
该局部的源码正文有只有个大略的流程的了解,许多细节斟酌不进去。次要流程是先判断以后运行的 g 是否为 g0 或者 gsignal,如果是则间接运行,不是则先切换到 g0,执行完函数后切换为 g 返回调用处。

// systemstack_switch is a dummy routine that systemstack leaves at the bottom
// of the G stack. We need to distinguish the routine that
// lives at the bottom of the G stack from the one that lives
// at the top of the system stack because the one at the top of
// the system stack terminates the stack walk (see topofstack()).
TEXT runtime·systemstack_switch(SB), NOSPLIT, $0-0
    UNDEF
    BL  (LR)    // make sure this function is not leaf
    RET

// func systemstack(fn func())
TEXT runtime·systemstack(SB), NOSPLIT, $0-8
    MOVD    fn+0(FP), R3    // R3 = fn
    MOVD    R3, R26     // context R26 = R3 = fn
    MOVD    g_m(g), R4  // R4 = m

    MOVD    m_gsignal(R4), R5   // R5 = m.gsignal
    CMP g, R5  // m.gsignal 是有权限执行 fn 的 g
    BEQ noswitch // 如果相等阐明曾经是 m.gsignale 了 则不须要切换

    MOVD    m_g0(R4), R5    // R5 = g0
    CMP g, R5  // 如果以后的 g 曾经是 g0 则阐明不必切换
    BEQ noswitch

    MOVD    m_curg(R4), R6 // R6 = m.curg
    CMP g, R6 // m.curg == g
    BEQ switch

    // Bad: g is not gsignal, not g0, not curg. What is it?
    // Hide call from linker nosplit analysis.
    MOVD    $runtime·badsystemstack(SB), R3
    BL  (R3)
    B   runtime·abort(SB)

switch:
    // save our state in g->sched. Pretend to
    // be systemstack_switch if the G stack is scanned.
    MOVD    $runtime·systemstack_switch(SB), R6
    ADD $8, R6  // get past prologue
    // 以下是惯例的保留以后 g 的上下文
    MOVD    R6, (g_sched+gobuf_pc)(g)
    MOVD    RSP, R0
    MOVD    R0, (g_sched+gobuf_sp)(g)
    MOVD    R29, (g_sched+gobuf_bp)(g)
    MOVD    $0, (g_sched+gobuf_lr)(g)
    MOVD    g, (g_sched+gobuf_g)(g)

    // switch to g0
    MOVD    R5, g  // g = R5 = g0
    BL  runtime·save_g(SB)
    MOVD    (g_sched+gobuf_sp)(g), R3 // R3 = sp
    // make it look like mstart called systemstack on g0, to stop traceback
    SUB $16, R3  // sp 地址 内存对齐
    AND $~15, R3
    MOVD    $runtime·mstart(SB), R4
    MOVD    R4, 0(R3)
    MOVD    R3, RSP
    MOVD    (g_sched+gobuf_bp)(g), R29 // R29 = g0.gobuf.bp

    // call target function
    MOVD    0(R26), R3  // code pointer
    BL  (R3)

    // switch back to g
    MOVD    g_m(g), R3
    MOVD    m_curg(R3), g
    BL  runtime·save_g(SB)
    MOVD    (g_sched+gobuf_sp)(g), R0
    MOVD    R0, RSP
    MOVD    (g_sched+gobuf_bp)(g), R29
    MOVD    $0, (g_sched+gobuf_sp)(g)
    MOVD    $0, (g_sched+gobuf_bp)(g)
    RET

noswitch:
    // already on m stack, just call directly
    // Using a tail call here cleans up tracebacks since we won't stop
    // at an intermediate systemstack.
    MOVD    0(R26), R3  // code pointer  R3 = R26 = fn
    MOVD.P  16(RSP), R30    // restore LR  R30 = RSP + 16(systemstack 调用实现后下条指令的 PC 值?)
    SUB $8, RSP, R29    // restore FP  R29 = RSP - 8 示意栈的
    B   (R3) 

asmcgocall 函数

asmcgocall 函数定义如下,传入的参数有 2 个为函数指针和参数指针,返回参数为 int32。

func asmcgocall(fn, arg unsafe.Pointer) int32 

asmcgocall 函数的作用是执行 cgo 代码,该局部代码只能在 g0(或 gsignal, osthread) 的栈执行,因而流程是先判断以后的栈是否要切换,如果无需切换则间接执行 nosave 而后返回,否则先保留以后 g 的上下文,而后切换到 g0,执行完 cgo 代码后切回 g,而后返回。

// func asmcgocall(fn, arg unsafe.Pointer) int32
// Call fn(arg) on the scheduler stack,
// aligned appropriately for the gcc ABI.
// See cgocall.go for more details.
TEXT ·asmcgocall(SB),NOSPLIT,$0-20
    MOVD    fn+0(FP), R1  // R1 = fn
    MOVD    arg+8(FP), R0  // R2 = arg

    MOVD    RSP, R2     // save original stack pointer
    CBZ g, nosave  // 如果 g 为 nil 则跳转到 nosave。g == nil 是否阐明以后是 osthread?MOVD    g, R4  // R4 = g

    // Figure out if we need to switch to m->g0 stack.
    // We get called to create new OS threads too, and those
    // come in on the m->g0 stack already.
    MOVD    g_m(g), R8 // R8 = g.m
    MOVD    m_gsignal(R8), R3 // R3 = g.m.gsignal
    CMP R3, g  // 如果 g == g.m.signal jump nosave
    BEQ nosave
    MOVD    m_g0(R8), R3 // 如果 g == m.g0 jump nosave
    CMP R3, g
    BEQ nosave

    // Switch to system stack.
    // save g 的上下文
    MOVD    R0, R9  // gosave<> and save_g might clobber R0
    BL  gosave<>(SB)
    MOVD    R3, g
    BL  runtime·save_g(SB)
    MOVD    (g_sched+gobuf_sp)(g), R0
    MOVD    R0, RSP
    MOVD    (g_sched+gobuf_bp)(g), R29
    MOVD    R9, R0

    // Now on a scheduling stack (a pthread-created stack).
    // Save room for two of our pointers /*, plus 32 bytes of callee
    // save area that lives on the caller stack. */
    MOVD    RSP, R13
    SUB $16, R13
    MOVD    R13, RSP  // RSP = RSP - 16
    MOVD    R4, 0(RSP)  // save old g on stack  RSP.0 = R4 = oldg
    MOVD    (g_stack+stack_hi)(R4), R4 // R4 = old.g.stack.hi
    SUB R2, R4  // R4 = oldg.stack.hi - old_RSP
    MOVD    R4, 8(RSP)  // save depth in old g stack (can't just save SP, as stack might be copied during a callback)
    BL  (R1) // R1 = fn
    MOVD    R0, R9 // R9 = R0 = errno?

    // Restore g, stack pointer. R0 is errno, so don't touch it
    MOVD    0(RSP), g  // g = RSP.0 = oldg
    BL  runtime·save_g(SB)
    MOVD    (g_stack+stack_hi)(g), R5 // R5 = g.stack.hi
    MOVD    8(RSP), R6 // R6 = RSP + 8 = oldg.stack.hi - old_RSP
    SUB R6, R5 // R5 = R5 - R6 = old_RSP
    MOVD    R9, R0 // R0 = R9 = errno
    MOVD    R5, RSP // RSP = R5 = old_RSP

    MOVW    R0, ret+16(FP) // ret = R0 = errno
    RET

nosave:
    // Running on a system stack, perhaps even without a g.
    // Having no g can happen during thread creation or thread teardown
    // (see needm/dropm on Solaris, for example).
    // This code is like the above sequence but without saving/restoring g
    // and without worrying about the stack moving out from under us
    // (because we're on a system stack, not a goroutine stack).
    // The above code could be used directly if already on a system stack,
    // but then the only path through this code would be a rare case on Solaris.
    // Using this code for all "already on system stack" calls exercises it more,
    // which should help keep it correct.
    MOVD    RSP, R13 
    SUB $16, R13  
    MOVD    R13, RSP // RSP = RSP - 16
    MOVD    $0, R4 // R4 = 0
    MOVD    R4, 0(RSP)  // Where above code stores g, in case someone looks during debugging.
    MOVD    R2, 8(RSP)  // Save original stack pointer.  RSP + 8 = old_R2
    BL  (R1)
    // Restore stack pointer.
    MOVD    8(RSP), R2  // R2 = RSP + 8 = old_R2
    MOVD    R2, RSP // RSP = old_R2 = old_RSP
    MOVD    R0, ret+16(FP) // ret = R0 = errno
    RET 

syscall 函数

Syscall 函数的定义如下,传入 4 个参数,返回 3 个参数。

func syscall(fn, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno) 

syscall 函数的作用是传入零碎调用的地址和参数,执行实现后返回。流程次要是零碎调用前执行 entersyscall,设置 g p 的状态,而后入参,执行后,写返回值而后执行 exitsyscall 设置 g p 的状态。
entersyscall 和 exitsyscall 在 g 的调用中细讲。

// func Syscall(trap int64, a1, a2, a3 uintptr) (r1, r2, err uintptr);
// Trap # in AX, args in DI SI DX R10 R8 R9, return in AX DX
// Note that this differs from "standard" ABI convention, which
// would pass 4th arg in CX, not R10.

// 4 个入参:PC param1 param2 param3
TEXT ·Syscall(SB),NOSPLIT,$0-56
    // 调用 entersyscall 判断是执行条件是否满足 记录调度信息 切换 g p 的状态
    CALL    runtime·entersyscall(SB)
    // 将参数存入寄存器中
    MOVQ    a1+8(FP), DI
    MOVQ    a2+16(FP), SI
    MOVQ    a3+24(FP), DX
    MOVQ    trap+0(FP), AX  // syscall entry
    SYSCALL
    CMPQ    AX, $0xfffffffffffff001
    JLS ok
    // 执行失败时 写返回值
    MOVQ    $-1, r1+32(FP)
    MOVQ    $0, r2+40(FP)
    NEGQ    AX
    MOVQ    AX, err+48(FP)
    // 调用 exitsyscall 记录调度信息
    CALL    runtime·exitsyscall(SB)
    RET
ok:
    // 执行胜利时 写返回值
    MOVQ    AX, r1+32(FP)
    MOVQ    DX, r2+40(FP)
    MOVQ    $0, err+48(FP)
    CALL    runtime·exitsyscall(SB)
    RET 

除了 Syscal 还有 Syscall6(除 fn 还有 6 个参数)对应有 6 个参数的零碎调用。实现大同小异,这里不剖析。

总结与思考

1. 汇编函数的作用。为什么 golang 肯定要引入汇编函数呢?因为 CPU 执行时的上下文是寄存器,只有汇编语言能力操作寄存器。
2.CPU 的上下文和 g.sched(gobuf) 构造体中的字段一一对应,只有 10 个以内的字段,因而切换上下文效率十分的高。
3. 除了 golang,其它在用的语言是否要有相似的汇编来实现语言和操作系统之间的交互?

最初

除了 mcall 函数,其它函数在具体执行细节上了解不够深,前面增强汇编相干的常识后再把这个坑填上。

退出移动版