[インデックス 18306] ファイルの概要
このコミットは、Goコンパイラのデバッグ出力、特に-live
フラグ使用時のクラッシュを修正するものです。src/cmd/gc/plive.c
ファイル内の変更に焦点を当てており、ライブネス解析(liveness analysis)に関連するコードの堅牢性を向上させています。
コミット
commit 8027660abc871b0b7a3a9374ba313be7d3e2ac99
Author: Russ Cox <rsc@golang.org>
Date: Tue Jan 21 13:31:22 2014 -0500
cmd/gc: fix crash in -live debugging output
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/53930043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8027660abc871b0b7a3a9374ba313be7d3e2ac99
元コミット内容
cmd/gc: fix crash in -live debugging output
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/53930043
変更の背景
このコミットの背景には、Goコンパイラのデバッグ機能、特に-live
フラグを使用した際に発生するクラッシュがありました。Goコンパイラ(cmd/gc
)は、プログラムの最適化やデバッグのために様々な解析を行います。その一つが「ライブネス解析(liveness analysis)」です。ライブネス解析は、ある時点である変数が「ライブ(live)」であるか、すなわちその変数の値が将来的に使用される可能性があるかを判断するデータフロー解析の一種です。この情報は、ガベージコレクション(GC)やレジスタ割り当てなどの最適化に不可欠です。
-live
デバッグフラグは、このライブネス解析の結果を詳細に出力するために使用されます。しかし、特定の条件下でこのデバッグ出力の生成ロジックにバグがあり、コンパイラがクラッシュする問題が発生していました。このクラッシュは、特にポインタが絡む複雑なケースや、変数のアドレスが取られている(addrtaken
)場合に顕著だったと考えられます。
元のコードでは、変数のアドレスが取られているかどうか(node->addrtaken
)によって、varkill
(変数が「kill」される、つまりその値がもはや必要ない、または上書きされる)の扱いが適切に分岐されていませんでした。また、ACALL
(関数呼び出し)命令のデバッグ出力において、p->to.node
がnil
である可能性が考慮されておらず、間接呼び出しの場合にクラッシュを引き起こす可能性がありました。
このコミットは、これらのエッジケースを適切に処理し、-live
デバッグ出力の堅牢性を高めることを目的としています。
前提知識の解説
Goコンパイラ (cmd/gc
)
Goコンパイラは、Go言語のソースコードを機械語に変換するツールチェーンの一部です。cmd/gc
は、Goのフロントエンドとバックエンドの一部を担っており、構文解析、型チェック、中間表現(IR)の生成、最適化、コード生成などを行います。
ライブネス解析 (Liveness Analysis)
ライブネス解析は、コンパイラのデータフロー解析の一種です。プログラムの各ポイントにおいて、どの変数が「ライブ」であるか(その変数の値が将来的に読み取られる可能性があるか)を決定します。ライブでない変数は「デッド(dead)」と呼ばれ、そのストレージは再利用可能になります。これは、ガベージコレクションの効率化やレジスタ割り当ての最適化に非常に重要です。
Prog
(Program Instruction)
Goコンパイラの内部では、プログラムはProg
構造体のリストとして表現されます。これは、アセンブリライクな中間表現(IR)の命令を表します。各Prog
は、操作コード(as
)、ソースオペランド(from
)、デスティネーションオペランド(to
)などを含みます。
Node
Node
は、Goコンパイラの抽象構文木(AST)や中間表現におけるノードを表す汎用的な構造体です。変数、定数、関数呼び出し、演算子など、プログラムの様々な要素を表現します。Node
には、その変数がアドレスを取られているかを示すaddrtaken
フラグなど、多くの情報が含まれます。
Bvec
(Bit Vector)
Bvec
はビットベクタ(bit vector)の略で、コンパイラ最適化において集合を効率的に表現するために使用されるデータ構造です。各ビットが特定の要素の存在(1)または不在(0)を示します。ライブネス解析では、変数の集合(例:ライブな変数、使用される変数、killされる変数)を表現するために使用されます。
uevar
, varkill
, avarinit
これらはライブネス解析の文脈で使用されるビットベクタです。
uevar
(used by this instruction): 現在の命令で「使用される」変数の集合。varkill
(killed by this instruction): 現在の命令で「killされる」変数の集合。変数がkillされるとは、その変数の値がもはや必要ない、または新しい値で上書きされることを意味します。avarinit
(initialized or referred to by this instruction, only for variables with address taken but not escaping to heap): アドレスが取られているがヒープにエスケープしない変数で、現在の命令で初期化または参照される変数の集合。
addrtaken
Node
構造体のフィールドで、その変数のアドレスがプログラム内で取られているかどうかを示すブール値です。アドレスが取られている変数は、ポインタを介してアクセスされる可能性があり、コンパイラはこれらの変数を特別に扱う必要があります。
技術的詳細
このコミットの技術的詳細は、主にsrc/cmd/gc/plive.c
ファイル内のprogeffects
関数とlivenessepilogue
関数の修正にあります。
progeffects
関数の修正
progeffects
関数は、個々のProg
(中間命令)がライブネス解析のビットベクタ(uevar
, varkill
, avarinit
)に与える影響を計算します。
修正前:
from
オペランド(命令のソース)とto
オペランド(命令のデスティネーション)の両方について、node->addrtaken
がtrue
の場合、avarinit
ビットベクタにセットしていました。しかし、varkill
のロジックはaddrtaken
の有無に関わらず一律に適用されていました。特に、to
オペランドの場合、AKILL
(変数を明示的にkillする命令)のような特殊なケースが考慮されていませんでした。
修正後:
-
from
オペランドの処理:from->node->addrtaken
がtrue
の場合、avarinit
にセットするロジックはそのままですが、uevar
とvarkill
のセットはelse
ブロック(addrtaken
がfalse
の場合)に移動されました。これは、アドレスが取られている変数とそうでない変数で、uevar
とvarkill
のセマンティクスが異なることを明確にするためです。- コメントが追加され、
varkill
の意味がより明確になりました。「アドレスが取られていない変数については、変数が設定されたことを意味する」と「アドレスが取られている変数については、変数がデッドとマークされたことを意味する」という違いが強調されています。
-
to
オペランドの処理:to->node->addrtaken
がtrue
の場合の処理が大幅に改善されました。- もし命令が
AKILL
(変数を明示的にkillする命令)であれば、varkill
にセットされます。これは、アドレスが取られている変数がAKILL
によってデッドとマークされることを正確に反映しています。 AKILL
でなければ、以前と同様にavarinit
にセットされます。else
ブロック(addrtaken
がfalse
の場合)では、uevar
とvarkill
のセットロジックが以前と同様に適用されます。
この変更により、アドレスが取られている変数とそうでない変数に対するvarkill
とavarinit
の計算がより正確になり、ライブネス解析の精度が向上しました。特に、アドレスが取られている変数がAKILL
命令によってデッドとマークされるケースが正しく処理されるようになりました。
livenessepilogue
関数の修正
livenessepilogue
関数は、ライブネス解析のデバッグ出力(-live
フラグ使用時)を生成する部分です。
修正前:
ACALL
(関数呼び出し)命令のデバッグ出力において、p->to.node
がnil
である可能性が考慮されていませんでした。ACALL
命令は、直接的な関数呼び出し(p->to.node
が呼び出される関数のシンボルを指す)と、間接的な関数呼び出し(p->to.node
がnil
で、p->from
が呼び出し対象の関数ポインタを指す)の両方があります。p->to.node
がnil
の場合にp->to.node->sym->name
にアクセスしようとすると、ヌルポインタ参照によるクラッシュが発生していました。
修正後:
if(p->as == ACALL)
の条件に&& p->to.node
が追加されました。これにより、ACALL
命令かつp->to.node
が非nil
の場合にのみ、fmtprint(&fmt, "call to %s:", p->to.node->sym->name);
が実行されます。
else if(p->as == ACALL)
という新しい条件が追加され、p->as == ACALL
だがp->to.node
がnil
の場合(すなわち間接呼び出しの場合)にfmtprint(&fmt, "indirect call:");
と出力されるようになりました。
この修正により、間接関数呼び出しのデバッグ出力時に発生する可能性のあるクラッシュが防止され、デバッグ出力の堅牢性が向上しました。
コアとなるコードの変更箇所
変更はsrc/cmd/gc/plive.c
ファイルに集中しています。
--- a/src/cmd/gc/plive.c
+++ b/src/cmd/gc/plive.c
@@ -635,7 +635,9 @@ isfunny(Node *node)
//
// The output vectors give bits for variables:
// uevar - used by this instruction
-// varkill - set by this instruction
+// varkill - killed by this instruction
+// for variables without address taken, means variable was set
+// for variables with address taken, means variable was marked dead
// avarinit - initialized or referred to by this instruction,
// only for variables with address taken but not escaping to heap
//
@@ -694,13 +696,15 @@ progeffects(Prog *prog, Array *vars, Bvec *uevar, Bvec *varkill, Bvec *avarinit)
pos = arrayindexof(vars, from->node);
if(pos == -1)
goto Next;
- if(from->node->addrtaken)
+ if(from->node->addrtaken) {
bvset(avarinit, pos);
- if(info.flags & (LeftRead | LeftAddr))
- bvset(uevar, pos);
- if(info.flags & LeftWrite)
- if(from->node != nil && (!isfat(from->node->type) || prog->as == AFATVARDEF))
- bvset(varkill, pos);
+ } else {
+ if(info.flags & (LeftRead | LeftAddr))
+ bvset(uevar, pos);
+ if(info.flags & LeftWrite)
+ if(from->node != nil && (!isfat(from->node->type) || prog->as == AFATVARDEF))
+ bvset(varkill, pos);
+ }
}
}
}
@@ -715,13 +719,18 @@ Next:
pos = arrayindexof(vars, to->node);
if(pos == -1)
goto Next1;
- if(to->node->addrtaken)
- bvset(avarinit, pos);
- if(info.flags & (RightRead | RightAddr))
- bvset(uevar, pos);
- if(info.flags & RightWrite)
- if(to->node != nil && (!isfat(to->node->type) || prog->as == AFATVARDEF))
- bvset(varkill, pos);
+ if(to->node->addrtaken) {
+ if(prog->as == AKILL)
+ bvset(varkill, pos);
+ else
+ bvset(avarinit, pos);
+ } else {
+ if(info.flags & (RightRead | RightAddr))
+ bvset(uevar, pos);
+ if(info.flags & RightWrite)
+ if(to->node != nil && (!isfat(to->node->type) || prog->as == AFATVARDEF))
+ bvset(varkill, pos);
+ }
}
}
}
@@ -1589,8 +1598,10 @@ livenessepilogue(Liveness *lv)
if(debuglive >= 1) {
fmtstrinit(&fmt);
fmtprint(&fmt, "%L: live at ", p->lineno);
- if(p->as == ACALL)
+ if(p->as == ACALL && p->to.node)
fmtprint(&fmt, "call to %s:", p->to.node->sym->name);
+ else if(p->as == ACALL)
+ fmtprint(&fmt, "indirect call:");
else
fmtprint(&fmt, "entry to %s:", p->from.node->sym->name);
numlive = 0;
コアとなるコードの解説
progeffects
関数内の変更
-
varkill
のコメント追加とロジックの分離:-// varkill - set by this instruction +// varkill - killed by this instruction +// for variables without address taken, means variable was set +// for variables with address taken, means variable was marked dead
varkill
の意味が「set by this instruction」から「killed by this instruction」に修正され、さらにアドレスが取られている変数とそうでない変数でその意味が異なることが明記されました。これは、後続のコード変更の意図を明確にするための重要な変更です。 -
from
オペランドの処理 (from->node->addrtaken
の分岐):- if(from->node->addrtaken) + if(from->node->addrtaken) { bvset(avarinit, pos);
-
if(info.flags & (LeftRead | LeftAddr))
-
bvset(uevar, pos);
-
if(info.flags & LeftWrite)
-
if(from->node != nil && (!isfat(from->node->type) || prog->as == AFATVARDEF))
-
bvset(varkill, pos);
-
} else {
-
if(info.flags & (LeftRead | LeftAddr))
-
bvset(uevar, pos);
-
if(info.flags & LeftWrite)
-
if(from->node != nil && (!isfat(from->node->type) || prog->as == AFATVARDEF))
-
bvset(varkill, pos);
-
}
以前は`from->node->addrtaken`が`true`の場合に`avarinit`をセットし、その後に`addrtaken`の有無に関わらず`uevar`と`varkill`をセットしていました。修正後は、`addrtaken`が`true`の場合は`avarinit`のみをセットし、`else`ブロックで`addrtaken`が`false`の場合に`uevar`と`varkill`をセットするようにロジックが分離されました。これにより、アドレスが取られている変数とそうでない変数に対するライブネス情報の計算がより正確になりました。
to
オペランドの処理 (to->node->addrtaken
の分岐とAKILL
の考慮):
ここが最も重要な変更点の一つです。- if(to->node->addrtaken) - bvset(avarinit, pos); - if(info.flags & (RightRead | RightAddr)) - bvset(uevar, pos); - if(info.flags & RightWrite) - if(to->node != nil && (!isfat(to->node->type) || prog->as == AFATVARDEF)) - bvset(varkill, pos); + if(to->node->addrtaken) { + if(prog->as == AKILL) + bvset(varkill, pos); + else + bvset(avarinit, pos); + } else { + if(info.flags & (RightRead | RightAddr)) + bvset(uevar, pos); + if(info.flags & RightWrite) + if(to->node != nil && (!isfat(to->node->type) || prog->as == AFATVARDEF)) + bvset(varkill, pos); + }
to->node->addrtaken
がtrue
の場合、以前は無条件にavarinit
をセットしていました。修正後は、さらにprog->as == AKILL
(命令が変数を明示的にkillするものであるか)をチェックします。- もし
AKILL
であれば、varkill
にセットします。これは、アドレスが取られている変数がAKILL
命令によってデッドとマークされるというセマンティクスを正確に反映しています。 AKILL
でなければ、以前と同様にavarinit
にセットします。else
ブロック(addrtaken
がfalse
の場合)では、uevar
とvarkill
のセットロジックが以前と同様に適用されます。 この変更により、アドレスが取られている変数のライブネス状態がより正確に追跡されるようになりました。
- もし
livenessepilogue
関数内の変更
@@ -1589,8 +1598,10 @@ livenessepilogue(Liveness *lv)
if(debuglive >= 1) {
fmtstrinit(&fmt);
fmtprint(&fmt, "%L: live at ", p->lineno);
- if(p->as == ACALL)
+ if(p->as == ACALL && p->to.node)
fmtprint(&fmt, "call to %s:", p->to.node->sym->name);
+ else if(p->as == ACALL)
+ fmtprint(&fmt, "indirect call:");
else
fmtprint(&fmt, "entry to %s:", p->from.node->sym->name);
numlive = 0;
この変更は、ACALL
命令のデバッグ出力ロジックを修正しています。
- 以前は
if(p->as == ACALL)
の条件のみでp->to.node->sym->name
にアクセスしていました。 - 修正後は、
if(p->as == ACALL && p->to.node)
という条件になり、p->to.node
がnil
でない場合にのみ、直接呼び出しのシンボル名を出力します。 - 新たに
else if(p->as == ACALL)
という条件が追加され、p->as == ACALL
だがp->to.node
がnil
の場合(間接呼び出し)に「indirect call:」と出力するように変更されました。これにより、ヌルポインタ参照によるクラッシュが回避されます。
これらの変更は、Goコンパイラのライブネス解析の正確性とデバッグ出力の堅牢性を向上させるための重要な修正です。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Go Code Review: https://go.dev/doc/contribute#code-reviews
- Go Compiler Internals (非公式ながら参考になる情報源): https://go.dev/doc/articles/go_compiler_internals.html (これは古い記事ですが、基本的な概念理解に役立つ可能性があります)
参考にした情報源リンク
- Go言語のコミット履歴: https://github.com/golang/go/commits/master
- Go Code Review (CL 53930043): https://golang.org/cl/53930043 (このリンクは古いGoのコードレビューシステムのもので、現在はGo Gerritにリダイレクトされる可能性があります。)
- ライブネス解析に関する一般的な情報 (例: Wikipedia): https://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%83%96%E3%83%8D%E3%82%B9%E8%A7%A3%E6%9E%90
- コンパイラのデータフロー解析に関する書籍やオンラインリソース。