[インデックス 15954] ファイルの概要
このコミットは、GoランタイムにおけるARMアーキテクチャのスタックトレースバック処理に関するバグ修正です。具体的には、アセンブリの戻り関数へのフレームリンケージ値の取り扱いが不適切であったために発生していた、スタックアンワインド時の「オフバイワン」エラーを修正し、フォワードプログレス(処理の進行)を保証します。
コミット
commit 8480e6f476c70af47158983052a9325447e57ab3
Author: Carl Shapiro <cshapiro@google.com>
Date: Tue Mar 26 11:43:09 2013 -0700
runtime: ensure forward progress when unwinding an arm stack frame
The arm gentraceback mishandled frame linkage values pointing
to the assembly return function. This function is special as
its frame size is zero and it contains only one instruction.
These conditions would preserve the frame pointer and result
in an off by one error when unwinding the caller.
Fixes #5124
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/8023043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8480e6f476c70af47158983052a9325447e57ab3
元コミット内容
runtime: ensure forward progress when unwinding an arm stack frame
The arm gentraceback mishandled frame linkage values pointing
to the assembly return function. This function is special as
its frame size is zero and it contains only one instruction.
These conditions would preserve the frame pointer and result
in an off by one error when unwinding the caller.
Fixes #5124
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/8023043
変更の背景
この変更は、GoランタイムがARMアーキテクチャ上でスタックトレースバックを行う際に発生していたバグ(Issue 5124)を修正するために行われました。
Goプログラムが実行される際、特にエラー発生時やデバッグ時に、現在の実行パス(コールスタック)を遡って関数呼び出しの履歴を辿る「スタックトレースバック」という処理が行われます。これは、どの関数がどの関数を呼び出したか、その時点でのプログラムの状態はどうだったか、といった情報を得るために不可欠です。
ARMアーキテクチャにおいて、特定の「アセンブリの戻り関数」(assembly return function)がスタックフレームのアンワインド(巻き戻し)処理において特殊な振る舞いをすることが問題の原因でした。この関数は、フレームサイズがゼロであり、かつ単一の命令しか含まないという特徴を持っていました。gentraceback
関数(スタックトレースバックを生成するGoランタイム内の関数)が、このような特殊な関数のフレームリンケージ値(スタックフレーム間の接続情報)を誤って解釈していました。
具体的には、この誤った解釈により、フレームポインタ(現在のスタックフレームの基点を指すレジスタ)が不適切に保持され、結果として呼び出し元(caller)のスタックフレームをアンワインドする際に「オフバイワン」エラーが発生していました。このエラーは、スタックトレースが正しく生成されない、あるいは途中で停止してしまう原因となり、デバッグやエラー解析を困難にしていました。
misc/cgo/test/callback.go
のテストコードには、このARM特有のアンワインド問題に関するコメントと条件分岐が含まれており、問題が認識されていたことを示しています。このコミットは、その根本原因を解決し、テストコードから一時的な回避策を削除することを目的としています。
前提知識の解説
- スタックトレースバック (Stack Traceback): プログラムの実行中に、現在実行中の関数から始まり、その関数を呼び出した関数、さらにその関数を呼び出した関数…と、関数呼び出しの連鎖を逆順に辿って表示する機能です。これにより、プログラムがどのようにして特定の位置に到達したかを理解できます。デバッグやエラー報告において非常に重要です。
- スタックフレーム (Stack Frame): 関数が呼び出されるたびに、その関数のローカル変数、引数、戻りアドレスなどがスタックメモリ上に確保される領域のことです。各関数呼び出しは独自のスタックフレームを持ち、関数が終了するとそのフレームはスタックから解放されます。
- フレームポインタ (Frame Pointer): スタックフレームの特定の場所(通常はフレームの先頭または末尾)を指すレジスタです。これにより、関数内のローカル変数や引数にアクセスしたり、スタックフレームをアンワインドしたりする際に基準点として使用されます。
- スタックアンワインド (Stack Unwinding): スタックトレースバックのプロセスで、現在のスタックフレームから一つ前の呼び出し元のスタックフレームへと、スタックを逆方向に辿っていく操作を指します。これには、フレームポインタや戻りアドレスなどの情報が利用されます。
- フレームリンケージ値 (Frame Linkage Values): スタックフレーム間で、呼び出し元と呼び出し先の関係を繋ぐための情報です。これには、呼び出し元の戻りアドレスや、呼び出し元のフレームポインタなどが含まれることがあります。
- アセンブリの戻り関数 (Assembly Return Function): Goランタイムのような低レベルなコードでは、Cgoコールやシステムコールなど、特定のアセンブリコードで実装された関数が存在します。これらの関数は、通常のGo関数とは異なるスタックフレーム構造を持つことがあり、スタックアンワインド時に特別な考慮が必要になる場合があります。
- オフバイワンエラー (Off-by-one Error): プログラミングにおける一般的なエラーの一種で、ループの回数、配列のインデックス、メモリのアドレス計算などで、期待される値よりも1つ多いか少ない値が使われることによって発生します。この文脈では、スタックフレームのサイズやポインタの計算が1バイト(または1ワード)ずれることで、誤ったメモリ位置を参照してしまうことを指します。
- フォワードプログレス (Forward Progress): システムやアルゴリズムが停止することなく、継続的に処理を進める能力を指します。スタックアンワインドの文脈では、トレースバック処理が途中で無限ループに陥ったり、誤った場所で停止したりすることなく、最後まで正しくスタックを辿りきれることを意味します。
- ARMアーキテクチャ: Advanced RISC Machineの略で、モバイルデバイスや組み込みシステムで広く使用されているCPUアーキテクチャです。Go言語は様々なアーキテクチャをサポートしており、それぞれのアーキテクチャに特化したランタイムコードが含まれています。
技術的詳細
このコミットの技術的詳細の中心は、src/pkg/runtime/traceback_arm.c
ファイル内のruntime·gentraceback
関数の修正にあります。この関数は、ARMアーキテクチャにおけるGoランタイムのスタックトレースバック処理を担当しています。
問題は、gentraceback
がアセンブリの戻り関数(f->entry
が指すエントリポイント)を処理する際に、そのスタックフレームの特性(フレームサイズがゼロで、単一命令のみ)を正しく考慮していなかった点にありました。
元のコードでは、以下のロジックがありました。
if(fp == nil) {
fp = sp;
if(pc > f->entry && f->frame >= 0)
fp += f->frame;
}
ここで、fp
はフレームポインタ、sp
はスタックポインタ、pc
はプログラムカウンタ、f
は現在の関数の情報を含むFunc
構造体です。
f->frame
は関数のスタックフレームサイズを示します。アセンブリの戻り関数では、このf->frame
がゼロであるにもかかわらず、pc > f->entry
の条件が真となる場合がありました。この時、fp += f->frame
はfp
を変化させず、結果としてfp
がsp
と同じ値を保持し続けることになります。
しかし、実際には、アセンブリの戻り関数はスタックに何もプッシュしないため、そのフレームサイズはゼロですが、呼び出し元の戻りアドレス(lr
レジスタに保存されている)はスタックに保存される必要があります。この戻りアドレスはuintptr
型であり、そのサイズはsizeof(uintptr)
です。
修正後のコードは以下のようになります。
if(fp == nil) {
fp = sp;
if(pc > f->entry && f->frame >= sizeof(uintptr))
fp += f->frame - sizeof(uintptr);
fp += sizeof(uintptr);
}
この変更のポイントは以下の2点です。
f->frame >= sizeof(uintptr)
の条件追加:if(pc > f->entry && f->frame >= 0)
がif(pc > f->entry && f->frame >= sizeof(uintptr))
に変更されました。 これにより、アセンブリの戻り関数のようにf->frame
がsizeof(uintptr)
よりも小さい(通常はゼロ)特殊なケースでは、最初のfp += f->frame - sizeof(uintptr);
の行が実行されなくなります。これは、フレームサイズがゼロの関数に対して不適切なオフセットを加算するのを防ぎます。fp += sizeof(uintptr);
の追加:if
ブロックの後に無条件でfp += sizeof(uintptr);
が追加されました。 これは、スタックアンワインドの際に、呼び出し元のスタックフレームの開始位置を正しく指すために、スタックポインタをsizeof(uintptr)
分だけ進めることを意味します。uintptr
は通常、ポインタのサイズ(32ビットシステムでは4バイト、64ビットシステムでは8バイト)を表します。アセンブリの戻り関数がスタックにプッシュする唯一のものが戻りアドレス(lr
)であるため、この調整によってフレームポインタが正しく次のフレームの開始位置を指すようになります。
この修正により、gentraceback
はアセンブリの戻り関数を通過する際に、フレームポインタを正確に更新できるようになり、結果としてスタックアンワインド時の「オフバイワン」エラーが解消され、スタックトレースバックのフォワードプログレスが保証されるようになりました。
また、misc/cgo/test/callback.go
の変更は、この修正が成功したことを示すものです。元のテストコードには、ARMアーキテクチャではruntime.goexit
までアンワインドできない、あるいはruntime.cgocall
以降のフレームをアンワインドできないという、Issue 5124に関連するコメントと条件分岐が含まれていました。根本的な問題が解決されたため、これらのARM特有の回避策やコメントは不要となり、削除されました。これにより、テストコードはよりシンプルになり、すべてのアーキテクチャで一貫したスタックトレースバックの振る舞いを期待できるようになりました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、以下の2つのファイルに集中しています。
-
src/pkg/runtime/traceback_arm.c
:runtime·gentraceback
関数内のスタックフレームポインタ(fp
)の計算ロジックが変更されました。- 具体的には、以下の行が変更されました。
--- a/src/pkg/runtime/traceback_arm.c +++ b/src/pkg/runtime/traceback_arm.c @@ -74,8 +74,9 @@ runtime·gentraceback(byte *pc0, byte *sp, byte *lr0, G *gp, int32 skip, uintptr tlr = *(uintptr*)sp; if(fp == nil) { fp = sp; - if(pc > f->entry && f->frame >= 0) - fp += f->frame; + if(pc > f->entry && f->frame >= sizeof(uintptr)) + fp += f->frame - sizeof(uintptr); + fp += sizeof(uintptr); } if(skip > 0)
-
misc/cgo/test/callback.go
:- ARMアーキテクチャ特有のスタックアンワインドに関するコメントと条件分岐が削除されました。
- 具体的には、
testCallbackCallers
関数内の以下の部分が変更されました。--- a/misc/cgo/test/callback.go +++ b/misc/cgo/test/callback.go @@ -160,9 +160,7 @@ func testCallbackCallers(t *testing.T) { nestedCall(func() { n = runtime.Callers(2, pc) }) - // The ARM cannot unwind all the way down to runtime.goexit. - // See issue 5124. - if n != len(name) && runtime.GOARCH != "arm" { + if n != len(name) { t.Errorf("expected %d frames, got %d", len(name), n) } for i := 0; i < n; i++ { @@ -179,10 +177,5 @@ func testCallbackCallers(t *testing.T) { if fname != name[i] { t.Errorf("expected function name %s, got %s", name[i], fname) } - // The ARM cannot unwind frames past runtime.cgocall. - // See issue 5124. - if runtime.GOARCH == "arm" && i == 4 { - break - } } }
コアとなるコードの解説
src/pkg/runtime/traceback_arm.c
の変更
このファイルは、GoランタイムがARMアーキテクチャ上でスタックトレースバックを実行するためのC言語コードを含んでいます。runtime·gentraceback
関数は、スタックフレームを一つずつ遡り、各フレームの情報を収集する役割を担っています。
変更の核心は、fp
(フレームポインタ)の計算方法の修正です。
-
変更前:
if(pc > f->entry && f->frame >= 0) fp += f->frame;
このロジックは、
pc
(現在のプログラムカウンタ)が関数のエントリポイントf->entry
を超えており、かつf->frame
(関数のスタックフレームサイズ)が0以上の場合に、fp
をf->frame
だけ進めるというものでした。しかし、アセンブリの戻り関数のようにf->frame
が0である特殊なケースでは、fp
はsp
(スタックポインタ)と同じ値を保持し続け、スタックアンワインドが正しく行われませんでした。これは、戻りアドレスがスタックにプッシュされているにもかかわらず、その分だけfp
が進まないため、次のフレームの開始位置を誤って指してしまう「オフバイワン」エラーを引き起こしていました。 -
変更後:
if(pc > f->entry && f->frame >= sizeof(uintptr)) fp += f->frame - sizeof(uintptr); fp += sizeof(uintptr);
if(pc > f->entry && f->frame >= sizeof(uintptr))
この条件は、f->frame
がsizeof(uintptr)
(ポインタのサイズ、通常4バイトまたは8バイト)以上の場合にのみ、fp += f->frame - sizeof(uintptr);
を実行するように変更されました。これにより、フレームサイズがゼロのアセンブリの戻り関数など、sizeof(uintptr)
よりも小さいフレームを持つ特殊な関数に対して、不適切なオフセット加算が行われるのを防ぎます。fp += sizeof(uintptr);
この行は、if
ブロックの後に無条件で追加されました。これは、スタックアンワインドの各ステップで、スタックポインタをsizeof(uintptr)
分だけ進めることを意味します。アセンブリの戻り関数がスタックにプッシュする唯一のものが戻りアドレス(lr
レジスタに保存されている)であるため、この調整によってフレームポインタが正しく次のフレームの開始位置を指すようになります。これにより、スタックアンワインドが正確に行われ、フォワードプログレスが保証されます。
misc/cgo/test/callback.go
の変更
このファイルは、Cgoコールバックのテストケースを含んでいます。元のコードには、ARMアーキテクチャにおけるスタックトレースバックの制限に関するコメントと条件分岐が含まれていました。
-
変更前:
// The ARM cannot unwind all the way down to runtime.goexit. // See issue 5124. if n != len(name) && runtime.GOARCH != "arm" { t.Errorf("expected %d frames, got %d", len(name), n) } // ... // The ARM cannot unwind frames past runtime.cgocall. // See issue 5124. if runtime.GOARCH == "arm" && i == 4 { break }
これらのコードは、ARMアーキテクチャではスタックトレースバックが完全に行えないという既知の問題(Issue 5124)に対する一時的な回避策でした。テストがARM上で実行される場合、特定のフレーム数でトレースバックを停止させたり、フレーム数のチェックをスキップしたりしていました。
-
変更後: 上記のコメントとARM特有の条件分岐が完全に削除されました。
if n != len(name) { // ARM特有の条件が削除 t.Errorf("expected %d frames, got %d", len(name), n) } // ... // ARM特有のbreak条件が削除
これは、
src/pkg/runtime/traceback_arm.c
における根本的な修正により、ARMアーキテクチャでもスタックトレースバックが正しく機能するようになったため、これらの回避策が不要になったことを示しています。テストコードがクリーンになり、すべてのアーキテクチャで同じ期待される振る舞いを検証できるようになりました。
これらの変更により、GoランタイムはARMアーキテクチャ上でもより堅牢なスタックトレースバック機能を提供するようになりました。
関連リンク
- Go Issue 5124: https://github.com/golang/go/issues/5124
- Go CL 8023043: https://golang.org/cl/8023043 (このコミットに対応するGoのコードレビューシステム上のチェンジリスト)
参考にした情報源リンク
- Go言語の公式ドキュメント (Go runtime, ARM architecture, stack unwindingに関する一般的な情報)
- Go言語のソースコード (特に
src/pkg/runtime/
ディレクトリ内の関連ファイル) - ARMアーキテクチャのリファレンスマニュアル (スタックフレーム、レジスタ、関数呼び出し規約に関する情報)
- スタックトレースバック、スタックアンワインドに関する一般的なコンピュータサイエンスの資料
- Issue 5124の議論内容 (GitHubのIssueページ)
- Goのチェンジリスト (CL) の詳細 (Goのコードレビューシステム)
[インデックス 15954] ファイルの概要
このコミットは、GoランタイムにおけるARMアーキテクチャのスタックトレースバック処理に関するバグ修正です。具体的には、アセンブリの戻り関数へのフレームリンケージ値の取り扱いが不適切であったために発生していた、スタックアンワインド時の「オフバイワン」エラーを修正し、フォワードプログレス(処理の進行)を保証します。
コミット
commit 8480e6f476c70af47158983052a9325447e57ab3
Author: Carl Shapiro <cshapiro@google.com>
Date: Tue Mar 26 11:43:09 2013 -0700
runtime: ensure forward progress when unwinding an arm stack frame
The arm gentraceback mishandled frame linkage values pointing
to the assembly return function. This function is special as
its frame size is zero and it contains only one instruction.
These conditions would preserve the frame pointer and result
in an off by one error when unwinding the caller.
Fixes #5124
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/8023043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8480e6f476c70af47158983052a9325447e57ab3
元コミット内容
runtime: ensure forward progress when unwinding an arm stack frame
The arm gentraceback mishandled frame linkage values pointing
to the assembly return function. This function is special as
its frame size is zero and it contains only one instruction.
These conditions would preserve the frame pointer and result
in an off by one error when unwinding the caller.
Fixes #5124
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/8023043
変更の背景
この変更は、GoランタイムがARMアーキテクチャ上でスタックトレースバックを行う際に発生していたバグ(Issue 5124)を修正するために行われました。
Goプログラムが実行される際、特にエラー発生時やデバッグ時に、現在の実行パス(コールスタック)を遡って関数呼び出しの履歴を辿る「スタックトレースバック」という処理が行われます。これは、どの関数がどの関数を呼び出したか、その時点でのプログラムの状態はどうだったか、といった情報を得るために不可欠です。
ARMアーキテクチャにおいて、特定の「アセンブリの戻り関数」(assembly return function)がスタックフレームのアンワインド(巻き戻し)処理において特殊な振る舞いをすることが問題の原因でした。この関数は、フレームサイズがゼロであり、かつ単一の命令しか含まないという特徴を持っていました。gentraceback
関数(スタックトレースバックを生成するGoランタイム内の関数)が、このような特殊な関数のフレームリンケージ値(スタックフレーム間の接続情報)を誤って解釈していました。
具体的には、この誤った解釈により、フレームポインタ(現在のスタックフレームの基点を指すレジスタ)が不適切に保持され、結果として呼び出し元(caller)のスタックフレームをアンワインドする際に「オフバイワン」エラーが発生していました。このエラーは、スタックトレースが正しく生成されない、あるいは途中で停止してしまう原因となり、デバッグやエラー解析を困難にしていました。
misc/cgo/test/callback.go
のテストコードには、このARM特有のアンワインド問題に関するコメントと条件分岐が含まれており、問題が認識されていたことを示しています。このコミットは、その根本原因を解決し、テストコードから一時的な回避策を削除することを目的としています。
なお、現在の公開されているGoのIssueトラッカーでは、直接「Issue 5124」という番号のIssueは見つかりませんでしたが、コミットメッセージに明記されていることから、当時の内部的なIssue番号であると考えられます。
前提知識の解説
- スタックトレースバック (Stack Traceback): プログラムの実行中に、現在実行中の関数から始まり、その関数を呼び出した関数、さらにその関数を呼び出した関数…と、関数呼び出しの連鎖を逆順に辿って表示する機能です。これにより、プログラムがどのようにして特定の位置に到達したかを理解できます。デバッグやエラー報告において非常に重要です。
- スタックフレーム (Stack Frame): 関数が呼び出されるたびに、その関数のローカル変数、引数、戻りアドレスなどがスタックメモリ上に確保される領域のことです。各関数呼び出しは独自のスタックフレームを持ち、関数が終了するとそのフレームはスタックから解放されます。
- フレームポインタ (Frame Pointer): スタックフレームの特定の場所(通常はフレームの先頭または末尾)を指すレジスタです。これにより、関数内のローカル変数や引数にアクセスしたり、スタックフレームをアンワインドしたりする際に基準点として使用されます。
- スタックアンワインド (Stack Unwinding): スタックトレースバックのプロセスで、現在のスタックフレームから一つ前の呼び出し元のスタックフレームへと、スタックを逆方向に辿っていく操作を指します。これには、フレームポインタや戻りアドレスなどの情報が利用されます。
- フレームリンケージ値 (Frame Linkage Values): スタックフレーム間で、呼び出し元と呼び出し先の関係を繋ぐための情報です。これには、呼び出し元の戻りアドレスや、呼び出し元のフレームポインタなどが含まれることがあります。
- アセンブリの戻り関数 (Assembly Return Function): Goランタイムのような低レベルなコードでは、Cgoコールやシステムコールなど、特定のアセンブリコードで実装された関数が存在します。これらの関数は、通常のGo関数とは異なるスタックフレーム構造を持つことがあり、スタックアンワインド時に特別な考慮が必要になる場合があります。
- オフバイワンエラー (Off-by-one Error): プログラミングにおける一般的なエラーの一種で、ループの回数、配列のインデックス、メモリのアドレス計算などで、期待される値よりも1つ多いか少ない値が使われることによって発生します。この文脈では、スタックフレームのサイズやポインタの計算が1バイト(または1ワード)ずれることで、誤ったメモリ位置を参照してしまうことを指します。
- フォワードプログレス (Forward Progress): システムやアルゴリズムが停止することなく、継続的に処理を進める能力を指します。スタックアンワインドの文脈では、トレースバック処理が途中で無限ループに陥ったり、誤った場所で停止したりすることなく、最後まで正しくスタックを辿りきれることを意味します。
- ARMアーキテクチャ: Advanced RISC Machineの略で、モバイルデバイスや組み込みシステムで広く使用されているCPUアーキテクチャです。Go言語は様々なアーキテクチャをサポートしており、それぞれのアーキテクチャに特化したランタイムコードが含まれています。
技術的詳細
このコミットの技術的詳細の中心は、src/pkg/runtime/traceback_arm.c
ファイル内のruntime·gentraceback
関数の修正にあります。この関数は、ARMアーキテクチャにおけるGoランタイムのスタックトレースバック処理を担当しています。
問題は、gentraceback
がアセンブリの戻り関数(f->entry
が指すエントリポイント)を処理する際に、そのスタックフレームの特性(フレームサイズがゼロで、単一命令のみ)を正しく考慮していなかった点にありました。
元のコードでは、以下のロジックがありました。
if(fp == nil) {
fp = sp;
if(pc > f->entry && f->frame >= 0)
fp += f->frame;
}
ここで、fp
はフレームポインタ、sp
はスタックポインタ、pc
はプログラムカウンタ、f
は現在の関数の情報を含むFunc
構造体です。
f->frame
は関数のスタックフレームサイズを示します。アセンブリの戻り関数では、このf->frame
がゼロであるにもかかわらず、pc > f->entry
の条件が真となる場合がありました。この時、fp += f->frame
はfp
を変化させず、結果としてfp
がsp
と同じ値を保持し続けることになります。
しかし、実際には、アセンブリの戻り関数はスタックに何もプッシュしないため、そのフレームサイズはゼロですが、呼び出し元の戻りアドレス(lr
レジスタに保存されている)はスタックに保存される必要があります。この戻りアドレスはuintptr
型であり、そのサイズはsizeof(uintptr)
です。
修正後のコードは以下のようになります。
if(fp == nil) {
fp = sp;
if(pc > f->entry && f->frame >= sizeof(uintptr))
fp += f->frame - sizeof(uintptr);
fp += sizeof(uintptr);
}
この変更のポイントは以下の2点です。
f->frame >= sizeof(uintptr)
の条件追加:if(pc > f->entry && f->frame >= 0)
がif(pc > f->entry && f->frame >= sizeof(uintptr))
に変更されました。 これにより、アセンブリの戻り関数のようにf->frame
がsizeof(uintptr)
よりも小さい(通常はゼロ)特殊なケースでは、最初のfp += f->frame - sizeof(uintptr);
の行が実行されなくなります。これは、フレームサイズがゼロの関数に対して不適切なオフセットを加算するのを防ぎます。fp += sizeof(uintptr);
の追加:if
ブロックの後に無条件でfp += sizeof(uintptr);
が追加されました。 これは、スタックアンワインドの際に、呼び出し元のスタックフレームの開始位置を正しく指すために、スタックポインタをsizeof(uintptr)
分だけ進めることを意味します。uintptr
は通常、ポインタのサイズ(32ビットシステムでは4バイト、64ビットシステムでは8バイト)を表します。アセンブリの戻り関数がスタックにプッシュする唯一のものが戻りアドレス(lr
)であるため、この調整によってフレームポインタが正しく次のフレームの開始位置を指すようになります。
この修正により、gentraceback
はアセンブリの戻り関数を通過する際に、フレームポインタを正確に更新できるようになり、結果としてスタックアンワインド時の「オフバイワン」エラーが解消され、スタックトレースバックのフォワードプログレスが保証されるようになりました。
また、misc/cgo/test/callback.go
の変更は、この修正が成功したことを示すものです。元のテストコードには、ARMアーキテクチャではruntime.goexit
までアンワインドできない、あるいはruntime.cgocall
以降のフレームをアンワインドできないという、Issue 5124に関連するコメントと条件分岐が含まれていました。根本的な問題が解決されたため、これらのARM特有の回避策やコメントは不要となり、削除されました。これにより、テストコードはよりシンプルになり、すべてのアーキテクチャで一貫したスタックトレースバックの振る舞いを期待できるようになりました。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、以下の2つのファイルに集中しています。
-
src/pkg/runtime/traceback_arm.c
:runtime·gentraceback
関数内のスタックフレームポインタ(fp
)の計算ロジックが変更されました。- 具体的には、以下の行が変更されました。
--- a/src/pkg/runtime/traceback_arm.c +++ b/src/pkg/runtime/traceback_arm.c @@ -74,8 +74,9 @@ runtime·gentraceback(byte *pc0, byte *sp, byte *lr0, G *gp, int32 skip, uintptr tlr = *(uintptr*)sp; if(fp == nil) { fp = sp; - if(pc > f->entry && f->frame >= 0) - fp += f->frame; + if(pc > f->entry && f->frame >= sizeof(uintptr)) + fp += f->frame - sizeof(uintptr); + fp += sizeof(uintptr); } if(skip > 0)
-
misc/cgo/test/callback.go
:- ARMアーキテクチャ特有のスタックアンワインドに関するコメントと条件分岐が削除されました。
- 具体的には、
testCallbackCallers
関数内の以下の部分が変更されました。--- a/misc/cgo/test/callback.go +++ b/misc/cgo/test/callback.go @@ -160,9 +160,7 @@ func testCallbackCallers(t *testing.T) { nestedCall(func() { n = runtime.Callers(2, pc) }) - // The ARM cannot unwind all the way down to runtime.goexit. - // See issue 5124. - if n != len(name) && runtime.GOARCH != "arm" { + if n != len(name) { t.Errorf("expected %d frames, got %d", len(name), n) } for i := 0; i < n; i++ {\ @@ -179,10 +177,5 @@ func testCallbackCallers(t *testing.T) { if fname != name[i] { t.Errorf("expected function name %s, got %s", name[i], fname) } - // The ARM cannot unwind frames past runtime.cgocall. - // See issue 5124. - if runtime.GOARCH == "arm" && i == 4 { - break - } } }
コアとなるコードの解説
src/pkg/runtime/traceback_arm.c
の変更
このファイルは、GoランタイムがARMアーキテクチャ上でスタックトレースバックを実行するためのC言語コードを含んでいます。runtime·gentraceback
関数は、スタックフレームを一つずつ遡り、各フレームの情報を収集する役割を担っています。
変更の核心は、fp
(フレームポインタ)の計算方法の修正です。
-
変更前:
if(pc > f->entry && f->frame >= 0) fp += f->frame;
このロジックは、
pc
(現在のプログラムカウンタ)が関数のエントリポイントf->entry
を超えており、かつf->frame
(関数のスタックフレームサイズ)が0以上の場合に、fp
をf->frame
だけ進めるというものでした。しかし、アセンブリの戻り関数のようにf->frame
が0である特殊なケースでは、fp
はsp
(スタックポインタ)と同じ値を保持し続け、スタックアンワインドが正しく行われませんでした。これは、戻りアドレスがスタックにプッシュされているにもかかわらず、その分だけfp
が進まないため、次のフレームの開始位置を誤って指してしまう「オフバイワン」エラーを引き起こしていました。 -
変更後:
if(fp == nil) { fp = sp; if(pc > f->entry && f->frame >= sizeof(uintptr)) fp += f->frame - sizeof(uintptr); fp += sizeof(uintptr); }
if(pc > f->entry && f->frame >= sizeof(uintptr))
この条件は、f->frame
がsizeof(uintptr)
(ポインタのサイズ、通常4バイトまたは8バイト)以上の場合にのみ、fp += f->frame - sizeof(uintptr);
を実行するように変更されました。これにより、フレームサイズがゼロのアセンブリの戻り関数など、sizeof(uintptr)
よりも小さいフレームを持つ特殊な関数に対して、不適切なオフセット加算が行われるのを防ぎます。fp += sizeof(uintptr);
この行は、if
ブロックの後に無条件で追加されました。これは、スタックアンワインドの各ステップで、スタックポインタをsizeof(uintptr)
分だけ進めることを意味します。アセンブリの戻り関数がスタックにプッシュする唯一のものが戻りアドレス(lr
レジスタに保存されている)であるため、この調整によってフレームポインタが正しく次のフレームの開始位置を指すようになります。
misc/cgo/test/callback.go
の変更
このファイルは、Cgoコールバックのテストケースを含んでいます。元のコードには、ARMアーキテクチャにおけるスタックトレースバックの制限に関するコメントと条件分岐が含まれていました。
-
変更前:
// The ARM cannot unwind all the way down to runtime.goexit. // See issue 5124. if n != len(name) && runtime.GOARCH != "arm" { t.Errorf("expected %d frames, got %d", len(name), n) } // ... // The ARM cannot unwind frames past runtime.cgocall. // See issue 5124. if runtime.GOARCH == "arm" && i == 4 { break }
これらのコードは、ARMアーキテクチャではスタックトレースバックが完全に行えないという既知の問題(Issue 5124)に対する一時的な回避策でした。テストがARM上で実行される場合、特定のフレーム数でトレースバックを停止させたり、フレーム数のチェックをスキップしたりしていました。
-
変更後: 上記のコメントとARM特有の条件分岐が完全に削除されました。
if n != len(name) { // ARM特有の条件が削除 t.Errorf("expected %d frames, got %d", len(name), n) } // ... // ARM特有のbreak条件が削除
これは、
src/pkg/runtime/traceback_arm.c
における根本的な修正により、ARMアーキテクチャでもスタックトレースバックが正しく機能するようになったため、これらの回避策が不要になったことを示しています。テストコードがクリーンになり、すべてのアーキテクチャで同じ期待される振る舞いを検証できるようになりました。
これらの変更により、GoランタイムはARMアーキテクチャ上でもより堅牢なスタックトレースバック機能を提供するようになりました。
関連リンク
- Go Issue 5124: https://github.com/golang/go/issues/5124 (現在の公開Issueトラッカーでは直接見つかりませんが、コミットメッセージに記載されています)
- Go CL 8023043: https://golang.org/cl/8023043 (このコミットに対応するGoのコードレビューシステム上のチェンジリスト)
参考にした情報源リンク
- Go言語の公式ドキュメント (Go runtime, ARM architecture, stack unwindingに関する一般的な情報)
- Go言語のソースコード (特に
src/pkg/runtime/
ディレクトリ内の関連ファイル) - ARMアーキテクチャのリファレンスマニュアル (スタックフレーム、レジスタ、関数呼び出し規約に関する情報)
- スタックトレースバック、スタックアンワインドに関する一般的なコンピュータサイエンスの資料
- Goのチェンジリスト (CL) の詳細 (Goのコードレビューシステム)