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

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

このコミットは、Goコンパイラ(cmd/gc)におけるchecknil関数の挙動を修正し、特に「race build」環境下での潜在的な問題を解決することを目的としています。具体的には、checknilがnilポインタチェックを行う際に、対象となるノードがアドレス可能(addable)であることを保証することで、コンパイル時の競合状態(race condition)による誤ったコード生成を防ぎます。

コミット

commit b75a08d03c3d0fc659191dbc7eed174d5cb6f6c3
Author: Russ Cox <rsc@golang.org>
Date:   Thu Aug 15 21:05:05 2013 -0400

    cmd/gc: ensure addable in checknil (fix race build)
    
    TBR=dvyukov
    CC=golang-dev
    https://golang.org/cl/12791044
---
 src/cmd/gc/pgen.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/cmd/gc/pgen.c b/src/cmd/gc/pgen.c
index 583e77e4cc..edeaa06a69 100644
--- a/src/cmd/gc/pgen.c
+++ b/src/cmd/gc/pgen.c
@@ -515,8 +515,8 @@ cgen_checknil(Node *n)\n 		return;\n 	while(n->op == ODOT || (n->op == OINDEX && isfixedarray(n->left->type->type))) // NOTE: not ODOTPTR\n 		n = n->left;\n-	if(thechar == '5' && n->op != OREGISTER) {\n-		regalloc(&reg, types[tptr], N);\n+	if((thechar == '5' && n->op != OREGISTER) || !n->addable) {\n+		regalloc(&reg, types[tptr], n);\n 		cgen(n, &reg);\n 		gins(ACHECKNIL, &reg, N);\n 		regfree(&reg);\n```

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

[https://github.com/golang/go/commit/b75a08d03c3d0fc659191dbc7eed174d5cb6f6c3](https://github.com/golang/go/commit/b75a08d03c3d0fc659191dbc7eed174d5cb6f6c3)

## 元コミット内容

`cmd/gc: ensure addable in checknil (fix race build)`

このコミットは、Goコンパイラの`cmd/gc`パッケージにおいて、`checknil`関数がnilポインタチェックを行う際に、対象となるノードがアドレス可能(`addable`)であることを保証するように変更します。これにより、「race build」環境下で発生する可能性のある問題を修正します。

## 変更の背景

Goコンパイラは、実行時にnilポインタ参照が発生しないように、コンパイル時にnilポインタチェックを挿入します。このチェックは`checknil`関数によって行われます。しかし、特定のコンパイル環境、特に「race build」と呼ばれる、データ競合検出を有効にしたビルド環境において、このnilポインタチェックの挿入ロジックに問題がありました。

問題の根源は、`checknil`がnilチェックの対象とするノード(`Node *n`)が、レジスタにロードできない、あるいはメモリ上のアドレスを持たない一時的な値である場合に発生していました。従来のコードでは、`thechar == '5'`(これはおそらく特定のアーキテクチャ、例えばARMv5を指す)かつノードがレジスタではない場合にのみ、レジスタを割り当ててnilチェックを行っていました。しかし、これではノードがレジスタではないが、かつアドレス可能でもない(つまり、メモリ上に実体がない)場合に、nilチェックのためのコード生成が適切に行われない可能性がありました。

「race build」は、Goプログラムの実行時にデータ競合を検出するための特別なビルドモードです。このモードでは、コンパイラが追加のインストゥルメンテーションコードを挿入し、メモリアクセスを監視します。このような特殊なビルド環境では、通常とは異なるコード生成パスがトリガーされることがあり、それが今回の問題を引き起こしたと考えられます。

このコミットは、`checknil`がnilチェックを行う対象のノードが、レジスタにロードされるか、またはメモリ上にアドレスを持つ(`addable`である)ことを常に保証することで、この問題を解決しようとしています。これにより、どのようなビルド環境でも、nilポインタチェックが正しく機能するようになります。

## 前提知識の解説

### Goコンパイラ (`cmd/gc`)

Go言語の公式コンパイラは、`gc`(Go Compiler)と呼ばれ、Goツールチェーンの一部として提供されています。`cmd/gc`は、Goソースコードを機械語に変換する主要なコンポーネントです。コンパイルプロセスは、字句解析、構文解析、抽象構文木(AST)の構築、型チェック、中間表現(IR)への変換、最適化、コード生成といった複数のフェーズに分かれています。

### 抽象構文木(AST: Abstract Syntax Tree)と`Node`

コンパイラの構文解析フェーズでは、ソースコードが抽象構文木(AST)に変換されます。ASTは、プログラムの構造を木構造で表現したものです。ASTの各要素は「ノード(`Node`)」と呼ばれ、変数、定数、演算子、関数呼び出し、制御構造など、プログラムのあらゆる要素を表します。Goコンパイラの内部では、これらの`Node`オブジェクトがプログラムのセマンティクスを保持し、後続のコンパイルフェーズで利用されます。

### `checknil`関数とnilポインタチェック

Go言語では、nilポインタのデリファレンス(nilポインタが指すメモリ領域にアクセスしようとすること)はランタイムパニックを引き起こします。これを防ぐため、Goコンパイラは、ポインタがデリファレンスされる前にその値がnilでないことを確認するコード(nilポインタチェック)を自動的に挿入します。`checknil`関数は、このnilポインタチェックをASTに挿入する役割を担っています。

### `race build` (データ競合検出ビルド)

Go言語には、プログラムの実行時にデータ競合(data race)を検出するための「race detector」が組み込まれています。これは、`go build -race`や`go test -race`のように`-race`フラグを付けてビルドすることで有効になります。`race build`は、通常のビルドに比べて、コンパイラが追加のインストゥルメンテーションコードを生成し、ランタイムがメモリアクセスを監視するようになります。これにより、並行処理における潜在的なバグ(データ競合)を特定できます。この追加のインストゥルメンテーションが、コンパイラの内部ロジック、特にコード生成パスに影響を与えることがあります。

### `addable`プロパティ

Goコンパイラの内部では、ASTの`Node`オブジェクトには様々なプロパティが付与されます。`addable`プロパティは、そのノードがメモリ上にアドレスを持つことができるかどうか、つまり、その値がメモリ上の特定の位置に格納されているか、あるいは格納可能であるかを示すものです。レジスタに直接格納される一時的な値や、コンパイル時に最適化によって消滅する可能性のある値は、`addable`ではない場合があります。nilポインタチェックを行うためには、対象のポインタがメモリ上のどこかに存在し、そのアドレスをレジスタにロードできる必要があります。

### `OREGISTER`

`OREGISTER`は、Goコンパイラの内部で使われるオペレーションコード(`Op`)の一つで、ノードがレジスタに格納されている値であることを示します。

### `regalloc`と`cgen`

`regalloc`は、レジスタを割り当てる関数です。コンパイラは、中間コード生成や最適化の過程で、変数や一時的な値をCPUのレジスタに割り当てて処理を高速化します。`cgen`は、コード生成(code generation)を行う関数で、ASTノードに対応する機械語命令を生成します。

## 技術的詳細

このコミットは、`src/cmd/gc/pgen.c`ファイル内の`cgen_checknil`関数を変更しています。`pgen.c`は、Goコンパイラのバックエンドの一部であり、プラットフォーム固有のコード生成(この場合はx86アーキテクチャを想定している可能性が高い)を担当しています。

`cgen_checknil`関数は、与えられたノード`n`に対してnilポインタチェックを挿入するロジックを含んでいます。元のコードでは、nilチェックが必要なノード`n`がレジスタにない場合(`n->op != OREGISTER`)かつ、特定のアーキテクチャ(`thechar == '5'`)の場合にのみ、レジスタを割り当てて`cgen`でコードを生成し、`ACHECKNIL`命令を挿入していました。

しかし、この条件では不十分でした。ノードがレジスタにない場合でも、それがメモリ上にアドレスを持たない一時的な値である場合、`regalloc`でレジスタを割り当てて`cgen`でコードを生成しようとしても、そのノードが「アドレス可能」でないために問題が発生する可能性がありました。特に、`race build`のような特殊なコンパイルモードでは、このようなエッジケースが顕在化しやすかったと考えられます。

新しいコードでは、`if`文の条件に`|| !n->addable`が追加されました。これは、「ノードがレジスタにない」という条件に加えて、「ノードがアドレス可能でない」という条件も満たす場合に、レジスタを割り当ててnilチェックのコードを生成するように変更されたことを意味します。

さらに重要な変更点として、`regalloc(&reg, types[tptr], N);`が`regalloc(&reg, types[tptr], n);`に変更されています。
*   元の`N`は、おそらく「nil」または「no node」を意味するグローバルな定数で、レジスタ割り当てのヒントとして使われていました。これは、レジスタ割り当て器に対して、特定のノードに関連付けずにレジスタを割り当てることを示唆していました。
*   新しい`n`は、nilチェックの対象となる実際のノードを`regalloc`に渡しています。これにより、レジスタ割り当て器は、`n`が持つプロパティ(例えば、`addable`であるかどうか)を考慮に入れて、より適切なレジスタ割り当てを行うことができるようになります。`n`が`addable`でない場合、`regalloc`は`n`をメモリに「スピル」(一時的にメモリに退避させる)する必要があることを認識し、そのためのコードを生成する可能性があります。これにより、nilチェックの対象が常に有効なメモリ位置を持つことが保証され、`race build`における問題が解決されます。

この変更により、`checknil`は、nilチェックの対象がレジスタにない場合や、アドレス可能でない場合でも、常にその値をレジスタにロードし、nilチェック命令を挿入するための適切なコードを生成できるようになります。

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

```diff
--- a/src/cmd/gc/pgen.c
+++ b/src/cmd/gc/pgen.c
@@ -515,8 +515,8 @@ cgen_checknil(Node *n)\n 		return;\n 	while(n->op == ODOT || (n->op == OINDEX && isfixedarray(n->left->type->type))) // NOTE: not ODOTPTR\n 		n = n->left;\n-	if(thechar == '5' && n->op != OREGISTER) {\n-		regalloc(&reg, types[tptr], N);\n+	if((thechar == '5' && n->op != OREGISTER) || !n->addable) {\n+		regalloc(&reg, types[tptr], n);\n 		cgen(n, &reg);\n 		gins(ACHECKNIL, &reg, N);\n 		regfree(&reg);\n```

## コアとなるコードの解説

変更は`src/cmd/gc/pgen.c`ファイルの`cgen_checknil`関数内で行われています。

1.  **変更前の`if`条件**:
    ```c
    if(thechar == '5' && n->op != OREGISTER) {
    ```
    この条件は、「現在のターゲットアーキテクチャが'5'(おそらくARMv5)であり、かつ、nilチェックの対象となるノード`n`がレジスタに格納されていない場合」にnilチェックのコードを生成するというものでした。

2.  **変更後の`if`条件**:
    ```c
    if((thechar == '5' && n->op != OREGISTER) || !n->addable) {
    ```
    この行がコミットの核心です。
    *   `|| !n->addable`: 新たに追加された条件です。これは、「ノード`n`がアドレス可能でない場合」を意味します。つまり、ノードがメモリ上に実体を持たない一時的な値である場合、この条件が真となります。
    *   この変更により、たとえノードがレジスタに格納されていなくても(`n->op != OREGISTER`)、あるいは特定のアーキテクチャに限定されずとも、そのノードがアドレス可能でない場合には、nilチェックのための特別な処理が必要であるとコンパイラが判断するようになります。

3.  **変更前の`regalloc`呼び出し**:
    ```c
    regalloc(&reg, types[tptr], N);
    ```
    `regalloc`はレジスタを割り当てる関数です。`types[tptr]`はポインタ型を示します。`N`は、レジスタ割り当て器に対して、特定のノードに関連付けずにレジスタを割り当てることを示唆していました。これは、nilチェックの対象が常に有効なメモリ位置を持つことを保証する上では不十分でした。

4.  **変更後の`regalloc`呼び出し**:
    ```c
    regalloc(&reg, types[tptr], n);
    ```
    この変更も非常に重要です。`N`の代わりに、nilチェックの対象となる実際のノード`n`が`regalloc`に渡されるようになりました。これにより、レジスタ割り当て器は`n`のプロパティ(特に`addable`であるかどうか)を考慮に入れて、レジスタを割り当てるか、あるいは`n`の値を一時的にメモリにスピルする(退避させる)などの適切なコードを生成できるようになります。これにより、nilチェックの対象が常に有効なメモリ位置を持つことが保証され、`race build`における問題が解決されます。

この二つの変更、特に`!n->addable`条件の追加と`regalloc`への`n`の引き渡しにより、Goコンパイラは、nilポインタチェックが必要なあらゆる状況において、対象のポインタが適切にメモリ上に存在するか、またはレジスタにロード可能であることを保証できるようになり、`race build`のような特殊な環境下での堅牢性が向上しました。

## 関連リンク

*   Go CL 12791044: [https://golang.org/cl/12791044](https://golang.org/cl/12791044)

## 参考にした情報源リンク

*   Go言語のコンパイラに関する一般的な情報 (Go compiler internals, AST, code generation)
*   Go race detectorに関する情報 (go build -race)
*   Goコンパイラのソースコード(`src/cmd/gc/`ディレクトリ内のファイル)
*   Go言語のnilポインタに関するドキュメント
*   Go言語のコミット履歴と関連する議論
*   [Go compiler source code on GitHub](https://github.com/golang/go/tree/master/src/cmd/compile) (General reference for Go compiler structure)
*   [Go Race Detector](https://go.dev/blog/race-detector) (Official Go blog post on race detector)
*   [Go Language Specification - Nil](https://go.dev/ref/spec#The_nil_value) (For understanding nil in Go)
*   [Go CLs (Code Reviews)](https://go.dev/cl/) (For understanding Go's code review process and CLs)