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

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

このコミットは、Goコンパイラ(特にx86アーキテクチャ向けの8g)におけるバグ修正に関するものです。具体的には、nilポインタのフィールドアクセスに対して誤ってLEAL(Load Effective Address)命令が生成され、コンパイル時に「gins LEAQ nil *A」のようなエラーが発生する問題を解決します。この修正により、コンパイラがnilポインタのデリファレンスを正しく検出し、適切なエラーメッセージを出力するようになります。

コミット

  • コミットハッシュ: 1bd4a7dbcbf833a5e37cf8d0a6e7fc55c557543b
  • 作者: Rémy Oudompheng oudomphe@phare.normalesup.org
  • コミット日時: 2012年11月21日 水曜日 08:39:45 +0100

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

https://github.com/golang/go/commit/1bd4a7dbcbf833a5e37cf8d0a6e7fc55c557543b

元コミット内容

cmd/8g: fix erroneous LEAL nil.

Fixes #4399.

R=golang-dev, nigeltao
CC=golang-dev
https://golang.org/cl/6845053

変更の背景

このコミットは、Goコンパイラ8g(x86アーキテクチャ向け)が、nilポインタのフィールドにアクセスしようとした際に、誤ったコードを生成してしまうバグ(Issue 4399)を修正するために行われました。

具体的には、((*A)(nil)).a のようなコード(型Anilポインタをデリファレンスしてフィールドaにアクセスする)をコンパイルしようとすると、8gコンパイラが「gins LEAQ nil *A」というエラーメッセージを出力して停止していました。これは、コンパイラがnilポインタに対するLEAL(Load Effective Address)命令を誤って生成しようとしたためです。

通常、nilポインタのデリファレンスは実行時パニック(runtime panic)を引き起こすべき動作であり、コンパイル時にこのような不正な命令生成エラーが発生するのはコンパイラのバグです。このコミットは、コンパイラがこのようなケースを正しく検出し、適切なエラー処理を行うように修正することを目的としています。

前提知識の解説

  • Goコンパイラ (8g): Go言語のコンパイラは、ターゲットアーキテクチャごとに異なる名前を持っています。8gはx86(32ビット)アーキテクチャ向けのGoコンパイラを指します。現代のGoでは、go buildコマンドが自動的に適切なコンパイラを選択するため、ユーザーが直接8gを意識することは少なくなりましたが、Goの初期のコンパイラ実装ではこのようにアーキテクチャ固有のコンパイラ名が使われていました。
  • LEAL (Load Effective Address): x86アセンブリ命令の一つで、「実効アドレスをロードする」という意味です。メモリ上のデータそのものではなく、そのデータが格納されているアドレスをレジスタに計算して格納するために使用されます。例えば、LEAL (%EAX, %EBX, 4), %ECX は、EAX + EBX*4 のアドレスを計算し、その結果をECXレジスタに格納します。この命令は、ポインタ演算や構造体フィールドへのアクセスなど、アドレス計算が必要な場面でコンパイラによって生成されます。
  • nilポインタ: Go言語におけるnilは、ポインタ、スライス、マップ、チャネル、インターフェース、関数などのゼロ値です。ポインタの場合、nilは「何も指していない」状態を表します。nilポインタをデリファレンス(つまり、nilが指すメモリ上の値にアクセスしようとすること)は、Goのランタイムにおいてパニックを引き起こす不正な操作です。
  • ODOTPTR: Goコンパイラの内部表現(AST: Abstract Syntax Tree)におけるノードの種類の一つです。これは「ポインタのフィールドアクセス」を表します。例えば、p.fieldというコードでpがポインタ型の場合、この操作はODOTPTRノードとして表現されます。
  • igen関数とcgen関数: Goコンパイラのバックエンドにおけるコード生成フェーズの関数です。
    • igen (intermediate code generation): 中間コードを生成する関数。
    • cgen (code generation): 最終的な機械語コードを生成する関数。 これらの関数は、ASTをトラバースしながら、Goのソースコードに対応するアセンブリ命令を生成する役割を担っています。
  • gins関数: Goコンパイラのバックエンドで、特定のアセンブリ命令を生成するためのユーティリティ関数です。gins(ALEAL, f, t)のように呼び出され、ALEALLEAL命令)を生成します。

技術的詳細

このバグは、src/cmd/8g/cgen.c 内の igen 関数が ODOTPTR ノードを処理する際のロジックに起因していました。

igen 関数は、ASTノードを評価し、その結果をレジスタまたはメモリに配置するためのコードを生成します。ODOTPTR(ポインタのフィールドアクセス)の場合、通常はポインタ自体を評価し、そのアドレスにオフセットを加えることでフィールドのアドレスを計算します。

問題のコードは以下の部分でした(修正前):

case ODOTPTR:
    if(n->left->addable
        || n->left->op == OCALLFUNC
        || n->left->op == OCALLMETH
        || n->left->op == OCALLINTER) {
        // igen-able nodes.
        igen(n->left, &n1, res);
        regalloc(a, types[tptr], &n1);
        gmove(&n1, a);
        regfree(&n1);
    } else {
        regalloc(a, types[tptr], res);
        cgen(n->left, a);
    }

ここで、n->left はポインタのベースとなる式(例: ((*A)(nil))((*A)(nil)) 部分)を表します。 n->left->addable は、その式が直接アドレス指定可能かどうかを示します。nilポリインタはaddableではありません。 OCALLFUNC, OCALLMETH, OCALLINTER は関数呼び出しを表します。

問題は、n->leftnil ポインタ定数である場合、上記の if 文の条件に合致せず、else ブロックに入ってしまうことでした。else ブロックでは、cgen(n->left, a) が呼び出されます。cgen は式を評価してその値をレジスタ a に格納しようとしますが、nilポインタ定数に対してLEAL命令を生成しようとすると、gins関数内で「gins LEAQ nil %T」というfatalエラーが発生していました。

これは、gins関数(src/cmd/8g/gsubr.c)のALEAL命令を処理する部分に、nil定数に対するLEAL命令の生成を禁止するチェックがあったためです。

case ALEAL:
    if(f != N && isconst(f, CTNIL))
        fatal("gins LEAQ nil %T", f->type);
    break;

コンパイラは、nilポインタのデリファレンスは実行時パニックを引き起こすべきであり、コンパイル時にLEAL nilのような不正な命令を生成すべきではない、という設計思想を持っています。しかし、ODOTPTRの処理ロジックがnilポインタ定数を適切に扱えていなかったため、このバグが発生していました。

修正は、ODOTPTRの処理において、n->leftODOTODOTPTR(つまり、連鎖的なフィールドアクセス)である場合も、OCALLFUNCなどと同様にigenで処理するように変更することで、nilポインタ定数がelseブロックに到達するのを防ぎました。これにより、nilポインタのデリファレンスは、コンパイル時に不正なLEAL命令を生成するのではなく、実行時にパニックを引き起こす正しい動作をするようになります。

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

src/cmd/8g/cgen.c

--- a/src/cmd/8g/cgen.c
+++ b/src/cmd/8g/cgen.c
@@ -827,16 +827,19 @@ igen(Node *n, Node *a, Node *res)
 		return;
 
 	case ODOTPTR:
-		if(n->left->addable
-			|| n->left->op == OCALLFUNC
-			|| n->left->op == OCALLMETH
-			|| n->left->op == OCALLINTER) {
+		switch(n->left->op) {
+		case ODOT:
+		case ODOTPTR:
+		case OCALLFUNC:
+		case OCALLMETH:
+		case OCALLINTER:
 			// igen-able nodes.
 			igen(n->left, &n1, res);
 			regalloc(a, types[tptr], &n1);
 			gmove(&n1, a);
 			regfree(&n1);
-		} else {
+			break;
+		default:
 			regalloc(a, types[tptr], res);
 			cgen(n->left, a);
 		}

src/cmd/8g/gsubr.c

--- a/src/cmd/8g/gsubr.c
+++ b/src/cmd/8g/gsubr.c
@@ -1747,7 +1747,7 @@ gins(int as, Node *f, Node *t)
 	
 	case ALEAL:
 		if(f != N && isconst(f, CTNIL))
-		 	fatal("gins LEAQ nil %T", f->type);
+		 	fatal("gins LEAL nil %T", f->type);
 		break;
 	}

test/fixedbugs/issue4399.go (新規ファイル)

// compile

// Copyright 2012 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.

// Issue 4399: 8g would print "gins LEAQ nil *A".

package main

type A struct{ a int }

func main() {
	println(((*A)(nil)).a)
}

コアとなるコードの解説

src/cmd/8g/cgen.c の変更

ODOTPTR(ポインタのフィールドアクセス)を処理するigen関数内のロジックが変更されました。

  • 変更前: n->left->addable または特定の関数呼び出しノード(OCALLFUNC, OCALLMETH, OCALLINTER)の場合にigenを再帰的に呼び出していました。
  • 変更後: switch文が導入され、n->left->opODOT(構造体のフィールドアクセス)やODOTPTR(ポインタのフィールドアクセス)である場合も、OCALLFUNCなどと同様にigenを再帰的に呼び出すようになりました。それ以外のケースはdefaultで処理されます。

この変更の意図は、((*A)(nil)).a のような式において、n->left((*A)(nil)) であり、そのオペレーションがODOTPTRnilポインタのデリファレンス)である場合に、igenが再帰的に呼び出されるようにすることです。これにより、nilポインタ定数に対するLEAL命令の生成を試みるcgenの呼び出しを回避し、コンパイラが不正なコードを生成するのを防ぎます。

src/cmd/8g/gsubr.c の変更

gins関数内のfatalメッセージがLEAQからLEALに修正されました。これはタイプミスであり、機能的な変更ではありませんが、より正確なエラーメッセージになります。LEAQLEALの別名のようなもので、文脈によっては同じ意味で使われることもありますが、ここではアセンブリ命令の正確な表記に合わせるための修正です。

test/fixedbugs/issue4399.go の追加

このテストケースは、修正されたバグを再現し、修正が正しく機能することを確認するために追加されました。println(((*A)(nil)).a) というコードは、nilポインタをデリファレンスしてフィールドにアクセスしようとするもので、このコミット以前はコンパイルエラーを引き起こしていました。修正後は、このコードはコンパイルが成功し、実行時にパニック(runtime error: invalid memory address or nil pointer dereference)を引き起こすことが期待されます。これにより、コンパイラが不正な命令を生成するのではなく、Goのランタイムがnilポインタのデリファレンスを正しく検出するようになります。

関連リンク

参考にした情報源リンク

  • Go Issue 4399の議論
  • Goコンパイラのソースコード(src/cmd/8g/cgen.c, src/cmd/8g/gsubr.c
  • x86アセンブリ命令 LEAL に関する一般的な情報
  • Go言語におけるnilポインタの動作に関する情報