[インデックス 15376] ファイルの概要
このコミットは、Goランタイムのデータ競合検出器(Race Detector)に関連する変更です。具体的には、src/pkg/runtime/race.c
と src/pkg/runtime/race_amd64.s
の2つのファイルが変更されています。
src/pkg/runtime/race.c
: Goランタイムのデータ競合検出器のC言語部分の実装ファイルです。src/pkg/runtime/race_amd64.s
: AMD64アーキテクチャ向けのGoランタイムのデータ競合検出器のアセンブリ言語部分の実装ファイルです。
コミット
commit ec892be1af5c2341c07fa77399a7f177b24e0f16
Author: Russ Cox <rsc@golang.org>
Date: Fri Feb 22 13:06:43 2013 -0500
runtime: preserve DX during racefuncenter
R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/7382049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ec892be1af5c2341c07fa77399a7f177b24e0f16
元コミット内容
runtime: preserve DX during racefuncenter
R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/7382049
変更の背景
このコミットは、Goのデータ競合検出器(Race Detector)が正しく機能するために、runtime·racefuncenter
関数が呼び出される際にDXレジスタの値を保存する必要があるという問題に対応しています。
Goのデータ競合検出器は、並行プログラムにおけるデータ競合を検出するための強力なツールです。これは、コンパイル時にコードを計測し、実行時にメモリアクセスと同期イベントを監視することで機能します。runtime·racefuncenter
は、関数エントリ時に呼び出される内部関数であり、レース検出器が実行フロー、ゴルーチンコンテキスト、およびコールスタックを追跡するのに役立ちます。
DXレジスタは、AMD64アーキテクチャにおいて、特定のGoの内部呼び出し規約、特にクロージャのコンテキストを渡すために使用されることがあります。runtime·racefuncenter
が呼び出される際にDXレジスタが上書きされてしまうと、その後に続く処理(特にクロージャの呼び出しなど)でDXレジスタに依存するコードが正しく動作しなくなる可能性がありました。
この変更は、runtime·racefuncenter
のアセンブリコード内でDXレジスタの値を保存し、関数終了時に復元することで、この問題を解決し、データ競合検出器の堅牢性を向上させることを目的としています。
前提知識の解説
Goのデータ競合検出器 (Race Detector)
Goのデータ競合検出器は、Go 1.1で導入された機能で、並行処理におけるデータ競合(data race)を検出するためのツールです。データ競合とは、複数のゴルーチンが共有データにアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが適切な同期メカニズム(ミューテックス、チャネルなど)によって保護されていない場合に発生します。データ競合は、プログラムの予測不能な動作やクラッシュの原因となることがあり、デバッグが非常に困難です。
Goのデータ競合検出器は、プログラムを-race
フラグ付きでコンパイルすることで有効になります(例: go run -race main.go
または go test -race
)。有効にすると、コンパイラはメモリアクセスや同期イベントを監視するための追加のコードを挿入します。実行時にデータ競合が検出されると、検出器は詳細なレポート(競合が発生した場所、関連するゴルーチンのスタックトレースなど)を出力します。
この検出器は、GoogleのThreadSanitizer (TSan) というC/C++向けのツールをベースにしており、シャドウメモリという概念を用いてメモリアクセスのメタデータを記録します。
アセンブリ言語とGoランタイム
Goのランタイムは、Goプログラムの実行を管理する低レベルのコンポーネントです。これには、スケジューラ、ガベージコレクタ、メモリ管理、プリミティブな同期メカニズムなどが含まれます。Goランタイムの一部は、パフォーマンスやハードウェアとの直接的な対話のために、C言語やアセンブリ言語で記述されています。
race_amd64.s
のようなファイルは、AMD64アーキテクチャ向けのアセンブリコードを含んでいます。アセンブリ言語は、CPUが直接実行できる機械語に非常に近い低レベルのプログラミング言語です。Goランタイムがアセンブリ言語を使用するのは、特定のレジスタ操作、システムコール、または非常にパフォーマンスが重要なコードパスを最適化するためです。
呼び出し規約 (Calling Convention)
呼び出し規約とは、関数が呼び出される際に、引数をどのように渡し、戻り値をどのように返し、レジスタをどのように使用するかといった、関数呼び出しに関する取り決めです。これには、スタックの使用方法、レジスタの保存・復元ルールなどが含まれます。
AMD64アーキテクチャでは、複数の呼び出し規約が存在します(例: System V AMD64 ABI、Microsoft x64 calling convention)。Go独自の内部呼び出し規約も存在し、特にGo 1.17以降ではレジスタベースのABIが導入され、パフォーマンスが向上しています。しかし、このコミットが作成された2013年時点では、Goは主にスタックベースの呼び出し規約(Plan 9 ABIに基づく)を使用していました。
DXレジスタ (Data Register)
DXレジスタは、AMD64(x86-64)アーキテクチャにおける汎用レジスタの一つです。歴史的に、x86アーキテクチャでは、特定の操作(例: 16ビットの乗算や除算、ポートI/O)でDXレジスタが暗黙的に使用されることがありました。
Goの内部呼び出し規約においては、特に古いバージョンや特定のアセンブリコードにおいて、DXレジスタが特別な意味を持つことがありました。例えば、クロージャ(closure)を呼び出す際に、クロージャのコンテキスト(funcval
構造体へのポインタ)をDXレジスタで渡すという内部的な取り決めが存在しました。
レジスタは有限のリソースであり、関数呼び出しの際に、呼び出し元(caller)が使用していたレジスタの値を、呼び出された関数(callee)が上書きしてしまうと、呼び出し元がその後の処理で誤った値を使用する可能性があります。これを防ぐために、呼び出し規約では、どのレジスタを呼び出し元が保存すべきか(caller-saved)、どのレジスタを呼び出された関数が保存すべきか(callee-saved)が定められています。
技術的詳細
このコミットの核心は、Goのデータ競合検出器が関数エントリ時に呼び出すアセンブリコード runtime·racefuncenter
において、DXレジスタの値を適切に保存・復元することです。
元の実装では、runtime·racefuncenter
は直接C言語の runtime·racefuncenter
関数(コミットで runtime·racefuncenter1
にリネームされたもの)を呼び出していました。しかし、この呼び出しの際に、DXレジスタが上書きされる可能性がありました。
Goのランタイムでは、クロージャの呼び出しなど、特定の内部処理でDXレジスタがクロージャのコンテキスト(funcval
)を保持するために使用されることがあります。もし runtime·racefuncenter
がDXレジスタを保存せずにC関数を呼び出し、そのC関数がDXレジスタを上書きした場合、runtime·racefuncenter
から戻った後に、呼び出し元が期待するDXレジスタの値が失われ、プログラムの誤動作につながる可能性がありました。
このコミットでは、runtime·racefuncenter
をアセンブリ言語で実装し、その中でC言語の runtime·racefuncenter1
を呼び出すように変更しています。アセンブリ言語の runtime·racefuncenter
は、まず PUSHQ DX
命令を使ってDXレジスタの現在の値をスタックに保存します。その後、C言語の runtime·racefuncenter1
を呼び出し、処理が完了したら POPQ DX
命令を使ってスタックからDXレジスタの値を復元します。これにより、runtime·racefuncenter
の呼び出し前後でDXレジスタの値が確実に保存されるようになります。
この変更は、データ競合検出器がGoプログラムの実行フローに介入する際に、既存のレジスタの使用規約を尊重し、副作用を最小限に抑えるための重要な修正です。
コアとなるコードの変更箇所
src/pkg/runtime/race.c
--- a/src/pkg/runtime/race.c
+++ b/src/pkg/runtime/race.c
@@ -87,10 +87,10 @@ runtime·raceread(uintptr addr)
}
}
-// Called from instrumented code.
+// Called from runtime·racefuncenter (assembly).
#pragma textflag 7
void
-runtime·racefuncenter(uintptr pc)
+runtime·racefuncenter1(uintptr pc)
{
// If the caller PC is lessstack, use slower runtime·callers
// to walk across the stack split to find the real caller.
runtime·racefuncenter
関数の名前がruntime·racefuncenter1
に変更されました。- コメントが「// Called from instrumented code.」から「// Called from runtime·racefuncenter (assembly).」に変更され、この関数がアセンブリコードから呼び出されることを明示しています。
src/pkg/runtime/race_amd64.s
--- /dev/null
+++ b/src/pkg/runtime/race_amd64.s
@@ -0,0 +1,11 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build race
+
+TEXT runtime·racefuncenter(SB),7,$0
+ PUSHQ DX // save function entry context (for closures)
+ CALL runtime·racefuncenter1(SB)
+ POPQ DX
+ RET
- 新しいファイル
race_amd64.s
が追加されました。 - このファイルは、
+build race
ディレクティブを含んでおり、-race
フラグが有効な場合にのみビルドされることを示します。 TEXT runtime·racefuncenter(SB),7,$0
は、runtime·racefuncenter
という名前のアセンブリ関数を定義しています。PUSHQ DX
命令は、DXレジスタの値をスタックにプッシュ(保存)します。コメントで「save function entry context (for closures)」とあり、クロージャのためのコンテキストを保存していることが示唆されています。CALL runtime·racefuncenter1(SB)
命令は、C言語で実装されたruntime·racefuncenter1
関数を呼び出します。POPQ DX
命令は、スタックからDXレジスタの値をポップ(復元)します。RET
命令は、関数から戻ります。
コアとなるコードの解説
この変更の目的は、runtime·racefuncenter
が呼び出される際にDXレジスタの値を保護することです。
-
race.c
の変更:runtime·racefuncenter
の名前をruntime·racefuncenter1
に変更することで、C言語の関数が直接呼び出されるのではなく、アセンブリラッパーを介して呼び出されることを明確にしています。これは、アセンブリコードがDXレジスタの保存・復元を担当するためです。
-
race_amd64.s
の追加:- 新しく追加されたアセンブリファイル
race_amd64.s
は、runtime·racefuncenter
という名前の関数を定義しています。この関数は、Goコンパイラによって計測されたコードから直接呼び出されるエントリポイントとなります。 PUSHQ DX
は、関数が実行される前にDXレジスタの現在の値をスタックに一時的に保存します。これにより、runtime·racefuncenter1
の実行中にDXレジスタが変更されても、元の値が失われることはありません。CALL runtime·racefuncenter1(SB)
は、実際のレース検出ロジックを含むC言語の関数runtime·racefuncenter1
を呼び出します。POPQ DX
は、runtime·racefuncenter1
の実行が完了した後、スタックに保存されていたDXレジスタの元の値を復元します。RET
は、runtime·racefuncenter
から呼び出し元に戻ります。
- 新しく追加されたアセンブリファイル
このアセンブリラッパーを導入することで、Goのデータ競合検出器が関数エントリ時に介入する際、DXレジスタがクロージャのコンテキストなどの重要な情報を保持している場合でも、その値が安全に保護されるようになります。これにより、データ競合検出器の動作が他のランタイム機能に与える影響が最小限に抑えられ、全体的な安定性と正確性が向上します。
関連リンク
- Go CL 7382049: https://golang.org/cl/7382049
参考にした情報源リンク
- Go Race Detector: https://go.dev/blog/race-detector
- Go Race Detector Internals (ThreadSanitizer): https://medium.com/@valyala/go-race-detector-internals-a7d079a5e7d
- Go Calling Conventions (Go 1.17+ ABI): https://go.dev/doc/go1.17#compiler
- Go Calling Conventions (Older versions and DX register for closures): https://dr-knz.net/go-calling-convention-amd64.html
- Go runtime assembly thunks: https://golang.bg/blog/go-runtime-assembly-thunks