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

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

このコミットは、Goコンパイラ(gc)における以前導入されたバグを修正するものです。具体的には、Go言語の構文解析器が生成するC言語のコード(y.tab.c)において、特定のプラットフォーム(Linux)でレジスタの衝突が発生し、コンパイラがクラッシュする問題に対処しています。他のシステムではコンパイラがこの問題を検知できていたとされています。

コミット

commit 2f3d695a61127ba563b945e7e081148f954a986e
Author: Russ Cox <rsc@golang.org>
Date:   Sat Feb 11 01:04:24 2012 -0500

    gc: fix bug introduced earlier
    
    Apparently l and $1 were the same register on Linux.
    On the other systems, the compiler caught it.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/5654061

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

https://github.com/golang/go/commit/2f3d695a61127ba563b945e7e081148f954a986e

元コミット内容

このコミットは、Goコンパイラの構文解析器(gc)が生成するC言語のコード(y.tab.c)におけるバグ修正です。具体的には、go.yファイルで定義されているstructdcl(構造体宣言)の構文規則において、lという名前のNodeList型変数と、Yaccが生成するコード内で使用される$1というセマンティック値が、Linux環境下で同じレジスタに割り当てられてしまうという問題が発生していました。

この問題は、go.ystructdclルール内で、NodeList *l;という変数宣言の直後にl = $1;という代入が追加されたことによって引き起こされました。Yaccは、構文規則のアクション部で$$$1などのセマンティック値を扱う際に、内部的にスタックやレジスタを使用します。通常、異なる変数には異なるレジスタが割り当てられますが、特定のコンパイラ(この場合はLinux上のGCCなど)の最適化やレジスタ割り当ての挙動により、l$1が意図せず同じレジスタを共有してしまったと考えられます。

その結果、l = $1;という代入が行われた際に、$1の元の値が破壊され、その後の処理で不正な値が使用されることでコンパイラがクラッシュするというバグが発生しました。コミットメッセージにある「On the other systems, the compiler caught it.」という記述から、他のOSやコンパイラ環境では、このレジスタ衝突が起こらなかったか、あるいはコンパイラがより厳密なチェックを行い、警告やエラーとして検知していたことが示唆されます。

変更の背景

このバグは、Goコンパイラの開発過程で、特定のプラットフォーム(Linux)でのみ顕在化したレジスタ割り当ての競合問題に起因しています。Goコンパイラは、Go言語のソースコードを解析し、中間表現を経て最終的な機械語コードを生成する複雑なソフトウェアです。その中でも、構文解析は非常に重要なフェーズであり、Yacc(Yet Another Compiler Compiler)のようなツールを用いて、Go言語の文法規則(go.y)からC言語の構文解析器(y.tab.c)を自動生成しています。

問題の背景には、Yaccが生成するCコードの特性と、Cコンパイラのレジスタ割り当て戦略の相互作用があります。Yaccは、構文規則のアクション部で、スタック上のセマンティック値($1, $2, $$など)を直接C変数として扱えるようにコードを生成します。これらのセマンティック値は、通常、Cコンパイラによってレジスタに割り当てられる可能性があります。

今回のケースでは、go.ystructdclルール内で、NodeList *l;というローカル変数と、構文スタックのトップにあるセマンティック値$1が、Linux環境のCコンパイラによって同じ物理レジスタに割り当てられてしまったことが原因でした。これにより、l = $1;という一見無害な代入が、実際には$1の元の値を破壊し、その後の構文解析処理に悪影響を与え、コンパイラのクラッシュを引き起こしました。

この問題は、コンパイラの移植性(portability)と、異なるプラットフォームにおけるCコンパイラの挙動の違いが重要であることを示しています。開発者は、特定のプラットフォームでのみ発生するこのような微妙なバグを特定し、修正する必要がありました。

前提知識の解説

1. Goコンパイラ (gc)

Goコンパイラは、Go言語のソースコードを機械語に変換するプログラムです。Go言語の公式コンパイラは通常gcと呼ばれ、Goツールチェーンの一部として提供されています。コンパイラは、字句解析、構文解析、意味解析、中間コード生成、最適化、コード生成といった複数のフェーズを経て動作します。

2. Yacc (Yet Another Compiler Compiler)

Yaccは、BNF(Backus-Naur Form)のような形式文法記述から、C言語で書かれた構文解析器(パーサ)を自動生成するツールです。コンパイラやインタプリタのフロントエンド開発で広く利用されます。

  • 文法ファイル (.y): Goコンパイラの文法はgo.yファイルで定義されています。このファイルには、Go言語の構文規則と、各規則が認識されたときに実行されるC言語のアクションが記述されています。
  • 生成されるパーサ (y.tab.c): Yaccは.yファイルからy.tab.cというC言語のソースファイルを生成します。このファイルには、構文解析ロジック(通常はLALR(1)パーサ)と、文法ファイルで定義されたアクションがC関数として含まれています。
  • セマンティック値 ($$, $1, $2など): Yaccの文法ファイルでは、構文規則の右辺のシンボルに対応するセマンティック値を参照するために$1, $2などを使用します。$$は、現在の規則の左辺のシンボルに対応するセマンティック値を設定するために使用されます。これらの値は、パーサの内部スタックに格納され、C言語の変数としてアクセスされます。

3. レジスタ割り当て (Register Allocation)

レジスタ割り当ては、コンパイラの最適化フェーズの一つで、プログラムの実行中に頻繁にアクセスされる変数をCPUの高速なレジスタに割り当てるプロセスです。レジスタはメモリよりもはるかに高速であるため、適切にレジスタを割り当てることでプログラムの実行速度を大幅に向上させることができます。 しかし、CPUのレジスタ数は限られているため、どの変数をどのレジスタに割り当てるか、あるいはメモリに退避させるか(スピル)を決定する必要があります。この決定は、コンパイラのバックエンド(コード生成部分)によって行われ、コンパイラやターゲットアーキテクチャ、最適化レベルによってその挙動は異なります。

4. NodeListNode

Goコンパイラの内部では、Go言語のソースコードを抽象構文木(AST: Abstract Syntax Tree)として表現します。NodeはASTの各ノードを表す構造体であり、NodeListNodeのリストを管理するための構造体であると推測されます。これらは構文解析中に構築され、その後のコンパイルフェーズで利用されます。

技術的詳細

このバグは、Yaccが生成するCコードのセマンティック値の扱いと、Cコンパイラのレジスタ割り当ての間の予期せぬ相互作用によって引き起こされました。

go.yファイル内のstructdcl規則の変更前は、以下のようなコードが生成される可能性がありました(簡略化)。

// go.y の structdcl 規則に対応する C コードの一部
structdcl:
    ...
    {
        NodeList *l; // ローカル変数 l を宣言
        // ...
        // ここで $1 の値が使用される
        if ($1 != nil && $1->next == nil && $1->n == nil) {
            // ...
        }
        // ...
    }
    ...

このコードでは、l$1は異なる目的で使用される変数です。しかし、Linux上の特定のCコンパイラ(例えばGCC)は、最適化の過程で、l$1が同時に使用されない期間があることを検知し、これらを同じ物理レジスタに割り当ててしまうことがありました。

コミットで追加されたl = $1;という行は、このレジスタ割り当ての競合を顕在化させました。

// go.y の structdcl 規則に対応する C コードの一部 (変更後)
structdcl:
    ...
    {
        NodeList *l; // ローカル変数 l を宣言
        l = $1;      // $1 の値を l に代入
        // ...
        // ここで $1 の値が使用されるが、l と同じレジスタの場合、既に l = $1; で上書きされている可能性がある
        if (l != nil && l->next == nil && l->n == nil) { // 修正後、l を参照
            // ...
        }
        // ...
    }
    ...

もしl$1が同じレジスタに割り当てられていた場合、l = $1;という代入は、$1が保持していた元のセマンティック値(構文スタックから取得された値)を、そのレジスタに書き込むことになります。しかし、その直後に$1を参照しようとすると、それはもはや元のセマンティック値ではなく、lに代入された後の値(つまり、自分自身に代入された値)になってしまっているか、あるいは他の値で上書きされてしまっている可能性があります。

この問題は、go.yの変更によって$1が参照される前にlに代入されるようになったことで、$1の「生存期間」(live range)が短くなり、Cコンパイラがl$1を同じレジスタに割り当てやすくなったために発生したと考えられます。他のシステムでは、Cコンパイラのレジスタ割り当て戦略が異なっていたため、この競合が発生しなかったか、あるいはより保守的な割り当てが行われていたため、問題が表面化しなかったと推測されます。

修正は、`go.y