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

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

このコミットは、GoランタイムがWindows環境でGetQueuedCompletionStatusEx関数が存在しない場合にクラッシュする問題を修正します。具体的には、GetQueuedCompletionStatusExが利用できない古いWindowsバージョンや特定の環境において、Goランタイムが安全に動作するように、この関数の動的なロードと存在チェックを導入しています。

コミット

commit 277a7b22f1131ea2a2fb98be9d7378c0f4ab5834
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Fri Mar 28 12:37:14 2014 +1100

    runtime: do not crash when GetQueuedCompletionStatusEx is missing
    
    Fixes #7635
    
    LGTM=minux.ma
    R=golang-codereviews, minux.ma
    CC=golang-codereviews
    https://golang.org/cl/80390043

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

https://github.com/golang/go/commit/277a7b22f1131ea2a2fb98be9d7378c0f4ab5834

元コミット内容

runtime: do not crash when GetQueuedCompletionStatusEx is missing

Fixes #7635

LGTM=minux.ma
R=golang-codereviews, minux.ma
CC=golang-codereviews
https://golang.org/cl/80390043

変更の背景

この変更の背景には、GoランタイムがWindowsのI/O Completion Ports (IOCP) を利用する際に、特定のWindows API関数であるGetQueuedCompletionStatusExの可用性に関する問題がありました。

GetQueuedCompletionStatusExは、Windows VistaおよびWindows Server 2008以降で導入された関数であり、I/O完了ポートから複数の完了パケットを効率的に取得するために使用されます。これ以前のWindowsバージョン(例えばWindows XPやWindows Server 2003など)では、この関数は存在しませんでした。

GoランタイムがGetQueuedCompletionStatusExを静的にリンクまたは直接呼び出そうとした場合、この関数が存在しない環境では、プログラムが起動時または実行時にクラッシュする可能性がありました。これは、特に古いWindows環境でのGoアプリケーションの互換性を損なう重大な問題でした。

コミットメッセージにあるFixes #7635は、この問題がGoのIssueトラッカーで報告され、修正が必要とされていたことを示しています。このコミットは、GetQueuedCompletionStatusExが利用できない場合でもGoランタイムがクラッシュしないように、そのロード方法を改善することを目的としています。

前提知識の解説

I/O Completion Ports (IOCP)

I/O Completion Ports (IOCP) は、Windowsオペレーティングシステムが提供する、高効率な非同期I/O処理のためのメカニズムです。特に、多数の同時接続を扱うサーバーアプリケーションなどでその真価を発揮します。

IOCPの基本的な考え方は以下の通りです。

  1. 完了ポートの作成: CreateIoCompletionPort関数を使用して、I/O完了ポートを作成します。
  2. デバイスの関連付け: ファイルハンドル、ソケット、名前付きパイプなどのI/Oデバイスを完了ポートに関連付けます。
  3. 非同期I/Oの開始: ReadFile, WriteFile, WSARecv, WSASendなどの非同期I/O操作を開始します。これらの操作はすぐに完了せず、バックグラウンドで実行されます。
  4. 完了通知の待機: I/O操作が完了すると、その結果は関連付けられた完了ポートにキューされます。アプリケーションは、GetQueuedCompletionStatusまたはGetQueuedCompletionStatusExを呼び出して、完了したI/O操作の通知を待機します。
  5. スレッドプールの利用: 通常、複数のスレッドが同じ完了ポートから完了通知を待機します。IOCPは、利用可能なスレッドに完了通知を効率的にディスパッチし、スレッドの過剰な生成やコンテキストスイッチのオーバーヘッドを最小限に抑えることで、高いスケーラビリティを実現します。

GetQueuedCompletionStatusEx

GetQueuedCompletionStatusExは、Windows VistaおよびWindows Server 2008で導入された、GetQueuedCompletionStatusの拡張版です。主な違いは、一度の呼び出しで複数のI/O完了パケットを取得できる点にあります。

  • GetQueuedCompletionStatus: 1回の呼び出しで1つの完了パケットしか取得できません。
  • GetQueuedCompletionStatusEx: 1回の呼び出しで指定された数の完了パケットを配列として取得できます。これにより、システムコール数を減らし、特に高負荷な環境でのパフォーマンスを向上させることができます。

Goランタイムは、Windows上でのネットワークI/OやファイルI/Oにおいて、このIOCPメカニズムを内部的に利用しています。そのため、GetQueuedCompletionStatusExのような重要な関数が利用できない環境で問題が発生すると、ランタイムの安定性に直接影響します。

GoランタイムのWindowsにおけるDLL動的ロード (dynimportGetProcAddress)

Windowsのプログラムは、通常、DLL (Dynamic Link Library) と呼ばれる共有ライブラリから関数を呼び出します。Goランタイムのような低レベルのシステムプログラミングでは、特定のDLL関数を動的にロードする必要がある場合があります。これは、主に以下の理由によります。

  1. 後方互換性: 特定の関数が古いWindowsバージョンには存在せず、新しいバージョンでのみ利用可能である場合、プログラムが古いOSでも動作するように、その関数を動的にロードし、存在しない場合は代替処理を行う必要があります。
  2. リソースの節約: プログラムの起動時にすべてのDLLをロードするのではなく、必要になったときにのみロードすることで、メモリ使用量を削減し、起動時間を短縮できます。

Goランタイムでは、dynimportというディレクティブを使用して、外部DLL関数を宣言します。しかし、dynimportはあくまで「この関数がこのDLLに存在する可能性がある」という宣言であり、実際にその関数が利用可能かどうかは実行時に確認する必要があります。

この確認のために使用されるのが、Windows APIのLoadLibraryAGetProcAddress関数です。

  • LoadLibraryA: 指定されたDLLをメモリにロードし、そのDLLのハンドルを返します。
  • GetProcAddress: ロードされたDLLのハンドルと関数名を指定して、その関数のエントリポイント(メモリ上のアドレス)を取得します。関数が存在しない場合、NULLを返します。

このコミットでは、GetQueuedCompletionStatusExdynimportで静的に宣言するのではなく、LoadLibraryAGetProcAddressを使って動的にロードすることで、関数が存在しない環境でもクラッシュしないように修正しています。

技術的詳細

このコミット以前のGoランタイムでは、GetQueuedCompletionStatusEx関数をsrc/pkg/runtime/os_windows.c内で#pragma dynimport runtime·GetQueuedCompletionStatusEx GetQueuedCompletionStatusEx "kernel32.dll"のように宣言し、直接利用しようとしていました。このdynimportは、コンパイル時にkernel32.dllからGetQueuedCompletionStatusExをインポートしようとしますが、もし実行環境のkernel32.dllにこの関数が存在しない場合(例: Windows XP)、ランタイムは未定義のシンボルを参照しようとしてクラッシュしていました。

この修正は、GetQueuedCompletionStatusExのロードを静的なインポートから動的なインポートに切り替えることで、この問題を解決します。具体的には、ランタイムの初期化フェーズ(runtime·osinit関数内)で、kernel32.dllを明示的にロードし、その中からGetQueuedCompletionStatusExのアドレスをGetProcAddressを使って取得します。

もしGetProcAddressNULLを返した場合、それはGetQueuedCompletionStatusExが現在のシステムで利用できないことを意味します。この場合、GoランタイムはGetQueuedCompletionStatusExを使用せず、代わりにGetQueuedCompletionStatusのような代替手段(または、この関数が必須でない場合は単にその機能を使用しない)にフォールバックすることで、クラッシュを回避します。このコミット自体はフォールバックロジックを直接追加するものではなく、あくまでクラッシュを回避するための動的ロードメカニズムを導入しています。

このアプローチにより、GoアプリケーションはGetQueuedCompletionStatusExが存在しない古いWindowsバージョンでも起動し、動作できるようになります。

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

変更はsrc/pkg/runtime/os_windows.cファイルに集中しています。

--- a/src/pkg/runtime/os_windows.c
+++ b/src/pkg/runtime/os_windows.c
@@ -21,7 +21,6 @@
 #pragma dynimport runtime·FreeEnvironmentStringsW FreeEnvironmentStringsW "kernel32.dll"
 #pragma dynimport runtime·GetEnvironmentStringsW GetEnvironmentStringsW "kernel32.dll"
 #pragma dynimport runtime·GetProcAddress GetProcAddress "kernel32.dll"
-#pragma dynimport runtime·GetQueuedCompletionStatusEx GetQueuedCompletionStatusEx "kernel32.dll"
 #pragma dynimport runtime·GetStdHandle GetStdHandle "kernel32.dll"
 #pragma dynimport runtime·GetSystemInfo GetSystemInfo "kernel32.dll"
 #pragma dynimport runtime·GetSystemTimeAsFileTime GetSystemTimeAsFileTime "kernel32.dll"
@@ -54,7 +53,6 @@ extern void *runtime·ExitProcess;
 extern void *runtime·FreeEnvironmentStringsW;
 extern void *runtime·GetEnvironmentStringsW;
 extern void *runtime·GetProcAddress;
-extern void *runtime·GetQueuedCompletionStatusEx;
 extern void *runtime·GetStdHandle;
 extern void *runtime·GetSystemInfo;
 extern void *runtime·GetSystemTimeAsFileTime;
@@ -74,6 +72,8 @@ extern void *runtime·WaitForSingleObject;
 extern void *runtime·WriteFile;
 extern void *runtime·timeBeginPeriod;
 
+void *runtime·GetQueuedCompletionStatusEx;
+
 extern uintptr runtime·externalthreadhandlerp;
 void runtime·externalthreadhandler(void);
 void runtime·sigtramp(void);
@@ -90,6 +90,8 @@ getproccount(void)
 void
 runtime·osinit(void)
 {
+	void *kernel32;
+
 	runtime·externalthreadhandlerp = (uintptr)runtime·externalthreadhandler;
 
 	runtime·stdcall(runtime·AddVectoredExceptionHandler, 2, (uintptr)1, (uintptr)runtime·sigtramp);
@@ -102,6 +104,11 @@ runtime·osinit(void)
 	// equivalent threads that all do a mix of GUI, IO, computations, etc.
 	// In such context dynamic priority boosting does nothing but harm, so we turn it off.
 	runtime·stdcall(runtime·SetProcessPriorityBoost, 2, (uintptr)-1, (uintptr)1);\n+\n+\tkernel32 = runtime·stdcall(runtime·LoadLibraryA, 1, \"kernel32.dll\");\n+\tif(kernel32 != nil) {\n+\t\truntime·GetQueuedCompletionStatusEx = runtime·stdcall(runtime·GetProcAddress, 2, kernel32, \"GetQueuedCompletionStatusEx\");\n+\t}\n }
 
 void

主な変更点は以下の通りです。

  1. #pragma dynimportの削除: GetQueuedCompletionStatusExに対する#pragma dynimport行が削除されました。これにより、コンパイル時にこの関数を静的にインポートしようとする試みがなくなります。
  2. extern void *runtime·GetQueuedCompletionStatusEx;の削除: 同様に、外部シンボルとしての宣言も削除されました。
  3. void *runtime·GetQueuedCompletionStatusEx;の追加: グローバル変数としてruntime·GetQueuedCompletionStatusExが宣言されました。これは、動的に取得した関数のアドレスを格納するためのポインタです。
  4. runtime·osinit関数内の動的ロードロジックの追加:
    • kernel32 = runtime·stdcall(runtime·LoadLibraryA, 1, "kernel32.dll");kernel32.dllを動的にロードします。
    • if(kernel32 != nil) { ... }kernel32.dllのロードが成功した場合にのみ、以下の処理を実行します。
    • runtime·GetQueuedCompletionStatusEx = runtime·stdcall(runtime·GetProcAddress, 2, kernel32, "GetQueuedCompletionStatusEx");:ロードしたkernel32.dllからGetQueuedCompletionStatusEx関数のアドレスを取得し、runtime·GetQueuedCompletionStatusExポインタに格納します。

コアとなるコードの解説

このコミットの核心は、GetQueuedCompletionStatusEx関数のロード方法を、コンパイル時の静的なインポートから実行時の動的なインポートへと変更した点にあります。

以前は、Goランタイムは#pragma dynimportを使用して、GetQueuedCompletionStatusExkernel32.dllに存在することを前提としていました。しかし、この関数はWindows Vista以降で導入されたため、Windows XPなどの古いシステムでは利用できませんでした。その結果、古いシステムでGoプログラムを実行しようとすると、存在しない関数への参照によってクラッシュが発生していました。

新しいコードでは、runtime·osinit(GoランタイムのWindows固有の初期化関数)内で、以下の手順を踏みます。

  1. kernel32.dllのロード: runtime·stdcall(runtime·LoadLibraryA, 1, "kernel32.dll"); を呼び出して、kernel32.dllを明示的にメモリにロードします。LoadLibraryAは、指定されたDLLのモジュールハンドルを返します。このハンドルは、そのDLL内の関数を検索するために使用されます。
  2. 関数のアドレス取得: kernel32nilでない(つまり、kernel32.dllのロードに成功した)場合、runtime·stdcall(runtime·GetProcAddress, 2, kernel32, "GetQueuedCompletionStatusEx"); を呼び出します。GetProcAddressは、指定されたモジュールハンドルと関数名に対応する関数のエントリポイントのアドレスを返します。もし関数が見つからない場合、NULLを返します。
  3. ポインタへの格納: 取得したアドレスは、グローバル変数runtime·GetQueuedCompletionStatusExに格納されます。

この変更により、GoランタイムはGetQueuedCompletionStatusExがシステムに存在するかどうかを実行時に確認できるようになります。もし関数が存在しない場合、runtime·GetQueuedCompletionStatusExNULLのままとなり、ランタイムはクラッシュすることなく、この関数が利用できないことを認識できます。その後のGoランタイムのコードは、このポインタがNULLであるかどうかをチェックし、適切な代替ロジック(例えば、GetQueuedCompletionStatusを使用するなど)にフォールバックするか、あるいはGetQueuedCompletionStatusExの機能が必須でない場合はその機能を使用しないように設計されていると推測されます。

この修正は、GoランタイムのWindowsにおける堅牢性と互換性を大幅に向上させるものです。

関連リンク

参考にした情報源リンク

  • Web検索結果: GetQueuedCompletionStatusExに関するMicrosoft Learnドキュメント(検索時に参照された可能性のある情報源)
  • Web検索結果: Go Gerrit Change-ID golang.org/cl/80390043に関する情報(検索時に参照された可能性のある情報源)