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

[インデックス 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という関数値(この場合はクロージャ)から、その関数が実行される実際のエントリポイントのアドレス(プログラムカウンタ)を取得する方法が変わったことを示しています。

  1. unsafe.Pointer(&f): これは、関数値fのアドレスをunsafe.Pointer型に変換します。これにより、Goの型システムを迂回して、fがメモリ上でどのように表現されているかを直接操作できるようになります。
  2. (*uintptr)(...): 変更前は、unsafe.Pointer(&f)が指すメモリ位置に直接uintptr型の値(つまり、関数のエントリポイントのアドレス)が格納されていると解釈していました。したがって、*で一度デリファレンスすることで、そのuintptr値を取得していました。
  3. (**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.Pointeruintptrへのポインタとして解釈。
      • *: そのポインタをデリファレンスして、uintptr値(関数のエントリポイントのアドレス)を取得。
    • 変更後: **(**uintptr)(unsafe.Pointer(&f))
      • unsafe.Pointer(&f): 関数fのアドレスをunsafe.Pointerに変換。
      • (**uintptr)(...): そのunsafe.Pointeruintptrへのポインタへのポインタとして解釈。
      • **: 二重にデリファレンスして、最終的なuintptr値(関数のエントリポイントのアドレス)を取得。

この変更は、Goランタイムがクロージャをメモリ上でどのように表現するかという低レベルな詳細が変更されたことを直接的に反映しています。以前は関数値fのメモリ表現の先頭に直接関数のエントリポイントのアドレスが格納されていたのに対し、新しい表現では、その先頭にはエントリポイントのアドレスを指す別のポインタが格納されるようになったため、二重のデリファレンスが必要になりました。これにより、テストは新しいクロージャ表現でも正しく関数の情報を取得し、nil参照によるクラッシュを回避できるようになりました。

関連リンク

このコミットの元コミットメッセージに含まれるhttps://golang.org/cl/10317045は、このコミットとは異なる「runtime: fix race in sweep termination」という別のコミット(Austin Clements氏による)を指しており、本コミットの修正内容とは直接関連がありません。

参考にした情報源リンク