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

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

このコミットは、GoランタイムのARMアーキテクチャ向けmemsetおよびmemclr関数におけるレジスタの使用方法を最適化し、特定のレジスタ(R9/R10)が意図せず上書きされる(clobberされる)問題を回避することを目的としています。これにより、Goプログラムの安定性と信頼性が向上します。

コミット

commit 07826038c39afb5021134b2c403f773f3f9591aa
Author: Dave Cheney <dave@cheney.net>
Date:   Mon Jun 25 07:59:50 2012 +1000

    runtime: avoid r9/r10 during memset
    
    Partially fixes issue 3718.
    
    R=golang-dev, rsc, minux.ma
    CC=golang-dev
    https://golang.org/cl/6300043

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

https://github.com/golang/go/commit/07826038c39afb5021134b2c403f773f3f9591aa

元コミット内容

このコミットは、Goランタイムにおいて、memset関数が実行中にR9およびR10レジスタを上書きする問題を回避するための変更です。これは、GoのIssue 3718を部分的に修正するものです。

変更の背景

この変更の背景には、Goランタイムのmemset関数がARMアーキテクチャ上で動作する際に、特定のレジスタ(R9とR10)を意図せず変更してしまうというバグがありました。コミットメッセージに記載されている「Partially fixes issue 3718」は、この問題がGoのIssue Trackerで報告されていたことを示しています。

Web検索の結果、GoのIssue 3718は「memset/memmove functions were clobbering registers r9/r10 on Linux/ARM systems」という内容であることが確認されました。Goランタイムでは、R9レジスタはm(現在のM(Machine)構造体へのポインタ)、R10レジスタはg(現在のG(Goroutine)構造体へのポインタ)を保持するために使用されます。これらのレジスタがmemsetのような低レベルのアセンブリ関数によって上書きされると、Goスケジューラやガベージコレクタなどのランタイムの重要な部分が誤動作し、クラッシュや予期せぬ動作を引き起こす可能性がありました。

このコミットは、memsetがこれらの重要なレジスタを破壊しないようにすることで、Goプログラムの安定性を向上させることを目的としています。

前提知識の解説

ARMアセンブリ言語

ARMアセンブリ言語は、ARMアーキテクチャのプロセッサ上で実行される低レベルのプログラミング言語です。レジスタ、メモリ操作、分岐命令などを直接操作します。Goランタイムのパフォーマンスが重要な部分や、ハードウェアに密接に関わる部分は、C言語ではなくアセンブリ言語で記述されることがあります。

ARMレジスタ

ARMプロセッサには、汎用レジスタ(R0-R12)、スタックポインタ(SP、R13)、リンクレジスタ(LR、R14)、プログラムカウンタ(PC、R15)などがあります。

  • R0-R3: 関数呼び出しの引数や戻り値に使用されることが多いです。
  • R4-R11: 汎用レジスタで、通常は呼び出し元が保存する必要があります(caller-saved)。
  • R9/R10: Goランタイムでは、R9がm(現在のM(Machine)構造体へのポインタ)、R10がg(現在のG(Goroutine)構造体へのポインタ)を保持するために特別な意味を持ちます。これらはGoのスケジューリングやコンテキストスイッチにおいて非常に重要です。
  • FP (Frame Pointer): スタックフレームのベースアドレスを指すレジスタ。
  • SP (Stack Pointer): スタックの現在のトップを指すレジスタ。

memsetmemclr

  • memset: メモリブロックを指定された値で埋める標準Cライブラリ関数に相当するものです。Goランタイムでは、メモリの初期化やゼロクリアなどに使用されます。
  • memclr: memsetの特殊なケースで、メモリブロックをゼロで埋めるために使用されます。Goランタイムでは、特に新しいメモリを割り当てた際に、その内容をゼロクリアするために頻繁に利用されます。

レジスタの「clobbering」(上書き)

「clobbering」とは、関数が呼び出された際に、その関数が呼び出し元が期待するレジスタの値を、呼び出し元に通知することなく変更してしまうことを指します。これは、特にアセンブリ言語で書かれた低レベル関数で問題となります。Goランタイムのように、特定のレジスタに重要なポインタ(mg)を保持している場合、これらのレジスタがclobberされると、ランタイムの状態が破壊され、プログラムがクラッシュする原因となります。

Goランタイムとアセンブリ

Goランタイムは、Goプログラムの実行を管理する非常に重要な部分です。スケジューラ、ガベージコレクタ、メモリ管理、システムコールなどが含まれます。これらの機能の一部は、パフォーマンスやハードウェアとの直接的な対話のために、アセンブリ言語で実装されています。アセンブリコードは、Goのコンパイラによって生成されるコードと連携して動作するため、レジスタの使用規約(calling convention)を厳密に守る必要があります。

技術的詳細

このコミットは、主にsrc/pkg/runtime/asm_arm.ssrc/pkg/runtime/memset_arm.sの2つのファイルに変更を加えています。

src/pkg/runtime/asm_arm.sの変更

このファイルでは、runtime·memclr関数の定義が変更されています。 変更前は、runtime·memclr内でmgレジスタの値をスタックに保存し、runtime·memset呼び出し後に復元していました。これは、memsetがこれらのレジスタをclobberする可能性を考慮した防御的なコードでした。 変更後は、runtime·memclrからmgの保存・復元処理が削除されています。これは、memset自体がR9/R10をclobberしないように修正されたため、memclr側での冗長な処理が不要になったことを意味します。また、スタックフレームサイズも$20から$0に削減されています。

src/pkg/runtime/memset_arm.sの変更

このファイルは、ARMアーキテクチャ向けのmemset関数のアセンブリ実装です。主要な変更点は以下の通りです。

  1. レジスタエイリアスの変更:

    • TO, TOE, N, TMPといった内部で使用するレジスタのエイリアスが変更されています。
    • 変更前: TO = 1, TOE = 2, N = 3, TMP = 3
    • 変更後: TO = 8, TOE = 11, N = 12, TMP = 12
    • これにより、memset関数内で使用されるレジスタが、Goランタイムでmgを保持するために使用されるR9/R10レジスタと衝突しないように再配置されています。特に、TOがR8、TOEがR11、NTMPがR12に割り当てられています。これにより、R9とR10がmemsetの内部処理で直接使用されることがなくなります。
  2. データレジスタの変更:

    • MOVW data+4(FP), R(4)MOVW data+4(FP), R(0) に変更されています。これは、memsetで埋めるバイト値が格納されているレジスタをR4からR0に変更しています。R0は通常、関数の引数として使用されるため、より標準的なレジスタの使用方法に沿っています。
  3. レジスタの複製ロジックの変更:

    • _4alignedラベル以降の、4バイト単位でメモリを埋めるループ(_f32loop)において、埋める値を複数のレジスタに複製する部分が変更されています。
    • 変更前はR4の値をR5からR11に複製していました。
    • 変更後はR0の値をR1からR7に複製しています。
    • これにより、MOVM.IA.W [R4-R11], (R(TO))MOVM.IA.W [R0-R7], (R(TO)) に変更され、memsetが一度に書き込むデータがR0-R7レジスタから取得されるようになります。この変更も、R9/R10レジスタの使用を避けるためのものです。

これらの変更により、memset関数はR9とR10レジスタを直接操作しなくなり、Goランタイムのmgポインタが安全に保持されるようになります。

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

src/pkg/runtime/asm_arm.s

--- a/src/pkg/runtime/asm_arm.s
+++ b/src/pkg/runtime/asm_arm.s
@@ -345,17 +345,11 @@ TEXT	runtime·cgocallback(SB),7,$16
 	// Done!
 	RET
 
-TEXT runtime·memclr(SB),7,$20
+TEXT runtime·memclr(SB),7,$0
 	MOVW	0(FP), R0
-\tMOVW	$0, R1		// c = 0
-\tMOVW	R1, -16(SP)
-\tMOVW	4(FP), R1	// n
-\tMOVW	R1, -12(SP)
-\tMOVW	m, -8(SP)	// Save m and g
-\tMOVW	g, -4(SP)
+\tMOVW	$0, R1
+\tMOVW	R1, 0(FP)
 	BL	runtime·memset(SB)
-\tMOVW	-8(SP), m	// Restore m and g, memset clobbers them
-\tMOVW	-4(SP), g
 	RET
 
 TEXT runtime·getcallerpc(SB),7,$-4

src/pkg/runtime/memset_arm.s

--- a/src/pkg/runtime/memset_arm.s
+++ b/src/pkg/runtime/memset_arm.s
@@ -23,17 +23,14 @@
 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 // THE SOFTWARE.
 
-TO = 1
-TOE = 2
-N = 3
-TMP = 3					/* N and TMP don't overlap */
-
-// TODO(kaib): memset clobbers R9 and R10 (m and g). This makes the
-// registers unpredictable if (when) memset SIGSEGV's. Fix it by
-// moving the R4-R11 register bank.
+TO = 8
+TOE = 11
+N = 12
+TMP = 12				/* N and TMP don't overlap */
+
 TEXT runtime·memset(SB), $0
 	MOVW	R0, R(TO)
-\tMOVW	data+4(FP), R(4)
+\tMOVW	data+4(FP), R(0)
 	MOVW	n+8(FP), R(N)
 
 	ADD	R(N), R(TO), R(TOE)	/* to end pointer */
@@ -41,17 +38,17 @@ TEXT runtime·memset(SB), $0
 	CMP	$4, R(N)		/* need at least 4 bytes to copy */
 	BLT	_1tail
 
-\tAND	$0xFF, R(4)		/* it's a byte */
-\tSLL	$8, R(4), R(TMP)	/* replicate to a word */
-\tORR	R(TMP), R(4)
-\tSLL	$16, R(4), R(TMP)
-\tORR	R(TMP), R(4)
+\tAND	$0xFF, R(0)		/* it's a byte */
+\tSLL	$8, R(0), R(TMP)	/* replicate to a word */
+\tORR	R(TMP), R(0)
+\tSLL	$16, R(0), R(TMP)
+\tORR	R(TMP), R(0)
 
 _4align:				/* align on 4 */
 	AND.S	$3, R(TO), R(TMP)
 	BEQ	_4aligned
 
-\tMOVBU.P	R(4), 1(R(TO))		/* implicit write back */
+\tMOVBU.P	R(0), 1(R(TO))		/* implicit write back */
 	B	_4align
 
 _4aligned:
@@ -59,19 +56,19 @@ _4aligned:
 	CMP	R(TMP), R(TO)
 	BHS	_4tail
 
-\tMOVW	R4, R5			/* replicate */
-\tMOVW	R4, R6
-\tMOVW	R4, R7
-\tMOVW	R4, R8
-\tMOVW	R4, R9
-\tMOVW	R4, R10
-\tMOVW	R4, R11
+\tMOVW	R0, R1			/* replicate */
+\tMOVW	R0, R2
+\tMOVW	R0, R3
+\tMOVW	R0, R4
+\tMOVW	R0, R5
+\tMOVW	R0, R6
+\tMOVW	R0, R7
 
 _f32loop:
 	CMP	R(TMP), R(TO)
 	BHS	_4tail
 
-\tMOVM.IA.W [R4-R11], (R(TO))
+\tMOVM.IA.W [R0-R7], (R(TO))
 	B	_f32loop
 
 _4tail:
@@ -80,14 +77,14 @@ _4loop:
 	CMP	R(TMP), R(TO)
 	BHS	_1tail
 
-\tMOVW.P	R(4), 4(R(TO))		/* implicit write back */
+\tMOVW.P	R(0), 4(R(TO))		/* implicit write back */
 	B	_4loop
 
 _1tail:
 	CMP	R(TO), R(TOE)
 	BEQ	_return
 
-\tMOVBU.P	R(4), 1(R(TO))		/* implicit write back */
+\tMOVBU.P	R(0), 1(R(TO))		/* implicit write back */
 	B	_1tail
 
 _return:

コアとなるコードの解説

src/pkg/runtime/asm_arm.sの変更点

  • TEXT runtime·memclr(SB),7,$20 から TEXT runtime·memclr(SB),7,$0:
    • $20はスタックフレームのサイズを示していました。以前はmgレジスタをスタックに保存するために20バイトのスペースが必要でしたが、memsetがR9/R10をclobberしなくなったため、この保存・復元処理が不要になり、スタックフレームサイズが0に削減されました。これにより、関数呼び出しのオーバーヘッドがわずかに減少します。
  • MOVW m, -8(SP)MOVW g, -4(SP) の削除:
    • mgレジスタの値をスタックに保存する命令が削除されました。
  • MOVW -8(SP), mMOVW -4(SP), g の削除:
    • memset呼び出し後にmgレジスタの値をスタックから復元する命令が削除されました。
    • これらの削除は、memsetがR9/R10レジスタを安全に扱うようになったため、memclr側でこれらのレジスタを保護する必要がなくなったことを示しています。

src/pkg/runtime/memset_arm.sの変更点

  • レジスタエイリアスの変更 (TO, TOE, N, TMP):
    • TO = 1 (R1) から TO = 8 (R8) へ、TOE = 2 (R2) から TOE = 11 (R11) へ、N = 3 (R3) から N = 12 (R12) へ、TMP = 3 (R3) から TMP = 12 (R12) へと変更されています。
    • この変更の最も重要な点は、memset関数が内部で使用するレジスタが、Goランタイムがmgポインタを保持するR9とR10レジスタと衝突しないように再配置されたことです。これにより、memsetが実行中にこれらの重要なレジスタの値を意図せず変更する(clobberする)ことがなくなります。
  • MOVW data+4(FP), R(4) から MOVW data+4(FP), R(0):
    • memsetでメモリを埋めるために使用されるバイト値が、スタックフレームポインタ(FP)からのオフセットdata+4から読み込まれ、R4レジスタではなくR0レジスタにロードされるようになりました。R0は通常、関数の最初の引数を保持するために使用されるため、これはより標準的なARMの呼び出し規約に沿った変更です。
  • AND $0xFF, R(4) などの変更:
    • 埋めるバイト値をワード(4バイト)に複製する処理において、ソースレジスタがR4からR0に変更されています。これは、前の変更(dataをR0にロードする)と一貫性を保つためのものです。
  • MOVBU.P R(4), 1(R(TO)) から MOVBU.P R(0), 1(R(TO)):
    • バイト単位での書き込み処理においても、ソースレジスタがR4からR0に変更されています。
  • MOVW R4, R5 ... MOVW R4, R11 から MOVW R0, R1 ... MOVW R0, R7:
    • 32バイト単位でメモリを埋めるループ(_f32loop)において、埋める値を複数のレジスタに複製する際に、ソースレジスタがR4からR0に変更され、複製先のレジスタもR5-R11からR1-R7に変更されています。
  • MOVM.IA.W [R4-R11], (R(TO)) から MOVM.IA.W [R0-R7], (R(TO)):
    • 複数のレジスタの値を一度にメモリに書き込む命令(MOVM.IA.W)において、書き込むレジスタの範囲がR4-R11からR0-R7に変更されています。これにより、R9とR10がこの操作に含まれなくなり、安全性が確保されます。

これらの変更は、Goランタイムのmemset関数がARMアーキテクチャ上で実行される際に、mgポインタを保持するR9とR10レジスタを保護することを保証します。これにより、ランタイムの安定性が向上し、Goプログラムがより堅牢に動作するようになります。

関連リンク

  • Go CL (Code Review) リンク: https://golang.org/cl/6300043

参考にした情報源リンク

  • GitHub Issue 3718 (golang/go): https://github.com/golang/go/issues/3718
  • GitHub Issue 3718 (golang/go) (Appspot link): https://golang.org/issue/3718 (これはGoのIssue Trackerへのリダイレクトです)
  • Web検索結果 (golang issue 3718): https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHtjgTrnF-zhdegXVuIqfT--77Y2p2Vjlq8iHaTjKQhS5b0nJvXCn9X3e374ifOzDt8fmATqtpiVx8T6397TM--5oncTxq1xyhpkSOx9PcZOnOEQvBA4ZHzml-Azo8bdLKPTvI= (Web検索で得られた情報源のリンク)