Turn to the article : https://studygolang.com/artic...
The following analysis is based on Golang1.14 edition .
Different hardware platforms use different assembly files , The functions analyzed in this paper mcall, systemstack, asmcgocall Is based on asm_arm64.s Assembly files .
No operating system platforms use different system calls , The functions analyzed in this paper syscall Is based on asm_linux_arm64.s Assembly files .
CPU The context of
The essence of these functions is to switch goroutine,goroutine When switching, you need to switch CPU Execution context , There are mainly 2 Value of registers SP（ The top address of the stack used by the current thread ）,PC（ The address of the next instruction to be executed ）.
mcall The function is defined as follows ,mcall The function pointer is passed in , The types of incoming functions are as follows , There is only one parameter goroutine The pointer to , No return value .
func mcall(fn func(*g)
mcall The function executes the scheduling code in the system stack , And the scheduling code doesn't return , Will be executed again during the run mcall.mcall The process is to save the current g The context of , Switch to g0 The context of , Pass in function parameters , Jump to function code execution .
// 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 // At this point, the thread's current sp pc bp The context is stored in the register You need to write the value of the register back to g Here's how to write back g The process of MOVD RSP, R0 // R0 = RSP MOVD R0, (g_sched+gobuf_sp)(g) // g_sp = RO preservation sp Register value MOVD R29, (g_sched+gobuf_bp)(g) // g_bp = R29 (R29 preservation bp value ) MOVD LR, (g_sched+gobuf_pc)(g) // g_pc = LR (LR preservation pc value ) 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. // Change the current g Cut it to g0 MOVD g, R3 // R3 = g (g Represents the current call mcall At the time of the goutine) MOVD g_m(g), R8 // R8 = g.m (R8 Express g The binding of m That is, the present m) MOVD m_g0(R8), g // g = m.g0 ( Will the current g Switch to a g0) BL runtime·save_g(SB) // ??? CMP g, R3 // g == g0 R3 == call mcall Of g It must not be equal BNE 2(PC) // If you don't want to wait, do it normally B runtime·badmcall(SB) // Equality means that there is bug call badmcall // fn Is the function to call Write to register MOVD fn+0(FP), R26 // context R26 Deposit is fn Of pc MOVD 0(R26), R4 // code pointer R4 It's also fn Of pc value MOVD (g_sched+gobuf_sp)(g), R0 // g0 Of sp Values are assigned to registers MOVD R0, RSP // sp = m->g0->sched.sp MOVD (g_sched+gobuf_bp)(g), R29 // g0 Of bp The value is assigned to the corresponding register MOVD R3, -8(RSP) // R3 Was assigned to call before mcall Of g Now write g0 In the stack of As fn The function parameter of MOVD $0, -16(RSP) // The null value here is not very understandable Only one parameter and no return value Why reserve in the stack 8 byte SUB $16, RSP // Offset stack 16byte（ above g $0 Each account 8byte） BL (R4) // R4 Now it's fn Of pc value Jump to it PC perform fn B runtime·badmcall2(SB) // This function never returns So this step can never be carried out in theory
Common calls mcall The functions executed are ：
mcall(gosched_m) mcall(park_m) mcall(goexit0) mcall(exitsyscall0) mcall(preemptPark) mcall(gopreempt_m)
systemstack The function is defined as follows , The function passed in has no parameters , No return value .
func systemstack(fn func())
systemstack The function is executed in the system stack and can only be executed by g0( or gsignal?) Scheduling code executed , and mcall The difference is , After executing the scheduling code, it will switch back to the code being executed now .
This part of the source code annotation has only a general understanding of the process , Many details can't be worked out . The main process is to judge the current running g Is it g0 perhaps gsignal, If so, run it directly , If not, switch to g0, After executing the function, switch to g Return to the use of transfer .
// 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 You have the authority to execute fn Of g BEQ noswitch // If it is equal, it is already m.gsignale 了 There is no need to switch MOVD m_g0(R4), R5 // R5 = g0 CMP g, R5 // If the current g It's already g0 It means that there is no need to switch 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 // The following is the general preservation of the current g The context of 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 Address Memory alignment 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 After the call is completed, the next instruction PC value ？) SUB $8, RSP, R29 // restore FP R29 = RSP - 8 It means stack B (R3)
asmcgocall The function is defined as follows , The parameters passed in are 2 Function pointer and parameter pointer , The return parameter is int32.
func asmcgocall(fn, arg unsafe.Pointer) int32
asmcgocall The function is used to execute cgo Code , This part of the code can only be found in g0( or gsignal, osthread) Stack execution of , Therefore, the process is to determine whether the current stack should be switched first , If there is no need to switch, execute directly nosave Then return , Otherwise, save the current g The context of , And then switch to g0, After execution cgo Code back to g, Then return .
// 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 // If g by nil The jump to nosave. g == nil Whether it indicates that the current is 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 // If g == g.m.signal jump nosave BEQ nosave MOVD m_g0(R8), R3 // If g== m.g0 jump nosave CMP R3, g BEQ nosave // Switch to system stack. // save g The context of 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 The function is defined as follows , Pass in 4 Parameters , return 3 Parameters .
func syscall(fn, a1, a2, a3 uintptr) (r1, r2 uintptr, err Errno)
syscall The function is used to pass in the address and parameters of the system call , Return... After execution . The process is mainly executed before system call entersyscall, Set up g p The state of , And then we'll go into the reference , After execution , Write the return value and execute exitsyscall Set up g p The state of .
entersyscall and exitsyscall stay g Call in detail .
// 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 Input parameters ：PC param1 param2 param3 TEXT ·Syscall(SB),NOSPLIT,$0-56 // call entersyscall Judgment is whether the execution conditions are satisfied Record scheduling information Switch g p The state of CALL runtime·entersyscall(SB) // Put the parameters in the register 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 // When execution fails Write the return value MOVQ $-1, r1+32(FP) MOVQ $0, r2+40(FP) NEGQ AX MOVQ AX, err+48(FP) // call exitsyscall Record scheduling information CALL runtime·exitsyscall(SB) RET ok: // When the execution is successful Write the return value MOVQ AX, r1+32(FP) MOVQ DX, r2+40(FP) MOVQ $0, err+48(FP) CALL runtime·exitsyscall(SB) RET
except Syscal also Syscall6（ except fn also 6 Parameters ） The due 6 System call with parameters . Achieve the same thing with little difference , There is no analysis here .
Summary and reflection
1. The function of assembly function . Why? golang Assembly functions must be introduced ？ because CPU The context of execution is register , Only assembly language can operate registers .
2.CPU The context and g.sched(gobuf) The fields in the structure correspond one by one , Only 10 Less than fields , Therefore, the efficiency of context switching is very high .
3. except golang, Whether other languages in use need similar assembly to realize the interaction between language and operating system ？
except mcall function , Other functions do not understand the details of the specific implementation , After strengthening the knowledge related to the compilation, fill in the hole .