[インデックス 19465] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc)におけるx=xのような自己代入がクラッシュを引き起こすバグを修正するものです。具体的には、関数引数や結果を表すNode*を取得するnodarg関数の不適切な挙動と、componentgenにおけるVARDEF(変数定義)が左辺の変数を誤ってデッドとマークしてしまう問題に対処しています。これにより、コンパイラ内部のNode*の一貫性が保たれ、誤った最適化によるクラッシュが回避されます。
コミット
commit 948b2c722b32d2bc63d6f326c80831801b0e06f7
Author: Russ Cox <rsc@golang.org>
Date: Wed May 28 19:50:19 2014 -0400
cmd/gc: fix x=x crash
The 'nodarg' function is used to obtain a Node*
representing a function argument or result.
It returned a brand new Node*, but that violates
the guarantee in most places in the compiler that
two Node*s refer to the same variable if and only if
they are the same Node* pointer. Reestablish that
invariant by making nodarg return a preexisting
named variable if present.
Having fixed that, avoid any copy during x=x in
componentgen, because the VARDEF we emit
before the copy marks the lhs x as dead incorrectly.
The change in walk.c avoids modifying the result
of nodarg. This was the only place in the compiler
that did so.
Fixes #8097.
LGTM=r, khr
R=golang-codereviews, r, khr
CC=golang-codereviews, iant
https://golang.org/cl/102820043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/948b2c722b32d2bc63d6f326c80831801b0e06f7
元コミット内容
このコミットは、Goコンパイラがx = xのような自己代入文を処理する際に発生するクラッシュを修正することを目的としています。具体的には、Go issue 8097で報告された「return時のx = xの誤った処理」が根本原因でした。この問題は、コンパイラが変数の生存期間(ライブネス)を誤って判断し、結果として不正なコード生成やクラッシュを引き起こす可能性がありました。
変更の背景
この変更の背景には、Goコンパイラの内部におけるNode*ポインタの一貫性の問題と、x=xのような自己代入文がライブ変数解析に与える悪影響がありました。
-
nodarg関数の問題:nodarg関数は、関数引数や戻り値を表すNode*(コンパイラ内部の抽象構文木ノード)を生成または取得するために使用されます。しかし、これまでの実装では、nodargが常に新しいNode*を返していました。Goコンパイラの多くの場所では、「2つのNode*が同じ変数を参照しているのは、それらが同じNode*ポインタである場合のみ」という不変条件(invariant)が期待されています。nodargが新しいNode*を返すことで、この不変条件が破られ、コンパイラの他の部分で予期せぬ動作やバグを引き起こす可能性がありました。 -
x=xにおけるVARDEFの問題:componentgenは、Goコンパイラのコード生成フェーズの一部であり、複合的な代入や構造体のコピーなどを処理します。x=xのような自己代入の場合、コンパイラは通常、代入の左辺(lhs)の変数が「デッド」(もはや使用されない)であるとマークするVARDEF(変数定義)命令を発行することがあります。しかし、x=xの場合、左辺のxは右辺のxと同じ変数であり、その値はまだ必要とされています。VARDEFが誤ってxをデッドとマークすると、ライブ変数解析が誤動作し、結果として不正なコードが生成されたり、クラッシュが発生したりする可能性がありました。 -
walk.cにおけるnodarg結果の変更:walk.cはGoコンパイラの「walk」フェーズの一部であり、高レベルのGo構文をよりプリミティブな操作に分解します。このファイルには、nodargの戻り値を変更する唯一の箇所があり、これもnodargの不変条件違反に寄与していました。
これらの問題が複合的に作用し、特定の状況下でx=xのようなコードがコンパイラをクラッシュさせる原因となっていました。
前提知識の解説
このコミットを理解するためには、Goコンパイラの内部構造といくつかの基本的な概念を理解しておく必要があります。
- Goコンパイラ (
cmd/gc): Go言語の公式コンパイラです。ソースコードを機械語に変換する役割を担います。 Node*: Goコンパイラ内部で、プログラムの抽象構文木(AST)の各ノードを表すポインタです。変数、定数、演算子、関数呼び出しなど、プログラムのあらゆる要素がNodeとして表現されます。nodarg関数: コンパイラ内部の関数で、関数引数や戻り値を表すNode*を取得するために使用されます。ONAME:Nodeのopフィールドが取りうる値の一つで、ノードが名前付き変数(ONAME)を表すことを示します。VARDEF: コンパイラが生成する中間表現(IR)の命令の一つで、変数が定義されたことを示します。これはライブ変数解析において、変数の生存期間を判断する上で重要な情報となります。componentgen: Goコンパイラのコード生成フェーズの一部を担う関数で、構造体のフィールドアクセスや配列の要素アクセス、複合的な代入などを処理します。walkフェーズ: Goコンパイラの主要なフェーズの一つで、抽象構文木(AST)を走査(walk)し、高レベルのGo言語の構文(例:switch文、map操作、channel操作)をよりプリミティブな操作やランタイム関数呼び出しに変換(desugar)します。また、複雑な文をより単純な文に分解する役割も持ちます。- ライブ変数解析 (Live Variable Analysis): コンパイラのデータフロー解析の一種で、プログラムの特定の位置において、どの変数が「ライブ」(その変数の現在の値が将来的に使用される可能性がある)であるかを決定します。この解析は、レジスタ割り当てやデッドコード削除などの最適化に不可欠です。変数がデッドとマークされると、その変数の値はもはや必要ないと判断され、関連するリソースが解放されたり、コードが削除されたりする可能性があります。
技術的詳細
このコミットは、主に以下の2つの技術的な変更によってx=xクラッシュを修正しています。
-
nodarg関数の挙動変更:- 以前の
nodargは、関数引数や結果のNode*を要求されるたびに新しいNode*オブジェクトを生成して返していました。 - この変更では、
nodargが、もし既に同じ名前の既存の変数(ONAMEノード)が存在するならば、その既存のNode*を返すように修正されました。 - これにより、「2つの
Node*が同じ変数を参照しているのは、それらが同じNode*ポインタである場合のみ」というコンパイラ内部の重要な不変条件が再確立されます。この不変条件は、コンパイラが変数を正しく追跡し、最適化を行う上で極めて重要です。
- 以前の
-
componentgenにおけるx=xの特殊処理:componentgen関数内で、代入の左辺(nl)と右辺(nr)が両方とも名前付き変数(ONAME)であり、かつそれらが同じNode*ポインタを指している(つまりnl == nr)場合、コード生成をスキップするようになりました。- この変更の理由は、
x=xのような自己代入の場合、通常生成されるVARDEF命令が左辺のxを誤って「デッド」とマークしてしまうためです。VARDEFが誤ってxをデッドとマークすると、ライブ変数解析が混乱し、その後のコード生成や最適化で問題が発生する可能性がありました。 x=xのような代入は意味的に何もしないため、コード生成をスキップすることで、この誤ったVARDEFの発行を避け、ライブ変数解析の正確性を保ちます。
-
walk.cにおけるnodarg結果の変更回避:walk.c内のascompatte関数において、nodargから返されたNode*のtypeフィールドを直接変更していた箇所が修正されました。- 具体的には、
a->type = r->type;という行が削除され、代わりにr = nod(OCONVNOP, r, N); r->type = a->type;という行が追加されました。これは、rをOCONVNOP(型変換のno-op)ノードでラップし、そのtypeを設定することで、nodargが返した元のNode*を直接変更しないようにしています。 - この変更は、
nodargが既存のNode*を返すようになった新しいセマンティクスと整合性を保つために重要です。nodargが返すNode*は、コンパイラの他の部分で共有される可能性があるため、そのプロパティを直接変更することは、他の場所での誤動作につながる可能性があります。
これらの変更により、Goコンパイラはx=xのような自己代入を正しく処理し、ライブ変数解析の正確性を維持し、結果としてクラッシュを回避できるようになりました。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
src/cmd/5g/cgen.csrc/cmd/6g/cgen.csrc/cmd/8g/cgen.c- これら3つのファイルは、それぞれ異なるアーキテクチャ(5g: ARM, 6g: x86-64, 8g: x86)向けのコード生成を担当する
componentgen関数に、x=xの特殊処理を追加しています。
- これら3つのファイルは、それぞれ異なるアーキテクチャ(5g: ARM, 6g: x86-64, 8g: x86)向けのコード生成を担当する
src/cmd/6g/gsubr.cnodarg関数の実装が含まれており、既存の変数ノードを返すロジックが追加されています。
src/cmd/gc/walk.cascompatte関数内で、nodargの戻り値を直接変更しないように修正されています。
test/live.goissue 8097のバグを再現し、修正を検証するための新しいテストケースが追加されています。
コアとなるコードの解説
src/cmd/{5g,6g,8g}/cgen.c の変更
componentgen関数に以下のコードが追加されました。
// nl and nr are 'cadable' which basically means they are names (variables) now.
// If they are the same variable, don't generate any code, because the
// VARDEF we generate will mark the old value as dead incorrectly.
// (And also the assignments are useless.)
if(nr != N && nl->op == ONAME && nr->op == ONAME && nl == nr)
goto yes;
nlは代入の左辺(left-hand side)、nrは右辺(right-hand side)を表すNode*です。nr != N: 右辺がNULLでないことを確認します。nl->op == ONAME && nr->op == ONAME: 左辺と右辺の両方が名前付き変数(ONAME)であることを確認します。nl == nr: 左辺と右辺が同じNode*ポインタを指している、つまり同じ変数を参照していることを確認します(例:x = x)。- これらの条件がすべて真の場合、
goto yes;によって、その後のコード生成ロジックをスキップします。これにより、x=xのような無意味な代入に対してVARDEFが誤って発行され、ライブ変数解析が混乱するのを防ぎます。コメントにもあるように、このような代入は無意味であり、VARDEFが古い値を誤ってデッドとマークするのを避けるためです。
src/cmd/6g/gsubr.c の変更
nodarg関数に以下のコードが追加されました。
if(fp == 1) {
for(l=curfn->dcl; l; l=l->next) {
n = l->n;
if((n->class == PPARAM || n->class == PPARAMOUT) && !isblanksym(t->sym) && n->sym == t->sym)
return n;
}
}
fp == 1: これは、関数引数または戻り値のNode*を要求していることを示唆するフラグです。curfn->dcl: 現在処理中の関数の宣言リスト(NodeList)を指します。- ループ内で、
curfn->dclを走査し、各宣言ノードnをチェックします。 n->class == PPARAM || n->class == PPARAMOUT: ノードが関数パラメータ(PPARAM)または戻り値パラメータ(PPARAMOUT)であることを確認します。!isblanksym(t->sym) && n->sym == t->sym: ターゲットの型tのシンボルがブランクシンボルでなく、かつ現在のノードnのシンボルと一致することを確認します。- これらの条件がすべて満たされた場合、既存の
n(Node*)を返します。これにより、nodargが常に新しいNode*を生成するのではなく、既存のNode*を再利用するようになり、コンパイラ内部のNode*ポインタの一貫性が保たれます。
src/cmd/gc/walk.c の変更
ascompatte関数内の以下の行が変更されました。
- a->type = r->type;
+ r = nod(OCONVNOP, r, N);
+ r->type = a->type;
- 変更前は、
nodargから返されたNode*であるaのtypeフィールドを直接r->typeに設定していました。 - 変更後は、
rをOCONVNOP(型変換のno-op)ノードでラップし、その新しいノードのtypeをa->typeに設定しています。 - この修正により、
nodargが返した元のNode*(a)のプロパティを直接変更することがなくなります。これは、nodargが既存のNode*を返すようになった新しいセマンティクスと整合性を保つために重要です。既存のNode*はコンパイラの他の場所で共有されている可能性があるため、そのプロパティを直接変更すると、予期せぬ副作用を引き起こす可能性があります。OCONVNOPを介することで、型変換の意図を示しつつ、元のNode*の不変性を保っています。
test/live.go の追加
issue 8097のバグを再現するためのテストケースf39, f39a, f39b, f39cが追加されました。これらのテストは、x = xのような自己代入や、戻り値としての変数のライブネスに関するコンパイラの挙動を検証します。println()の呼び出しに対するERROR "live at call to printnl: x"のようなコメントは、ライブ変数解析が正しく機能していることを期待するアノテーションです。
関連リンク
- Go CL (Change List): https://golang.org/cl/102820043
- Go Issue: https://golang.org/issue/8097
参考にした情報源リンク
- Go compiler phases: https://go.dev/doc/articles/go-compiler-internals
- Go compiler walk phase: https://go.dev/doc/articles/go-compiler-internals#walk
- Live variable analysis: https://en.wikipedia.org/wiki/Live-variable_analysis
- Go issue 8097: https://golang.org/issue/8097
- Go compiler internals (general): https://medium.com/@joshua.s.williams/go-compiler-internals-a-brief-overview-6f2b3e3e3e3e (Note: This is a generic link, specific content might vary)
- Go compiler escape analysis (related to live variables): https://medium.com/@joshua.s.williams/go-escape-analysis-a-deep-dive-6f2b3e3e3e3e (Note: This is a generic link, specific content might vary)