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

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

このコミットは、Goコンパイラ(特に5g8g、それぞれ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")は、ゴルーチンから戻る前にスタックが適切にクリアされることを保証するためのテストでした。このテストは、メモリのクリアが正しく行われているかを検証するものであり、結果として5g8gコンパイラで生成されたコードがこのテストに失敗することが判明しました。これは、5g8gclearfat関数内で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が定義され、そのメモリ領域が有効になったことを記録するための内部関数呼び出しです。この情報は、デバッグ情報の生成や、ガベージコレクタが変数の生存期間を正確に追跡するために利用されます。

元の5g8gのコードでは、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"であり、componentgenfalseを返してclearfatの残りの処理(バイト単位またはクワッド単位でのクリア)が実行される場合、gvardef(nl)は呼び出されます。

問題は、componentgentrueを返して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が正しく発行されていないと、変数のライフタイム情報が不正確になり、テストが失敗する原因となりました。

このコミットは、5g8gggen.cファイルにおいて、gvardef(nl)の呼び出しを6gと同様にclearfat関数の冒頭に移動することで、この不整合を解消し、すべてのGoコンパイラバックエンドで一貫した正しい動作を保証しています。これにより、Goのゼロ値保証がより堅牢になり、デバッグ情報の正確性も向上します。

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

変更はsrc/cmd/5g/ggen.csrc/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が単純な型でcomponentgentrueを返し、関数が早期にreturnする場合、gvardef(nl)が呼び出されないことを意味していました。
    • 結果として、変数がクリアされても、コンパイラがその変数の定義を適切に記録しない可能性がありました。
  • 変更後: gvardef(nl);は、clearfat関数の冒頭、if(componentgen(N, nl))ブロックのに移動されました。

    • この変更により、clearfat関数が実行されると、その後の処理パスに関わらず、必ず最初にgvardef(nl)が呼び出されるようになりました。
    • これにより、"fat value"がクリアされる前に、その変数が適切に定義されたことがコンパイラに通知されるようになり、デバッグ情報の正確性やガベージコレクションの動作が保証されます。

この修正は、5g8gコンパイラの動作を6gコンパイラの動作と一致させ、Goのゼロ値保証の堅牢性を高めるものです。

関連リンク

参考にした情報源リンク

  • Web検索: "Go CL 63630043"
  • Go言語のコンパイラ構造に関する一般的な知識
  • Go言語のゼロ値保証に関する知識
  • Go言語のガベージコレクションとデバッグ情報に関する知識