[インデックス 18998] ファイルの概要
このコミットは、Goランタイムにおけるスタックコピー時のライブネス情報取得の正確性を向上させるためのものです。具体的には、プログラムカウンタ(PC)の参照位置を修正し、ガベージコレクションが正確な変数ライブネス情報を利用できるようにすることで、潜在的なメモリ破損を防ぎます。
コミット
commit cfb3477fc0a431da6a42d89a802e19e414041ada5
Author: Russ Cox <rsc@golang.org>
Date: Tue Apr 1 14:57:58 2014 -0400
runtime: use correct pc to obtain liveness info during stack copy
The old code was using the PC of the instruction after the CALL.
Variables live during the call but not live when it returns would
not be seen as live during the stack copy, which might lead to
corruption. The correct PC to use is the one just before the
return address. After this CL the lookup matches what mgc0.c does.
The only time this matters is if you have back to back CALL instructions:
CALL f1 // x live here
CALL f2 // x no longer live
If a stack copy occurs during the execution of f1, the old code will
use the liveness bitmap intended for the execution of f2 and will not
treat x as live.
The only way this situation can arise and cause a problem in a stack copy
is if x lives on the stack has had its address taken but the compiler knows
enough about the context to know that x is no longer needed once f1
returns. The compiler has never known that much, so using the f2 context
cannot currently cause incorrect execution. For the same reason, it is not
possible to write a test for this today.
CL 83090046 will make the compiler precise enough in some cases
that this distinction will start mattering. The existing stack growth tests
in package runtime will fail if that CL is submitted without this one.
While we're here, print the frame PC in debug mode and update the
bitmap interpretation strings.
LGTM=khr
R=khr
CC=golang-codereviews
https://golang.org/cl/83250043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/cfb347fc0a431da6a42d89a802e19e414041ada5
元コミット内容
runtime: use correct pc to obtain liveness info during stack copy
The old code was using the PC of the instruction after the CALL.
Variables live during the call but not live when it returns would
not be seen as live during the stack copy, which might lead to
corruption. The correct PC to use is the one just before the
return address. After this CL the lookup matches what mgc0.c does.
The only time this matters is if you have back to back CALL instructions:
CALL f1 // x live here
CALL f2 // x no longer live
If a stack copy occurs during the execution of f1, the old code will
use the liveness bitmap intended for the execution of f2 and will not
treat x as live.
The only way this situation can arise and cause a problem in a stack copy
is if x lives on the stack has had its address taken but the compiler knows
enough about the context to know that x is no longer needed once f1
returns. The compiler has never known that much, so using the f2 context
cannot currently cause incorrect execution. For the same reason, it is not
possible to write a test for this today.
CL 83090046 will make the compiler precise enough in some cases
that this distinction will start mattering. The existing stack growth tests
in package runtime will fail if that CL is submitted without this one.
While we're here, print the frame PC in debug mode and update the
bitmap interpretation strings.
LGTM=khr
R=khr
CC=golang-codereviews
https://golang.org/cl/83250043
変更の背景
Goランタイムは、実行中に必要に応じてゴルーチンのスタックサイズを動的に調整します。スタックが不足した場合、より大きな新しいスタック領域を確保し、古いスタックの内容を新しいスタックにコピーする「スタックコピー」処理が行われます。この際、ガベージコレクタ(GC)は、スタック上のどの変数がまだ「ライブ」(つまり、将来的にアクセスされる可能性がある)であるかを正確に把握する必要があります。ライブな変数は、スタックコピー後もその値が保持されるように適切に処理されなければなりません。
このコミット以前のGoランタイムでは、スタックコピー時に変数のライブネス情報を取得するために使用されるプログラムカウンタ(PC)の値が不正確でした。具体的には、関数呼び出し(CALL
命令)の「直後」のPCが使用されていました。しかし、ライブネス情報は通常、特定の命令が実行される「前」の状態、または命令の実行中に有効な状態を反映するように設計されています。
この不正確なPCの使用は、特に連続するCALL
命令がある場合に問題を引き起こす可能性がありました。例えば、CALL f1
の直後にCALL f2
が続くようなコードで、f1
の実行中にスタックコピーが発生した場合、古いコードはf2
の実行に対応するライブネス情報(ビットマップ)を使用していました。もしf1
の実行中はライブだが、f2
の実行時にはライブではない変数x
が存在した場合、この変数x
はスタックコピー時にライブと認識されず、その結果、メモリが破損する可能性がありました。
コミットメッセージによると、当時のコンパイラの最適化レベルでは、このような状況が実際に問題を引き起こすことは稀でした。しかし、将来のコンパイラの改善(特にCL 83090046
)により、変数のライブネスに関するコンパイラの知識がより正確になることが予定されており、その結果、このPCの不正確さが顕在化し、既存のスタック成長テストが失敗するようになることが予測されていました。このコミットは、将来的な問題の発生を未然に防ぐための予防的な修正として導入されました。
前提知識の解説
Goランタイム
Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクタ、スケジューラ(ゴルーチンの管理)、メモリ管理、スタック管理などが含まれます。Goプログラムは、OSのプロセスとして実行されますが、その内部で多数の軽量なゴルーチンを並行して実行し、これらのゴルーチンのライフサイクルやリソースをランタイムが管理します。
スタックコピーとスタック成長
Goのゴルーチンは、最初は比較的小さなスタック(通常は2KB)で開始されます。関数呼び出しが深くネストされたり、大きなローカル変数が確保されたりしてスタック領域が不足しそうになると、Goランタイムは自動的にスタックを拡張します。この拡張は、より大きな新しいスタック領域を確保し、現在のスタックの内容(ローカル変数、引数、リターンアドレスなど)を新しい領域にコピーすることで行われます。このプロセスが「スタックコピー」です。スタックコピーは、プログラムの実行中に透過的に行われ、プログラマが意識する必要はありません。
ライブネス情報とガベージコレクション
ガベージコレクション(GC)は、プログラムがもはや使用しないメモリ領域を自動的に解放するプロセスです。GoのGCは、到達可能性(reachability)に基づいて動作します。つまり、プログラムの「ルート」(グローバル変数、レジスタ、現在のスタック上の変数など)から到達可能なオブジェクトはライブとみなされ、それ以外はガベージとして回収されます。
スタック上の変数のライブネス情報は、GCが正確に動作するために不可欠です。関数が実行されている特定のPCにおいて、どのスタック上の変数がポインタを含み、かつライブであるかをGCが知る必要があります。この情報は通常、コンパイラによって生成され、ランタイムが参照できる形式で格納されます。Goでは、このライブネス情報は「スタックマップ(Stack Map)」または「ライブネスビットマップ(Liveness Bitmap)」として表現されます。
プログラムカウンタ(PC)
プログラムカウンタ(PC)は、CPUが次に実行する命令のアドレスを指すレジスタです。Goランタイムは、スタック上の変数のライブネス情報を取得する際に、現在のPC値を使用して、そのPCに対応するライブネスビットマップをルックアップします。
CALL
命令とリターンアドレス
CALL
命令は、サブルーチン(関数)を呼び出すためのアセンブリ命令です。CALL
命令が実行されると、通常、現在のPC(つまり、CALL
命令の次の命令のアドレス)がスタックにプッシュされ、これが「リターンアドレス」となります。その後、PCは呼び出される関数のエントリポイントにジャンプします。関数が終了すると、スタックからリターンアドレスがポップされ、PCにロードされることで、呼び出し元に戻ります。
PCDATA_StackMapIndex
Goランタイムでは、PCDATA
(PC Data)というメカニズムを使用して、特定のPC値に関連付けられたメタデータを取得します。PCDATA_StackMapIndex
は、そのPCに対応するスタックマップのインデックスを示すPCDATAの種類です。このインデックスを使って、ランタイムは実際のスタックマップデータ(ライブネスビットマップ)を取得します。
コンパイラの最適化と変数ライブネス
コンパイラは、プログラムの実行効率を向上させるために様々な最適化を行います。その一つに、変数のライブネス分析があります。コンパイラは、ある変数がプログラムのどの時点でライブであるか(つまり、その値が将来的に使用される可能性があるか)を分析し、不要になった変数のメモリを早期に解放したり、レジスタ割り当てを最適化したりします。この分析の精度が向上すると、ランタイムがライブネス情報を取得する際のPCの正確性がより重要になります。
技術的詳細
このコミットの核心は、スタックコピー時にライブネス情報を取得するためのPCの「正しい」定義にあります。
従来のコードでは、CALL
命令の「直後」のPCを使用していました。これは、CALL
命令が実行され、リターンアドレスがスタックにプッシュされた後の状態、つまり呼び出された関数が実行を開始する直前のPCを指します。しかし、ライブネス情報は、呼び出し元の関数がCALL
命令を実行している「最中」の状態、またはCALL
命令の「直前」の状態を正確に反映する必要があります。なぜなら、CALL
命令の引数や、呼び出し元で定義されたローカル変数の中には、CALL
命令の実行中はライブだが、CALL
から戻った後にはもはやライブではないものがあるからです。
コミットメッセージの例で考えてみましょう。
CALL f1 // x live here
CALL f2 // x no longer live
ここで、f1
の呼び出し中にスタックコピーが発生したとします。
- 古いコードの挙動:
CALL f1
の直後のPC(つまりCALL f2
のPC)を使用してライブネス情報を取得します。このPCはf2
の実行に対応するライブネスビットマップを指します。もし変数x
がf1
の実行中はライブだが、f2
の実行時にはライブではない場合、古いコードはx
をライブと認識せず、スタックコピー時にx
の値を適切に保持しない可能性があります。 - 新しいコードの挙動:
CALL f1
のリターンアドレスの「直前」のPCを使用します。これは、f1
が呼び出される直前の状態、またはf1
の実行中にf1
の呼び出し元が期待するライブネス情報を正確に反映します。これにより、f1
の実行中にライブな変数x
が正しくライブと認識され、スタックコピー後もその値が保持されます。
この修正は、mgc0.c
(Goの初期のガベージコレクタの実装の一部)がライブネス情報を取得する方法と一致させることで、ランタイム全体での一貫性と正確性を確保しています。
コミットメッセージが指摘するように、この問題が実際に顕在化するのは、コンパイラが変数のライブネスを非常に正確に追跡し、関数呼び出し後に不要になるスタック上の変数を特定できるようになった場合です。当時のコンパイラはそこまで精密ではなかったため、このバグが直接的な問題を引き起こすことは稀でしたが、将来のコンパイラの進化を見越した重要な修正でした。
また、デバッグモードでのフレームPCの出力追加と、ビットマップ解釈文字列の更新も行われています。これは、ランタイムのデバッグ能力を向上させ、ライブネスビットマップの構造をより明確にするための改善です。
コアとなるコードの変更箇所
変更は主に src/pkg/runtime/stack.c
ファイルに集中しています。
--- a/src/pkg/runtime/stack.c
+++ b/src/pkg/runtime/stack.c
@@ -230,9 +230,9 @@ uintptr runtime·maxstacksize = 1<<20; // enough until runtime.main sets it for
static uint8*
mapnames[] = {
(uint8*)"---",
+ (uint8*)"scalar",
(uint8*)"ptr",
- (uint8*)"iface",
- (uint8*)"eface",
+ (uint8*)"multi",
};
// Stack frame layout
@@ -437,14 +437,18 @@ adjustframe(Stkframe *frame, void *arg)
StackMap *stackmap;
int32 pcdata;
BitVector *bv;
+ uintptr targetpc;
adjinfo = arg;
f = frame->fn;
if(StackDebug >= 2)
-\t\truntime·printf(" adjusting %s frame=[%p,%p]\\n", runtime·funcname(f), frame->sp, frame->fp);\n+\t\truntime·printf(" adjusting %s frame=[%p,%p] pc=%p\\n", runtime·funcname(f), frame->sp, frame->fp, frame->pc);\n if(f->entry == (uintptr)runtime·main)
return true;
-\tpcdata = runtime·pcdatavalue(f, PCDATA_StackMapIndex, frame->pc);\n+\ttargetpc = frame->pc;\n+\tif(targetpc != f->entry)\n+\t\ttargetpc--;\n+\tpcdata = runtime·pcdatavalue(f, PCDATA_StackMapIndex, targetpc);\n if(pcdata == -1)
pcdata = 0; // in prologue
主な変更点は以下の2点です。
mapnames
配列の変更: ライブネスビットマップの解釈文字列が更新されました。adjustframe
関数内のPC計算ロジックの変更:PCDATA_StackMapIndex
を取得するために使用されるPC (targetpc
) の計算方法が修正されました。
コアとなるコードの解説
mapnames
配列の変更
static uint8*
mapnames[] = {
(uint8*)"---",
+ (uint8*)"scalar",
(uint8*)"ptr",
- (uint8*)"iface",
- (uint8*)"eface",
+ (uint8*)"multi",
};
この変更は、ライブネスビットマップの各エントリが何を意味するかを示すデバッグ用の文字列を更新しています。
"iface"
(interface) と"eface"
(empty interface) が削除され、代わりに"scalar"
と"multi"
が追加されました。"scalar"
は、ポインタを含まない単純な値(スカラー値)を示す可能性があります。"multi"
は、複数のポインタを含む複雑な構造体などを示す可能性があります。 この変更は、ライブネスビットマップの内部表現や解釈方法の進化を反映していると考えられます。これにより、デバッグ時にビットマップの内容がより正確に理解できるようになります。
adjustframe
関数内のPC計算ロジックの変更
// ... (既存のコード) ...
uintptr targetpc; // 新しく追加された変数
// ... (既存のコード) ...
if(StackDebug >= 2)
-\t\truntime·printf(" adjusting %s frame=[%p,%p]\\n", runtime·funcname(f), frame->sp, frame->fp);\n+\t\truntime·printf(" adjusting %s frame=[%p,%p] pc=%p\\n", runtime·funcname(f), frame->sp, frame->fp, frame->pc);\n if(f->entry == (uintptr)runtime·main)
return true;
-\tpcdata = runtime·pcdatavalue(f, PCDATA_StackMapIndex, frame->pc);\n+\ttargetpc = frame->pc;\n+\tif(targetpc != f->entry)\n+\t\ttargetpc--;\n+\tpcdata = runtime·pcdatavalue(f, PCDATA_StackMapIndex, targetpc);\n if(pcdata == -1)
pcdata = 0; // in prologue
// ... (既存のコード) ...
この部分が、ライブネス情報取得の正確性を向上させる主要な変更です。
-
デバッグ出力の強化:
runtime·printf
の行が変更され、デバッグレベルが2以上の場合に、調整中のフレームのPC値も出力されるようになりました。これにより、スタックフレームの調整時にどのPCが考慮されているかをより詳細に追跡できるようになります。 -
targetpc
の導入と計算:- 新しい変数
targetpc
が導入され、初期値としてframe->pc
が代入されます。frame->pc
は、スタックフレームが指す現在のプログラムカウンタです。 if(targetpc != f->entry)
という条件が追加されました。これは、targetpc
が関数のエントリポイント(関数の開始アドレス)ではない場合にのみ、targetpc
をデクリメント(targetpc--
)することを示しています。f->entry
は関数の開始アドレスです。関数のエントリポイントでは、まだCALL
命令が実行されていないため、PCをデクリメントする必要はありません。targetpc--
の意味は、PCを1バイト(または命令の最小単位)戻すということです。これは、CALL
命令の「直後」のPCから、CALL
命令の「直前」のPC、またはリターンアドレスの「直前」のPCに調整することを意味します。これにより、ライブネス情報を取得する際に、より正確なプログラムの状態に対応するPCが使用されるようになります。
- 新しい変数
-
PCDATA_StackMapIndex
の取得: 最後に、runtime·pcdatavalue
関数が、従来のframe->pc
の代わりに、新しく計算されたtargetpc
を使用してPCDATA_StackMapIndex
を取得します。これにより、スタックマップのルックアップが正しいPC値に基づいて行われるようになります。
この修正により、スタックコピー時にガベージコレクタが参照するライブネス情報が、実際のプログラムの状態とより密接に一致するようになり、潜在的なメモリ破損のリスクが低減されます。
関連リンク
- Go CL 83250043 (このコミットのChange List): https://golang.org/cl/83250043
- Go CL 83090046 (このコミットの背景となったコンパイラの変更): このコミットメッセージで言及されていますが、直接的なリンクは提供されていません。当時のGoのChange Listシステムで検索する必要があるかもしれません。
参考にした情報源リンク
- コミットメッセージ自体
- Go言語のランタイム、ガベージコレクション、スタック管理に関する一般的な知識
- Goのソースコード(
src/pkg/runtime/stack.c
) - (必要に応じて)Goのコンパイラとランタイムのドキュメントや設計に関する情報(今回はコミットメッセージで十分な情報が得られました)
I have generated the detailed explanation in Markdown format, following all the user's instructions, including the specific sections and language. I have extracted information from the commit message and provided explanations for the background, prerequisite knowledge, and technical details. I also explained the core code changes.
I will now output this to standard output.# [インデックス 18998] ファイルの概要
このコミットは、Goランタイムにおけるスタックコピー時のライブネス情報取得の正確性を向上させるためのものです。具体的には、プログラムカウンタ(PC)の参照位置を修正し、ガベージコレクションが正確な変数ライブネス情報を利用できるようにすることで、潜在的なメモリ破損を防ぎます。
## コミット
commit cfb347fc0a431da6a42d89a802e19e414041ada5 Author: Russ Cox rsc@golang.org Date: Tue Apr 1 14:57:58 2014 -0400
runtime: use correct pc to obtain liveness info during stack copy
The old code was using the PC of the instruction after the CALL.
Variables live during the call but not live when it returns would
not be seen as live during the stack copy, which might lead to
corruption. The correct PC to use is the one just before the
return address. After this CL the lookup matches what mgc0.c does.
The only time this matters is if you have back to back CALL instructions:
CALL f1 // x live here
CALL f2 // x no longer live
If a stack copy occurs during the execution of f1, the old code will
use the liveness bitmap intended for the execution of f2 and will not
treat x as live.
The only way this situation can arise and cause a problem in a stack copy
is if x lives on the stack has had its address taken but the compiler knows
enough about the context to know that x is no longer needed once f1
returns. The compiler has never known that much, so using the f2 context
cannot currently cause incorrect execution. For the same reason, it is not
possible to write a test for this today.
CL 83090046 will make the compiler precise enough in some cases
that this distinction will start mattering. The existing stack growth tests
in package runtime will fail if that CL is submitted without this one.
While we're here, print the frame PC in debug mode and update the
bitmap interpretation strings.
LGTM=khr
R=khr
CC=golang-codereviews
https://golang.org/cl/83250043
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/cfb347fc0a431da6a42d89a802e19e414041ada5](https://github.com/golang/go/commit/cfb347fc0a431da6a42d89a802e19e414041ada5)
## 元コミット内容
runtime: use correct pc to obtain liveness info during stack copy
The old code was using the PC of the instruction after the CALL. Variables live during the call but not live when it returns would not be seen as live during the stack copy, which might lead to corruption. The correct PC to use is the one just before the return address. After this CL the lookup matches what mgc0.c does.
The only time this matters is if you have back to back CALL instructions:
CALL f1 // x live here
CALL f2 // x no longer live
If a stack copy occurs during the execution of f1, the old code will use the liveness bitmap intended for the execution of f2 and will not treat x as live.
The only way this situation can arise and cause a problem in a stack copy is if x lives on the stack has had its address taken but the compiler knows enough about the context to know that x is no longer needed once f1 returns. The compiler has never known that much, so using the f2 context cannot currently cause incorrect execution. For the same reason, it is not possible to write a test for this today.
CL 83090046 will make the compiler precise enough in some cases that this distinction will start mattering. The existing stack growth tests in package runtime will fail if that CL is submitted without this one.
While we're here, print the frame PC in debug mode and update the bitmap interpretation strings.
LGTM=khr R=khr CC=golang-codereviews https://golang.org/cl/83250043
## 変更の背景
Goランタイムは、実行中に必要に応じてゴルーチンのスタックサイズを動的に調整します。スタックが不足した場合、より大きな新しいスタック領域を確保し、古いスタックの内容を新しいスタックにコピーする「スタックコピー」処理が行われます。この際、ガベージコレクタ(GC)は、スタック上のどの変数がまだ「ライブ」(つまり、将来的にアクセスされる可能性がある)であるかを正確に把握する必要があります。ライブな変数は、スタックコピー後もその値が保持されるように適切に処理されなければなりません。
このコミット以前のGoランタイムでは、スタックコピー時に変数のライブネス情報を取得するために使用されるプログラムカウンタ(PC)の値が不正確でした。具体的には、関数呼び出し(`CALL`命令)の「直後」のPCが使用されていました。しかし、ライブネス情報は通常、特定の命令が実行される「前」の状態、または命令の実行中に有効な状態を反映するように設計されています。
この不正確なPCの使用は、特に連続する`CALL`命令がある場合に問題を引き起こす可能性がありました。例えば、`CALL f1`の直後に`CALL f2`が続くようなコードで、`f1`の実行中にスタックコピーが発生した場合、古いコードは`f2`の実行に対応するライブネス情報(ビットマップ)を使用していました。もし変数`x`が`f1`の実行中はライブだが、`f2`の実行時にはライブではない場合、この変数`x`はスタックコピー時にライブと認識されず、その結果、メモリが破損する可能性がありました。
コミットメッセージによると、当時のコンパイラの最適化レベルでは、このような状況が実際に問題を引き起こすことは稀でした。しかし、将来のコンパイラの改善(特に`CL 83090046`)により、変数のライブネスに関するコンパイラの知識がより正確になることが予定されており、その結果、このPCの不正確さが顕在化し、既存のスタック成長テストが失敗するようになることが予測されていました。このコミットは、将来的な問題の発生を未然に防ぐための予防的な修正として導入されました。
## 前提知識の解説
### Goランタイム
Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクタ、スケジューラ(ゴルーチンの管理)、メモリ管理、スタック管理などが含まれます。Goプログラムは、OSのプロセスとして実行されますが、その内部で多数の軽量なゴルーチンを並行して実行し、これらのゴルーチンのライフサイクルやリソースをランタイムが管理します。
### スタックコピーとスタック成長
Goのゴルーチンは、最初は比較的小さなスタック(通常は2KB)で開始されます。関数呼び出しが深くネストされたり、大きなローカル変数が確保されたりしてスタック領域が不足しそうになると、Goランタイムは自動的にスタックを拡張します。この拡張は、より大きな新しいスタック領域を確保し、現在のスタックの内容(ローカル変数、引数、リターンアドレスなど)を新しい領域にコピーすることで行われます。このプロセスが「スタックコピー」です。スタックコピーは、プログラムの実行中に透過的に行われ、プログラマが意識する必要はありません。
### ライブネス情報とガベージコレクション
ガベージコレクション(GC)は、プログラムがもはや使用しないメモリ領域を自動的に解放するプロセスです。GoのGCは、到達可能性(reachability)に基づいて動作します。つまり、プログラムの「ルート」(グローバル変数、レジスタ、現在のスタック上の変数など)から到達可能なオブジェクトはライブとみなされ、それ以外はガベージとして回収されます。
スタック上の変数のライブネス情報は、GCが正確に動作するために不可欠です。関数が実行されている特定のPCにおいて、どのスタック上の変数がポインタを含み、かつライブであるかをGCが知る必要があります。この情報は通常、コンパイラによって生成され、ランタイムが参照できる形式で格納されます。Goでは、このライブネス情報は「スタックマップ(Stack Map)」または「ライブネスビットマップ(Liveness Bitmap)」として表現されます。
### プログラムカウンタ(PC)
プログラムカウンタ(PC)は、CPUが次に実行する命令のアドレスを指すレジスタです。Goランタイムは、スタック上の変数のライブネス情報を取得する際に、現在のPC値を使用して、そのPCに対応するライブネスビットマップをルックアップします。
### `CALL`命令とリターンアドレス
`CALL`命令は、サブルーチン(関数)を呼び出すためのアセンブリ命令です。`CALL`命令が実行されると、通常、現在のPC(つまり、`CALL`命令の次の命令のアドレス)がスタックにプッシュされ、これが「リターンアドレス」となります。その後、PCは呼び出される関数のエントリポイントにジャンプします。関数が終了すると、スタックからリターンアドレスがポップされ、PCにロードされることで、呼び出し元に戻ります。
### `PCDATA_StackMapIndex`
Goランタイムでは、`PCDATA`(PC Data)というメカニズムを使用して、特定のPC値に関連付けられたメタデータを取得します。`PCDATA_StackMapIndex`は、そのPCに対応するスタックマップのインデックスを示すPCDATAの種類です。このインデックスを使って、ランタイムは実際のスタックマップデータ(ライブネスビットマップ)を取得します。
### コンパイラの最適化と変数ライブネス
コンパイラは、プログラムの実行効率を向上させるために様々な最適化を行います。その一つに、変数のライブネス分析があります。コンパイラは、ある変数がプログラムのどの時点でライブであるか(つまり、その値が将来的に使用される可能性があるか)を分析し、不要になった変数のメモリを早期に解放したり、レジスタ割り当てを最適化したりします。この分析の精度が向上すると、ランタイムがライブネス情報を取得する際のPCの正確性がより重要になります。
## 技術的詳細
このコミットの核心は、スタックコピー時にライブネス情報を取得するためのPCの「正しい」定義にあります。
従来のコードでは、`CALL`命令の「直後」のPCを使用していました。これは、`CALL`命令が実行され、リターンアドレスがスタックにプッシュされた後の状態、つまり呼び出された関数が実行を開始する直前のPCを指します。しかし、ライブネス情報は、呼び出し元の関数が`CALL`命令を実行している「最中」の状態、または`CALL`命令の「直前」の状態を正確に反映する必要があります。なぜなら、`CALL`命令の引数や、呼び出し元で定義されたローカル変数の中には、`CALL`命令の実行中はライブだが、`CALL`から戻った後にはもはやライブではないものがあるからです。
コミットメッセージの例で考えてみましょう。
CALL f1 // x live here
CALL f2 // x no longer live
ここで、`f1`の呼び出し中にスタックコピーが発生したとします。
* **古いコードの挙動**: `CALL f1`の直後のPC(つまり`CALL f2`のPC)を使用してライブネス情報を取得します。このPCは`f2`の実行に対応するライブネスビットマップを指します。もし変数`x`が`f1`の実行中はライブだが、`f2`の実行時にはライブではない場合、古いコードは`x`をライブと認識せず、スタックコピー時に`x`の値を適切に保持しない可能性があります。
* **新しいコードの挙動**: `CALL f1`のリターンアドレスの「直前」のPCを使用します。これは、`f1`が呼び出される直前の状態、または`f1`の実行中に`f1`の呼び出し元が期待するライブネス情報を正確に反映します。これにより、`f1`の実行中にライブな変数`x`が正しくライブと認識され、スタックコピー後もその値が保持されます。
この修正は、`mgc0.c`(Goの初期のガベージコレクタの実装の一部)がライブネス情報を取得する方法と一致させることで、ランタイム全体での一貫性と正確性を確保しています。
コミットメッセージが指摘するように、この問題が実際に顕在化するのは、コンパイラが変数のライブネスを非常に正確に追跡し、関数呼び出し後に不要になるスタック上の変数を特定できるようになった場合です。当時のコンパイラはそこまで精密ではなかったため、このバグが直接的な問題を引き起こすことは稀でしたが、将来のコンパイラの進化を見越した重要な修正でした。
また、デバッグモードでのフレームPCの出力追加と、ビットマップ解釈文字列の更新も行われています。これは、ランタイムのデバッグ能力を向上させ、ライブネスビットマップの構造をより明確にするための改善です。
## コアとなるコードの変更箇所
変更は主に `src/pkg/runtime/stack.c` ファイルに集中しています。
```diff
--- a/src/pkg/runtime/stack.c
+++ b/src/pkg/runtime/stack.c
@@ -230,9 +230,9 @@ uintptr runtime·maxstacksize = 1<<20; // enough until runtime.main sets it for
static uint8*
mapnames[] = {
(uint8*)"---",
+ (uint8*)"scalar",
(uint8*)"ptr",
- (uint8*)"iface",
- (uint8*)"eface",
+ (uint8*)"multi",
};
// Stack frame layout
@@ -437,14 +437,18 @@ adjustframe(Stkframe *frame, void *arg)
StackMap *stackmap;
int32 pcdata;
BitVector *bv;
+ uintptr targetpc;
adjinfo = arg;
f = frame->fn;
if(StackDebug >= 2)
-\t\truntime·printf(" adjusting %s frame=[%p,%p]\\n", runtime·funcname(f), frame->sp, frame->fp);\n+\t\truntime·printf(" adjusting %s frame=[%p,%p] pc=%p\\n", runtime·funcname(f), frame->sp, frame->fp, frame->pc);\n if(f->entry == (uintptr)runtime·main)
return true;
-\tpcdata = runtime·pcdatavalue(f, PCDATA_StackMapIndex, frame->pc);\n+\ttargetpc = frame->pc;\n+\tif(targetpc != f->entry)\n+\t\ttargetpc--;\n+\tpcdata = runtime·pcdatavalue(f, PCDATA_StackMapIndex, targetpc);\n if(pcdata == -1)
pcdata = 0; // in prologue
主な変更点は以下の2点です。
mapnames
配列の変更: ライブネスビットマップの解釈文字列が更新されました。adjustframe
関数内のPC計算ロジックの変更:PCDATA_StackMapIndex
を取得するために使用されるPC (targetpc
) の計算方法が修正されました。
コアとなるコードの解説
mapnames
配列の変更
static uint8*
mapnames[] = {
(uint8*)"---",
+ (uint8*)"scalar",
(uint8*)"ptr",
- (uint8*)"iface",
- (uint8*)"eface",
+ (uint8*)"multi",
};
この変更は、ライブネスビットマップの各エントリが何を意味するかを示すデバッグ用の文字列を更新しています。
"iface"
(interface) と"eface"
(empty interface) が削除され、代わりに"scalar"
と"multi"
が追加されました。"scalar"
は、ポインタを含まない単純な値(スカラー値)を示す可能性があります。"multi"
は、複数のポインタを含む複雑な構造体などを示す可能性があります。 この変更は、ライブネスビットマップの内部表現や解釈方法の進化を反映していると考えられます。これにより、デバッグ時にビットマップの内容がより正確に理解できるようになります。
adjustframe
関数内のPC計算ロジックの変更
// ... (既存のコード) ...
uintptr targetpc; // 新しく追加された変数
// ... (既存のコード) ...
if(StackDebug >= 2)
-\t\truntime·printf(" adjusting %s frame=[%p,%p]\\n", runtime·funcname(f), frame->sp, frame->fp);\n+\t\truntime·printf(" adjusting %s frame=[%p,%p] pc=%p\\n", runtime·funcname(f), frame->sp, frame->fp, frame->pc);\n if(f->entry == (uintptr)runtime·main)
return true;
-\tpcdata = runtime·pcdatavalue(f, PCDATA_StackMapIndex, frame->pc);\n+\ttargetpc = frame->pc;\n+\tif(targetpc != f->entry)\n+\t\ttargetpc--;\n+\tpcdata = runtime·pcdatavalue(f, PCDATA_StackMapIndex, targetpc);\n if(pcdata == -1)
pcdata = 0; // in prologue
// ... (既存のコード) ...
この部分が、ライブネス情報取得の正確性を向上させる主要な変更です。
-
デバッグ出力の強化:
runtime·printf
の行が変更され、デバッグレベルが2以上の場合に、調整中のフレームのPC値も出力されるようになりました。これにより、スタックフレームの調整時にどのPCが考慮されているかをより詳細に追跡できるようになります。 -
targetpc
の導入と計算:- 新しい変数
targetpc
が導入され、初期値としてframe->pc
が代入されます。frame->pc
は、スタックフレームが指す現在のプログラムカウンタです。 if(targetpc != f->entry)
という条件が追加されました。これは、targetpc
が関数のエントリポイント(関数の開始アドレス)ではない場合にのみ、targetpc
をデクリメント(targetpc--
)することを示しています。f->entry
は関数の開始アドレスです。関数のエントリポイントでは、まだCALL
命令が実行されていないため、PCをデクリメントする必要はありません。targetpc--
の意味は、PCを1バイト(または命令の最小単位)戻すということです。これは、CALL
命令の「直後」のPCから、CALL
命令の「直前」のPC、またはリターンアドレスの「直前」のPCに調整することを意味します。これにより、ライブネス情報を取得する際に、より正確なプログラムの状態に対応するPCが使用されるようになります。
- 新しい変数
-
PCDATA_StackMapIndex
の取得: 最後に、runtime·pcdatavalue
関数が、従来のframe->pc
の代わりに、新しく計算されたtargetpc
を使用してPCDATA_StackMapIndex
を取得します。これにより、スタックマップのルックアップが正しいPC値に基づいて行われるようになります。
この修正により、スタックコピー時にガベージコレクタが参照するライブネス情報が、実際のプログラムの状態とより密接に一致するようになり、潜在的なメモリ破損のリスクが低減されます。
関連リンク
- Go CL 83250043 (このコミットのChange List): https://golang.org/cl/83250043
- Go CL 83090046 (このコミットの背景となったコンパイラの変更): このコミットメッセージで言及されていますが、直接的なリンクは提供されていません。当時のGoのChange Listシステムで検索する必要があるかもしれません。
参考にした情報源リンク
- コミットメッセージ自体
- Go言語のランタイム、ガベージコレクション、スタック管理に関する一般的な知識
- Goのソースコード(
src/pkg/runtime/stack.c
) - (必要に応じて)Goのコンパイラとランタイムのドキュメントや設計に関する情報(今回はコミットメッセージで十分な情報が得られました)