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

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

このコミットは、Goランタイムのsrc/pkg/runtime/softfloat_arm.cファイルに対する変更です。具体的には、ARMアーキテクチャにおけるソフトウェア浮動小数点演算に関連する_sfloat2関数のスタックフレーム情報の記録方法を改善しています。

コミット

commit 8166b2da192919679cd4583c4edb34becbe36e8c
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jul 18 12:23:38 2013 -0400

    runtime: record full frame size for arm _sfloat2
    
    With preemption, _sfloat2 can show up in stack traces.
    Write the function prototype in a way that accurately
    shows the frame size and the fact that it might contain
    pointers.
    
    R=golang-dev, dvyukov
    CC=golang-dev
    https://golang.org/cl/11523043

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

https://github.com/golang/go/commit/8166b2da192919679cd4583c4edb34becbe36e8c

元コミット内容

このコミットの元のメッセージは以下の通りです。

runtime: record full frame size for arm _sfloat2

With preemption, _sfloat2 can show up in stack traces.
Write the function prototype in a way that accurately
shows the frame size and the fact that it might contain
pointers.

R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/11523043

変更の背景

この変更の背景には、Goランタイムにおけるプリエンプション(preemption)の導入と、それに伴うスタックトレースの正確性の問題があります。

Goランタイムは、ガベージコレクション(GC)やスケジューリングのために、実行中のGoroutineを一時停止(プリエンプト)する機能を持っています。プリエンプションが発生すると、現在のGoroutineの実行状態(レジスタの値、スタックの内容など)が保存され、後で再開できるようにする必要があります。この保存された情報がスタックトレースとして表示されることがあります。

_sfloat2関数は、ARMアーキテクチャにおけるソフトウェア浮動小数点演算の一部として、コンパイラによって生成される可能性のあるヘルパー関数です。この関数は、複数のレジスタを引数として受け取りますが、元のプロトタイプではこれらのレジスタが正確に表現されていませんでした。特に、可変引数リスト(...)を使用していたため、ランタイムがスタックフレームのサイズや、その中にポインタが含まれているかどうかを正確に判断できませんでした。

プリエンプションが導入されると、_sfloat2のような低レベルのランタイム関数がスタックトレースに現れる可能性が増えます。このとき、スタックフレームのサイズが不正確だと、ガベージコレクタがポインタを正しく識別できず、誤って解放してしまったり、逆に解放すべきでないメモリを保持し続けたりする可能性があります。これは、メモリリークやクラッシュの原因となり得ます。

このコミットは、_sfloat2関数のプロトタイプを修正し、スタックフレームのサイズとポインタの有無をランタイムに正確に伝えることで、プリエンプション発生時のスタックトレースの正確性を向上させ、メモリ安全性を確保することを目的としています。

前提知識の解説

このコミットを理解するためには、以下の概念について理解しておく必要があります。

1. Goランタイム (Go Runtime)

Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクタ、スケジューラ、メモリ管理、Goroutineの管理などが含まれます。Goプログラムは、OSのプロセスとして実行されますが、その内部でGoランタイムがGoroutineのスケジューリングやメモリの割り当て・解放といった低レベルのタスクを処理します。

2. プリエンプション (Preemption)

プリエンプションとは、実行中のタスク(この場合はGoroutine)を、そのタスクが自発的に制御を放棄することなく、外部から強制的に中断させるメカニズムです。Goランタイムでは、長時間実行されるGoroutineが他のGoroutineの実行を妨げないように、定期的にプリエンプションポイントを挿入し、必要に応じてGoroutineを中断させます。これにより、Goroutine間の公平なスケジューリングと、ガベージコレクションなどのランタイムタスクの実行が可能になります。

3. スタックトレース (Stack Trace)

スタックトレースは、プログラムが特定の時点(例えば、エラー発生時やプリエンプション時)で実行していた関数の呼び出し履歴を示すリストです。各エントリは、呼び出された関数と、その関数が呼び出されたソースコード上の位置を示します。デバッグやエラー解析において非常に重要な情報となります。

4. スタックフレーム (Stack Frame)

関数が呼び出されるたびに、その関数のローカル変数、引数、戻りアドレスなどを格納するために、スタック上に確保されるメモリ領域をスタックフレームと呼びます。スタックトレースは、これらのスタックフレームを逆順に辿ることで生成されます。

5. ガベージコレクション (Garbage Collection, GC)

ガベージコレクションは、プログラムが動的に割り当てたメモリのうち、もはや使用されていない(参照されていない)メモリ領域を自動的に解放するプロセスです。GoのGCは、スタックやヒープ上のポインタをスキャンし、到達可能なオブジェクトを特定することで、不要なメモリを判断します。このプロセスにおいて、スタックフレーム内のポインタを正確に識別することが極めて重要です。

6. ARMアーキテクチャ (ARM Architecture)

ARMは、モバイルデバイスや組み込みシステムで広く使用されているCPUアーキテクチャです。GoはARMを含む様々なアーキテクチャをサポートしており、それぞれのアーキテクチャに特化したランタイムコードを持っています。

7. ソフトウェア浮動小数点演算 (Software Floating-Point Operations)

一部のARMプロセッサや、特定のコンパイル設定では、ハードウェアによる浮動小数点演算ユニット(FPU)が利用できない場合があります。このような場合、浮動小数点演算はソフトウェアによってエミュレートされます。_sfloat2のような関数は、このソフトウェアエミュレーションの一部として使用されることがあります。

8. ポインタ (Pointer)

ポインタは、メモリ上の特定のアドレスを指し示す変数です。Goのガベージコレクタは、ポインタを辿って到達可能なオブジェクトを識別するため、スタックフレーム内にポインタが存在する場合、そのポインタがどこを指しているのかを正確に知る必要があります。

技術的詳細

このコミットの核心は、runtime·_sfloat2関数のプロトタイプ(シグネチャ)の変更にあります。

元のプロトタイプは以下のようでした。

uint32*
runtime·_sfloat2(uint32 *lr, uint32 r0, ...)

ここで注目すべきは、...(可変引数リスト)が使用されている点です。C言語において、可変引数リストは、関数が受け取る引数の数が不定であることを示します。しかし、Goランタイムのスタックフレーム管理においては、この可変引数リストは問題を引き起こします。ランタイムは、スタックフレームの正確なサイズや、どの引数がポインタであるかを、コンパイル時に静的に判断する必要があります。...では、これらの情報が不明瞭になります。

_sfloat2関数は、実際にはr0からr15までの16個のレジスタを引数として受け取っていました。これらのレジスタの中には、ポインタが含まれている可能性があります。プリエンプションが発生し、この関数がスタックトレースに現れた場合、ランタイムのガベージコレクタは、これらのレジスタが指すメモリ領域を正しくスキャンできなければなりません。

新しいプロトタイプでは、Sfregsという構造体が導入されました。

typedef struct Sfregs Sfregs;

// NOTE: These are all recorded as pointers because they are possibly live registers,
// and we don't know what they contain. Recording them as pointers should be
// safer than not.
struct Sfregs
{
	uint32 *r0;
	uint32 *r1;
	uint32 *r2;
	uint32 *r3;
	uint32 *r4;
	uint32 *r5;
	uint32 *r6;
	uint32 *r7;
	uint32 *r8;
	uint32 *r9;
	uint32 *r10;
	uint32 *r11;
	uint32 *r12;
	uint32 *r13;
	uint32 cspr;
};

#pragma textflag 7
uint32*
runtime·_sfloat2(uint32 *lr, Sfregs regs)

Sfregs構造体は、r0からr13までのレジスタを明示的にuint32 *(32ビット符号なし整数へのポインタ)として定義しています。コメントにもあるように、これらのレジスタが「ポインタとして記録される」のは、それらが「生きているレジスタであり、何を含んでいるか分からない」ためです。ポインタとして扱うことで、ガベージコレクタはこれらのレジスタが指す可能性のあるメモリ領域を確実にスキャンし、メモリ安全性を高めることができます。csprは、ARMのCurrent Program Status Register(現在のプログラムステータスレジスタ)を指すと考えられますが、ここではポインタとして扱われていません。

この変更により、runtime·_sfloat2関数のスタックフレームのレイアウトがランタイムに対して明確になります。ランタイムは、Sfregs構造体のサイズと、その中に含まれるポインタのオフセットを正確に把握できるようになります。これにより、プリエンプション発生時にスタックトレースが生成される際、ガベージコレクタがスタック上のポインタを正確に識別し、マーク&スイーププロセスを適切に実行できるようになります。

また、関数内部のstepflt関数の呼び出しも、&r0から(uint32*)&regs.r0に変更されています。これは、stepfltがレジスタの値を直接操作するのではなく、Sfregs構造体内のレジスタのポインタを介して操作するように変更されたことを示しています。

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

変更はsrc/pkg/runtime/softfloat_arm.cファイルに集中しています。

--- a/src/pkg/runtime/softfloat_arm.c
+++ b/src/pkg/runtime/softfloat_arm.c
@@ -576,23 +576,44 @@ done:
 	return 0;
 }
 
-// The ... here is because there are actually 16 registers
-// being passed (r0, r1, and so on) amd we are too lazy
-// to list them all.
+typedef struct Sfregs Sfregs;
+// NOTE: These are all recorded as pointers because they are possibly live registers,
+// and we don't know what they contain. Recording them as pointers should be
+// safer than not.
+struct Sfregs
+{
+	uint32 *r0;
+	uint32 *r1;
+	uint32 *r2;
+	uint32 *r3;
+	uint32 *r4;
+	uint32 *r5;
+	uint32 *r6;
+	uint32 *r7;
+	uint32 *r8;
+	uint32 *r9;
+	uint32 *r10;
+	uint32 *r11;
+	uint32 *r12;
+	uint32 *r13;
+	uint32 cspr;
+};
+
 #pragma textflag 7
 uint32*
-runtime·_sfloat2(uint32 *lr, uint32 r0, ...)
+runtime·_sfloat2(uint32 *lr, Sfregs regs)
 {
 	uint32 skip;
 
-	skip = stepflt(lr, &r0);
+	skip = stepflt(lr, (uint32*)&regs.r0);
 	if(skip == 0) {
 		runtime·printf("sfloat2 %p %x\n", lr, *lr);
 		fabort(); // not ok to fail first instruction
 	}
 
 	lr += skip;
-	while(skip = stepflt(lr, &r0))
+	while(skip = stepflt(lr, (uint32*)&regs.r0))
 		lr += skip;
 	return lr;
 }

コアとなるコードの解説

  1. Sfregs構造体の追加: typedef struct Sfregs Sfregs;struct Sfregs { ... }; が追加されました。この構造体は、_sfloat2関数が内部的に扱う可能性のあるARMレジスタ(r0からr13、およびcspr)を明示的に定義しています。特に重要なのは、r0からr13uint32 *(ポインタ)として宣言されている点です。これにより、Goのガベージコレクタは、これらのフィールドがポインタである可能性を認識し、スタックフレームをスキャンする際にそれらを適切に処理できるようになります。これは、プリエンプション時にスタックトレースが生成される際に、メモリ安全性を確保するために不可欠です。

  2. runtime·_sfloat2関数のプロトタイプ変更: 元のプロトタイプ runtime·_sfloat2(uint32 *lr, uint32 r0, ...) が、 runtime·_sfloat2(uint32 *lr, Sfregs regs) に変更されました。 これにより、可変引数リスト(...)が削除され、代わりにSfregs構造体のインスタンスregsが引数として渡されるようになりました。この変更により、関数の引数として渡されるレジスタの数と型がコンパイル時に明確になり、ランタイムがスタックフレームのサイズとポインタの有無を正確に判断できるようになります。

  3. stepflt関数の引数変更: 関数内部のstepfltの呼び出しが、stepflt(lr, &r0) から stepflt(lr, (uint32*)&regs.r0) に変更されました。 これは、stepflt関数が、以前は直接r0レジスタの値を参照していたのに対し、変更後はSfregs構造体内のr0フィールドへのポインタを介してレジスタの値を操作するように修正されたことを意味します。これにより、_sfloat2関数が受け取ったレジスタ群(regs構造体)全体を、stepflt関数が適切に処理できるようになります。

これらの変更は、GoランタイムがARMアーキテクチャ上でプリエンプションをより堅牢にサポートし、スタックトレースの正確性とガベージコレクションの効率性を向上させるために不可欠でした。特に、スタックフレーム内のポインタの正確な識別は、メモリリークやクラッシュを防ぐ上で極めて重要です。

関連リンク

参考にした情報源リンク

  • Goの公式ドキュメント
  • Goのソースコードリポジトリ (特にsrc/pkg/runtimeディレクトリ)
  • Goのコミット履歴とコードレビューコメント
  • 一般的なコンピュータアーキテクチャとオペレーティングシステムの概念に関する知識
  • C言語の可変引数リストに関する知識
  • ガベージコレクションのアルゴリズムに関する知識
  • ARMアーキテクチャのレジスタと呼び出し規約に関する知識