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

[インデックス 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.nodenilである可能性が考慮されておらず、間接呼び出しの場合にクラッシュを引き起こす可能性がありました。

このコミットは、これらのエッジケースを適切に処理し、-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->addrtakentrueの場合、avarinitビットベクタにセットしていました。しかし、varkillのロジックはaddrtakenの有無に関わらず一律に適用されていました。特に、toオペランドの場合、AKILL(変数を明示的にkillする命令)のような特殊なケースが考慮されていませんでした。

修正後:

  1. fromオペランドの処理:

    • from->node->addrtakentrueの場合、avarinitにセットするロジックはそのままですが、uevarvarkillのセットはelseブロック(addrtakenfalseの場合)に移動されました。これは、アドレスが取られている変数とそうでない変数で、uevarvarkillのセマンティクスが異なることを明確にするためです。
    • コメントが追加され、varkillの意味がより明確になりました。「アドレスが取られていない変数については、変数が設定されたことを意味する」と「アドレスが取られている変数については、変数がデッドとマークされたことを意味する」という違いが強調されています。
  2. toオペランドの処理:

    • to->node->addrtakentrueの場合の処理が大幅に改善されました。
    • もし命令がAKILL(変数を明示的にkillする命令)であれば、varkillにセットされます。これは、アドレスが取られている変数がAKILLによってデッドとマークされることを正確に反映しています。
    • AKILLでなければ、以前と同様にavarinitにセットされます。
    • elseブロック(addrtakenfalseの場合)では、uevarvarkillのセットロジックが以前と同様に適用されます。

この変更により、アドレスが取られている変数とそうでない変数に対するvarkillavarinitの計算がより正確になり、ライブネス解析の精度が向上しました。特に、アドレスが取られている変数がAKILL命令によってデッドとマークされるケースが正しく処理されるようになりました。

livenessepilogue関数の修正

livenessepilogue関数は、ライブネス解析のデバッグ出力(-liveフラグ使用時)を生成する部分です。

修正前: ACALL(関数呼び出し)命令のデバッグ出力において、p->to.nodenilである可能性が考慮されていませんでした。ACALL命令は、直接的な関数呼び出し(p->to.nodeが呼び出される関数のシンボルを指す)と、間接的な関数呼び出し(p->to.nodenilで、p->fromが呼び出し対象の関数ポインタを指す)の両方があります。p->to.nodenilの場合に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.nodenilの場合(すなわち間接呼び出しの場合)に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関数内の変更

  1. 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」に修正され、さらにアドレスが取られている変数とそうでない変数でその意味が異なることが明記されました。これは、後続のコード変更の意図を明確にするための重要な変更です。

  2. 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`をセットするようにロジックが分離されました。これにより、アドレスが取られている変数とそうでない変数に対するライブネス情報の計算がより正確になりました。
    
    
  1. 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->addrtakentrueの場合、以前は無条件にavarinitをセットしていました。修正後は、さらにprog->as == AKILL(命令が変数を明示的にkillするものであるか)をチェックします。
    • もしAKILLであれば、varkillにセットします。これは、アドレスが取られている変数がAKILL命令によってデッドとマークされるというセマンティクスを正確に反映しています。
    • AKILLでなければ、以前と同様にavarinitにセットします。
    • elseブロック(addrtakenfalseの場合)では、uevarvarkillのセットロジックが以前と同様に適用されます。 この変更により、アドレスが取られている変数のライブネス状態がより正確に追跡されるようになりました。

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.nodenilでない場合にのみ、直接呼び出しのシンボル名を出力します。
  • 新たにelse if(p->as == ACALL)という条件が追加され、p->as == ACALLだがp->to.nodenilの場合(間接呼び出し)に「indirect call:」と出力するように変更されました。これにより、ヌルポインタ参照によるクラッシュが回避されます。

これらの変更は、Goコンパイラのライブネス解析の正確性とデバッグ出力の堅牢性を向上させるための重要な修正です。

関連リンク

参考にした情報源リンク