Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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では、WriteFileGetStdHandleなどが代表的なシステムコールです。

アセンブリ言語 (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·writeruntime·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の引数として-12STD_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·writeruntime·badcallbackがC言語で実装されていましたが、このコミットでアセンブリ言語に移行されたため、thread_windows.cからこれらのC言語実装が削除されています。

  • runtime·write関数のC言語実装が完全に削除されました。
  • badcallbackメッセージの定義と、runtime·badcallback関数のC言語実装が削除され、代わりにアセンブリから参照されるruntime·badcallbackmsgruntime·badcallbacklenが定義されました。

スタックフットプリントの削減

アセンブリ言語でこれらの関数を実装する主な理由は、スタックフットプリントの削減です。C言語のコンパイラが生成するコードは、一般的に汎用性を考慮してスタックを多めに使用する傾向があります。しかし、アセンブリで直接記述することで、必要なレジスタの保存・復元、引数の渡し方、ローカル変数の配置などを厳密に制御し、スタックの使用量を最小限に抑えることができます。これは、特にCgoコールバックのように、Goランタイムがスタックを完全に制御できない状況で、スタックオーバーフローのリスクを低減するために重要です。

runtime·callbackasmの変更 (AMD64)

src/pkg/runtime/sys_windows_amd64.sruntime·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·writeruntime·badcallbackのアセンブリ実装

これらの関数は、Windows APIのGetStdHandleWriteFileを呼び出すためのラッパーとして機能します。

  • 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·writeruntime·badcallbackのC言語実装が削除されたことは、これらの機能が完全にアセンブリ言語に移行されたことを意味します。これにより、GoランタイムはWindows固有の低レベルI/O操作とエラー報告を、より効率的かつ制御された方法で実行できるようになります。また、runtime·badcallbackmsgruntime·badcallbacklenがCファイルに残されたのは、アセンブリコードから参照される定数データとして機能するためです。

関連リンク

参考にした情報源リンク