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

[インデックス 17292] ファイルの概要

このコミットは、Go言語のコンパイラ(cmd/5gcmd/6gcmd/8g)におけるスタックフレームのゼロ初期化処理を、新しいビットマップ形式に対応させるための変更です。具体的には、スタック上のポインタ情報を管理するビットマップのフォーマットが変更されたことに伴い、そのビットマップを走査してポインタを特定するロジックが更新されています。

コミット

commit d3b04f46b5828c112053a3e7bd7384fa82dfe921
Author: Carl Shapiro <cshapiro@google.com>
Date:   Fri Aug 16 01:15:04 2013 -0400

    cmd/5g, cmd/6g, cmd/8g: update frame zeroing for new bitmap format
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/12740046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d3b04f46b5828c112053a3e7bd7384fa82dfe921

元コミット内容

cmd/5g, cmd/6g, cmd/8g: update frame zeroing for new bitmap format

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/12740046

変更の背景

Go言語のガベージコレクション(GC)は、スタック上のポインタを正確に識別し、それらが指すオブジェクトをマークする必要があります。そのためには、スタックフレーム内のどの位置にポインタが格納されているかを正確に記述した情報(ポインタビットマップ)が必要です。

このコミットが行われた2013年頃のGo言語のGCは、主にマーク&スイープ方式を採用しており、ポインタの正確な識別がGCの安全性と効率性に直結していました。スタックフレームのゼロ初期化(frame zeroing)は、関数呼び出し時に新しいスタックフレームが割り当てられた際、その領域をゼロで埋める処理を指します。これは、古い(無効な)ポインタがGCによって誤って参照されることを防ぎ、プログラムの安全性を確保するために重要です。

このコミットの背景には、Goランタイムにおけるポインタビットマップの内部表現の変更があります。以前は1ビットでポインタの有無を示していたものが、より詳細な情報を表現するために2ビット形式に変更されたと考えられます。この変更は、GCの精度向上や、将来的なGCアルゴリズムの改善を見据えたものと推測されます。新しいビットマップ形式に対応するため、スタックフレームのゼロ初期化処理において、ポインタの有無を判断するロジックを更新する必要が生じました。

前提知識の解説

  • ガベージコレクション (GC): プログラムが動的に確保したメモリ領域のうち、もはや使用されなくなった領域を自動的に解放する仕組みです。Go言語のGCは、並行マーク&スイープ方式を基本としています。GCが正しく動作するためには、メモリ上のどこにポインタが存在するかを正確に把握する必要があります。
  • スタックフレーム: 関数が呼び出されるたびに、その関数が使用するローカル変数、引数、戻りアドレスなどが格納されるメモリ領域です。スタックはLIFO(Last-In, First-Out)の原則で動作します。
  • ポインタビットマップ: ガベージコレクタがスタックフレームやヒープ上のオブジェクトをスキャンする際に、どのメモリ位置にポインタが含まれているかを示すためのデータ構造です。ビットマップ形式の場合、各ビットが特定のメモリワードがポインタであるかどうかを示します。
  • フレームゼロ初期化 (Frame Zeroing): 関数が呼び出され、新しいスタックフレームが割り当てられる際に、その領域をゼロで埋める処理です。これにより、以前の関数呼び出しで残された古いデータ(特にポインタ)が、新しい関数で誤って有効なポインタとして扱われることを防ぎ、GCの正確性を保証します。
  • cmd/5g, cmd/6g, cmd/8g: これらはGo言語のコンパイラの一部で、それぞれ異なるCPUアーキテクチャに対応しています。
    • cmd/5g: ARMアーキテクチャ(例: ARMv5, ARMv6, ARMv7)
    • cmd/6g: x86-64アーキテクチャ(AMD64)
    • cmd/8g: x86アーキテクチャ(32-bit Intel/AMD) これらのコンパイラは、Goのソースコードを各アーキテクチャの機械語に変換する役割を担っています。

技術的詳細

このコミットの核心は、スタックフレームのポインタビットマップの解釈方法の変更です。以前のビットマップ形式では、スタック上の各ポインタサイズワード(widthptr)に対して1ビットが割り当てられ、そのビットが1であればポインタ、0であれば非ポインタを示していました。bvget(bv, index)関数は、指定されたインデックスのビットを取得するものです。

新しいビットマップ形式では、各ポインタサイズワードに対して2ビットが割り当てられるようになりました。これにより、ポインタの種類や特性に関するより詳細な情報をエンコードできるようになります。例えば、通常のポインタ、スタックポインタ、または特定のGCセマンティクスを持つポインタなどを区別するために使用される可能性があります。

この変更に伴い、defframe関数内のループ処理が修正されました。 元のコードでは、iがスタックオフセットをwidthptr(ポインタのサイズ、通常は4バイトまたは8バイト)ずつインクリメントし、i/widthptrでビットマップのインデックスを計算していました。

// 古いロジック
for(i=0; i<stkptrsize; i+=widthptr) {
    if(bvget(bv, i/widthptr)) {
        // ポインタが見つかった場合の処理
    }
}

新しいコードでは、jという新しい変数が導入され、jはビットマップのインデックスを2ずつインクリメントします。そして、bvget(bv, j) || bvget(bv, j+1)という条件で、現在のポインタサイズワードに対応する2つのビットのいずれかがセットされているかを確認します。これは、2ビットのうち少なくとも1ビットがポインタであることを示している場合に、そのワードをポインタとして扱うことを意味します。

// 新しいロジック
for(i=0, j=0; i<stkptrsize; i+=widthptr, j+=2) {
    if(bvget(bv, j) || bvget(bv, j+1)) {
        // ポインタが見つかった場合の処理
    }
}

この変更により、コンパイラは新しい2ビット形式のポインタビットマップを正しく解釈し、スタックフレームのゼロ初期化処理において、ポインタとしてマークされた領域を適切に処理できるようになります。これにより、GCがスタックをスキャンする際に、正確なポインタ情報を利用できるようになり、GCの正確性と堅牢性が向上します。

コアとなるコードの変更箇所

変更は、src/cmd/5g/ggen.csrc/cmd/6g/ggen.csrc/cmd/8g/ggen.c の3つのファイルに共通して見られます。具体的には、defframe 関数内のスタックフレームゼロ初期化に関連するループの条件式と変数宣言が変更されています。

src/cmd/5g/ggen.c の変更点:

--- a/src/cmd/5g/ggen.c
+++ b/src/cmd/5g/ggen.c
@@ -14,7 +14,7 @@ static Prog* appendp(Prog*, int, int, int, int32, int, int, int32);\n void
 defframe(Prog *ptxt, Bvec *bv)\n {\n-	int i, first;\n+	int i, j, first;\n 	uint32 frame;\n 	Prog *p, *p1;\n 	\n@@ -49,8 +49,8 @@ defframe(Prog *ptxt, Bvec *bv)\n 		patch(p, p1);\n 	} else {\n 		first = 1;\n-		for(i=0; i<stkptrsize; i+=widthptr) {\n-			if(bvget(bv, i/widthptr)) {\n+		for(i=0, j=0; i<stkptrsize; i+=widthptr, j+=2) {\n+			if(bvget(bv, j) || bvget(bv, j+1)) {\n 				if(first) {\n 					p = appendp(p, AMOVW, D_CONST, NREG, 0, D_REG, 0, 0);\n 					first = 0;

src/cmd/6g/ggen.c の変更点:

--- a/src/cmd/6g/ggen.c
+++ b/src/cmd/6g/ggen.c
@@ -14,7 +14,7 @@ static Prog* appendp(Prog*, int, int, vlong, int, vlong);\n void
 defframe(Prog *ptxt, Bvec *bv)\n {\n-	int i;\n+	int i, j;\n 	uint32 frame;\n 	Prog *p;\n \n@@ -37,8 +37,8 @@ defframe(Prog *ptxt, Bvec *bv)\n 		p = appendp(p, AREP, D_NONE, 0, D_NONE, 0);\n 		appendp(p, ASTOSQ, D_NONE, 0, D_NONE, 0);\n 	} else {\n-		for(i=0; i<stkptrsize; i+=widthptr)\n-			if(bvget(bv, i/widthptr))\n+		for(i=0, j=0; i<stkptrsize; i+=widthptr, j+=2)\n+			if(bvget(bv, j) || bvget(bv, j+1))\n 				p = appendp(p, AMOVQ, D_CONST, 0, D_SP+D_INDIR, frame-stkptrsize+i);\n 	}\n }\

src/cmd/8g/ggen.c の変更点:

--- a/src/cmd/8g/ggen.c
+++ b/src/cmd/8g/ggen.c
@@ -16,7 +16,7 @@ defframe(Prog *ptxt, Bvec *bv)\n {\n 	uint32 frame;\n 	Prog *p;\n-	int i;\n+	int i, j;\n \n 	// fill in argument size\n 	ptxt->to.offset2 = rnd(curfn->type->argwid, widthptr);\n@@ -39,8 +39,8 @@ defframe(Prog *ptxt, Bvec *bv)\n 		p = appendp(p, AREP, D_NONE, 0, D_NONE, 0);\n 		appendp(p, ASTOSL, D_NONE, 0, D_NONE, 0);\n 	} else {\n-		for(i=0; i<stkptrsize; i+=widthptr)\n-			if(bvget(bv, i/widthptr))\n+		for(i=0, j=0; i<stkptrsize; i+=widthptr, j+=2)\n+			if(bvget(bv, j) || bvget(bv, j+1))\n 				p = appendp(p, AMOVL, D_CONST, 0, D_SP+D_INDIR, frame-stkptrsize+i);\n 	}\n }\

コアとなるコードの解説

各ファイルの defframe 関数は、Goコンパイラが関数のプロローグ(関数が呼び出された直後に行われる初期設定)の一部として、スタックフレームをセットアップする際に呼び出されます。この関数は、特にスタック上のポインタを含む可能性のある領域をゼロ初期化する責任を負っています。

変更の核心は、for ループの条件と bvget の呼び出しにあります。

  • int i, j; の追加: 新しい変数 j が導入されました。i はスタック上のオフセットを widthptr(ポインタのバイトサイズ)単位で進むのに対し、j はビットマップのインデックスを 2 単位で進みます。これは、1つのポインタサイズワードがビットマップ上で2ビットで表現されるようになったことを明確に示しています。
  • for(i=0, j=0; i<stkptrsize; i+=widthptr, j+=2):
    • i=0, j=0: ループの開始時に ij を両方とも0に初期化します。
    • i<stkptrsize: ループはスタックポインタのサイズ stkptrsize に達するまで続きます。
    • i+=widthptr: i は各イテレーションで widthptr だけ増加します。これは、スタック上の次のポインタサイズワードに移動することを意味します。
    • j+=2: j は各イテレーションで 2 だけ増加します。これは、ビットマップ上で次のポインタサイズワードに対応する2ビットのブロックに移動することを意味します。
  • if(bvget(bv, j) || bvget(bv, j+1)):
    • bvget(bv, j): ビットマップ bv から j 番目のビットを取得します。
    • bvget(bv, j+1): ビットマップ bv から j+1 番目のビットを取得します。
    • ||: 論理OR演算子。これは、j 番目のビットまたは j+1 番目のビットのどちらか一方がセットされていれば(つまり、1であれば)、そのスタック位置にポインタが存在すると判断します。

この変更により、コンパイラは新しい2ビット形式のポインタビットマップを正しく解釈し、スタックフレームのゼロ初期化処理において、ポインタとしてマークされた領域を適切に処理できるようになります。これにより、GCがスタックをスキャンする際に、正確なポインタ情報を利用できるようになり、GCの正確性と堅牢性が向上します。

関連リンク

参考にした情報源リンク

  • Go CL 12740046 (このコミットの元のコードレビューページ)
  • Go言語のソースコード (特に src/cmd/ 以下のコンパイラ関連ファイル)
  • Go言語のガベージコレクションに関する一般的な知識と資料
  • ビットマップデータ構造に関する一般的な情報
  • Go言語のコンパイラ設計に関する一般的な情報# [インデックス 17292] ファイルの概要

このコミットは、Go言語のコンパイラ(cmd/5gcmd/6gcmd/8g)におけるスタックフレームのゼロ初期化処理を、新しいビットマップ形式に対応させるための変更です。具体的には、スタック上のポインタ情報を管理するビットマップのフォーマットが変更されたことに伴い、そのビットマップを走査してポインタを特定するロジックが更新されています。

コミット

commit d3b04f46b5828c112053a3e7bd7384fa82dfe921
Author: Carl Shapiro <cshapiro@google.com>
Date:   Fri Aug 16 01:15:04 2013 -0400

    cmd/5g, cmd/6g, cmd/8g: update frame zeroing for new bitmap format
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/12740046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d3b04f46b5828c112053a3e7bd7384fa82dfe921

元コミット内容

cmd/5g, cmd/6g, cmd/8g: update frame zeroing for new bitmap format

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/12740046

変更の背景

Go言語のガベージコレクション(GC)は、スタック上のポインタを正確に識別し、それらが指すオブジェクトをマークする必要があります。そのためには、スタックフレーム内のどの位置にポインタが格納されているかを正確に記述した情報(ポインタビットマップ)が必要です。

このコミットが行われた2013年頃のGo言語のGCは、主にマーク&スイープ方式を採用しており、ポインタの正確な識別がGCの安全性と効率性に直結していました。スタックフレームのゼロ初期化(frame zeroing)は、関数呼び出し時に新しいスタックフレームが割り当てられた際、その領域をゼロで埋める処理を指します。これは、古い(無効な)ポインタがGCによって誤って参照されることを防ぎ、プログラムの安全性を確保するために重要です。

このコミットの背景には、Goランタイムにおけるポインタビットマップの内部表現の変更があります。以前は1ビットでポインタの有無を示していたものが、より詳細な情報を表現するために2ビット形式に変更されたと考えられます。この変更は、GCの精度向上や、将来的なGCアルゴリズムの改善を見据えたものと推測されます。新しいビットマップ形式に対応するため、スタックフレームのゼロ初期化処理において、ポインタの有無を判断するロジックを更新する必要が生じました。

前提知識の解説

  • ガベージコレクション (GC): プログラムが動的に確保したメモリ領域のうち、もはや使用されなくなった領域を自動的に解放する仕組みです。Go言語のGCは、並行マーク&スイープ方式を基本としています。GCが正しく動作するためには、メモリ上のどこにポインタが存在するかを正確に把握する必要があります。
  • スタックフレーム: 関数が呼び出されるたびに、その関数が使用するローカル変数、引数、戻りアドレスなどが格納されるメモリ領域です。スタックはLIFO(Last-In, First-Out)の原則で動作します。
  • ポインタビットマップ: ガベージコレクタがスタックフレームやヒープ上のオブジェクトをスキャンする際に、どのメモリ位置にポインタが含まれているかを示すためのデータ構造です。ビットマップ形式の場合、各ビットが特定のメモリワードがポインタであるかどうかを示します。
  • フレームゼロ初期化 (Frame Zeroing): 関数が呼び出され、新しいスタックフレームが割り当てられる際に、その領域をゼロで埋める処理です。これにより、以前の関数呼び出しで残された古いデータ(特にポインタ)が、新しい関数で誤って有効なポインタとして扱われることを防ぎ、GCの正確性を保証します。
  • cmd/5g, cmd/6g, cmd/8g: これらはGo言語のコンパイラの一部で、それぞれ異なるCPUアーキテクチャに対応しています。
    • cmd/5g: ARMアーキテクチャ(例: ARMv5, ARMv6, ARMv7)
    • cmd/6g: x86-64アーキテクチャ(AMD64)
    • cmd/8g: x86アーキテクチャ(32-bit Intel/AMD) これらのコンパイラは、Goのソースコードを各アーキテクチャの機械語に変換する役割を担っています。

技術的詳細

このコミットの核心は、スタックフレームのポインタビットマップの解釈方法の変更です。以前のビットマップ形式では、スタック上の各ポインタサイズワード(widthptr)に対して1ビットが割り当てられ、そのビットが1であればポインタ、0であれば非ポインタを示していました。bvget(bv, index)関数は、指定されたインデックスのビットを取得するものです。

新しいビットマップ形式では、各ポインタサイズワードに対して2ビットが割り当てられるようになりました。これにより、ポインタの種類や特性に関するより詳細な情報をエンコードできるようになります。例えば、通常のポインタ、スタックポインタ、または特定のGCセマンティクスを持つポインタなどを区別するために使用される可能性があります。

この変更に伴い、defframe関数内のループ処理が修正されました。 元のコードでは、iがスタックオフセットをwidthptr(ポインタのサイズ、通常は4バイトまたは8バイト)ずつインクリメントし、i/widthptrでビットマップのインデックスを計算していました。

// 古いロジック
for(i=0; i<stkptrsize; i+=widthptr) {
    if(bvget(bv, i/widthptr)) {
        // ポインタが見つかった場合の処理
    }
}

新しいコードでは、jという新しい変数が導入され、jはビットマップのインデックスを2ずつインクリメントします。そして、bvget(bv, j) || bvget(bv, j+1)という条件で、現在のポインタサイズワードに対応する2つのビットのいずれかがセットされているかを確認します。これは、2ビットのうち少なくとも1ビットがポインタであることを示している場合に、そのワードをポインタとして扱うことを意味します。

// 新しいロジック
for(i=0, j=0; i<stkptrsize; i+=widthptr, j+=2) {
    if(bvget(bv, j) || bvget(bv, j+1)) {
        // ポインタが見つかった場合の処理
    }
}

この変更により、コンパイラは新しい2ビット形式のポインタビットマップを正しく解釈し、スタックフレームのゼロ初期化処理において、ポインタとしてマークされた領域を適切に処理できるようになります。これにより、GCがスタックをスキャンする際に、正確なポインタ情報を利用できるようになり、GCの正確性と堅牢性が向上します。

コアとなるコードの変更箇所

変更は、src/cmd/5g/ggen.csrc/cmd/6g/ggen.csrc/cmd/8g/ggen.c の3つのファイルに共通して見られます。具体的には、defframe 関数内のスタックフレームゼロ初期化に関連するループの条件式と変数宣言が変更されています。

src/cmd/5g/ggen.c の変更点:

--- a/src/cmd/5g/ggen.c
+++ b/src/cmd/5g/ggen.c
@@ -14,7 +14,7 @@ static Prog* appendp(Prog*, int, int, int, int32, int, int, int32);\n void
 defframe(Prog *ptxt, Bvec *bv)\n {\n-	int i, first;\n+	int i, j, first;\n 	uint32 frame;\n 	Prog *p, *p1;\n 	\n@@ -49,8 +49,8 @@ defframe(Prog *ptxt, Bvec *bv)\n 		patch(p, p1);\n 	} else {\n 		first = 1;\n-		for(i=0; i<stkptrsize; i+=widthptr) {\n-			if(bvget(bv, i/widthptr)) {\n+		for(i=0, j=0; i<stkptrsize; i+=widthptr, j+=2) {\n+			if(bvget(bv, j) || bvget(bv, j+1)) {\n 				if(first) {\n 					p = appendp(p, AMOVW, D_CONST, NREG, 0, D_REG, 0, 0);\n 					first = 0;

src/cmd/6g/ggen.c の変更点:

--- a/src/cmd/6g/ggen.c
+++ b/src/cmd/6g/ggen.c
@@ -14,7 +14,7 @@ static Prog* appendp(Prog*, int, int, vlong, int, vlong);\n void
 defframe(Prog *ptxt, Bvec *bv)\n {\n-	int i;\n+	int i, j;\n 	uint32 frame;\n 	Prog *p;\n \n@@ -37,8 +37,8 @@ defframe(Prog *ptxt, Bvec *bv)\n 		p = appendp(p, AREP, D_NONE, 0, D_NONE, 0);\n 		appendp(p, ASTOSQ, D_NONE, 0, D_NONE, 0);\n 	} else {\n-		for(i=0; i<stkptrsize; i+=widthptr)\n-			if(bvget(bv, i/widthptr))\n+		for(i=0, j=0; i<stkptrsize; i+=widthptr, j+=2)\n+			if(bvget(bv, j) || bvget(bv, j+1))\n 				p = appendp(p, AMOVQ, D_CONST, 0, D_SP+D_INDIR, frame-stkptrsize+i);\n 	}\n }\

src/cmd/8g/ggen.c の変更点:

--- a/src/cmd/8g/ggen.c
+++ b/src/cmd/8g/ggen.c
@@ -16,7 +16,7 @@ defframe(Prog *ptxt, Bvec *bv)\n {\n 	uint32 frame;\n 	Prog *p;\n-	int i;\n+	int i, j;\n \n 	// fill in argument size\n 	ptxt->to.offset2 = rnd(curfn->type->argwid, widthptr);\n@@ -39,8 +39,8 @@ defframe(Prog *ptxt, Bvec *bv)\n 		p = appendp(p, AREP, D_NONE, 0, D_NONE, 0);\n 		appendp(p, ASTOSL, D_NONE, 0, D_NONE, 0);\n 	} else {\n-		for(i=0; i<stkptrsize; i+=widthptr)\n-			if(bvget(bv, i/widthptr))\n+		for(i=0, j=0; i<stkptrsize; i+=widthptr, j+=2)\n+			if(bvget(bv, j) || bvget(bv, j+1))\n 				p = appendp(p, AMOVL, D_CONST, 0, D_SP+D_INDIR, frame-stkptrsize+i);\n 	}\n }\

コアとなるコードの解説

各ファイルの defframe 関数は、Goコンパイラが関数のプロローグ(関数が呼び出された直後に行われる初期設定)の一部として、スタックフレームをセットアップする際に呼び出されます。この関数は、特にスタック上のポインタを含む可能性のある領域をゼロ初期化する責任を負っています。

変更の核心は、for ループの条件と bvget の呼び出しにあります。

  • int i, j; の追加: 新しい変数 j が導入されました。i はスタック上のオフセットを widthptr(ポインタのバイトサイズ)単位で進むのに対し、j はビットマップのインデックスを 2 単位で進みます。これは、1つのポインタサイズワードがビットマップ上で2ビットで表現されるようになったことを明確に示しています。
  • for(i=0, j=0; i<stkptrsize; i+=widthptr, j+=2):
    • i=0, j=0: ループの開始時に ij を両方とも0に初期化します。
    • i<stkptrsize: ループはスタックポインタのサイズ stkptrsize に達するまで続きます。
    • i+=widthptr: i は各イテレーションで widthptr だけ増加します。これは、スタック上の次のポインタサイズワードに移動することを意味します。
    • j+=2: j は各イテレーションで 2 だけ増加します。これは、ビットマップ上で次のポインタサイズワードに対応する2ビットのブロックに移動することを意味します。
  • if(bvget(bv, j) || bvget(bv, j+1)):
    • bvget(bv, j): ビットマップ bv から j 番目のビットを取得します。
    • bvget(bv, j+1): ビットマップ bv から j+1 番目のビットを取得します。
    • ||: 論理OR演算子。これは、j 番目のビットまたは j+1 番目のビットのどちらか一方がセットされていれば(つまり、1であれば)、そのスタック位置にポインタが存在すると判断します。

この変更により、コンパイラは新しい2ビット形式のポインタビットマップを正しく解釈し、スタックフレームのゼロ初期化処理において、ポインタとしてマークされた領域を適切に処理できるようになります。これにより、GCがスタックをスキャンする際に、正確なポインタ情報を利用できるようになり、GCの正確性と堅牢性が向上します。

関連リンク

参考にした情報源リンク

  • Go CL 12740046 (このコミットの元のコードレビューページ)
  • Go言語のソースコード (特に src/cmd/ 以下のコンパイラ関連ファイル)
  • Go言語のガベージコレクションに関する一般的な知識と資料
  • ビットマップデータ構造に関する一般的な情報
  • Go言語のコンパイラ設計に関する一般的な情報