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

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

このコミットは、Goコンパイラのcmd/gcパッケージにおけるchecknil関数の挙動を修正するものです。具体的には、nilリテラルに対するchecknilが内部エラーを引き起こす問題を解決します。この修正には、src/cmd/gc/pgen.cのコード変更と、その問題を再現し修正を検証するための新しいテストケースtest/fixedbugs/issue7346.goの追加が含まれます。

コミット

commit 52e6d7c6224612a3b60caa799bc22bd50ab16acb
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Tue Mar 4 08:18:17 2014 +0100

    cmd/gc: use a register to checknil constants.
    
    Fixes #7346.
    
    LGTM=rsc
    R=rsc, iant, khr
    CC=golang-codereviews
    https://golang.org/cl/69050044

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

https://github.com/golang/go/commit/52e6d7c6224612a3b60caa799bc22bd50ab16acb

元コミット内容

cmd/gc: use a register to checknil constants.

Fixes #7346.

LGTM=rsc
R=rsc, iant, khr
CC=golang-codereviews
https://golang.org/cl/69050044

変更の背景

この変更は、Goコンパイラがnilリテラルに対してchecknil操作を実行しようとした際に発生する内部エラー("doasm"エラー)を修正するために行われました。この問題はGoのIssue 7346として報告されており、*(*int)(nil)のようなコードがコンパイル時にクラッシュするというものでした。

Go言語では、ポインタの逆参照(デリファレンス)を行う前に、そのポインタがnilでないことを確認する「nilチェック」が実行されます。これはランタイムパニックを防ぐための重要な安全機構です。しかし、特定の条件下でnilリテラル自体に対してこのチェックが適用されると、コンパイラが予期しない状態に陥り、内部エラーが発生していました。

前提知識の解説

このコミットを理解するためには、Goコンパイラのバックエンドにおけるコード生成と最適化に関するいくつかの概念を理解する必要があります。

  • cmd/gc: Goコンパイラの主要な部分であり、Goソースコードを中間表現に変換し、最終的にアセンブリコードを生成する役割を担います。
  • checknil: Goコンパイラが生成する命令の一つで、ポインタがnilであるかどうかをチェックし、nilであればランタイムパニックを引き起こします。これは、ポインタのデリファレンス(例: *p)の前に挿入されます。
  • Node: コンパイラ内部でコードの抽象構文木(AST)を表すデータ構造です。各Nodeは、変数、定数、演算子、関数呼び出しなど、プログラムの要素を表します。
  • OREGISTER: NodeopフィールドがOREGISTERである場合、そのノードがレジスタに格納された値を表すことを示します。
  • OLITERAL: NodeopフィールドがOLITERALである場合、そのノードがリテラル値(定数)を表すことを示します。
  • addable: コンパイラ内部の概念で、あるNodeが直接アセンブリ命令のオペランドとして使用できるかどうかを示します。例えば、メモリ上のアドレスやレジスタに直接対応する値はaddableです。
  • cgen: コードジェネレータの一部で、Nodeツリーをトラバースしてアセンブリコードを生成する関数です。
  • regalloc: レジスタを割り当てる関数です。コンパイラは、計算中に値を一時的に保持するためにCPUレジスタを使用します。
  • gins: アセンブリ命令を生成する関数です。
  • ACHECKNIL: checknil操作に対応するアセンブリ命令です。

Goコンパイラは、コードを生成する際に、可能な限り効率的なアセンブリ命令を生成しようとします。これには、値を直接メモリからロードしたり、レジスタに格納したりする判断が含まれます。checknilのような操作は、通常、メモリ上のポインタやレジスタに格納されたポインタに対して実行されます。

技術的詳細

問題は、cgen_checknil関数がnilリテラル(OLITERAL)を適切に処理していなかった点にありました。

元のコードでは、cgen_checknil関数内で、thechar == '5'(これはおそらく特定のアーキテクチャやコンパイラのフェーズを示す内部的なフラグ)の場合、またはノードがaddableでない場合に、レジスタを割り当ててからcgengins(ACHECKNIL)を呼び出すというロジックがありました。

// Original code snippet from src/cmd/gc/pgen.c
if((thechar == '5' && n->op != OREGISTER) || !n->addable) {
    regalloc(&reg, types[tptr], n);
    cgen(n, &reg);
    gins(ACHECKNIL, &reg, N);
}

この条件式は、n->op == OLITERAL(つまりnilリテラル)の場合に問題を引き起こしました。nilリテラルは通常addableではありません。そのため、!n->addableの条件に合致し、レジスタ割り当てとcgenが試みられます。しかし、nilリテラルはメモリ上のアドレスを持たないため、cgennilリテラルをレジスタにロードしようとすると、コンパイラ内部で矛盾が生じ、"doasm"エラーが発生していました。

修正は、この条件式にn->op == OLITERALという条件を追加することで、nilリテラルがOREGISTERでない場合でも、明示的にレジスタを介してchecknilが実行されるようにしました。

// Modified code snippet from src/cmd/gc/pgen.c
if((thechar == '5' && n->op != OREGISTER) || !n->addable || n->op == OLITERAL) {
    regalloc(&reg, types[tptr], n);
    cgen(n, &reg);
    gins(ACHECKNIL, &reg, N);
}

この変更により、nilリテラルがchecknilの対象となった場合、常に一時的なレジスタにロードされ、そのレジスタの値に対してACHECKNIL命令が生成されるようになります。これにより、コンパイラがnilリテラルを直接デリファレンスしようとするのではなく、レジスタを介して安全に処理できるようになり、内部エラーが回避されます。

新しいテストケースtest/fixedbugs/issue7346.goは、この問題を再現する最小限のコードを含んでいます。

package main

func main() {
	_ = *(*int)(nil)
}

このコードは、nil*int型にキャストし、その結果をデリファレンスしようとします。修正前はこれがコンパイルエラーを引き起こしましたが、修正後は正しくランタイムパニック(nil pointer dereference)を発生させるようになります。これは、コンパイラが正しくchecknilを挿入し、そのチェックがnilリテラルに対しても機能することを示しています。

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

src/cmd/gc/pgen.ccgen_checknil関数内の条件式が変更されました。

--- a/src/cmd/gc/pgen.c
+++ b/src/cmd/gc/pgen.c
@@ -476,7 +476,7 @@ cgen_checknil(Node *n)\n 	\tdump("checknil", n);\n 	\tfatal("bad checknil");\n 	}\n-\tif((thechar == '5' && n->op != OREGISTER) || !n->addable) {\n+\tif((thechar == '5' && n->op != OREGISTER) || !n->addable || n->op == OLITERAL) {\n \t\tregalloc(&reg, types[tptr], n);\n \t\tcgen(n, &reg);\n \t\tgins(ACHECKNIL, &reg, N);\

また、問題を再現するための新しいテストファイルが追加されました。

--- /dev/null
+++ b/test/fixedbugs/issue7346.go
@@ -0,0 +1,14 @@
+// compile
+
+// Copyright 2014 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 7346 : internal error "doasm" error due to checknil
+// of a nil literal.
+
+package main
+
+func main() {
+	_ = *(*int)(nil)
+}

コアとなるコードの解説

変更された行は以下の通りです。

-\tif((thechar == '5' && n->op != OREGISTER) || !n->addable) {
+\tif((thechar == '5' && n->op != OREGISTER) || !n->addable || n->op == OLITERAL) {

この変更は、cgen_checknil関数が、checknil操作の対象となるNode nをどのように処理するかを決定する条件式を修正しています。

  • thechar == '5' && n->op != OREGISTER: これは特定のコンパイラ設定(おそらくターゲットアーキテクチャや最適化レベル)において、ノードがレジスタにない場合にレジスタを介した処理が必要であることを示唆しています。
  • !n->addable: ノードが直接アセンブリ命令のオペランドとして使用できない場合(例: 複雑な式の結果など)に、レジスタを介した処理が必要であることを示します。
  • n->op == OLITERAL: このコミットで追加された条件です。 これにより、ノードがnilリテラルである場合、たとえそれがaddableであると誤って判断されたり、他の条件に合致しなかったりしても、明示的にレジスタを割り当ててchecknilを実行するパスを通るようになります。これにより、nilリテラルが直接デリファレンスされることによるコンパイラの内部エラーが回避されます。

この修正により、nilリテラルに対するchecknilが常に安全な方法で処理されるようになり、コンパイラの堅牢性が向上しました。

関連リンク

参考にした情報源リンク