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

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

このコミットは、Go言語のCGOテストスイートがWindows環境で正しく動作するようにするための修正を目的としています。特に、Windows固有のCGOの挙動、コンパイラの呼び出し規約、環境変数の扱い、およびスタックサイズに関する問題に対処しています。これにより、Goのクロスプラットフォーム互換性が向上し、Windows上でのCGOを利用した開発とテストがより信頼性の高いものになります。

コミット

commit 8d6958fc041eee42e78ba3c20569c71c35795b8b
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Fri Jan 20 12:59:44 2012 +1100

    misc/cgo/test: make tests run on windows
    
    - use proper Win64 gcc calling convention when
      calling initcgo on amd64
    - increase g0 stack size to 64K on amd64 to make
      it the same as 386
    - implement C.sleep
    - do not use C.stat, since it is renamed to C._stat by mingw
    - use fopen to implement TestErrno, since C.strtol
      always succeeds on windows
    - skip TestSetEnv on windows, because os.Setenv
      sets windows process environment, while C.getenv
      inspects internal C runtime variable instead
    
    R=golang-dev, vcc.163, rsc
    CC=golang-dev
    https://golang.org/cl/5500094

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

https://github.com/golang/go/commit/8d6958fc041eee42e78ba3c20569c71c35795b8b

元コミット内容

commit 8d6958fc041eee42e78ba3c20569c71c35795b8b
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Fri Jan 20 12:59:44 2012 +1100

    misc/cgo/test: make tests run on windows
    
    - use proper Win64 gcc calling convention when
      calling initcgo on amd64
    - increase g0 stack size to 64K on amd64 to make
      it the same as 386
    - implement C.sleep
    - do not use C.stat, since it is renamed to C._stat by mingw
    - use fopen to implement TestErrno, since C.strtol
      always succeeds on windows
    - skip TestSetEnv on windows, because os.Setenv
      sets windows process environment, while C.getenv
      inspects internal C runtime variable instead
    
    R=golang-dev, vcc.163, rsc
    CC=golang-dev
    https://golang.org/cl/5500094

変更の背景

Go言語のCGO(C言語との相互運用機能)は、GoプログラムからCライブラリを呼び出すための重要な機能です。しかし、異なるオペレーティングシステム(特にWindows)やアーキテクチャ(amd64)では、C言語のコンパイラやランタイムの挙動に差異があるため、CGOを利用したテストが正しく動作しないという問題が発生していました。

このコミットの主な背景は、GoのCGOテストスイートがWindows環境で失敗する問題を解決することです。具体的には、以下の点が課題となっていました。

  1. 呼び出し規約の不一致: Windows上のamd64アーキテクチャにおけるGCCの呼び出し規約が、Goのランタイムが期待するものと異なっていたため、initcgo関数の呼び出しで問題が発生していました。
  2. スタックサイズの差異: g0(Goランタイムの初期ゴルーチン)のスタックサイズが、amd64と386アーキテクチャで異なっており、amd64で不足する可能性がありました。
  3. C標準ライブラリ関数の差異: sleepstatといったC標準ライブラリ関数が、Windows(MinGW)環境で異なる名前や挙動を示すことがありました。特にstat_statにリネームされることが一般的です。
  4. エラーハンドリングの差異: strtolのような関数がWindowsでは常に成功し、期待されるエラーを返さない場合がありました。
  5. 環境変数の管理: os.SetenvとC言語のgetenvが、Windows上で異なる環境変数のスコープを参照するため、テストが意図した通りに動作しませんでした。os.SetenvはOSプロセス全体の環境変数を設定するのに対し、CランタイムのgetenvはCランタイムが起動時にコピーした内部の環境変数セットを参照します。

これらの問題を解決し、GoのCGO機能がWindows環境でも安定して動作し、テストがパスするようにすることが、このコミットの重要な目的でした。

前提知識の解説

このコミットを理解するためには、以下の技術的な概念について理解しておく必要があります。

  1. CGO:

    • Go言語とC言語の相互運用を可能にするGoの機能です。GoコードからC関数を呼び出したり、CコードからGo関数を呼び出したりできます。
    • import "C"という特殊なインポート宣言を使用し、Goコード内にCコードを直接記述したり、既存のCライブラリをリンクしたりします。
    • CGOは、Goのビルドプロセス中にCコンパイラ(通常はGCC)を呼び出してCコードをコンパイルし、Goの実行可能ファイルにリンクします。
  2. 呼び出し規約 (Calling Convention):

    • 関数が呼び出される際に、引数がどのようにレジスタやスタックに渡され、戻り値がどのように返されるか、スタックがどのようにクリーンアップされるかなどを定義するルールセットです。
    • 異なるアーキテクチャ(例: x86、amd64)や異なるコンパイラ(例: MSVC、GCC)では、異なる呼び出し規約が使用されることがあります。
    • Win64 GCC Calling Convention: Windows上の64ビットGCCコンパイラが使用する呼び出し規約です。これは、LinuxやmacOSのSystem V AMD64 ABIとは異なります。例えば、最初の4つの整数/ポインタ引数を渡すレジスタが異なります(WindowsではRCX, RDX, R8, R9、System VではRDI, RSI, RDX, RCX, R8, R9)。
  3. g0 (Goランタイムの初期ゴルーチン):

    • Goランタイムが起動する際に最初に作成される特別なゴルーチンです。
    • Goのスケジューラやメモリ管理など、ランタイムの低レベルな処理を実行するために使用されます。
    • g0は、Goのユーザーコードが実行される通常のゴルーチンとは異なり、システムコールやCGO呼び出しの際に使用されるスタックを持っています。このスタックサイズが不足すると、スタックオーバーフローなどの問題が発生する可能性があります。
  4. _chkstk.o:

    • Windows環境で、スタックの拡張を処理するためのヘルパー関数を含むオブジェクトファイルです。
    • 大きなスタックフレームを割り当てる際に、スタックガードページをチェックし、必要に応じてスタックをコミットするために使用されます。
    • GCCコンパイラが生成するコードで、スタックを動的に拡張する必要がある場合(特に大きなローカル変数や再帰呼び出しが多い場合)にリンクされます。
  5. C.statC._stat:

    • statは、ファイルの状態(サイズ、パーミッション、最終更新時刻など)を取得するためのPOSIX標準のC関数です。
    • WindowsのMinGW(Minimalist GNU for Windows)環境では、MicrosoftのCランタイムライブラリとの互換性のために、stat関数が_statという名前にリネームされていることがよくあります。これは、Windows APIの命名規則に合わせるためです。CGOでC関数を呼び出す際には、この名前の差異を考慮する必要があります。
  6. os.SetenvC.getenv (Windowsにおける挙動):

    • os.Setenv (Go): Goの標準ライブラリ関数で、オペレーティングシステムレベルの環境変数を設定します。Windowsでは、SetEnvironmentVariableというWin32 APIを呼び出し、プロセス全体の環境ブロックを変更します。
    • C.getenv (CGO経由のC関数): C標準ライブラリの関数で、環境変数の値を取得します。WindowsのCランタイムでは、プロセス起動時にOSの環境変数をコピーして内部的に保持することが一般的です。getenvはこの内部コピーを参照するため、os.Setenvで変更されたOSレベルの環境変数が、Cランタイムの内部コピーには即座に反映されない場合があります。これにより、GoとCの間で環境変数の認識に不一致が生じることがあります。

技術的詳細

このコミットは、Windows環境でのCGOテストの安定性を向上させるために、複数の技術的な側面から修正を加えています。

  1. Win64 GCC 呼び出し規約の適用 (src/pkg/runtime/asm_amd64.s):

    • amd64アーキテクチャのWindows環境では、GCCの呼び出し規約がLinuxやmacOSとは異なります。特に、関数呼び出しの際に引数を渡すレジスタが異なります。
    • initcgo関数を呼び出す際、Goランタイムは最初の引数(g0のポインタ)を特定のレジスタに配置する必要があります。WindowsのWin64呼び出し規約では、最初の引数はRCXレジスタに渡されます。
    • 修正前は、Goランタイムがg0DIレジスタに配置してCALL AXinitcgo)を呼び出していました。これはSystem V AMD64 ABI(Linux/macOSで一般的)に準拠しています。
    • 修正では、MOVQ DI, CXという命令を追加し、DIレジスタに格納されているg0の値をCXレジスタにコピーしてからCALL AXを実行するように変更されました。これにより、WindowsのWin64呼び出し規約に適合し、initcgoが正しく引数を受け取れるようになります。
  2. g0 スタックサイズの増加 (src/pkg/runtime/asm_amd64.s):

    • g0ゴルーチンは、Goランタイムの初期化やCGO呼び出しなど、重要な低レベル処理に使用されます。
    • 386アーキテクチャではg0のスタックサイズが64KBでしたが、amd64では8KBに設定されていました。これは、CGO呼び出しなどでスタックが不足する可能性がありました。
    • 修正では、LEAQ (-8192+104)(SP), BXLEAQ (-64*1024+104)(SP), BX に変更し、g0のスタックサイズを8KBから64KBに増やしました。これにより、amd64環境でも386と同じスタックサイズが確保され、スタックオーバーフローのリスクが軽減されます。
  3. C.sleep の実装 (misc/cgo/test/sleep_windows.gomisc/cgo/test/issue1560.go):

    • POSIXシステムではsleep関数が利用できますが、Windowsには直接的なsleep関数はありません。代わりにSleep(大文字S)というWin32 API関数があります。
    • misc/cgo/test/sleep_windows.goに新しいファイルが追加され、CGO経由でSleep Win32 APIを呼び出すsleep関数がC言語で実装されました。これにより、GoのテストコードからC.sleepを呼び出せるようになります。
    • misc/cgo/test/issue1560.goでは、この新しいsleep関数のプロトタイプがCGOのコメントブロックに追加され、Goコードから利用可能になりました。
  4. C.stat の使用回避と C._stat への対応 (misc/cgo/test/basic.go):

    • WindowsのMinGW環境では、stat関数が_statにリネームされているため、C.statを直接呼び出すとリンクエラーや未定義シンボルエラーが発生する可能性がありました。
    • コミットでは、Size関数(C.statを使用していた)をbasic.goから削除しました。これは、C.statの代わりにC._statを使用するように変更するのではなく、テストの目的上、stat関数自体が必須ではなかったため、よりシンプルな解決策として削除が選択されたと考えられます。
  5. TestErrnoC.fopen を用いた実装 (misc/cgo/test/basic.go):

    • TestErrnoは、CGO呼び出しでエラーが発生した際に、Goのos.Errnoが正しく設定されるかをテストするものです。
    • 以前はC.strtolを使用していましたが、Windowsではstrtolが常に成功し、無効な入力に対してもエラーを返さない場合があるため、テストが意図した通りに機能しませんでした。
    • 修正では、存在しないファイルをC.fopenで開こうとすることでエラーを発生させるように変更されました。fopenはファイルが見つからない場合にNULLを返し、errnoENOENTに設定するため、Windowsでも確実にエラーを発生させ、os.ENOENTが正しく取得できることをテストできます。
  6. TestSetEnv の Windows でのスキップ (misc/cgo/test/env.go):

    • TestSetEnvは、Goのos.Setenvで設定した環境変数がCGO経由でC.getenvから正しく読み取れるかをテストするものです。
    • しかし、Windowsではos.SetenvがOSプロセス環境を変更するのに対し、Cランタイムのgetenvはプロセス起動時にコピーされた内部の環境変数セットを参照します。このため、os.Setenvで変更してもC.getenvには反映されないという不一致が生じます。
    • この問題を回避するため、runtime.GOOS == "windows"の場合にTestSetEnvをスキップするように変更されました。これにより、Windowsでのテストの誤った失敗を防ぎます。
  7. Makefileの変更 (misc/cgo/test/Makefile):

    • Windows環境でのビルドをサポートするために、Makefileが更新されました。
    • GOOSwindowsの場合に、GCCのバージョンとアーキテクチャ(386またはamd64)に基づいて適切なGCCライブラリディレクトリ(GCCLIBDIR)を設定します。
    • _chkstk.oまたは_chkstk_ms.o(amd64の場合)をCGOのオブジェクトファイルに追加し、libgcc.aから抽出するように指示しています。これは、Windowsで大きなスタックを扱う際に必要となるスタックチェックルーチンをリンクするためです。
    • sleep_windows.goCGOFILESに追加され、ビルドプロセスに含まれるようになりました。

これらの変更により、GoのCGOテストスイートはWindows環境でもより堅牢になり、Goのクロスプラットフォーム開発の信頼性が向上しました。

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

このコミットで変更された主要なファイルとその内容は以下の通りです。

  • misc/cgo/test/Makefile:

    • Windows (GOOS=windows) 環境でのビルド設定を追加。
    • GCCのバージョンとアーキテクチャ (386/amd64) に応じて、適切なGCCライブラリディレクトリ (GCCLIBDIR) を設定。
    • _chkstk.o または _chkstk_ms.o をCGOのオブジェクトファイル (CGO_OFILES) に追加し、libgcc.a から抽出するルールを定義。
    • sleep_windows.go をCGOのソースファイル (CGOFILES) に追加。
  • misc/cgo/test/basic.go:

    • Size 関数(C.stat を使用していた)を削除。
    • testErrno 関数を修正。C.strtol の代わりに C.fopen を使用してエラーをテストするように変更。存在しないファイルをオープンしようとすることで os.ENOENT が発生することを確認。
  • misc/cgo/test/env.go:

    • testSetEnv 関数にWindows環境でのスキップロジックを追加。runtime.GOOS == "windows" の場合、テストをスキップする。これは、os.SetenvC.getenv のWindowsにおける挙動の差異によるもの。
  • misc/cgo/test/issue1560.go:

    • CGOコメントブロック内に unsigned int sleep(unsigned int seconds); のプロトタイプ宣言を追加。これにより、Goコードから C.sleep を呼び出せるようになる。
  • misc/cgo/test/sleep_windows.go (新規ファイル):

    • Windows環境で C.sleep を実装するための新しいファイル。
    • CGOコメントブロック内で、Win32 APIの Sleep 関数を呼び出す sleep 関数をC言語で定義。
  • src/pkg/runtime/asm_amd64.s:

    • _rt0_amd64 関数内で、initcgo を呼び出す際の引数渡しを修正。
      • MOVQ DI, CX を追加し、g0 のポインタを DI から CX レジスタにコピー。これはWin64 GCCの呼び出し規約に合わせるため。
    • g0 のスタックサイズを8KBから64KBに増加。LEAQ (-8192+104)(SP), BXLEAQ (-64*1024+104)(SP), BX に変更。
  • src/run.bash:

    • CGOテストの実行条件から、GOHOSTOSwindows の場合にスキップする条件を削除。これにより、WindowsでもCGOテストが実行されるようになる。

コアとなるコードの解説

src/pkg/runtime/asm_amd64.s の変更

--- a/src/pkg/runtime/asm_amd64.s
+++ b/src/pkg/runtime/asm_amd64.s
@@ -16,7 +16,7 @@ TEXT _rt0_amd64(SB),7,$-8
 	// create istack out of the given (operating system) stack.
 	// initcgo may update stackguard.
 	MOVQ	$runtime·g0(SB), DI
-	LEAQ	(-8192+104)(SP), BX
+	LEAQ	(-64*1024+104)(SP), BX
 	MOVQ	BX, g_stackguard(DI)
 	MOVQ	SP, g_stackbase(DI)
 
@@ -24,7 +24,9 @@ TEXT _rt0_amd64(SB),7,$-8
 	MOVQ	initcgo(SB), AX
 	TESTQ	AX, AX
 	JZ	needtls
-\tCALL	AX  // g0 already in DI
+\t// g0 already in DI
+\tMOVQ	DI, CX	// Win64 uses CX for first parameter
+\tCALL	AX
 	CMPL	runtime·iswindows(SB), $0
 	JEQ ok

このアセンブリコードは、Goランタイムの初期化ルーチンの一部です。

  1. LEAQ (-64*1024+104)(SP), BX:

    • LEAQ (Load Effective Address Quadword) 命令は、指定されたアドレスを計算し、その結果をレジスタに格納します。
    • SP はスタックポインタです。(-64*1024+104)(SP) は、現在のスタックポインタから64KB(64*1024バイト)を引いたアドレスを計算しています。
    • これは、g0ゴルーチンのスタックガード(スタックオーバーフローを検出するための境界)を設定する部分です。以前は8KB (-8192) でしたが、この変更によりg0のスタックサイズが64KBに拡張されました。これにより、CGO呼び出しなどでより多くのスタック領域が必要な場合に、スタックオーバーフローを防ぎます。
  2. MOVQ DI, CX // Win64 uses CX for first parameter:

    • initcgo関数を呼び出す直前に追加された命令です。
    • initcgoは、Goランタイムの初期化に関連するCGOのセットアップを行う関数です。
    • MOVQ DI, CX は、DIレジスタに格納されている値をCXレジスタにコピーします。
    • コメントにあるように、Win64の呼び出し規約では、関数の最初の引数はRCX(64ビットレジスタの場合)またはCX(16ビットレジスタの場合)レジスタに渡されます。Goのランタイムは通常、System V AMD64 ABI(Linux/macOSで一般的)に従い、最初の引数をRDIレジスタに配置します。
    • この変更により、Windows環境でinitcgoが正しく引数を受け取れるようになり、CGOの初期化が正常に行われるようになります。

misc/cgo/test/basic.go の変更

--- a/misc/cgo/test/basic.go
+++ b/misc/cgo/test/basic.go
@@ -69,17 +69,6 @@ func uuidgen() {\n \tC.uuid_generate(&uuid[0])\n }\n \n-func Size(name string) (int64, error) {\n-\tvar st C.struct_stat\n-\tp := C.CString(name)\n-\t_, err := C.stat(p, &st)\n-\tC.free(unsafe.Pointer(p))\n-\tif err != nil {\n-\t\treturn 0, err\n-\t}\n-\treturn int64(C.ulong(st.st_size)), nil\n-}\n-\n func Strtol(s string, base int) (int, error) {\n \tp := C.CString(s)\n \tn, err := C.strtol(p, nil, C.int(base))\n@@ -112,9 +101,17 @@ func testAtol(t *testing.T) {\n }\n \n func testErrno(t *testing.T) {\n-\tn, err := Strtol(\"asdf\", 123)\n-\tif n != 0 || err != os.EINVAL {\n-\t\tt.Error(\"Strtol: \", n, err)\n+\tp := C.CString(\"no-such-file\")\n+\tm := C.CString(\"r\")
+\tf, err := C.fopen(p, m)\n+\tC.free(unsafe.Pointer(p))\n+\tC.free(unsafe.Pointer(m))\n+\tif err == nil {\n+\t\tC.fclose(f)\n+\t\tt.Fatalf(\"C.fopen: should fail\")\n+\t}\n+\tif err != os.ENOENT {\n+\t\tt.Fatalf(\"C.fopen: unexpected error: \", err)\n \t}\n }\n \n```
1.  **`Size` 関数の削除**:
    *   `Size`関数は、Cの`stat`関数を使用してファイルのサイズを取得していました。
    *   WindowsのMinGW環境では、`stat`関数が`_stat`にリネームされているため、`C.stat`を直接呼び出すと問題が発生する可能性がありました。このテストの目的上、`stat`関数自体が必須ではなかったため、削除されました。

2.  **`testErrno` 関数の修正**:
    *   以前は`Strtol`(Cの`strtol`をラップ)を使用してエラーをテストしていましたが、Windowsでは`strtol`が無効な入力に対してもエラーを返さない場合があるため、テストが機能しませんでした。
    *   新しい実装では、`C.fopen`を使用して存在しないファイル(`"no-such-file"`)を読み取りモード(`"r"`)で開こうとします。
    *   `C.fopen`はファイルが見つからない場合に`NULL`を返し、`errno`を`ENOENT`(No such file or directory)に設定します。
    *   この変更により、Windows環境でも確実にエラーを発生させ、Goの`os.ENOENT`が正しく取得できることをテストできるようになりました。これは、CGO呼び出しにおけるエラー伝播の正確性を保証するために重要です。

### `misc/cgo/test/env.go` の変更

```diff
--- a/misc/cgo/test/env.go
+++ b/misc/cgo/test/env.go
@@ -10,12 +10,21 @@ package cgotest
 import "C"\n import (\n \t"os"\n+\t"runtime"\n \t"testing"\n \t"unsafe"\n )\n \n // This is really an os package test but here for convenience.\n func testSetEnv(t *testing.T) {\n+\tif runtime.GOOS == "windows" {\n+\t\t// Go uses SetEnvironmentVariable on windows. Howerver,\n+\t\t// C runtime takes a *copy* at process startup of thei\n+\t\t// OS environment, and stores it in environ/envp.\n+\t\t// It is this copy that\tgetenv/putenv manipulate.\n+\t\tt.Logf("skipping test")\n+\t\treturn\n+\t}\n \tconst key = "CGO_OS_TEST_KEY"\n \tconst val = "CGO_OS_TEST_VALUE"\n \tos.Setenv(key, val)\n```
*   `testSetEnv` 関数は、Goの`os.Setenv`で設定した環境変数がCGO経由で`C.getenv`から正しく読み取れるかをテストするものでした。
*   追加された`if runtime.GOOS == "windows"`ブロックは、Windows環境でのこのテストの実行をスキップします。
*   コメントで説明されているように、Windowsでは`os.Setenv`がOSプロセス全体の環境変数を変更するのに対し、Cランタイムの`getenv`はプロセス起動時に作成された内部コピーを参照します。このため、Go側で設定した環境変数がC側から見えないという不一致が生じ、テストが失敗します。このスキップは、テストの誤った失敗を防ぐための実用的な解決策です。

### `misc/cgo/test/sleep_windows.go` (新規ファイル)

```go
// Copyright 2011 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cgotest

/*
#include <windows.h>

unsigned int sleep(unsigned int seconds) {
	Sleep(1000 * seconds);
	return 0;
}

*/
import "C"

この新規ファイルは、Windows環境でCGO経由でsleep関数を提供します。

  • CGOコメントブロック内で、C言語のsleep関数が定義されています。
  • このsleep関数は、WindowsのWin32 APIであるSleep関数(ミリ秒単位で待機)を呼び出しています。Sleep(1000 * seconds)とすることで、引数で渡された秒数をミリ秒に変換してSleepに渡しています。
  • これにより、GoのテストコードがC.sleepを呼び出した際に、Windows環境でも正しく指定された時間だけ実行を一時停止できるようになります。

関連リンク

参考にした情報源リンク