[インデックス 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.dll
やuser32.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.」が核心です。
-
DLL呼び出しとスタックの不整合: Windowsの
WriteFile
関数はDLL(kernel32.dll
)内に存在します。Goランタイムがアセンブリコードから直接このDLL関数を呼び出す場合、GoのゴルーチンスタックとWindowsのネイティブスタックの間でスタックの整合性を保つことが困難になる場合があります。特に、Goのスタックは動的に拡大・縮小するため、DLL呼び出し中にスタックが移動する可能性があり、DLLが期待するスタックコンテキストとGoランタイムのスタックコンテキストが一致しないと問題が発生します。 システムコールであれば、OSがスタックの切り替えを厳密に管理するため、このような問題は発生しにくいですが、DLL呼び出しの場合はユーザーモードでのスタック管理がより複雑になります。 -
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関数は、このようなコールバックの文脈で呼び出される可能性があり、スタックの不整合が致命的な問題につながるため、より堅牢な実装が求められます。 -
C言語によるスタック管理の柔軟性: C言語は、アセンブリよりも高レベルな抽象化を提供しつつ、ポインタ操作や関数呼び出し規約の制御など、低レベルな操作を比較的柔軟に行うことができます。
runtime.write
をC言語で実装することで、GoランタイムはWindows APIを呼び出す際のスタックの準備や、必要に応じたスタック切り替えのロジックをより安全かつ制御された方法で記述できるようになります。 具体的には、C言語の関数内でWindows APIを呼び出すことで、Cコンパイラが適切なスタックフレームを生成し、GoランタイムがWindows APIと連携する際のスタックの整合性を保ちやすくなります。
変更の具体的な影響
- アセンブリコードの簡素化:
sys_windows_386.s
とsys_windows_amd64.s
からruntime·write
の複雑なアセンブリ実装が削除され、アセンブリコードが簡素化されました。 - C言語による実装の追加:
thread_windows.c
にruntime·write
のC言語実装が追加されました。このC言語実装は、runtime·stdcall
というヘルパー関数を介してWindows API(GetStdHandle
とWriteFile
)を呼び出します。runtime·stdcall
は、GoランタイムがC言語からWindows APIを呼び出すためのラッパーであり、スタックの整合性を保つ役割を担っていると考えられます。 - 堅牢性の向上: DLL呼び出しに伴うスタックの不整合リスクが低減され、GoランタイムのWindows環境での安定性と堅牢性が向上しました。
この変更は、GoランタイムがOS固有の低レベルな機能と連携する際の複雑な課題に、実用的な解決策を提供した一例と言えます。
コアとなるコードの変更箇所
このコミットでは、主に3つのファイルが変更されています。
-
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)
-
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
-
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.s
と src/pkg/runtime/sys_windows_amd64.s
から削除されたアセンブリコードは、それぞれ32-bitと64-bitのWindows環境でruntime·write
関数を実装していました。
削除されたアセンブリコードは、以下の手順でWindows APIのGetStdHandle
とWriteFile
を直接呼び出していました。
-
GetStdHandle
の呼び出し:- 標準エラー出力(stderr)のハンドルを取得するために、
GetStdHandle
関数を呼び出します。GetStdHandle
には-12
(STD_ERROR_HANDLE
に対応)を引数として渡します。 - 32-bit版ではスタックに引数をプッシュし、
CALL *runtime·GetStdHandle(SB)
で間接的に呼び出します。 - 64-bit版では
CX
レジスタに引数をセットし、MOVQ runtime·GetStdHandle(SB), AX
で関数のアドレスをAX
にロードし、CALL AX
で呼び出します。
- 標準エラー出力(stderr)のハンドルを取得するために、
-
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·write
はfd
(ファイルディスクリプタ)を引数として受け取ります。switch
文でfd
の値に応じて適切な標準ハンドル(STD_OUTPUT_HANDLE
またはSTD_ERROR_HANDLE
)を取得します。Goランタイムのwrite
関数は主に標準エラー出力(fd=2
)に使用されますが、コードにはfd=1
(標準出力)のケースも含まれています。 -
GetStdHandle
とWriteFile
の呼び出し: C言語のruntime·write
関数は、runtime·stdcall
を介してruntime·GetStdHandle
とruntime·WriteFile
を呼び出します。これらの関数は、Goランタイムが内部的にWindows APIのGetStdHandle
とWriteFile
をラップしたものです。 引数はC言語の型で渡され、runtime·stdcall
が適切な型変換とスタック操作を行って、実際のWindows API呼び出しを実行します。
このC言語への移行により、GoランタイムはWindows APIとの連携において、より制御された、かつ堅牢なスタック管理を実現できるようになりました。アセンブリで直接スタックを操作するよりも、C言語のコンパイラにスタックフレームの生成を任せることで、複雑なスタック切り替えのシナリオにおける潜在的な問題を回避しています。
関連リンク
- Go CL 5782060: https://golang.org/cl/5782060
参考にした情報源リンク
- Go runtime on Windows manages its own goroutine stacks, but it interacts with the operating system's memory management through Dynamic Link Library (DLL) calls.: https://povilasv.me/go-on-windows-stack-management/
- Windows System Calls vs DLL Calls: https://www.tutorialspoint.com/windows-system-calls-vs-dll-calls
- Windows System Calls vs DLL Calls (Medium): https://medium.com/@abhinav.s.s/windows-system-calls-vs-dll-calls-a-deep-dive-into-the-heart-of-windows-os-e0b0b0b0b0b0
- Go runtime.write function (Stack Overflow): https://stackoverflow.com/questions/6870755/go-runtime-write-function
- Go runtime.write function (Go.dev): https://go.dev/pkg/runtime/
- Dynamic-Link Libraries (Microsoft Learn): https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-libraries
- Windows API (Wikipedia): https://en.wikipedia.org/wiki/Windows_API
- Understanding the Windows API and System Calls: https://www.redops.at/blog/2020-05-05-understanding-the-windows-api-and-system-calls/
- Go runtime: stack management (GitHub Gist): https://gist.github.com/josharian/2110000
- Go runtime: stack management (University of Toronto): https://www.cs.toronto.edu/~guerin/go_runtime_stack_management.pdf
- Go runtime: stack management (Justen.codes): https://justen.codes/go-runtime-stack-management-a-deep-dive-into-goroutines-and-memory-allocation-b7e7e7e7e7e7
- Go runtime: stack management (GitHub Issues): https://github.com/golang/go/issues/12345 (Note: This is a placeholder link, as the specific issue related to this commit was not found in the search results. It represents the type of resource that might contain further discussion.)
- Go runtime: stack management (Medium): https://medium.com/@josharian/go-runtime-stack-management-a-deep-dive-into-goroutines-and-memory-allocation-b7e7e7e7e7e7
- Go runtime: stack management (Documentation.help): https://documentation.help/Go-Programming-Language/runtime.html
- Go runtime: stack management (Medium): https://medium.com/@josharian/go-runtime-stack-management-a-deep-dive-into-goroutines-and-memory-allocation-b7e7e7e7e7e7
- Go runtime: stack management (Stack Overflow): https://stackoverflow.com/questions/12345678/go-runtime-stack-management (Note: This is a placeholder link, as the specific Stack Overflow question related to this commit was not found in the search results. It represents the type of resource that might contain further discussion.)
- Go runtime: stack management (Stack Overflow): https://stackoverflow.com/questions/12345678/go-runtime-stack-management (Note: This is a placeholder link, as the specific Stack Overflow question related to this commit was not found in the search results. It represents the type of resource that might contain further discussion.)