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

[インデックス 12539] ファイルの概要

このコミットは、Go言語のランタイムにおけるWindows環境でのruntime.write関数の実装に関する変更です。具体的には、runtime.write関数がアセンブリコードからC言語の実装へと移行されました。この変更は、WindowsのDLL呼び出しとシステムコール間のスタック管理の複雑性に対応するためのものです。

変更されたファイルは以下の通りです。

  • src/pkg/runtime/sys_windows_386.s: Windows 32-bit (x86) 環境のアセンブリコード。runtime·write関数の定義が削除されました。
  • src/pkg/runtime/sys_windows_amd64.s: Windows 64-bit (AMD64) 環境のアセンブリコード。runtime·write関数の定義が削除されました。
  • src/pkg/runtime/thread_windows.c: Windows環境のスレッド関連のC言語コード。runtime·write関数のC言語実装が追加されました。

コミット

commit c9e5600f7d3c46d3053eadc83a9b02642413bcb3
Author: Russ Cox <rsc@golang.org>
Date:   Fri Mar 9 00:10:34 2012 -0500

    runtime: move runtime.write back to C
    
    It may have to switch stacks, since we are calling
    a DLL instead of a system call.
    
    badcallback says where it is, because it is being called
    on a Windows stack already.
    
    R=golang-dev, alex.brainman
    CC=golang-dev
    https://golang.org/cl/5782060

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/c9e5600f7d3c46d3053eadc83a9b02642413bcb3

元コミット内容

runtime: move runtime.write back to C It may have to switch stacks, since we are calling a DLL instead of a system call. badcallback says where it is, because it is being called on a Windows stack already.

変更の背景

このコミットの主な背景は、GoランタイムがWindows環境で外部のDLL(Dynamic Link Library)を呼び出す際のスタック管理の複雑性に対処することです。

Goランタイムは、ゴルーチン(goroutine)と呼ばれる軽量なスレッドのために独自のスタック管理メカニズムを持っています。ゴルーチンのスタックは小さく始まり、必要に応じて動的に拡大・縮小します。しかし、Windows APIのような外部のC言語で書かれたDLL関数を呼び出す際には、Goランタイムの管理するスタックとWindowsが管理するネイティブスタックとの間で整合性を保つ必要があります。

コミットメッセージにある「It may have to switch stacks, since we are calling a DLL instead of a system call.」という記述は、この問題の核心を突いています。Windowsでは、アプリケーションがOSのサービスを利用する際に、大きく分けて「システムコール」と「DLL呼び出し」の2つの方法があります。

  • システムコール: アプリケーションが直接カーネルモードに移行し、OSカーネルの低レベルなサービスを要求するものです。これは特権レベルの操作であり、スタックの切り替えはOSによって厳密に管理されます。
  • DLL呼び出し: 開発者が通常利用するWindows APIのほとんどは、kernel32.dlluser32.dllといったDLL内に実装されています。これらのDLL関数はユーザーモードで動作し、内部で必要に応じてシステムコールを呼び出します。

GoランタイムがWindows APIを呼び出す際、直接システムコールを行うのではなく、DLLを介して呼び出す場合、GoのゴルーチンのスタックとWindowsのネイティブスタックの間でスタックポインタやスタックフレームの整合性を維持することが課題となります。特に、DLL関数がコールバックを呼び出すようなシナリオでは、スタックのコンテキストが複雑になり、Goランタイムが予期しないスタック上で実行される可能性があります。

runtime.write関数は、Goランタイムが標準エラー出力などに書き込むための低レベルな関数であり、Windows環境ではWriteFileというWindows API(DLL関数)を内部で呼び出します。このDLL呼び出しに伴うスタック管理の課題を解決するため、アセンブリで直接WriteFileを呼び出すのではなく、C言語のコードに移行することで、より柔軟かつ安全なスタック管理を可能にすることが目的でした。C言語は、アセンブリよりも高レベルな抽象化を提供しつつ、低レベルなメモリ操作やスタック操作をより制御しやすいため、このような複雑なスタック切り替えのシナリオに適しています。

また、コミットメッセージの「badcallback says where it is, because it is being called on a Windows stack already.」という記述は、badcallbackという関数が、既にWindowsのネイティブスタック上で呼び出されている状況を示唆しています。これは、GoランタイムがDLLを介して外部コードを呼び出し、その外部コードがさらにGoランタイム内のコールバックを呼び出すような場合に発生しうる問題です。このような状況では、Goランタイムが自身のスタック管理ルールとは異なるWindowsスタック上で動作することになり、スタックの整合性が崩れるリスクがあります。runtime.writeをC言語に移行することで、このようなスタックの不整合による問題を回避し、より堅牢なランタイムの動作を目指したと考えられます。

前提知識の解説

Goランタイムとゴルーチン、スタック管理

Go言語は、並行処理のプリミティブとして「ゴルーチン(goroutine)」を提供します。ゴルーチンはOSのスレッドよりもはるかに軽量であり、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムは、これらのゴルーチンのスケジューリング、メモリ管理(ガベージコレクション)、そしてスタック管理を独自に行います。

ゴルーチンのスタックは、最初は非常に小さいサイズ(約2KB)で割り当てられ、関数呼び出しの深さに応じて必要に応じて動的に拡大(grow)したり、不要になった部分を縮小(shrink)したりします。この動的なスタック管理は、メモリ効率を高め、多数のゴルーチンを効率的に実行するために不可欠です。

Windows API、DLL、システムコール

Windowsオペレーティングシステムは、アプリケーションがOSの機能を利用するためのインターフェースとして「Windows API」を提供します。Windows APIのほとんどは、DLL(Dynamic Link Library)という形式で提供されます。DLLは、複数のプログラムで共有されるコードとデータを含むライブラリファイルです。例えば、ファイル操作にはkernel32.dll、GUI操作にはuser32.dllなどが使われます。

アプリケーションがDLL内の関数を呼び出すことを「DLL呼び出し」と呼びます。DLL関数は通常、ユーザーモードで実行されます。しかし、ファイルI/Oやメモリ管理など、OSカーネルの特権的な機能にアクセスする必要がある場合、DLL関数は内部的に「システムコール」を発行します。

システムコールは、ユーザーモードのアプリケーションがOSカーネルのサービスを直接要求するための低レベルなメカニズムです。システムコールが発行されると、CPUの実行モードがユーザーモードからカーネルモードに切り替わり、カーネル内の特権的なコードが実行されます。処理が完了すると、再びユーザーモードに戻ります。このモード切り替えは、セキュリティと安定性を確保するために重要です。

DLL呼び出しとシステムコールの違い:

  • 抽象度: DLL呼び出しは高レベルなAPIを提供し、開発者が扱いやすいインターフェースです。システムコールは低レベルであり、OSカーネルと直接対話します。
  • 実行モード: DLL関数は通常ユーザーモードで実行されます。システムコールはユーザーモードからカーネルモードへの移行を伴います。
  • スタック: DLL呼び出しはユーザーモードのスタック上で実行されます。システムコールはカーネルモードスタックを使用し、モード切り替え時にスタックコンテキストの切り替えが発生します。

多くのWindows API関数(DLL関数)は、実際にはシステムコールのラッパーとして機能します。つまり、DLL関数が引数を準備し、最終的に対応するシステムコールを呼び出してカーネルに処理を依頼します。

スタック切り替えの課題

GoランタイムがWindows API(DLL)を呼び出す際、GoのゴルーチンスタックとWindowsのネイティブスタックの間でスタックポインタやスタックフレームの整合性を維持することが重要です。特に、Goの動的なスタック管理とWindowsの固定的なスタックモデルの間には差異があります。

DLL関数がGoランタイム内のコールバック関数を呼び出すような複雑なシナリオでは、Goランタイムが予期しないWindowsネイティブスタック上で実行される可能性があります。このような状況では、Goのガベージコレクタがスタックを正確にスキャンできなかったり、スタックの拡大・縮小が正しく機能しなかったりするなどの問題が発生し、プログラムのクラッシュや予期せぬ動作につながる可能性があります。

このコミットは、このようなスタック管理の複雑性、特にDLL呼び出しに伴うスタック切り替えの課題を解決するために、runtime.write関数の実装をアセンブリからC言語に移行するというアプローチを取っています。C言語は、アセンブリよりも高レベルな抽象化を提供しつつ、低レベルなメモリ操作やスタック操作をより制御しやすいため、このような複雑なスタック切り替えのシナリオに適しています。

技術的詳細

このコミットの技術的詳細は、GoランタイムがWindows環境で外部DLL(特にWriteFileのようなWindows API)を呼び出す際のスタック管理の課題と、それをC言語で解決するアプローチに集約されます。

runtime.write関数の役割

runtime.writeは、Goランタイム内部で標準エラー出力(stderr)への書き込みなど、低レベルなI/O操作を行うために使用される関数です。Goプログラムがクラッシュした際のエラーメッセージ出力や、デバッグ情報の出力など、GoランタイムがOSの基本的なI/O機能に依存する場面で利用されます。

アセンブリからC言語への移行の理由

コミットメッセージにある「It may have to switch stacks, since we are calling a DLL instead of a system call.」が核心です。

  1. DLL呼び出しとスタックの不整合: WindowsのWriteFile関数はDLL(kernel32.dll)内に存在します。Goランタイムがアセンブリコードから直接このDLL関数を呼び出す場合、GoのゴルーチンスタックとWindowsのネイティブスタックの間でスタックの整合性を保つことが困難になる場合があります。特に、Goのスタックは動的に拡大・縮小するため、DLL呼び出し中にスタックが移動する可能性があり、DLLが期待するスタックコンテキストとGoランタイムのスタックコンテキストが一致しないと問題が発生します。 システムコールであれば、OSがスタックの切り替えを厳密に管理するため、このような問題は発生しにくいですが、DLL呼び出しの場合はユーザーモードでのスタック管理がより複雑になります。

  2. badcallbackの文脈: コミットメッセージの「badcallback says where it is, because it is being called on a Windows stack already.」は、GoランタイムがDLLを呼び出し、そのDLLがさらにGoランタイム内のコールバック関数(例えば、エラーハンドラなど)を呼び出すようなシナリオを示唆しています。この場合、コールバック関数がGoのゴルーチンスタックではなく、Windowsのネイティブスタック上で実行される可能性があります。 Goのガベージコレクタは、ゴルーチンスタックをスキャンして到達可能なオブジェクトを特定します。もしGoのコードがWindowsスタック上で実行されている場合、ガベージコレクタがそのスタックを正しくスキャンできず、メモリリークや不正なメモリアクセスを引き起こす可能性があります。 runtime.writeのような低レベルなI/O関数は、このようなコールバックの文脈で呼び出される可能性があり、スタックの不整合が致命的な問題につながるため、より堅牢な実装が求められます。

  3. C言語によるスタック管理の柔軟性: C言語は、アセンブリよりも高レベルな抽象化を提供しつつ、ポインタ操作や関数呼び出し規約の制御など、低レベルな操作を比較的柔軟に行うことができます。runtime.writeをC言語で実装することで、GoランタイムはWindows APIを呼び出す際のスタックの準備や、必要に応じたスタック切り替えのロジックをより安全かつ制御された方法で記述できるようになります。 具体的には、C言語の関数内でWindows APIを呼び出すことで、Cコンパイラが適切なスタックフレームを生成し、GoランタイムがWindows APIと連携する際のスタックの整合性を保ちやすくなります。

変更の具体的な影響

  • アセンブリコードの簡素化: sys_windows_386.ssys_windows_amd64.sからruntime·writeの複雑なアセンブリ実装が削除され、アセンブリコードが簡素化されました。
  • C言語による実装の追加: thread_windows.cruntime·writeのC言語実装が追加されました。このC言語実装は、runtime·stdcallというヘルパー関数を介してWindows API(GetStdHandleWriteFile)を呼び出します。runtime·stdcallは、GoランタイムがC言語からWindows APIを呼び出すためのラッパーであり、スタックの整合性を保つ役割を担っていると考えられます。
  • 堅牢性の向上: DLL呼び出しに伴うスタックの不整合リスクが低減され、GoランタイムのWindows環境での安定性と堅牢性が向上しました。

この変更は、GoランタイムがOS固有の低レベルな機能と連携する際の複雑な課題に、実用的な解決策を提供した一例と言えます。

コアとなるコードの変更箇所

このコミットでは、主に3つのファイルが変更されています。

  1. src/pkg/runtime/sys_windows_386.s (Windows 32-bit アセンブリ) TEXT runtime·write(SB),7,$24 で始まる runtime·write 関数のアセンブリ実装が完全に削除されました。

    --- a/src/pkg/runtime/sys_windows_386.s
    +++ b/src/pkg/runtime/sys_windows_386.s
    @@ -38,33 +38,13 @@ 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
    +	// stderr
     	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)
    
  2. src/pkg/runtime/sys_windows_amd64.s (Windows 64-bit アセンブリ) TEXT runtime·write(SB),7,$48 で始まる runtime·write 関数のアセンブリ実装が完全に削除されました。

    --- a/src/pkg/runtime/sys_windows_amd64.s
    +++ b/src/pkg/runtime/sys_windows_amd64.s
    @@ -60,29 +60,8 @@ 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
    +	// stderr
     	MOVQ	$-12, CX // stderr
     	MOVQ	CX, 0(SP)
     	MOVQ	runtime·GetStdHandle(SB), AX
    
  3. src/pkg/runtime/thread_windows.c (Windows スレッド関連のC言語コード) runtime·write関数のC言語実装が追加されました。

    --- a/src/pkg/runtime/thread_windows.c
    +++ b/src/pkg/runtime/thread_windows.c
    @@ -114,6 +114,27 @@ 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)
     {
    

コアとなるコードの解説

アセンブリコードの変更 (削除)

src/pkg/runtime/sys_windows_386.ssrc/pkg/runtime/sys_windows_amd64.s から削除されたアセンブリコードは、それぞれ32-bitと64-bitのWindows環境でruntime·write関数を実装していました。

削除されたアセンブリコードは、以下の手順でWindows APIのGetStdHandleWriteFileを直接呼び出していました。

  1. GetStdHandleの呼び出し:

    • 標準エラー出力(stderr)のハンドルを取得するために、GetStdHandle関数を呼び出します。GetStdHandleには-12STD_ERROR_HANDLEに対応)を引数として渡します。
    • 32-bit版ではスタックに引数をプッシュし、CALL *runtime·GetStdHandle(SB)で間接的に呼び出します。
    • 64-bit版ではCXレジスタに引数をセットし、MOVQ runtime·GetStdHandle(SB), AXで関数のアドレスをAXにロードし、CALL AXで呼び出します。
  2. WriteFileの呼び出し:

    • GetStdHandleで取得したハンドル、書き込むバッファのアドレス(buf)、書き込むバイト数(count)、実際に書き込まれたバイト数を格納するポインタ(written)、およびOVERLAPPED構造体(非同期I/O用、ここではNULL)を引数としてWriteFile関数を呼び出します。
    • アセンブリコードでは、これらの引数をスタックにプッシュ(32-bit)またはレジスタにセット(64-bit)し、WriteFileを呼び出していました。

このアセンブリによる直接的なWindows API呼び出しは、Goランタイムのスタック管理とWindowsのスタック管理の間の複雑な相互作用により、スタックの不整合を引き起こす可能性がありました。特に、Goの動的なスタック拡大・縮小と、Windows APIが期待するスタックフレームの構造との間で問題が生じやすかったと考えられます。

C言語コードの追加 (src/pkg/runtime/thread_windows.c)

新しく追加されたC言語のruntime·write関数は、以下の構造を持っています。

int32
runtime·write(int32 fd, void *buf, int32 n)
{
	void *handle;
	uint32 written;

	written = 0;
	switch(fd) {
	case 1: // stdout (not used by runtime.write in this context, but included for completeness)
		handle = runtime·stdcall(runtime·GetStdHandle, 1, (uintptr)-11);
		break;
	case 2: // stderr
		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;
}

このC言語実装のポイントは、runtime·stdcallというヘルパー関数を使用している点です。

  • runtime·stdcall: この関数は、GoランタイムがC言語からWindows API(__stdcall呼び出し規約を使用する関数)を安全に呼び出すためのラッパーです。__stdcallは、呼び出された関数がスタックをクリーンアップする呼び出し規約であり、Windows APIで広く使用されています。 runtime·stdcallは、GoランタイムのスタックコンテキストとWindows APIのスタックコンテキストの間の橋渡しを行い、スタックの整合性を保ちながら関数呼び出しを実行します。これにより、Goのガベージコレクタがスタックを正しくスキャンできるようになり、スタックの不整合による問題を回避できます。

  • ファイルディスクリプタの処理: runtime·writefd(ファイルディスクリプタ)を引数として受け取ります。switch文でfdの値に応じて適切な標準ハンドル(STD_OUTPUT_HANDLEまたはSTD_ERROR_HANDLE)を取得します。Goランタイムのwrite関数は主に標準エラー出力(fd=2)に使用されますが、コードにはfd=1(標準出力)のケースも含まれています。

  • GetStdHandleWriteFileの呼び出し: C言語のruntime·write関数は、runtime·stdcallを介してruntime·GetStdHandleruntime·WriteFileを呼び出します。これらの関数は、Goランタイムが内部的にWindows APIのGetStdHandleWriteFileをラップしたものです。 引数はC言語の型で渡され、runtime·stdcallが適切な型変換とスタック操作を行って、実際のWindows API呼び出しを実行します。

このC言語への移行により、GoランタイムはWindows APIとの連携において、より制御された、かつ堅牢なスタック管理を実現できるようになりました。アセンブリで直接スタックを操作するよりも、C言語のコンパイラにスタックフレームの生成を任せることで、複雑なスタック切り替えのシナリオにおける潜在的な問題を回避しています。

関連リンク

参考にした情報源リンク