[インデックス 19626] ファイルの概要
このコミットは、Goランタイムにおけるnacl amd64p32
アーキテクチャ上での不安定性(flakiness)を修正するものです。具体的には、newproc
(新しいゴルーチンを作成する内部関数)に関連するスタックポインタの計算において、レジスタのサイズとポインタのサイズが異なる環境での誤った仮定を修正しています。これにより、Goのビルドシステム上で発生していたランダムな問題が解決されました。
コミット
commit 84a36434d92e18eb12d8a86770bdb4936dff4703
Author: Russ Cox <rsc@golang.org>
Date: Fri Jun 27 20:13:16 2014 -0400
runtime: fix nacl amd64p32 flakiness
newproc takes two extra pointers, not two extra registers.
On amd64p32 (nacl) they are different.
We diagnosed this before the 1.3 cut but the tree was frozen.
I believe this is causing the random problems on the builder.
Fixes #8199.
TBR=r
CC=golang-codereviews
https://golang.org/cl/102710043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/84a36434d92e18eb12d8a86770bdb4936dff4703
元コミット内容
runtime: fix nacl amd64p32 flakiness
newproc takes two extra pointers, not two extra registers.
On amd64p32 (nacl) they are different.
We diagnosed this before the 1.3 cut but the tree was frozen.
I believe this is causing the random problems on the builder.
Fixes #8199.
変更の背景
この変更は、Goランタイムがnacl amd64p32
という特定のアーキテクチャ上で不安定な動作を示す問題を解決するために行われました。nacl amd64p32
は、Google Native Client (NaCl) 環境で使用されるアーキテクチャで、64ビットの命令セットを持ちながら、ポインタのサイズが32ビットに制限されているという特殊な特性があります。
Goランタイムのnewproc
関数(新しいゴルーチンを生成する際に呼び出される内部関数)は、ゴルーチンのスタックフレームをセットアップする過程で、スタックポインタのオフセットを計算します。この計算において、従来のコードでは「2つの追加ポインタ」分のスペースを確保する際に、誤って「2つの追加レジスタ」のサイズを仮定していました。
通常のamd64
アーキテクチャでは、レジスタのサイズ(uintreg
)もポインタのサイズ(uintptr
)も共に8バイト(64ビット)であるため、この違いは問題になりません。しかし、nacl amd64p32
では、レジスタは8バイトですが、ポインタは4バイトです。この不一致により、スタックポインタの計算が誤り、結果としてスタックフレームが正しく設定されず、ランダムなメモリ破壊やクラッシュといった「flakiness」(不安定性)がGoのビルドシステム上で発生していました。
この問題はGo 1.3のリリース前に診断されていましたが、リリースツリーが凍結されていたため、このコミットで修正が適用されました。コミットメッセージには「I believe this is causing the random problems on the builder.」とあり、ビルドシステムでの不安定性の根本原因であると確信されていたことが示されています。
前提知識の解説
Go Runtime (Goランタイム)
Goランタイムは、Goプログラムの実行を管理するGo言語の不可欠な部分です。これには、ガベージコレクション(GC)、ゴルーチン(goroutine)のスケジューリング、メモリ管理、チャネル操作、システムコールなどが含まれます。Goプログラムは、OS上で直接実行されるのではなく、Goランタイムによって抽象化された環境で動作します。
Goroutines (ゴルーチン) と newproc
ゴルーチンは、Go言語における軽量な並行処理の単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行することも可能です。newproc
は、Goランタイム内部で新しいゴルーチンを作成する際に呼び出される関数です。この関数は、新しいゴルーチンのスタックを割り当て、初期化し、スケジューラに登録する役割を担います。スタックの割り当てと初期化の際には、スタックポインタの正確な計算が不可欠です。
Stack Pointers (スタックポインタ - SP)
スタックポインタ(SP)は、CPUのレジスタの一つで、現在の関数呼び出しのスタックフレームの最上部(または最下部、アーキテクチャによる)を指し示します。関数が呼び出されると、引数、戻りアドレス、ローカル変数などがスタックにプッシュされ、SPが更新されます。関数から戻る際には、これらの情報がポップされ、SPが元に戻ります。スタックポインタの計算が誤ると、メモリ上の不正な領域にアクセスしたり、スタックオーバーフローを引き起こしたりする可能性があります。
amd64p32
(Native Client - NaCl)
amd64p32
は、GoogleのNative Client (NaCl) プロジェクトで使用された特定のターゲットアーキテクチャです。これは、64ビットの命令セット(amd64
)を使用しながらも、ポインタのサイズが32ビット(p32
)に制限されているという特徴を持ちます。
- 64ビット命令セット: CPUは64ビットのレジスタと命令を扱えます。
- 32ビットポインタ: メモリアドレスを指すポインタは32ビット幅(4バイト)であり、これにより4GBのアドレス空間に制限されます。
この「64ビット命令セットだが32ビットポインタ」という特性が、通常のamd64
アーキテクチャ(レジスタもポインタも64ビット)との間で、sizeof(uintreg)
(レジスタのサイズ)とsizeof(uintptr)
(ポインタのサイズ)が異なるという重要な違いを生み出します。
sizeof(uintreg)
:amd64p32
では8バイト(64ビットレジスタのサイズ)sizeof(uintptr)
:amd64p32
では4バイト(32ビットポインタのサイズ)
Goランタイムは、異なるアーキテクチャに対応するために、これらの型定義を適切に利用する必要があります。
Flakiness (不安定性)
「Flakiness」とは、ソフトウェアテストやシステム動作において、同じコードや設定で実行しても、成功したり失敗したりと結果がランダムに変わる現象を指します。再現性が低いため、デバッグが非常に困難です。このコミットのケースでは、スタックポインタの計算ミスが、特定の条件下でメモリ破壊を引き起こし、それがランダムなクラッシュや誤動作として現れていたと考えられます。
技術的詳細
このコミットの技術的な核心は、Goランタイムがスタックフレームのサイズを計算する際に、uintreg
(レジスタのサイズ)とuintptr
(ポインタのサイズ)を混同していた点にあります。
Goランタイムのスタックトレースバック(runtime·gentraceback
関数)やゴルーチン作成(newproc
に関連する処理)のコードでは、スタックポインタ(SP)を調整する際に、特定のオフセットを加算する必要があります。このオフセットは、関数呼び出しの引数や戻り値、あるいは内部的なスタックフレームの構造によって決まります。
問題のコードは、newproc
が「2つの追加ポインタ」を受け取るという文脈で、スタックポインタを調整するために2 * sizeof(uintreg)
という計算を行っていました。
-
誤った計算:
2 * sizeof(uintreg)
uintreg
はレジスタのサイズを表し、amd64p32
では8バイトです。- したがって、この計算は
2 * 8 = 16バイト
となります。
-
正しい計算:
2 * sizeof(uintptr)
uintptr
はポインタのサイズを表し、amd64p32
では4バイトです。- したがって、この計算は
2 * 4 = 8バイト
となります。
この8バイトの差が、スタックポインタの誤った位置決めを引き起こしていました。スタックポインタが本来の位置よりも8バイトずれることで、関数がスタック上の間違ったメモリ領域を読み書きしようとし、結果としてデータ破損、不正な命令実行、またはクラッシュといった予測不能な動作(flakiness)が発生していました。
この修正は、スタック上のオフセット計算にはポインタのサイズ(uintptr
)を使用すべきであるという、アーキテクチャ固有の特性を考慮した正確なメモリ管理の原則に基づいています。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/traceback_x86.c
ファイルの一箇所です。
--- a/src/pkg/runtime/traceback_x86.c
+++ b/src/pkg/runtime/traceback_x86.c
@@ -214,7 +214,7 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,\n // the SP is two words lower than normal.\n \tsparg = frame.sp;\n \tif(wasnewproc)\n-\t\t\t\tsparg += 2*sizeof(uintreg);\n+\t\t\t\tsparg += 2*sizeof(uintptr);\n \n \t\t// Determine frame\'s \'continuation PC\', where it can continue.\n \t\t// Normally this is the return address on the stack, but if sigpanic
コアとなるコードの解説
変更された行は、runtime·gentraceback
関数内にあります。この関数は、Goランタイムがスタックトレースを生成する際に使用されるもので、ゴルーチンのスタックフレームを遡って、各関数の呼び出し元や引数などを特定します。
if(wasnewproc)
のブロックは、現在のスタックフレームがnewproc
によって作成されたゴルーチンの初期スタックフレームである場合に実行されるロジックです。newproc
は、ゴルーチンを起動する際に、通常の関数呼び出しとは異なる方法でスタックをセットアップすることがあります。特に、追加のポインタをスタックに配置することがあります。
元のコードでは、sparg += 2*sizeof(uintreg);
となっていました。
sparg
: スタックポインタの引数(または関連するスタックオフセット)を保持する変数。sizeof(uintreg)
: レジスタのサイズ。amd64p32
では8バイト。
この行は、newproc
が追加した2つのポインタ分のスペースを考慮して、スタックポインタを調整しようとしていました。しかし、amd64p32
環境では、ポインタのサイズは4バイトであり、レジスタのサイズ(8バイト)とは異なります。そのため、2 * sizeof(uintreg)
は16バイト
となり、本来必要な2 * sizeof(uintptr)
(2 * 4 = 8バイト
)よりも8バイト多くスタックポインタをずらしてしまっていました。
修正後のコードは、sparg += 2*sizeof(uintptr);
となっています。
sizeof(uintptr)
: ポインタのサイズ。amd64p32
では4バイト。
これにより、スタックポインタの調整が2 * 4 = 8バイト
となり、newproc
がスタックに配置した2つの32ビットポインタのサイズと正確に一致するようになりました。この修正によって、スタックトレースバックの処理が正しく行われるようになり、nacl amd64p32
上でのランダムなクラッシュや誤動作が解消されました。
この変更は、Goランタイムが特定のアーキテクチャのメモリモデルとポインタサイズの違いを正確に扱うことの重要性を示しています。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/84a36434d92e18eb12d8a86770bdb4936dff4703
- Go Code Review (CL): https://golang.org/cl/102710043
- Go Issue #8199 (コミットメッセージで参照されているが、公開されているGoのIssueトラッカーでは見つからない可能性あり。内部的なトラッキング番号の可能性):
Fixes #8199.
参考にした情報源リンク
- Goにおける
nacl/amd64p32
アーキテクチャに関する情報:- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQG_yYji0cTpAuLEhTYBZX9kIqwqe7v_9Gzik-OSIs5dMSZb5RVNQTVf9RiI36sY7-BQwphKz7hJg4R1k0nMl_cHSGihYyqbL8GXTQQ9jHOuk0ZXrx8sfUMNk9tnBkx_WYnhWyXXfb8frUYXiAr3Jw==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGr6mb6N9jL3c-SXTCnfOUclglfYo2XMCkUb31ykcbAKxs75cLpN13QXEy9wFzV9jFYhGNAzOVQ_7hLvlUiD9_m2-bw_H38M1FRlI2F786H3xYhui0xKey0fIWGdS223vG1ysrlQ7HNuA8vtqT8i4c=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQECz3bP6N4v3QM12i6UeV36KZyZQc0A8OzpeLL8S5oqSrYw6_RoCCYdZ9dto22dwuwGcb8pvUTL7bdzoVclxVfNZutIgebZDgc2nA1YqMMMJb-jRTzizHfWghq3oFCP-v21qZ0-PTDepUKXpXs-q5wEpGNlkC7lodr40o7KrFdr_5wpb-yhO79yzPlOHS3Ws7FQcEvk5C4FPlv6IgvaRppcbEklgOLOPx2c8dMC
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEcxb1mMsRgvXEMZmoOaxqUmNNOY5WrkwwFaZ0Z3QnQRQWg-LCrslTmwu4kUndYSYcyJZ4o2hkaPrFNTqxzNVyKnafmzswrL93m64W4Dt2AviE8pwuxtCuYAUCjOK99X3tqmPLchUevKxYizV0Ht4wvg_z0MZzAWzNU4EApKBKSC40IEA==
- Goランタイムの
newproc
関数に関する情報:- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGG-h3rpbw0LsJaBqClbtRNH8QlcbdFTS92dCyfPaukE_hC55ycpxH2GCey5SoKWi-0ZhM1xGksNzxuWMIdCBOgx8LyPpWBlnSflCshSKWPy2dB35roOD1xonDB7b0=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHmttSw4lrwfdolKLZqI-mA8pCuLOEjCVBDLyKc-s2j3oDQR7YpBxfrxS6Al0ksVHJ0Ojrk_XU3v_1138sYXr1G4Gsu_jEsQtO-PdbrNlRYl_YOoZ6X