[インデックス 16580] ファイルの概要
このコミットは、Goランタイムのstack_test.go
ファイルにおけるテストの修正に関するものです。具体的には、新しいクロージャ表現に対応するための変更であり、既存のテストがnil
参照でクラッシュする問題を解決します。
コミット
- コミットハッシュ:
f84cbd0950bd05209df7a18e79c1be6b3d31811a
- 作者: Dmitriy Vyukov dvyukov@google.com
- 日付: Mon Jun 17 15:41:17 2013 +0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f84cbd0950bd05209df7a18e79c1be6b3d31811a
元コミット内容
runtime: fix test for new closure representation
I've hit it several times already.
Currently it crashes with nil deref.
R=golang-dev, daniel.morsing, r
CC=golang-dev
https://golang.org/cl/10317045
変更の背景
このコミットの背景には、Go言語のランタイムにおけるクロージャの内部表現の変更があります。コミットメッセージにある「new closure representation(新しいクロージャ表現)」が導入されたことにより、既存のテストコードがその変更に対応できなくなり、nil
参照によるクラッシュが発生していました。
Goのクロージャは、それが定義された環境(レキシカル環境)の変数を捕捉(キャプチャ)できる匿名関数です。Goコンパイラは、クロージャが捕捉した変数がクロージャの生存期間を超えてアクセスされる可能性がある場合、その変数をスタックではなくヒープに割り当てる「エスケープ解析」を行います。これにより、クロージャが返された後も捕捉された変数に安全にアクセスできるようになります。
2013年当時のGoのクロージャ表現は、関数コードと捕捉された変数へのポインタを含む「参照環境」の組み合わせとして概念的に表現されていました。これは通常、関数のアドレスと捕捉された変数へのポインタを保持する構造体として実装されます。この内部表現が変更されたことで、stack_test.go
内のテストが、関数ポインタを直接取得しようとした際に、期待する値が得られずnil
参照を引き起こすようになったと考えられます。
前提知識の解説
Goのクロージャ (Closures)
Goのクロージャは、関数リテラル(匿名関数)が、その関数が定義されたスコープ内の非ローカル変数を参照できる機能です。これらの変数は、クロージャが呼び出される際に、そのクロージャの「環境」の一部として捕捉されます。
unsafe.Pointer
unsafe.Pointer
は、Goの型安全性をバイパスして、任意の型のポインタとuintptr
(符号なし整数型)の間で変換を行うことができる特殊な型です。これにより、Goのメモリモデルを直接操作することが可能になりますが、誤用するとメモリ破壊や未定義動作を引き起こす可能性があるため、「unsafe」とされています。
uintptr
uintptr
は、ポインタ値を保持できる符号なし整数型です。unsafe.Pointer
と組み合わせて使用することで、ポインタの値を整数として操作したり、その逆を行ったりすることができます。これは、メモリ上の特定のアドレスを指すために使用されます。
runtime.FuncForPC
runtime.FuncForPC
関数は、プログラムカウンタ(PC)のアドレスを受け取り、そのアドレスに対応する関数の情報(*runtime.Func
型)を返します。runtime.Func
オブジェクトからは、関数の名前、ファイル名、行番号などのメタデータを取得できます。これは、スタックトレースの生成やプロファイリングなど、ランタイムレベルでの関数情報の取得に利用されます。
スタックとヒープ
Goプログラムでは、変数は主にスタックまたはヒープに割り当てられます。
- スタック: 関数呼び出しごとにフレームが積まれ、ローカル変数や関数の引数が格納されます。関数の終了とともに解放されます。高速ですが、サイズに限りがあります。
- ヒープ: プログラムの実行中に動的にメモリを確保する領域です。ガベージコレクタによって管理され、不要になったメモリは自動的に解放されます。スタックよりも低速ですが、より大きなデータを扱えます。 クロージャが捕捉する変数は、エスケープ解析の結果によってスタックかヒープのどちらかに割り当てられます。
技術的詳細
このコミットの核心は、src/pkg/runtime/stack_test.go
内の以下の行の変更です。
変更前:
fun := FuncForPC(*(*uintptr)(unsafe.Pointer(&f)))
変更後:
fun := FuncForPC(**(**uintptr)(unsafe.Pointer(&f)))
この変更は、f
という関数値(この場合はクロージャ)から、その関数が実行される実際のエントリポイントのアドレス(プログラムカウンタ)を取得する方法が変わったことを示しています。
unsafe.Pointer(&f)
: これは、関数値f
のアドレスをunsafe.Pointer
型に変換します。これにより、Goの型システムを迂回して、f
がメモリ上でどのように表現されているかを直接操作できるようになります。(*uintptr)(...)
: 変更前は、unsafe.Pointer(&f)
が指すメモリ位置に直接uintptr
型の値(つまり、関数のエントリポイントのアドレス)が格納されていると解釈していました。したがって、*
で一度デリファレンスすることで、そのuintptr
値を取得していました。(**uintptr)(...)
: 変更後は、unsafe.Pointer(&f)
が指すメモリ位置にuintptr
へのポインタが格納されていると解釈しています。つまり、f
の内部表現が、直接アドレスを持つのではなく、アドレスへのポインタを持つようになったことを意味します。そのため、**
と二重にデリファレンスすることで、最終的に関数のエントリポイントのアドレスであるuintptr
値を取得しています。
この二重デリファレンスが必要になったのは、「新しいクロージャ表現」が導入されたためです。これは、クロージャの内部構造が変更され、関数ポインタが直接埋め込まれるのではなく、別のポインタを介して間接的に参照されるようになったことを示唆しています。この変更により、以前のテストコードが期待するアドレスを直接取得できなくなり、nil
参照エラーが発生していました。この修正は、新しい表現に合わせて正しいアドレスの取得方法を調整したものです。
コアとなるコードの変更箇所
diff --git a/src/pkg/runtime/stack_test.go b/src/pkg/runtime/stack_test.go
index da0181a66e..00c2d0e061 100644
--- a/src/pkg/runtime/stack_test.go
+++ b/src/pkg/runtime/stack_test.go
@@ -49,7 +49,7 @@ func TestStackSplit(t *testing.T) {
sp, guard := f()
bottom := guard - StackGuard
if sp < bottom+StackLimit {
- fun := FuncForPC(*(*uintptr)(unsafe.Pointer(&f)))
+ fun := FuncForPC(**(**uintptr)(unsafe.Pointer(&f)))
t.Errorf("after %s: sp=%#x < limit=%#x (guard=%#x, bottom=%#x)",
fun.Name(), sp, bottom+StackLimit, guard, bottom)
}
コアとなるコードの解説
変更された行は、TestStackSplit
関数内で、スタック分割のテスト中に、特定の関数f
の情報を取得しようとしている部分です。
f()
: これはテスト対象の関数(クロージャ)を呼び出し、その結果としてスタックポインタsp
とガード値guard
を取得しています。if sp < bottom+StackLimit
: この条件は、スタックポインタが期待される制限値よりも下にある場合にエラーを報告するためのものです。fun := FuncForPC(...)
: ここで、f
の関数情報を取得しようとしています。- 変更前:
*(*uintptr)(unsafe.Pointer(&f))
unsafe.Pointer(&f)
: 関数f
のアドレスをunsafe.Pointer
に変換。(*uintptr)(...)
: そのunsafe.Pointer
をuintptr
へのポインタとして解釈。*
: そのポインタをデリファレンスして、uintptr
値(関数のエントリポイントのアドレス)を取得。
- 変更後:
**(**uintptr)(unsafe.Pointer(&f))
unsafe.Pointer(&f)
: 関数f
のアドレスをunsafe.Pointer
に変換。(**uintptr)(...)
: そのunsafe.Pointer
をuintptr
へのポインタへのポインタとして解釈。**
: 二重にデリファレンスして、最終的なuintptr
値(関数のエントリポイントのアドレス)を取得。
- 変更前:
この変更は、Goランタイムがクロージャをメモリ上でどのように表現するかという低レベルな詳細が変更されたことを直接的に反映しています。以前は関数値f
のメモリ表現の先頭に直接関数のエントリポイントのアドレスが格納されていたのに対し、新しい表現では、その先頭にはエントリポイントのアドレスを指す別のポインタが格納されるようになったため、二重のデリファレンスが必要になりました。これにより、テストは新しいクロージャ表現でも正しく関数の情報を取得し、nil
参照によるクラッシュを回避できるようになりました。
関連リンク
このコミットの元コミットメッセージに含まれるhttps://golang.org/cl/10317045
は、このコミットとは異なる「runtime: fix race in sweep termination」という別のコミット(Austin Clements氏による)を指しており、本コミットの修正内容とは直接関連がありません。