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

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

このコミットは、GoランタイムにおけるARMアーキテクチャでのプリエンプション(横取り)に関するバグ修正です。特に、ソフトウェア浮動小数点演算中に発生する可能性のあるレジスタの状態破損を防ぐことを目的としています。

コミット

commit cba880e04a074806f0e948fbdee8e2fe31705f14
Author: Russ Cox <rsc@golang.org>
Date:   Thu Aug 1 00:16:31 2013 -0400

    runtime: fix arm preemption
    
    Preemption during the software floating point code
    could cause m (R9) to change, so that when the
    original registers were restored at the end of the
    floating point handler, the changed and correct m
    would be replaced by the old and incorrect m.
    
    TBR=dvyukov
    CC=golang-dev
    https://golang.org/cl/11883045

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

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

元コミット内容

--- a/src/pkg/runtime/vlop_arm.s
+++ b/src/pkg/runtime/vlop_arm.s
@@ -59,7 +59,15 @@ TEXT _sfloat(SB), 7, $64-0 // 4 arg + 14*4 saved regs + cpsr
  	MOVW	64(R13), R1
  	WORD	$0xe128f001	// msr cpsr_f, r1
  	MOVW	$12(R13), R0
-	MOVM.IA.W	(R0), [R1-R12]
+	// Restore R1-R8 and R11-R12, but ignore the saved R9 (m) and R10 (g).
+	// Both are maintained by the runtime and always have correct values,
+	// so there is no need to restore old values here.
+	// The g should not have changed, but m may have, if we were preempted
+	// and restarted on a different thread, in which case restoring the old
+	// value is incorrect and will cause serious confusion in the runtime.
+	MOVM.IA.W	(R0), [R1-R8]
+	MOVW	$52(R13), R0
+	MOVM.IA.W	(R0), [R11-R12]
  	MOVW	8(R13), R0
  	RET

変更の背景

このコミットは、GoランタイムがARMアーキテクチャ上で動作する際に発生する可能性のある、プリエンプション(横取り)とレジスタの状態管理に関する深刻なバグを修正するために行われました。

Goランタイムは、ユーザーレベルのスレッド(goroutine)をOSのスレッド(M: Machine)にマッピングし、効率的な並行処理を実現しています。この際、ランタイムはgoroutineのスケジューリングやプリエンプションを自身で管理します。プリエンプションとは、実行中のgoroutineを一時停止させ、別のgoroutineにCPUを割り当てる処理のことです。これにより、協調的ではない(cooperativeではない)プリエンプションが可能になり、長時間実行されるgoroutineが他のgoroutineの実行を妨げることを防ぎます。

問題は、ARMアーキテクチャにおけるソフトウェア浮動小数点演算のコンテキストで発生しました。ARMプロセッサの中にはハードウェア浮動小数点ユニットを持たないものがあり、その場合、浮動小数点演算はソフトウェアでエミュレートされます。このソフトウェアエミュレーションコードの実行中にプリエンプションが発生すると、特定のレジスタ(特にmを表すR9レジスタ)の値が変更される可能性がありました。

Goランタイムは、プリエンプションが発生した際に現在のレジスタの状態を保存し、プリエンプションから復帰する際にその状態を復元します。しかし、ソフトウェア浮動小数点ハンドラ内でmレジスタが変更された場合、復元時に古い(そして誤った)mレジスタの値が上書きされてしまうという問題がありました。これにより、ランタイムが誤ったmレジスタを参照し、深刻な混乱やクラッシュを引き起こす可能性がありました。

このバグは、特にプリエンプションが頻繁に発生するような高負荷な状況や、ソフトウェア浮動小数点演算が多用されるアプリケーションで顕在化する可能性がありました。

前提知識の解説

Goランタイムのスケジューラ (GMPモデル)

Goランタイムは、goroutineのスケジューリングにGMPモデル(Goroutine, Machine, Processor)を採用しています。

  • G (Goroutine): Goの軽量スレッド。ユーザーコードが実行される単位。
  • M (Machine): OSのスレッド。GoランタイムはM上でGを実行します。MはOSによってスケジューリングされます。
  • P (Processor): 論理プロセッサ。MがGを実行するために必要なコンテキスト(スケジューリングキューなど)を提供します。Pの数は通常、CPUのコア数に等しいです。

Goランタイムは、MがPにアタッチされ、PがGを実行するという形で動作します。プリエンプションは、Mが実行中のGを中断し、別のGに切り替えるプロセスです。

ARMアーキテクチャのレジスタ

ARMアーキテクチャには汎用レジスタ(R0-R15)があります。Goランタイムはこれらのレジスタを特定の目的で使用します。

  • R9 (m): Goランタイムでは、現在のM(Machine、OSスレッド)のポインタを保持するためにR9レジスタが慣習的に使用されます。これは、ランタイムが現在の実行コンテキストに関する情報に素早くアクセスするために重要です。
  • R10 (g): 同様に、現在のG(Goroutine)のポインタを保持するためにR10レジスタが使用されます。

これらのレジスタは、Goランタイムの内部動作において非常に重要な役割を果たします。

ソフトウェア浮動小数点演算

一部のARMプロセッサ(特に古いものや組み込みシステム向けのもの)は、ハードウェア浮動小数点ユニット(FPU)を持っていません。このような環境では、浮動小数点演算はソフトウェアによってエミュレートされます。これは、浮動小数点数の加算、減算、乗算、除算などの操作を、整数演算とビット操作の組み合わせで実現するものです。

ソフトウェア浮動小数点演算はハードウェアFPUに比べて非常に遅く、多くのCPUサイクルを消費します。そのため、これらの演算が実行されている間にプリエンプションが発生する可能性が高まります。

MOVM.IA.W 命令 (ARMアセンブリ)

MOVM (Move Multiple) 命令は、ARMアセンブリにおいて複数のレジスタの値を一度にメモリにロードまたはストアするために使用されます。

  • MOVM.IA.W (R0), [R1-R12] のような形式は、R0が指すアドレスから、R1からR12までのレジスタに値をロードすることを意味します。.IA は "Increment After" を意味し、メモリポインタが各レジスタのロード後にインクリメントされることを示します。.W はワード(32ビット)単位での操作を示します。

この命令は、関数呼び出しの際にレジスタの状態を保存したり、復元したりする際によく使用されます。

技術的詳細

このバグの核心は、Goランタイムがプリエンプション時にレジスタの状態を保存・復元するメカニズムと、ARMのソフトウェア浮動小数点ハンドラの動作の間の相互作用にありました。

  1. レジスタの保存: プリエンプションが発生すると、Goランタイムは現在のgoroutineの実行コンテキスト(レジスタの値を含む)をスタックに保存します。これにより、後でそのgoroutineが再開されたときに、中断された時点から正確に実行を再開できます。
  2. ソフトウェア浮動小数点ハンドラの動作: ソフトウェア浮動小数点演算が実行されている間、ハンドラは内部的にレジスタを使用します。問題は、このハンドラがmレジスタ(R9)の値を変更する可能性があったことです。
  3. プリエンプションとmレジスタの変更: ソフトウェア浮動小数点ハンドラの実行中にプリエンプションが発生し、その間にスケジューラが現在のM(OSスレッド)を別のMに切り替える、あるいは同じMが別のPにアタッチされるなど、mレジスタが指すコンテキストが変更される可能性がありました。
  4. 誤ったレジスタの復元: プリエンプションから復帰する際、ランタイムは以前に保存したレジスタの状態を復元します。このとき、MOVM.IA.W (R0), [R1-R12] のような命令が使用され、保存されたR9(古いmの値)が現在のR9に上書きされていました。しかし、もしプリエンプション中にmレジスタが指すMが変更されていた場合、古いmの値を復元することは誤りであり、ランタイムが不正な状態に陥る原因となります。gレジスタ(R10)については、プリエンプション中に変更されることはないはずですが、mレジスタと同様に一括で復元されるため、同様の問題を引き起こす可能性がありました。

この修正は、mレジスタ(R9)とgレジスタ(R10)がランタイムによって常に正しい値に維持されているという事実に基づいています。これらのレジスタは、プリエンプションが発生しても、ランタイムの内部ロジックによって適切に更新されるか、あるいは変更されないことが保証されています。したがって、プリエンプションから復帰する際に、これらのレジスタの古い値を復元する必要はなく、むしろ復元すべきではありません。

修正は、MOVM.IA.W命令を分割し、R9とR10以外のレジスタのみを復元するように変更することで、この問題を解決しています。これにより、プリエンプション中にmレジスタが変更された場合でも、復帰時に正しいmレジスタの値が維持されるようになります。

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

変更は src/pkg/runtime/vlop_arm.s ファイル内の _sfloat 関数にあります。このファイルは、ARMアーキテクチャにおけるソフトウェア浮動小数点演算のハンドラを定義するアセンブリコードです。

具体的には、レジスタを復元する以下の行が変更されました。

変更前:

	MOVM.IA.W	(R0), [R1-R12]

変更後:

	// Restore R1-R8 and R11-R12, but ignore the saved R9 (m) and R10 (g).
	// Both are maintained by the runtime and always have correct values,
	// so there is no need to restore old values here.
	// The g should not have changed, but m may have, if we were preempted
	// and restarted on a different thread, in which case restoring the old
	// value is incorrect and will cause serious confusion in the runtime.
	MOVM.IA.W	(R0), [R1-R8]
	MOVW	$52(R13), R0
	MOVM.IA.W	(R0), [R11-R12]

コアとなるコードの解説

変更前の MOVM.IA.W (R0), [R1-R12] 命令は、R0が指すメモリ位置からR1からR12までのすべてのレジスタを一度に復元していました。これには、Goランタイムが特別な意味を持つR9(m)とR10(g)レジスタも含まれていました。

変更後のコードでは、この一括復元が2つのステップに分割されています。

  1. MOVM.IA.W (R0), [R1-R8]
    • この命令は、R0が指すメモリ位置からR1からR8までのレジスタを復元します。これにより、R9とR10は復元対象から除外されます。
  2. MOVW $52(R13), R0
    • これは、スタックポインタR13からオフセット52バイトの位置にある値をR0にロードします。このオフセットは、R11とR12が保存されているメモリ位置を指すように計算されています。これは、元の MOVM.IA.W (R0), [R1-R12] がR0をインクリメントしながらレジスタをロードしていくため、R1-R8をロードした後のR0の値ではR11-R12の開始位置を指さないため、R0を再設定する必要があるためです。
  3. MOVM.IA.W (R0), [R11-R12]
    • この命令は、R0が指す新しいメモリ位置からR11とR12レジスタを復元します。

この変更により、R9(m)とR10(g)レジスタは、ソフトウェア浮動小数点ハンドラが終了してレジスタが復元される際に、以前に保存された古い値で上書きされることがなくなりました。これにより、プリエンプション中にmレジスタが変更された場合でも、ランタイムは常に正しいmレジスタの値を参照できるようになり、ランタイムの安定性と正確性が向上します。

コメントで明確に述べられているように、gレジスタはプリエンプション中に変更されるべきではありませんが、mレジスタは異なるスレッドで再開された場合に変更される可能性があります。この修正は、両方のレジスタを復元対象から除外することで、この潜在的な問題を包括的に解決しています。

関連リンク

参考にした情報源リンク