アセンブリ関数mcall systemstack asmcgocall syscall
11202 ワード
https://studygolang.com/artic...
宣言
以下の分析はGolang 1に基づく.14リリース.異なるハードウェアプラットフォームで使用されるアセンブリファイルは異なり、本明細書で分析した関数mcall、systemstack、asmcgocallはasm_に基づいている.arm64.sアセンブリファイル.オペレーティングシステムプラットフォームで使用しないシステム呼び出しは異なり、本明細書で分析した関数syscallはasm_に基づいている.linux_arm64.sアセンブリファイル.
CPUのコンテキスト
これらの関数の本質はいずれもgoroutineを切り替えるためであり,goroutine切替時にCPU実行のコンテキストを切り替える必要があり,主に2つのレジスタの値SP(現在のスレッドで使用されているスタックのスタックトップアドレス),PC(次の実行する命令のアドレス)がある.
mcall関数
mcall関数の定義は以下の通りであり、mcallが伝達するのは関数ポインタであり、伝達関数のタイプは以下の通りであり、パラメータgoroutineのポインタは1つしかなく、戻り値はない.
mcall関数の役割は、システムスタックでスケジューリングコードを実行し、スケジューリングコードが返されず、実行中に再びmcallを実行することです.mcallの流れは,現在のgのコンテキストを保存し,g 0のコンテキストに切り替え,関数パラメータを入力し,関数コード実行にジャンプする.
一般的な呼び出しmcallで実行される関数は、次のとおりです.
Systemstack関数
Systemstack関数の定義は、入力された関数にはパラメータがなく、戻り値がない.
Systemstack関数の役割は、システムスタックで実行できるのはg 0(またはgsignal?)のみです.実行されるスケジューリングコードは、mcallとは異なり、スケジューリングコードが実行されると、現在実行中のコードに戻ります.この部分のソースコード注釈には大まかな流れの理解しかなく,多くの細部が推敲されていない.メインフローは、現在実行されているgがg 0またはgsignalであるか否かを先に判断し、もしそうであれば直接実行し、そうでなければ先にg 0に切り替え、関数を実行した後にg戻り呼び出し先に切り替える.
asmcgocall関数
asmcgocall関数は以下のように定義され、入力されたパラメータは2つが関数ポインタとパラメータポインタであり、戻りパラメータはint 32である.
asmcgocall関数の役割は、g 0(またはgsignal,osthread)のスタックでのみ実行できるcgoコードを実行することである.したがって、プロセスは、現在のスタックを切り替えるかどうかを判断し、切り替える必要がなければnosaveを直接実行して戻る.そうしないと、現在のgの上下文を保存してからg 0に切り替え、cgoコードを実行した後、gを切り取って戻る.
Syscall関数
Syscall関数の定義は次のとおりで、4つのパラメータを入力し、3つのパラメータを返します.
Syscall関数の役割は、システム呼び出しのアドレスとパラメータを入力し、実行が完了したら返すことです.プロセスは主にシステム呼び出し前にentersyscallを実行し、g pの状態を設定し、次にパラメータを入力し、実行後、戻り値を書き、exitsyscallを実行してg pの状態を設定する.entersyscallとexitsyscallはgの呼び出しで詳しく説明します.
Syscalの他にSyscall 6(fn以外に6つのパラメータ)が6つのパラメータを持つシステム呼び出しに対応しています.大同小異を実現するには,ここでは分析しない.
総括と思考
1.アセンブリ関数の役割.なぜgolangはアセンブリ関数を導入しなければならないのですか?CPU実行時のコンテキストはレジスタであるため,アセンブリ言語のみがレジスタを操作できる.2.CPUのコンテキストはg.sched(gobuf)構造体のフィールドと1つずつ対応しており、10個以内のフィールドしかないため、コンテキストの切り替え効率が非常に高い.3.golang以外の言語では、言語とオペレーティングシステム間のインタラクションを実現するために類似のアセンブリが必要ですか?
最後に
mcall関数を除いて、他の関数は具体的な実行の詳細を理解していないので、後でアセンブリに関する知識を強化してからこの穴を埋めます.
宣言
以下の分析はGolang 1に基づく.14リリース.異なるハードウェアプラットフォームで使用されるアセンブリファイルは異なり、本明細書で分析した関数mcall、systemstack、asmcgocallはasm_に基づいている.arm64.sアセンブリファイル.オペレーティングシステムプラットフォームで使用しないシステム呼び出しは異なり、本明細書で分析した関数syscallはasm_に基づいている.linux_arm64.sアセンブリファイル.
CPUのコンテキスト
これらの関数の本質はいずれもgoroutineを切り替えるためであり,goroutine切替時にCPU実行のコンテキストを切り替える必要があり,主に2つのレジスタの値SP(現在のスレッドで使用されているスタックのスタックトップアドレス),PC(次の実行する命令のアドレス)がある.
mcall関数
mcall関数の定義は以下の通りであり、mcallが伝達するのは関数ポインタであり、伝達関数のタイプは以下の通りであり、パラメータgoroutineのポインタは1つしかなく、戻り値はない.
func mcall(fn func(*g)
mcall関数の役割は、システムスタックでスケジューリングコードを実行し、スケジューリングコードが返されず、実行中に再びmcallを実行することです.mcallの流れは,現在のgのコンテキストを保存し,g 0のコンテキストに切り替え,関数パラメータを入力し,関数コード実行にジャンプする.
// 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関数の役割は、システムスタックで実行できるのはg 0(またはgsignal?)のみです.実行されるスケジューリングコードは、mcallとは異なり、スケジューリングコードが実行されると、現在実行中のコードに戻ります.この部分のソースコード注釈には大まかな流れの理解しかなく,多くの細部が推敲されていない.メインフローは、現在実行されているgがg 0またはgsignalであるか否かを先に判断し、もしそうであれば直接実行し、そうでなければ先にg 0に切り替え、関数を実行した後に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つが関数ポインタとパラメータポインタであり、戻りパラメータはint 32である.
func asmcgocall(fn, arg unsafe.Pointer) int32
asmcgocall関数の役割は、g 0(またはgsignal,osthread)のスタックでのみ実行できるcgoコードを実行することである.したがって、プロセスは、現在のスタックを切り替えるかどうかを判断し、切り替える必要がなければnosaveを直接実行して戻る.そうしないと、現在のgの上下文を保存してからg 0に切り替え、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の他にSyscall 6(fn以外に6つのパラメータ)が6つのパラメータを持つシステム呼び出しに対応しています.大同小異を実現するには,ここでは分析しない.
総括と思考
1.アセンブリ関数の役割.なぜgolangはアセンブリ関数を導入しなければならないのですか?CPU実行時のコンテキストはレジスタであるため,アセンブリ言語のみがレジスタを操作できる.2.CPUのコンテキストはg.sched(gobuf)構造体のフィールドと1つずつ対応しており、10個以内のフィールドしかないため、コンテキストの切り替え効率が非常に高い.3.golang以外の言語では、言語とオペレーティングシステム間のインタラクションを実現するために類似のアセンブリが必要ですか?
最後に
mcall関数を除いて、他の関数は具体的な実行の詳細を理解していないので、後でアセンブリに関する知識を強化してからこの穴を埋めます.