[インデックス 12535] ファイルの概要
このコミットは、Go言語のランタイムにおけるWindowsビルドの修正を目的としています。具体的には、runtime·write
関数とruntime·badcallback
関数をアセンブリ言語で実装し、スタックフットプリント(スタック使用量)を削減することで、Windows環境での安定性と効率性を向上させています。
コミット
commit 8a1b3d5a579ef4b20357ed6d0254976b0d1a19d8
Author: Russ Cox <rsc@golang.org>
Date: Thu Mar 8 15:53:11 2012 -0500
runtime: fix windows build
Implement runtime·write, like on the other systems,
and also runtime·badcallback, in assembly to reduce
stack footprint.
TBR=golang-dev
CC=golang-dev
https://golang.org/cl/5785055
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8a1b3d5a579ef4b20357ed6d0254976b0d1a19d8
元コミット内容
GoランタイムのWindowsビルドを修正します。
他のシステムと同様にruntime·write
を実装し、さらにruntime·badcallback
もアセンブリで実装することで、スタックフットプリントを削減します。
変更の背景
Go言語のランタイムは、異なるオペレーティングシステム(OS)上で動作するために、OS固有のシステムコールを適切に扱う必要があります。Windows環境において、runtime·write
(標準出力や標準エラー出力への書き込みを担当する低レベル関数)とruntime·badcallback
(Cgoコールバックが不正なスレッドで実行された場合にエラーメッセージを出力する関数)の実装に問題があったと考えられます。
特に、Cgo(GoとC言語の相互運用機能)のコールバック処理は、Goランタイムが管理していない外部スレッドから呼び出される可能性があるため、スタックの管理が非常に重要になります。C言語の関数呼び出し規約とGoのそれとが異なる場合、スタックの整合性が崩れる可能性があります。このコミットの目的は、これらの関数をアセンブリ言語で直接実装することで、Windows固有のシステムコールをより効率的かつ安全に呼び出し、特にスタックフットプリントを最小限に抑えることにあります。これにより、潜在的なスタックオーバーフローやパフォーマンスの問題を回避し、Windows上でのGoプログラムの安定性を向上させることが狙いです。
前提知識の解説
Goランタイム (Go Runtime)
Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクション、スケジューラ(ゴルーチンの管理)、メモリ割り当て、システムコールインターフェースなどが含まれます。Goプログラムは、OSのネイティブスレッド上で動作しますが、Goランタイムがゴルーチンをこれらのスレッドにマッピングし、並行処理を効率的に行います。
システムコール (System Call)
システムコールは、ユーザー空間のプログラムがOSカーネルのサービスを要求するためのインターフェースです。例えば、ファイルの読み書き、メモリの割り当て、プロセスの作成などはシステムコールを通じて行われます。Windowsでは、WriteFile
やGetStdHandle
などが代表的なシステムコールです。
アセンブリ言語 (Assembly Language)
アセンブリ言語は、CPUが直接実行できる機械語と1対1に対応する低レベルプログラミング言語です。特定のCPUアーキテクチャ(例: x86, AMD64)に特化しており、レジスタの操作、メモリへの直接アクセス、ジャンプ命令などを通じて、非常に細かくハードウェアを制御できます。Goランタイムのようなパフォーマンスが要求される部分や、OS固有の低レベルな処理(システムコール呼び出し規約の厳密な遵守など)では、アセンブリ言語が使用されることがあります。
スタックフットプリント (Stack Footprint)
スタックフットプリントとは、プログラムが実行中に使用するスタックメモリの量のことです。関数が呼び出されるたびに、その関数のローカル変数、引数、リターンアドレスなどがスタックに積まれます。スタックフットプリントが大きいと、スタックオーバーフロー(スタック領域を使い果たしてしまうエラー)のリスクが高まります。特に、再帰呼び出しが多い場合や、Cgoのように異なる言語間の呼び出しが行われる場合には、スタックの管理が重要になります。アセンブリ言語で関数を実装することで、コンパイラが生成するコードよりもスタック使用量を厳密に制御し、最小限に抑えることが可能になります。
Cgo (C Foreign Function Interface for Go)
Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoの機能です。これにより、既存のCライブラリを利用したり、パフォーマンスが重要な部分をCで記述したりすることができます。Cgoを使用する際には、GoとCの間のデータ型変換や、関数呼び出し規約の違いに注意が必要です。特に、Cgoコールバック(CコードからGo関数を呼び出す場合)は、Goランタイムが管理していないスレッドから呼び出される可能性があるため、スタックの整合性やスレッドのライフサイクル管理が複雑になることがあります。
Windows API (Application Programming Interface)
Windows APIは、Microsoft Windowsオペレーティングシステムが提供する関数群です。アプリケーションはこれらのAPIを呼び出すことで、OSの機能(ファイル操作、プロセス管理、GUIなど)を利用できます。このコミットでは、GetStdHandle
(標準入出力デバイスのハンドルを取得)やWriteFile
(ファイルやデバイスにデータを書き込む)といったAPIが使用されています。
技術的詳細
このコミットの主要な変更点は、Windows環境におけるruntime·write
とruntime·badcallback
のGoアセンブリ実装です。
runtime·write
の実装
runtime·write
は、Goランタイムが標準エラー出力にメッセージを書き込むために使用する低レベル関数です。Windowsでは、この機能はWriteFile
システムコールを通じて実現されます。
-
src/pkg/runtime/sys_windows_386.s
(32-bit x86):TEXT runtime·write(SB),7,$24
:runtime·write
関数の定義。スタックフレームサイズは24バイト。MOVL $-12, 0(SP)
:GetStdHandle
の引数として-12
(STD_ERROR_HANDLE
に対応)をスタックにプッシュ。CALL *runtime·GetStdHandle(SB)
:GetStdHandle
システムコールを呼び出し、標準エラー出力のハンドルを取得。MOVL AX, 0(SP)
: 取得したハンドルをWriteFile
の第一引数としてスタックにプッシュ。MOVL buf+4(FP), DX
/MOVL DX, 4(SP)
: 書き込むバッファのアドレスをスタックにプッシュ。MOVL count+8(FP), DX
/MOVL DX, 8(SP)
: 書き込むバイト数をスタックにプッシュ。LEAL 20(SP), DX
/MOVL $0, 0(DX)
/MOVL DX, 12(SP)
: 書き込まれたバイト数を格納するポインタ(&written
)をスタックにプッシュ。初期値は0。MOVL $0, 16(SP)
:OVERLAPPED
構造体へのポインタ(非同期I/O用、ここではNULL
)をスタックにプッシュ。CALL *runtime·WriteFile(SB)
:WriteFile
システムコールを呼び出し。RET
: 関数からリターン。
-
src/pkg/runtime/sys_windows_amd64.s
(64-bit x86-64):TEXT runtime·write(SB),7,$48
:runtime·write
関数の定義。スタックフレームサイズは48バイト。- 64-bit環境では、関数呼び出し規約(Microsoft x64 calling convention)が異なり、最初の4つの引数はレジスタ(RCX, RDX, R8, R9)で渡されます。
MOVQ $-12, CX
:GetStdHandle
の第一引数(STD_ERROR_HANDLE
)をCX
レジスタにセット。CALL AX
:GetStdHandle
を呼び出し。MOVQ AX, CX
: 取得したハンドルをWriteFile
の第一引数としてCX
レジスタにセット。MOVQ buf+8(FP), DX
: バッファアドレスをDX
レジスタにセット。MOVL count+16(FP), R8
: バイト数をR8
レジスタにセット。LEAQ 40(SP), R9
:&written
ポインタをR9
レジスタにセット。MOVQ $0, 32(SP)
:OVERLAPPED
ポインタをスタックにプッシュ(レジスタで渡せない5番目の引数以降はスタック)。CALL AX
:WriteFile
を呼び出し。RET
: 関数からリターン。
runtime·badcallback
の実装
runtime·badcallback
は、CgoコールバックがGoランタイムによって作成されていないスレッドで発生した場合に、エラーメッセージを標準エラー出力に書き込むための関数です。これもruntime·write
と同様に、アセンブリで実装されています。
src/pkg/runtime/sys_windows_386.s
(32-bit x86) およびsrc/pkg/runtime/sys_windows_amd64.s
(64-bit x86-64):- 基本的な構造は
runtime·write
と非常に似ています。 - 異なる点は、書き込むデータが固定の文字列
runtime·badcallbackmsg
("runtime: cgo callback on thread not created by Go.\n")であることです。 - この文字列の長さは
runtime·badcallbacklen
として定義されています。 - これにより、不正なコールバックが発生した際に、Goランタイムが管理するスタックを汚染することなく、直接OSのAPIを呼び出してエラーメッセージを出力できます。
- 基本的な構造は
src/pkg/runtime/thread_windows.c
からの削除
以前はruntime·write
とruntime·badcallback
がC言語で実装されていましたが、このコミットでアセンブリ言語に移行されたため、thread_windows.c
からこれらのC言語実装が削除されています。
runtime·write
関数のC言語実装が完全に削除されました。badcallback
メッセージの定義と、runtime·badcallback
関数のC言語実装が削除され、代わりにアセンブリから参照されるruntime·badcallbackmsg
とruntime·badcallbacklen
が定義されました。
スタックフットプリントの削減
アセンブリ言語でこれらの関数を実装する主な理由は、スタックフットプリントの削減です。C言語のコンパイラが生成するコードは、一般的に汎用性を考慮してスタックを多めに使用する傾向があります。しかし、アセンブリで直接記述することで、必要なレジスタの保存・復元、引数の渡し方、ローカル変数の配置などを厳密に制御し、スタックの使用量を最小限に抑えることができます。これは、特にCgoコールバックのように、Goランタイムがスタックを完全に制御できない状況で、スタックオーバーフローのリスクを低減するために重要です。
runtime·callbackasm
の変更 (AMD64)
src/pkg/runtime/sys_windows_amd64.s
のruntime·callbackasm
関数にも変更があります。この関数はCgoコールバックの入り口となるアセンブリコードです。
- 以前は
PUSHQ
命令を使って引数をスタックにプッシュしていましたが、これはスタックフレームチェックに影響を与える可能性がありました。 - 変更後、
SUBQ $24, SP
を使ってスタックポインタを直接減らすことで、スタックフレームチェックから隠蔽し、より低レベルでスタックを操作しています。 - これにより、
runtime·cgocallback
への引数渡しがより制御され、スタックの整合性が保たれます。
コアとなるコードの変更箇所
src/pkg/runtime/sys_windows_386.s
--- a/src/pkg/runtime/sys_windows_386.s
+++ b/src/pkg/runtime/sys_windows_386.s
@@ -38,6 +38,46 @@ TEXT runtime·asmstdcall(SB),7,$0
RET
+TEXT runtime·write(SB),7,$24
+ // write only writes to stderr; ignore fd
+ MOVL $-12, 0(SP)
+ MOVL SP, BP
+ CALL *runtime·GetStdHandle(SB)
+ MOVL BP, SP
+
+ MOVL AX, 0(SP) // handle
+ MOVL buf+4(FP), DX // pointer
+ MOVL DX, 4(SP)
+ MOVL count+8(FP), DX // count
+ MOVL DX, 8(SP)
+ LEAL 20(SP), DX // written count
+ MOVL $0, 0(DX)
+ MOVL DX, 12(SP)
+ MOVL $0, 16(SP) // overlapped
+ CALL *runtime·WriteFile(SB)
+ MOVL BP, SI
+ RET
+
+TEXT runtime·badcallback(SB),7,$24
+ // write only writes to stderr; ignore fd
+ MOVL $-12, 0(SP)
+ MOVL SP, BP
+ CALL *runtime·GetStdHandle(SB)
+ MOVL BP, SP
+
+ MOVL AX, 0(SP) // handle
+ MOVL $runtime·badcallbackmsg(SB), DX // pointer
+ MOVL DX, 4(SP)
+ MOVL runtime·badcallbacklen(SB), DX // count
+ MOVL DX, 8(SP)
+ LEAL 20(SP), DX // written count
+ MOVL $0, 0(DX)
+ MOVL DX, 12(SP)
+ MOVL $0, 16(SP) // overlapped
+ CALL *runtime·WriteFile(SB)
+ MOVL BP, SI
+ RET
+
// faster get/set last error
TEXT runtime·getlasterror(SB),7,$0
MOVL 0x34(FS), AX
src/pkg/runtime/sys_windows_amd64.s
--- a/src/pkg/runtime/sys_windows_amd64.s
+++ b/src/pkg/runtime/sys_windows_amd64.s
@@ -60,6 +60,49 @@ loadregs:
RET
+TEXT runtime·write(SB),7,$48
+ // write only ever writes to stderr; ignore fd
+ MOVQ $-12, CX // stderr
+ MOVQ CX, 0(SP)
+ MOVQ runtime·GetStdHandle(SB), AX
+ CALL AX
+
+ MOVQ AX, CX // handle
+ MOVQ CX, 0(SP)
+ MOVQ buf+8(FP), DX // pointer
+ MOVQ DX, 8(SP)
+ MOVL count+16(FP), R8 // count
+ MOVQ R8, 16(SP)
+ LEAQ 40(SP), R9 // written count
+ MOVQ $0, 0(R9)
+ MOVQ R9, 24(SP)
+ MOVQ $0, 32(SP) // overlapped
+ MOVQ runtime·WriteFile(SB), AX
+ CALL AX
+
+ RET
+
+TEXT runtime·badcallback(SB),7,$48
+ MOVQ $-12, CX // stderr
+ MOVQ CX, 0(SP)
+ MOVQ runtime·GetStdHandle(SB), AX
+ CALL AX
+
+ MOVQ AX, CX // handle
+ MOVQ CX, 0(SP)
+ MOVQ $runtime·badcallbackmsg(SB), DX // pointer
+ MOVQ DX, 8(SP)
+ MOVL $runtime·badcallbacklen(SB), R8 // count
+ MOVQ R8, 16(SP)
+ LEAQ 40(SP), R9 // written count
+ MOVQ $0, 0(R9)
+ MOVQ R9, 24(SP)
+ MOVQ $0, 32(SP) // overlapped
+ MOVQ runtime·WriteFile(SB), AX
+ CALL AX
+
+ RET
+
// faster get/set last error
TEXT runtime·getlasterror(SB),7,$0
MOVQ 0x30(GS), AX
@@ -207,15 +250,18 @@ TEXT runtime·callbackasm(SB),7,$0
MOVQ R14, 8(SP)
MOVQ R15, 0(SP)
+ // prepare call stack. use SUBQ to hide from stack frame checks
// cgocallback(void (*fn)(void*), void *frame, uintptr framesize)
- PUSHQ DX // uintptr framesize
- PUSHQ CX // void *frame
- PUSHQ AX // void (*fn)(void*)\n
+ SUBQ $24, SP
+ MOVQ DX, 16(SP) // uintptr framesize
+ MOVQ CX, 8(SP) // void *frame
+ MOVQ AX, 0(SP) // void (*fn)(void*)
CLD
CALL runtime·cgocallback(SB)
- POPQ AX
- POPQ CX
- POPQ DX
+ MOVQ 0(SP), AX
+ MOVQ 8(SP), CX
+ MOVQ 16(SP), DX
+ ADDQ $24, SP
// restore registers as required for windows callback
// 6l does not allow writing many POPs here issuing a warning "nosplit stack overflow"
src/pkg/runtime/thread_windows.c
--- a/src/pkg/runtime/thread_windows.c
+++ b/src/pkg/runtime/thread_windows.c
@@ -114,27 +114,6 @@ runtime·exit(int32 code)
runtime·stdcall(runtime·ExitProcess, 1, (uintptr)code);
}
-int32
-runtime·write(int32 fd, void *buf, int32 n)
-{
- void *handle;
- uint32 written;
-
- written = 0;
- switch(fd) {
- case 1:
- handle = runtime·stdcall(runtime·GetStdHandle, 1, (uintptr)-11);
- break;
- case 2:
- handle = runtime·stdcall(runtime·GetStdHandle, 1, (uintptr)-12);
- break;
- default:
- return -1;
- }
- runtime·stdcall(runtime·WriteFile, 5, handle, buf, (uintptr)n, &written, (uintptr)0);
- return written;
-}
-
void
runtime·osyield(void)
{
@@ -423,21 +402,5 @@ runtime·setprof(bool on)
USED(on);
}
-static int8 badcallback[] = "runtime: cgo callback on thread not created by Go.\\n";
-
-// This runs on a foreign stack, without an m or a g. No stack split.\n
-#pragma textflag 7
-void
-runtime·badcallback(void)
-{
- uint32 written;
-
- runtime·stdcall(
- runtime·WriteFile, 5,
- runtime·stdcall(runtime·GetStdHandle, 1, (uintptr)-12), // stderr
- badcallback,
- (uintptr)(sizeof badcallback - 1),
- &written,
- nil
- );
-}
+int8 runtime·badcallbackmsg[] = "runtime: cgo callback on thread not created by Go.\\n";
+int32 runtime·badcallbacklen = sizeof runtime·badcallbackmsg - 1;
コアとなるコードの解説
runtime·write
とruntime·badcallback
のアセンブリ実装
これらの関数は、Windows APIのGetStdHandle
とWriteFile
を呼び出すためのラッパーとして機能します。
-
GetStdHandle
の呼び出し:MOVL $-12, 0(SP)
(32-bit) またはMOVQ $-12, CX
(64-bit): 標準エラー出力のハンドル(STD_ERROR_HANDLE
、値は-12
)を取得するための引数を設定します。CALL *runtime·GetStdHandle(SB)
(32-bit) またはCALL AX
(64-bit):GetStdHandle
関数を呼び出します。この関数は、Goランタイムが提供する外部関数へのポインタを介して呼び出されます。
-
WriteFile
の呼び出し:MOVL AX, 0(SP)
(32-bit) またはMOVQ AX, CX
(64-bit):GetStdHandle
から返されたハンドル(AX
レジスタに格納されている)をWriteFile
の第一引数として設定します。buf+4(FP)
/buf+8(FP)
: 書き込むデータのバッファへのポインタをフレームポインタ(FP
)からのオフセットで取得し、引数として設定します。count+8(FP)
/count+16(FP)
: 書き込むバイト数を引数として設定します。LEAL 20(SP), DX
/LEAQ 40(SP), R9
: 書き込まれたバイト数を格納するためのポインタ(&written
)をスタック上の適切な位置に設定します。MOVL $0, 16(SP)
/MOVQ $0, 32(SP)
:OVERLAPPED
構造体へのポインタ(非同期I/O用、ここではNULL
)を引数として設定します。CALL *runtime·WriteFile(SB)
(32-bit) またはCALL AX
(64-bit):WriteFile
関数を呼び出します。
これらのアセンブリ実装は、Windowsのシステムコール呼び出し規約に厳密に従い、レジスタとスタックを直接操作することで、C言語のコンパイラが生成するコードよりもオーバーヘッドを削減し、スタックフットプリントを最小限に抑えています。
runtime·callbackasm
のスタック操作 (AMD64)
runtime·callbackasm
におけるスタック操作の変更は、Cgoコールバックの堅牢性を高めるためのものです。
PUSHQ
からSUBQ
への変更:- 以前の
PUSHQ DX
,PUSHQ CX
,PUSHQ AX
は、それぞれ引数をスタックにプッシュしていました。これは一般的な関数呼び出しのパターンですが、Goランタイムのスタックフレームチェック機構から見ると、予期せぬスタック操作と見なされる可能性がありました。 - 新しい
SUBQ $24, SP
は、スタックポインタ(SP
)を直接24バイト減らすことで、必要なスタック領域を確保します。この方法は、Goのスタックフレームチェック機構に対して「隠蔽」されたスタック操作となり、より低レベルで直接的なスタック管理を可能にします。 - その後、
MOVQ DX, 16(SP)
,MOVQ CX, 8(SP)
,MOVQ AX, 0(SP)
によって、確保されたスタック領域に引数を配置します。 - 関数呼び出し後、
ADDQ $24, SP
でスタックポインタを元に戻し、スタックをクリーンアップします。
- 以前の
この変更により、CgoコールバックがGoランタイムの管理外のスレッドから呼び出された場合でも、スタックの整合性がより確実に保たれ、潜在的なスタックオーバーフローやクラッシュのリスクが低減されます。
thread_windows.c
からのC言語実装の削除
runtime·write
とruntime·badcallback
のC言語実装が削除されたことは、これらの機能が完全にアセンブリ言語に移行されたことを意味します。これにより、GoランタイムはWindows固有の低レベルI/O操作とエラー報告を、より効率的かつ制御された方法で実行できるようになります。また、runtime·badcallbackmsg
とruntime·badcallbacklen
がCファイルに残されたのは、アセンブリコードから参照される定数データとして機能するためです。
関連リンク
- Go言語のランタイムに関するドキュメント: https://go.dev/doc/go1.1 (Go 1.1のリリースノートなど、当時の情報源を探すのが望ましい)
- Go言語のCgoに関するドキュメント: https://go.dev/cmd/cgo/
- Windows APIの
GetStdHandle
関数: https://learn.microsoft.com/ja-jp/windows/win32/api/winbase/nf-winbase-getstdhandle - Windows APIの
WriteFile
関数: https://learn.microsoft.com/ja-jp/windows/win32/api/fileapi/nf-fileapi-writefile
参考にした情報源リンク
- Go CL 5785055: https://golang.org/cl/5785055 (コミットメッセージに記載されているGoのコードレビューシステムへのリンク)
- Microsoft x64 calling convention: https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170 (64-bit Windowsにおける関数呼び出し規約)
- Go Assembly Language: https://go.dev/doc/asm (Goのアセンブリ言語に関する公式ドキュメント)
- Go runtime source code: https://github.com/golang/go/tree/master/src/runtime (Goランタイムのソースコード)
- Stack overflow (programming): https://en.wikipedia.org/wiki/Stack_overflow (スタックオーバーフローに関するWikipedia記事)
- System call: https://en.wikipedia.org/wiki/System_call (システムコールに関するWikipedia記事)
- Assembly language: https://en.wikipedia.org/wiki/Assembly_language (アセンブリ言語に関するWikipedia記事)
- Cgo: https://go.dev/blog/cgo (Cgoに関するGo公式ブログ記事)
- Go runtime scheduler: https://go.dev/blog/go-concurrency-patterns-pipelines (Goのスケジューラに関するGo公式ブログ記事、直接的ではないがGoランタイムの理解に役立つ)
- Go memory management: https://go.dev/blog/go-memory-management-and-garbage-collection (Goのメモリ管理とガベージコレクションに関するGo公式ブログ記事)