[インデックス 18518] ファイルの概要
このコミットは、Goコンパイラ(特に5g
と8g
、それぞれARMとx86-32アーキテクチャ向けのコンパイラ)におけるビルドの問題を修正するものです。具体的には、"fat value"(大きな構造体や配列など、レジスタに収まらない値)をクリアする際に、VARDEF
命令が正しく発行されないために発生していたバグを修正しています。この問題は、以前追加されたテスト(CL 63630043)によって顕在化しました。
コミット
commit 7addda685d9f624479cd2248a86a16b5a810f225
Author: Russ Cox <rsc@golang.org>
Date: Thu Feb 13 22:30:35 2014 -0500
cmd/5g, cmd/8g: fix build
The test added in CL 63630043 fails on 5g and 8g because they
were not emitting the VARDEF instruction when clearing a fat
value by clearing the components. 6g had the call in the right place.
Hooray tests.
TBR=iant
CC=golang-codereviews
https://golang.org/cl/63660043
---
src/cmd/5g/ggen.c | 3 +--
src/cmd/8g/ggen.c | 4 ++--
2 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/cmd/5g/ggen.c b/src/cmd/5g/ggen.c
index 16aef3d1a5..ebf2391f5a 100644
--- a/src/cmd/5g/ggen.c
+++ b/src/cmd/5g/ggen.c
@@ -766,14 +766,13 @@ clearfat(Node *nl)
if(debug['g'])
dump("\nclearfat", nl);
+ gvardef(nl);
+
w = nl->type->width;
// Avoid taking the address for simple enough types.
if(componentgen(N, nl))
return;
- gvardef(nl);
-
c = w % 4; // bytes
q = w / 4; // quads
diff --git a/src/cmd/8g/ggen.c b/src/cmd/8g/ggen.c
index c0d25013fe..2ea92980c1 100644
--- a/src/cmd/8g/ggen.c
+++ b/src/cmd/8g/ggen.c
@@ -73,13 +73,13 @@ clearfat(Node *nl)
if(debug['g'])
dump("\nclearfat", nl);
+ gvardef(nl);
+
w = nl->type->width;
// Avoid taking the address for simple enough types.
if(componentgen(N, nl))
return;
- gvardef(nl);
-
c = w % 4; // bytes
q = w / 4; // quads
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7addda685d9f624479cd2248a86a16b5a810f225
元コミット内容
このコミットは、Goコンパイラの5g
(ARMアーキテクチャ向け)と8g
(x86-32アーキテクチャ向け)におけるビルドの不具合を修正するものです。具体的には、大きな値("fat value")を個々のコンポーネントに分解してクリアする際に、VARDEF
命令が適切に発行されていなかった点が問題でした。6g
(x86-64アーキテクチャ向け)コンパイラではこの問題は発生しておらず、VARDEF
の呼び出しが正しい位置にありました。このバグは、新しく追加されたテスト(CL 63630043)によって発見されました。コミットメッセージは「テスト万歳」と締めくくられており、テストの重要性を強調しています。
変更の背景
この変更の背景には、Goコンパイラが特定のアーキテクチャ(ARMとx86-32)において、大きなデータ構造("fat value")を初期化またはクリアする際のコード生成に不備があったという問題があります。
Go言語では、変数が宣言された際にゼロ値で初期化されることが保証されています。これは、セキュリティ上の理由(以前のメモリ内容が漏洩するのを防ぐ)や、プログラミングの利便性(未初期化変数の使用によるバグを防ぐ)のために非常に重要です。
問題は、大きな構造体や配列といった「fat value」をクリアする際に発生しました。これらの値はレジスタに収まらないため、メモリ上で直接操作される必要があります。Goコンパイラは、このような値をクリアするために、そのコンポーネント(個々のフィールドや要素)をゼロで埋めるコードを生成します。
このプロセスにおいて、VARDEF
という命令が重要な役割を果たします。VARDEF
は、変数が定義され、そのメモリ領域が有効になったことをコンパイラに通知する命令です。これにより、コンパイラは変数のライフタイムを正確に追跡し、デバッグ情報などを適切に生成できます。
以前追加されたCL 63630043("go: runtime: fix: make sure to clear the stack before returning to the goroutine")は、ゴルーチンから戻る前にスタックが適切にクリアされることを保証するためのテストでした。このテストは、メモリのクリアが正しく行われているかを検証するものであり、結果として5g
と8g
コンパイラで生成されたコードがこのテストに失敗することが判明しました。これは、5g
と8g
がclearfat
関数内でVARDEF
命令を適切なタイミングで発行していなかったためです。一方、6g
コンパイラではこの問題は発生せず、VARDEF
の呼び出しが正しい位置にありました。
このコミットは、テストによって発見されたこの不整合を修正し、すべてのアーキテクチャでGoのゼロ値保証が正しく機能するようにすることを目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念について知っておく必要があります。
-
Goコンパイラとアーキテクチャ固有のバックエンド:
- Go言語のコンパイラは、フロントエンド(Goソースコードの解析、AST生成、型チェックなど)とバックエンド(特定アーキテクチャ向けの機械語コード生成)に分かれています。
cmd/5g
: ARMアーキテクチャ(32ビット)向けのGoコンパイラバックエンドです。cmd/8g
: x86-32アーキテクチャ向けのGoコンパイラバックエンドです。cmd/6g
: x86-64アーキテクチャ向けのGoコンパイラバックエンドです。- これらのコンパイラは、Goのソースコードをそれぞれのターゲットアーキテクチャのバイナリに変換する役割を担っています。
-
fat value (ファットバリュー):
- Go言語において、"fat value"とは、レジスタに直接収まらないような大きなサイズの値を指します。これには、大きな構造体、配列、またはインターフェース値などが含まれます。
- これらの値は、通常、メモリ上に割り当てられ、その内容を操作する際にはメモリへのアクセスが必要になります。
-
VARDEF 命令:
VARDEF
は、Goコンパイラの内部表現における命令の一つです。これは「変数が定義された」ことを示すマーカーのようなものです。- コンパイラがコードを生成する過程で、変数がメモリ上に割り当てられ、そのライフタイムが始まる時点で
VARDEF
命令が発行されます。 - この命令は、デバッグ情報の生成、ガベージコレクションの正確性、および最適化のヒントとして利用されます。変数が使用される前にそのメモリ領域が有効であることを保証するために重要です。
-
clearfat 関数:
clearfat
関数は、Goコンパイラのバックエンド(ggen.c
ファイル内)に存在する関数で、"fat value"のメモリ領域をゼロでクリアする役割を担っています。- Goの仕様では、変数は宣言時にゼロ値で初期化されることが保証されています。
clearfat
はこの保証を実装するための一環として、大きな値のメモリ領域を効率的にゼロで埋めるためのコードを生成します。 componentgen
関数は、clearfat
内で呼び出され、"fat value"を個々のコンポーネント(例えば、構造体の各フィールドや配列の各要素)に分解してクリアする処理を行うかどうかを決定します。単純な型であれば、一括でクリアする方が効率的ですが、複雑な型の場合はコンポーネントごとに処理が必要になることがあります。
-
CL (Change List):
- Goプロジェクトでは、コードの変更は「Change List (CL)」として提出されます。これは、Gitのコミットに相当する概念ですが、Goの開発プロセスではGerritというコードレビューシステムが使われており、CLはそのシステムにおける変更単位を指します。
- CLには、変更内容、レビュー担当者、関連するバグトラッカーのリンクなどが含まれます。
技術的詳細
このコミットの核心は、clearfat
関数におけるgvardef(nl)
の呼び出し位置の修正です。
clearfat
関数は、Goコンパイラのコード生成フェーズにおいて、大きな構造体や配列などの「fat value」をゼロクリアするために使用されます。Goのセマンティクスでは、変数は宣言時にゼロ値で初期化されることが保証されており、これはセキュリティと堅牢性のために不可欠です。
gvardef(nl)
は、コンパイラが変数nl
が定義され、そのメモリ領域が有効になったことを記録するための内部関数呼び出しです。この情報は、デバッグ情報の生成や、ガベージコレクタが変数の生存期間を正確に追跡するために利用されます。
元の5g
と8g
のコードでは、gvardef(nl)
の呼び出しがif(componentgen(N, nl))
ブロックの後に配置されていました。
w = nl->type->width;
// Avoid taking the address for simple enough types.
if(componentgen(N, nl))
return;
gvardef(nl); // ここにあった
componentgen(N, nl)
は、nl
が単純な型であり、その場でクリア処理を完了できる場合にtrue
を返し、関数からreturn
します。この場合、gvardef(nl)
は呼び出されません。しかし、nl
が複雑な"fat value"であり、componentgen
がfalse
を返してclearfat
の残りの処理(バイト単位またはクワッド単位でのクリア)が実行される場合、gvardef(nl)
は呼び出されます。
問題は、componentgen
がtrue
を返してclearfat
が早期リターンする場合に発生しました。この場合、変数はクリアされるものの、VARDEF
命令が発行されないため、コンパイラは変数が「定義された」ことを適切に記録できませんでした。これは、特にデバッグ情報や、変数のライフタイムに依存する他のコンパイラパスに影響を与える可能性がありました。
6g
コンパイラでは、gvardef(nl)
の呼び出しはclearfat
関数の冒頭、componentgen
のチェックの前に配置されていました。
if(debug['g'])
dump("\nclearfat", nl);
gvardef(nl); // ここにあった
w = nl->type->width;
// Avoid taking the address for simple enough types.
if(componentgen(N, nl))
return;
この配置であれば、clearfat
がどのように終了するかにかかわらず、変数がクリアされる前に必ずVARDEF
命令が発行され、変数の定義が適切に記録されます。
CL 63630043で追加されたテストは、このVARDEF
命令の欠落が引き起こす問題を顕在化させました。このテストは、ゴルーチンが終了する際にスタックが適切にクリアされていることを検証するものであり、VARDEF
が正しく発行されていないと、変数のライフタイム情報が不正確になり、テストが失敗する原因となりました。
このコミットは、5g
と8g
のggen.c
ファイルにおいて、gvardef(nl)
の呼び出しを6g
と同様にclearfat
関数の冒頭に移動することで、この不整合を解消し、すべてのGoコンパイラバックエンドで一貫した正しい動作を保証しています。これにより、Goのゼロ値保証がより堅牢になり、デバッグ情報の正確性も向上します。
コアとなるコードの変更箇所
変更はsrc/cmd/5g/ggen.c
とsrc/cmd/8g/ggen.c
の2つのファイルにあります。
src/cmd/5g/ggen.c の変更点:
--- a/src/cmd/5g/ggen.c
+++ b/src/cmd/5g/ggen.c
@@ -766,14 +766,13 @@ clearfat(Node *nl)
if(debug['g'])
dump("\nclearfat", nl);
+ gvardef(nl);
+
w = nl->type->width;
// Avoid taking the address for simple enough types.
if(componentgen(N, nl))
return;
- gvardef(nl);
-
c = w % 4; // bytes
q = w / 4; // quads
src/cmd/8g/ggen.c の変更点:
--- a/src/cmd/8g/ggen.c
+++ b/src/cmd/8g/ggen.c
@@ -73,13 +73,13 @@ clearfat(Node *nl)
if(debug['g'])
dump("\nclearfat", nl);
+ gvardef(nl);
+
w = nl->type->width;
// Avoid taking the address for simple enough types.
if(componentgen(N, nl))
return;
- gvardef(nl);
-
c = w % 4; // bytes
q = w / 4; // quads
コアとなるコードの解説
両方のファイルにおいて、clearfat
関数内のgvardef(nl);
の行が移動されています。
-
変更前:
gvardef(nl);
は、if(componentgen(N, nl))
ブロックの後にありました。- これは、
nl
が単純な型でcomponentgen
がtrue
を返し、関数が早期にreturn
する場合、gvardef(nl)
が呼び出されないことを意味していました。 - 結果として、変数がクリアされても、コンパイラがその変数の定義を適切に記録しない可能性がありました。
- これは、
-
変更後:
gvardef(nl);
は、clearfat
関数の冒頭、if(componentgen(N, nl))
ブロックの前に移動されました。- この変更により、
clearfat
関数が実行されると、その後の処理パスに関わらず、必ず最初にgvardef(nl)
が呼び出されるようになりました。 - これにより、"fat value"がクリアされる前に、その変数が適切に定義されたことがコンパイラに通知されるようになり、デバッグ情報の正確性やガベージコレクションの動作が保証されます。
- この変更により、
この修正は、5g
と8g
コンパイラの動作を6g
コンパイラの動作と一致させ、Goのゼロ値保証の堅牢性を高めるものです。
関連リンク
- Go CL 63660043: https://golang.org/cl/63660043
- Go CL 63630043 (関連するテスト): https://golang.org/cl/63630043
参考にした情報源リンク
- Web検索: "Go CL 63630043"
- Go言語のコンパイラ構造に関する一般的な知識
- Go言語のゼロ値保証に関する知識
- Go言語のガベージコレクションとデバッグ情報に関する知識