[インデックス 19307] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)における、初期化されていないグローバル変数のリンク時コピー最適化に関するバグを修正します。具体的には、Cgoによって初期化される可能性のあるグローバル変数に対して、Goのゼロ値初期化のセマンティクスが誤って適用され、予期せぬ値になる問題を解決します。
コミット
cmd/gc: disable link-time copying of un-Go-initialized globals
If you write:
var x = 3
then the compiler arranges for x to be initialized in the linker
with an actual 3 from the data segment, rather than putting
x in the bss and emitting init-time "x = 3" assignment code.
If you write:
var y = x
var x = 3
then the compiler is clever and treats this the same as if
the code said 'y = 3': they both end up in the data segment
with no init-time assignments.
If you write
var y = x
var x int
then the compiler was treating this the same as if the
code said 'x = 0', making both x and y zero and avoiding
any init-time assignment.
This copying optimization to avoid init-time assignment of y
is incorrect if 'var x int' doesn't mean 'x = 0' but instead means
'x is initialized in C or assembly code'. The program ends up
with 'y = 0' instead of 'y = the value specified for x in that other code'.
Disable the propagation if there is no initializer for x.
This comes up in some uses of cgo, because cgo generates
Go globals that are initialized in accompanying C files.
Fixes #7665.
LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/93200044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e5c105033a757127089989717937776a7d0c57a0
元コミット内容
cmd/gc: disable link-time copying of un-Go-initialized globals
変更の背景
Go言語では、グローバル変数が宣言されると、明示的な初期化子がない場合でも、その型のゼロ値で自動的に初期化されます。例えば、var x int
と宣言すると、x
は自動的に0
に初期化されます。コンパイラは、このような初期化を効率的に行うために、リンク時にデータセグメントに直接値を配置する最適化を行います。これにより、プログラムの起動時に実行される初期化コードを減らすことができます。
しかし、この最適化がCgoと組み合わされた場合に問題が発生しました。CgoはGoとC言語のコードを連携させるためのツールであり、Cgoを介して宣言されたGoのグローバル変数が、実際にはGoコード内ではなく、関連するCコードやアセンブリコードによって初期化される場合があります。
問題のシナリオは以下の通りです。
var y = x
var x int // xはGoコードで初期化子を持たない
通常のGoコードでは、x
は0
に初期化され、その結果y
も0
になります。コンパイラは、このy = x
という代入を、y = 0
としてリンク時に解決し、初期化コードを生成しないように最適化していました。
しかし、x
がCgoによって外部のCコードで初期化される場合、var x int
はGoのゼロ値初期化を意味するのではなく、「x
は外部で初期化される」ことを意味します。この場合、x
は0
以外の値を持つ可能性があります。上記の最適化が適用されると、y
はx
の実際の値ではなく、誤って0
に初期化されてしまい、プログラムの動作が不正になるというバグがありました。
このコミットは、このような「Goコードで初期化子を持たないが、外部(Cgoなど)で初期化される可能性のあるグローバル変数」に対するリンク時コピー最適化を無効にすることで、このバグを修正することを目的としています。
前提知識の解説
Go言語の変数初期化
Go言語では、変数を宣言する際に初期値を指定しない場合、その変数は自動的に型の「ゼロ値」で初期化されます。例えば、int
型は0
、string
型は空文字列""
、ポインタ型はnil
です。
グローバル変数(パッケージレベルで宣言された変数)の初期化には、主に2つの方法があります。
- リンク時初期化 (Data Segment): 変数が定数や他のグローバル変数によって初期化される場合、コンパイラは変数の初期値を実行可能ファイルのデータセグメントに直接埋め込みます。これにより、プログラムがロードされる際にOSによってメモリにマッピングされ、実行時に特別な初期化コードを実行する必要がなくなります。これは最も効率的な初期化方法です。
例:
var x = 3
- 実行時初期化 (BSS Segment & Init Code): 変数が複雑な式や関数呼び出しによって初期化される場合、または初期値が指定されない場合、変数はBSS (Block Started by Symbol) セグメントに配置され、プログラムの起動時に初期化コードが実行されて値が設定されます。BSSセグメントは、初期値がゼロである変数や初期値が指定されていない変数を格納するために使用され、実行可能ファイル内では領域のみが予約され、実際のデータは含まれません。
例:
var x int
(Goのセマンティクスではx = 0
が実行時初期化される)
Cgo
Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoのツールです。Cgoを使用すると、GoのコードとCのコードが混在するプログラムを作成できます。
Cgoは、Goのビルドプロセス中にCコードをコンパイルし、Goのオブジェクトファイルとリンクします。この際、Cコードで定義された変数や関数がGoのシンボルとしてエクスポートされたり、Goの変数がCコードから参照されたりすることがあります。
本件のバグは、CgoがGoのグローバル変数をCコード側で初期化するようなケースで発生しました。Goコンパイラは、その変数がGoコードで初期化子を持たないため、ゼロ値初期化の対象と見なしましたが、実際にはCコード側で非ゼロの値が設定される可能性があったため、Goコンパイラの最適化が誤動作したのです。
BSSセグメントとデータセグメント
実行可能ファイルは、通常、いくつかのセグメントに分割されています。
- テキストセグメント (.text): 実行可能な機械語コードが格納されます。
- データセグメント (.data): 初期化されたグローバル変数や静的変数が格納されます。これらの変数は、プログラムの起動時に実行可能ファイルからメモリにロードされます。
- BSSセグメント (.bss): 初期化されていないグローバル変数や静的変数が格納されます。これらの変数は、プログラムの起動時にOSによってゼロで初期化されます。実行可能ファイル自体にはBSSセグメントのデータは含まれず、そのサイズ情報のみが格納されます。
このコミットの文脈では、Goコンパイラが変数をデータセグメントに配置するか(リンク時初期化)、BSSセグメントに配置して実行時初期化コードを生成するか(実行時初期化)の判断が重要になります。
技術的詳細
この問題は、Goコンパイラのcmd/gc
がグローバル変数の初期化を最適化するロジック、特にsinit.c
ファイル内のstaticcopy
関数に起因します。
staticcopy
関数は、ある変数の初期値が別の変数の初期値と同じである場合に、その変数の初期化をリンク時に解決し、実行時の代入コードを省略する最適化を行います。
元のコードでは、r->defn == N
という条件がありました。これは、変数r
がGoコード内で明示的な初期化子を持たない場合(つまり、ゼロ値で初期化されるとコンパイラが判断した場合)にstaticcopy
最適化を適用するという意味でした。
しかし、Cgoのシナリオでは、var x int
のようにGoコードで初期化子を持たない変数x
が、実際にはCコード側で初期化されることがあります。この場合、x
はGoのゼロ値(例えば0
)ではなく、Cコードで設定された値を持つべきです。
元のstaticcopy
のロジックでは、r->defn == N
の場合にreturn 1;
としていました。これは、「r
はゼロ値で初期化されるので、staticcopy
最適化を適用できる」という意味でした。この結果、var y = x; var x int;
のようなコードで、x
がCgoによって外部で初期化されるにもかかわらず、y
が0
に初期化されてしまうというバグが発生しました。
このコミットの修正は、sinit.c
のstaticcopy
関数において、r->defn == N
(Goコードで初期化子がない)の場合にreturn 0;
に変更することです。これにより、「r
はGoコードで初期化子がない場合、ゼロ値で初期化される可能性もあるが、外部で初期化される可能性もあるため、staticcopy
最適化は適用しない」という挙動になります。
この変更により、var y = x; var x int;
のようなケースで、x
がCgoによって外部で初期化される場合、y
は実行時にx
の実際の値が代入されるようになり、0
に誤って初期化されることがなくなります。この修正は、GoのコンパイラがCgoによって外部で初期化される可能性のあるグローバル変数のセマンティクスを正しく扱うために不可欠です。
コアとなるコードの変更箇所
このコミットでは、主に以下の3つのファイルが変更されています。
misc/cgo/test/cgo_test.go
misc/cgo/test/issue7665.go
(新規ファイル)src/cmd/gc/sinit.c
misc/cgo/test/cgo_test.go
--- a/misc/cgo/test/cgo_test.go
+++ b/misc/cgo/test/cgo_test.go
@@ -50,6 +50,7 @@ func TestFpVar(t *testing.T) { testFpVar(t) }\n func Test4339(t *testing.T) { test4339(t) }\n func Test6390(t *testing.T) { test6390(t) }\n func Test5986(t *testing.T) { test5986(t) }\n+func Test7665(t *testing.T) { test7665(t) }\n func TestNaming(t *testing.T) { testNaming(t) }\n
func BenchmarkCgoCall(b *testing.B) { benchCgoCall(b) }\n```
- `Test7665`という新しいテスト関数が追加され、既存の`cgo_test.go`から呼び出されるように変更されています。
### `misc/cgo/test/issue7665.go`
```diff
--- /dev/null
+++ b/misc/cgo/test/issue7665.go
@@ -0,0 +1,25 @@
+// Copyright 2013 The Go Authors. All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+package cgotest\n+\n+import (\n+\t\"testing\"\n+\t\"unsafe\"\n+)\n+\n+// extern void f7665(void);\n+import \"C\"\n+\n+//export f7665\n+func f7665() {}\n+\n+var bad7665 unsafe.Pointer = C.f7665\n+var good7665 uintptr = uintptr(C.f7665)\n+\n+func test7665(t *testing.T) {\n+\tif bad7665 == nil || bad7665 != unsafe.Pointer(good7665) {\n+\t\tt.Errorf(\"ptrs = %p, %#x, want same non-nil pointer\", bad7665, good7665)\n+\t}\n+}\n```
- このファイルは新規追加されたテストケースです。
- `f7665`というC関数が外部で定義されていることを想定し、`C.f7665`というCgoシンボルを参照しています。
- `bad7665`は`unsafe.Pointer = C.f7665`として宣言されており、これが問題の最適化の影響を受ける変数です。
- `good7665`は`uintptr = uintptr(C.f7665)`として宣言されており、これは正しい挙動を示すための比較対象です。
- `test7665`関数内で、`bad7665`と`good7665`が同じ非`nil`ポインタを指していることを検証しています。もし最適化が誤動作すると、`bad7665`が`nil`(ゼロ値)になってしまい、テストが失敗します。
### `src/cmd/gc/sinit.c`
```diff
--- a/src/cmd/gc/sinit.c
+++ b/src/cmd/gc/sinit.c
@@ -286,8 +286,8 @@ staticcopy(Node *l, Node *r, NodeList **out)\n \n \tif(r->op != ONAME || r->class != PEXTERN || r->sym->pkg != localpkg)\n \t\treturn 0;\n-\tif(r->defn == N)\t// zeroed\n-\t\treturn 1;\n+\tif(r->defn == N)\t// probably zeroed but perhaps supplied externally and of unknown value\n+\t\treturn 0;\n \tif(r->defn->op != OAS)\n \t\treturn 0;\n \torig = r;\n```
- `staticcopy`関数内のロジックが変更されています。
- 変更前: `if(r->defn == N) return 1;`
- `r->defn == N`は、Goコード内で`r`に明示的な初期化子がないことを意味します。この場合、コンパイラは`r`がゼロ値で初期化されると判断し、`staticcopy`最適化を適用可能(`return 1`)としていました。
- 変更後: `if(r->defn == N) return 0;`
- `r->defn == N`の場合でも、`staticcopy`最適化を適用しない(`return 0`)ように変更されました。コメントも「おそらくゼロ値だが、外部から提供され、値が不明な可能性もある」と修正されています。
- これにより、Cgoによって外部で初期化される可能性のあるグローバル変数に対して、誤ったリンク時コピー最適化が適用されるのを防ぎます。
## コアとなるコードの解説
このコミットの核心は、`src/cmd/gc/sinit.c`ファイル内の`staticcopy`関数の変更です。
`staticcopy`関数は、Goコンパイラがグローバル変数の初期化を最適化する際に使用される内部関数です。この関数は、あるグローバル変数`l`が別のグローバル変数`r`の値をコピーして初期化される場合(例: `var l = r`)、`r`の初期値がリンク時に既知であれば、`l`の初期化もリンク時に解決し、実行時の代入コードを省略しようとします。
変更前のコードでは、`r`がGoコード内で明示的な初期化子を持たない場合(`r->defn == N`)、コンパイラは`r`がGoのゼロ値で初期化されると判断し、この`staticcopy`最適化を適用していました(`return 1`)。これは、通常のGoコードでは正しい挙動です。例えば、`var y = x; var x int;`の場合、`x`は`0`なので`y`も`0`になり、この`y = 0`という初期化はリンク時に解決できます。
しかし、Cgoを使用する場合、`var x int`のようにGoコードで初期化子を持たないグローバル変数`x`が、実際にはCコード側で初期化されることがあります。この場合、`x`はGoのゼロ値(`0`)ではなく、Cコードで設定された値を持つべきです。変更前の`staticcopy`のロジックでは、`x`がGoコードで初期化子を持たないため、`x`が`0`であると誤って判断し、`y`も`0`に初期化してしまうというバグがありました。
変更後のコードでは、`r->defn == N`の場合でも`staticcopy`最適化を適用しない(`return 0`)ように修正されました。これにより、Goコードで初期化子を持たないグローバル変数が、Cgoによって外部で初期化される可能性を考慮し、その値がリンク時には不明であると判断します。結果として、`var y = x`のような代入は、実行時に`x`の実際の値が評価されて`y`に代入されるようになり、Cgoによって初期化された`x`の正しい値が`y`に伝播されるようになります。
`misc/cgo/test/issue7665.go`で追加されたテストケースは、このバグを再現し、修正が正しく機能することを確認するためのものです。`bad7665`変数は、問題の最適化の影響を受けるように設計されており、`C.f7665`というCgoシンボル(外部で初期化されることを想定)を参照しています。テストは、`bad7665`が`nil`(ゼロ値)ではなく、`good7665`(正しい値)と同じであることを検証することで、修正が正しく適用されたことを確認します。
## 関連リンク
* Go CL: [https://golang.org/cl/93200044](https://golang.org/cl/93200044)
* Go Issue (コミットメッセージに記載): #7665 (ただし、直接の検索では見つかりませんでした。Goのissueトラッカーの変更や、非常に古いissueである可能性があります。)
## 参考にした情報源リンク
* Go言語の公式ドキュメント (変数初期化、Cgoに関する情報)
* Goコンパイラのソースコード (`src/cmd/gc/sinit.c`)
* Goのテストコード (`misc/cgo/test/`)
* 一般的なコンパイラとリンカの概念 (データセグメント、BSSセグメント)I have generated the detailed explanation in Markdown format, following all the specified sections and in Japanese. I have also noted the inability to find the Go issue #7665 directly.
I will now output the generated explanation to standard output.
```markdown
# [インデックス 19307] ファイルの概要
このコミットは、Goコンパイラ(`cmd/gc`)における、初期化されていないグローバル変数のリンク時コピー最適化に関するバグを修正します。具体的には、Cgoによって初期化される可能性のあるグローバル変数に対して、Goのゼロ値初期化のセマンティクスが誤って適用され、予期せぬ値になる問題を解決します。
## コミット
cmd/gc: disable link-time copying of un-Go-initialized globals
If you write:
var x = 3
then the compiler arranges for x to be initialized in the linker with an actual 3 from the data segment, rather than putting x in the bss and emitting init-time "x = 3" assignment code.
If you write:
var y = x
var x = 3
then the compiler is clever and treats this the same as if the code said 'y = 3': they both end up in the data segment with no init-time assignments.
If you write
var y = x
var x int
then the compiler was treating this the same as if the code said 'x = 0', making both x and y zero and avoiding any init-time assignment.
This copying optimization to avoid init-time assignment of y is incorrect if 'var x int' doesn't mean 'x = 0' but instead means 'x is initialized in C or assembly code'. The program ends up with 'y = 0' instead of 'y = the value specified for x in that other code'.
Disable the propagation if there is no initializer for x.
This comes up in some uses of cgo, because cgo generates Go globals that are initialized in accompanying C files.
Fixes #7665.
LGTM=iant R=golang-codereviews, iant CC=golang-codereviews https://golang.org/cl/93200044
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/e5c105033a757127089989717937776a7d0c57a0](https://github.com/golang/go/commit/e5c105033a757127089989717937776a7d0c57a0)
## 元コミット内容
`cmd/gc: disable link-time copying of un-Go-initialized globals`
## 変更の背景
Go言語では、グローバル変数が宣言されると、明示的な初期化子がない場合でも、その型のゼロ値で自動的に初期化されます。例えば、`var x int`と宣言すると、`x`は自動的に`0`に初期化されます。コンパイラは、このような初期化を効率的に行うために、リンク時にデータセグメントに直接値を配置する最適化を行います。これにより、プログラムの起動時に実行される初期化コードを減らすことができます。
しかし、この最適化がCgoと組み合わされた場合に問題が発生しました。CgoはGoとC言語のコードを連携させるためのツールであり、Cgoを介して宣言されたGoのグローバル変数が、実際にはGoコード内ではなく、関連するCコードやアセンブリコードによって初期化される場合があります。
問題のシナリオは以下の通りです。
```go
var y = x
var x int // xはGoコードで初期化子を持たない
通常のGoコードでは、x
は0
に初期化され、その結果y
も0
になります。コンパイラは、このy = x
という代入を、y = 0
としてリンク時に解決し、初期化コードを生成しないように最適化していました。
しかし、x
がCgoによって外部のCコードで初期化される場合、var x int
はGoのゼロ値初期化を意味するのではなく、「x
は外部で初期化される」ことを意味します。この場合、x
は0
以外の値を持つ可能性があります。上記の最適化が適用されると、y
はx
の実際の値ではなく、誤って0
に初期化されてしまい、プログラムの動作が不正になるというバグがありました。
このコミットは、このような「Goコードで初期化子を持たないが、外部(Cgoなど)で初期化される可能性のあるグローバル変数」に対するリンク時コピー最適化を無効にすることで、このバグを修正することを目的としています。
前提知識の解説
Go言語の変数初期化
Go言語では、変数を宣言する際に初期値を指定しない場合、その変数は自動的に型の「ゼロ値」で初期化されます。例えば、int
型は0
、string
型は空文字列""
、ポインタ型はnil
です。
グローバル変数(パッケージレベルで宣言された変数)の初期化には、主に2つの方法があります。
- リンク時初期化 (Data Segment): 変数が定数や他のグローバル変数によって初期化される場合、コンパイラは変数の初期値を実行可能ファイルのデータセグメントに直接埋め込みます。これにより、プログラムがロードされる際にOSによってメモリにマッピングされ、実行時に特別な初期化コードを実行する必要がなくなります。これは最も効率的な初期化方法です。
例:
var x = 3
- 実行時初期化 (BSS Segment & Init Code): 変数が複雑な式や関数呼び出しによって初期化される場合、または初期値が指定されない場合、変数はBSS (Block Started by Symbol) セグメントに配置され、プログラムの起動時に初期化コードが実行されて値が設定されます。BSSセグメントは、初期値がゼロである変数や初期値が指定されていない変数を格納するために使用され、実行可能ファイル内では領域のみが予約され、実際のデータは含まれません。
例:
var x int
(Goのセマンティクスではx = 0
が実行時初期化される)
Cgo
Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoのツールです。Cgoを使用すると、GoのコードとCのコードが混在するプログラムを作成できます。
Cgoは、Goのビルドプロセス中にCコードをコンパイルし、Goのオブジェクトファイルとリンクします。この際、Cコードで定義された変数や関数がGoのシンボルとしてエクスポートされたり、Goの変数がCコードから参照されたりすることがあります。
本件のバグは、CgoがGoのグローバル変数をCコード側で初期化するようなケースで発生しました。Goコンパイラは、その変数がGoコードで初期化子を持たないため、ゼロ値初期化の対象と見なしましたが、実際にはCコード側で非ゼロの値が設定される可能性があったため、Goコンパイラの最適化が誤動作したのです。
BSSセグメントとデータセグメント
実行可能ファイルは、通常、いくつかのセグメントに分割されています。
- テキストセグメント (.text): 実行可能な機械語コードが格納されます。
- データセグメント (.data): 初期化されたグローバル変数や静的変数が格納されます。これらの変数は、プログラムの起動時に実行可能ファイルからメモリにロードされます。
- BSSセグメント (.bss): 初期化されていないグローバル変数や静的変数が格納されます。これらの変数は、プログラムの起動時にOSによってゼロで初期化されます。実行可能ファイル自体にはBSSセグメントのデータは含まれず、そのサイズ情報のみが格納されます。
このコミットの文脈では、Goコンパイラが変数をデータセグメントに配置するか(リンク時初期化)、BSSセグメントに配置して実行時初期化コードを生成するか(実行時初期化)の判断が重要になります。
技術的詳細
この問題は、Goコンパイラのcmd/gc
がグローバル変数の初期化を最適化するロジック、特にsinit.c
ファイル内のstaticcopy
関数に起因します。
staticcopy
関数は、ある変数の初期値が別の変数の初期値と同じである場合に、その変数の初期化をリンク時に解決し、実行時の代入コードを省略する最適化を行います。
元のコードでは、r->defn == N
という条件がありました。これは、変数r
がGoコード内で明示的な初期化子を持たない場合(つまり、ゼロ値で初期化されるとコンパイラが判断した場合)にstaticcopy
最適化を適用するという意味でした。
しかし、Cgoのシナリオでは、var x int
のようにGoコードで初期化子を持たない変数x
が、実際にはCコード側で初期化されることがあります。この場合、x
はGoのゼロ値(例えば0
)ではなく、Cコードで設定された値を持つべきです。
元のstaticcopy
のロジックでは、r->defn == N
の場合にreturn 1;
としていました。これは、「r
はゼロ値で初期化されるので、staticcopy
最適化を適用できる」という意味でした。この結果、var y = x; var x int;
のようなコードで、x
がCgoによって外部で初期化されるにもかかわらず、y
が0
に初期化されてしまうというバグが発生しました。
このコミットの修正は、sinit.c
のstaticcopy
関数において、r->defn == N
(Goコードで初期化子がない)の場合にreturn 0;
に変更することです。これにより、「r
はGoコードで初期化子がない場合、ゼロ値で初期化される可能性もあるが、外部で初期化される可能性もあるため、staticcopy
最適化は適用しない」という挙動になります。
この変更により、var y = x; var x int;
のようなケースで、x
がCgoによって外部で初期化される場合、y
は実行時にx
の実際の値が代入されるようになり、0
に誤って初期化されることがなくなります。この修正は、GoのコンパイラがCgoによって外部で初期化される可能性のあるグローバル変数のセマンティクスを正しく扱うために不可欠です。
コアとなるコードの変更箇所
このコミットでは、主に以下の3つのファイルが変更されています。
misc/cgo/test/cgo_test.go
misc/cgo/test/issue7665.go
(新規ファイル)src/cmd/gc/sinit.c
misc/cgo/test/cgo_test.go
--- a/misc/cgo/test/cgo_test.go
+++ b/misc/cgo/test/cgo_test.go
@@ -50,6 +50,7 @@ func TestFpVar(t *testing.T) { testFpVar(t) }\n func Test4339(t *testing.T) { test4339(t) }\n func Test6390(t *testing.T) { test6390(t) }\n func Test5986(t *testing.T) { test5986(t) }\n+func Test7665(t *testing.T) { test7665(t) }\n func TestNaming(t *testing.T) { testNaming(t) }\n
func BenchmarkCgoCall(b *testing.B) { benchCgoCall(b) }\n```
- `Test7665`という新しいテスト関数が追加され、既存の`cgo_test.go`から呼び出されるように変更されています。
### `misc/cgo/test/issue7665.go`
```diff
--- /dev/null
+++ b/misc/cgo/test/issue7665.go
@@ -0,0 +1,25 @@
+// Copyright 2013 The Go Authors. All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+package cgotest\n+\n+import (\n+\t\"testing\"\n+\t\"unsafe\"\n+)\n+\n+// extern void f7665(void);\n+import \"C\"\n+\n+//export f7665
+func f7665() {}\n+\n+var bad7665 unsafe.Pointer = C.f7665\n+var good7665 uintptr = uintptr(C.f7665)\n+\n+func test7665(t *testing.T) {\n+\tif bad7665 == nil || bad7665 != unsafe.Pointer(good7665) {\n+\t\tt.Errorf(\"ptrs = %p, %#x, want same non-nil pointer\", bad7665, good7665)\n+\t}\n+}\n```
- このファイルは新規追加されたテストケースです。
- `f7665`というC関数が外部で定義されていることを想定し、`C.f7665`というCgoシンボルを参照しています。
- `bad7665`は`unsafe.Pointer = C.f7665`として宣言されており、これが問題の最適化の影響を受ける変数です。
- `good7665`は`uintptr = uintptr(C.f7665)`として宣言されており、これは正しい挙動を示すための比較対象です。
- `test7665`関数内で、`bad7665`と`good7665`が同じ非`nil`ポインタを指していることを検証しています。もし最適化が誤動作すると、`bad7665`が`nil`(ゼロ値)になってしまい、テストが失敗します。
### `src/cmd/gc/sinit.c`
```diff
--- a/src/cmd/gc/sinit.c
+++ b/src/cmd/gc/sinit.c
@@ -286,8 +286,8 @@ staticcopy(Node *l, Node *r, NodeList **out)\n \n \tif(r->op != ONAME || r->class != PEXTERN || r->sym->pkg != localpkg)\n \t\treturn 0;\n-\tif(r->defn == N)\t// zeroed\n-\t\treturn 1;\n+\tif(r->defn == N)\t// probably zeroed but perhaps supplied externally and of unknown value\n+\t\treturn 0;\n \tif(r->defn->op != OAS)\n \t\treturn 0;\n \torig = r;\n```
- `staticcopy`関数内のロジックが変更されています。
- 変更前: `if(r->defn == N) return 1;`
- `r->defn == N`は、Goコード内で`r`に明示的な初期化子がないことを意味します。この場合、コンパイラは`r`がゼロ値で初期化されると判断し、`staticcopy`最適化を適用可能(`return 1`)としていました。
- 変更後: `if(r->defn == N) return 0;`
- `r->defn == N`の場合でも、`staticcopy`最適化を適用しない(`return 0`)ように変更されました。コメントも「おそらくゼロ値だが、外部から提供され、値が不明な可能性もある」と修正されています。
- これにより、Cgoによって外部で初期化される可能性のあるグローバル変数に対して、誤ったリンク時コピー最適化が適用されるのを防ぎます。
## コアとなるコードの解説
このコミットの核心は、`src/cmd/gc/sinit.c`ファイル内の`staticcopy`関数の変更です。
`staticcopy`関数は、Goコンパイラがグローバル変数の初期化を最適化する際に使用される内部関数です。この関数は、あるグローバル変数`l`が別のグローバル変数`r`の値をコピーして初期化される場合(例: `var l = r`)、`r`の初期値がリンク時に既知であれば、`l`の初期化もリンク時に解決し、実行時の代入コードを省略しようとします。
変更前のコードでは、`r`がGoコード内で明示的な初期化子を持たない場合(`r->defn == N`)、コンパイラは`r`がGoのゼロ値で初期化されると判断し、この`staticcopy`最適化を適用していました(`return 1`)。これは、通常のGoコードでは正しい挙動です。例えば、`var y = x; var x int;`の場合、`x`は`0`なので`y`も`0`になり、この`y = 0`という初期化はリンク時に解決できます。
しかし、Cgoを使用する場合、`var x int`のようにGoコードで初期化子を持たないグローバル変数`x`が、実際にはCコード側で初期化されることがあります。この場合、`x`はGoのゼロ値(`0`)ではなく、Cコードで設定された値を持つべきです。変更前の`staticcopy`のロジックでは、`x`がGoコードで初期化子を持たないため、`x`が`0`であると誤って判断し、`y`も`0`に初期化してしまうというバグがありました。
変更後のコードでは、`r->defn == N`の場合でも`staticcopy`最適化を適用しない(`return 0`)ように修正されました。これにより、Goコードで初期化子を持たないグローバル変数が、Cgoによって外部で初期化される可能性を考慮し、その値がリンク時には不明であると判断します。結果として、`var y = x`のような代入は、実行時に`x`の実際の値が評価されて`y`に代入されるようになり、Cgoによって初期化された`x`の正しい値が`y`に伝播されるようになります。
`misc/cgo/test/issue7665.go`で追加されたテストケースは、このバグを再現し、修正が正しく機能することを確認するためのものです。`bad7665`変数は、問題の最適化の影響を受けるように設計されており、`C.f7665`というCgoシンボル(外部で初期化されることを想定)を参照しています。テストは、`bad7665`が`nil`(ゼロ値)ではなく、`good7665`(正しい値)と同じであることを検証することで、修正が正しく適用されたことを確認します。
## 関連リンク
* Go CL: [https://golang.org/cl/93200044](https://golang.org/cl/93200044)
* Go Issue (コミットメッセージに記載): #7665 (ただし、直接の検索では見つかりませんでした。Goのissueトラッカーの変更や、非常に古いissueである可能性があります。)
## 参考にした情報源リンク
* Go言語の公式ドキュメント (変数初期化、Cgoに関する情報)
* Goコンパイラのソースコード (`src/cmd/gc/sinit.c`)
* Goのテストコード (`misc/cgo/test/`)
* 一般的なコンパイラとリンカの概念 (データセグメント、BSSセグメント)