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

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

このコミットは、Go言語のランタイムにおけるmemmove関数のARMアーキテクチャ向けアセンブリ実装、具体的にはsrc/pkg/runtime/memmove_arm.sファイルに対する変更です。memmoveはメモリブロックをコピーするための標準的な関数であり、その効率的な実装はシステム全体のパフォーマンスに直結します。このファイルは、GoランタイムがARMプロセッサ上でメモリ操作を最適化するために使用する低レベルのアセンブリコードを含んでいます。

コミット

このコミットは、ARMアーキテクチャにおけるmemmoveルーチンが、特定のレジスタ(R9およびR10)を意図せず破壊(clobber)してしまう問題を修正することを目的としています。この問題は、Goランタイムの安定性と正確性に影響を与える可能性がありました。変更は、レジスタの使用方法を調整し、必要に応じてスタックにレジスタの値を一時的に保存することで、レジスタの破壊を回避しています。

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

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

元コミット内容

commit e34079bb5919e1cf66b6008a1b7e8ee36a03c487
Author: Dave Cheney <dave@cheney.net>
Date:   Mon Jun 25 08:28:30 2012 +1000

    runtime: avoid r9/r10 during memmove
    
    Fixes #3718.
    
    Requires CL 6300043.
    
    R=rsc, minux.ma, extraterrestrial.neighbour
    CC=golang-dev
    https://golang.org/cl/6305100

変更の背景

この変更の背景には、Go言語のランタイムがARMアーキテクチャ上でメモリコピー操作を行う際に発生していた、レジスタの誤用問題があります。具体的には、Go issue #3718「runtime: memset/memmove clobbers r9/r10 on Linux/ARM」で報告された問題に対応しています。

ARMアーキテクチャでは、特定のレジスタ(R9, R10など)が、特定のコンテキストやABI(Application Binary Interface)において、特別な用途や呼び出し規約を持つことがあります。例えば、Goのランタイムでは、R9とR10がそれぞれ現在のgoroutineのG構造体と現在のOSスレッドのM構造体へのポインタを保持するために使用されることがあります。これらのレジスタが、memmoveのような低レベルの関数内で、その内容を保存せずに上書きされてしまうと、Goランタイムの内部状態が破壊され、予期せぬクラッシュや不正な動作を引き起こす可能性がありました。

このコミットは、memmoveルーチンがR9とR10を一時的な作業レジスタとして使用しないように修正することで、このレジスタ破壊の問題を解決しています。これにより、GoプログラムがARM上でより堅牢に動作するようになります。

前提知識の解説

ARMアセンブリ言語

ARMアセンブリ言語は、ARMアーキテクチャのプロセッサが直接実行できる機械語命令を人間が読める形式で記述したものです。レジスタ、メモリ操作、分岐命令などを直接制御します。

ARMレジスタ

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

  • R0-R3: 関数呼び出しの引数や戻り値、一時的な値に使用されます。
  • R4-R11: 汎用レジスタ。関数呼び出し間で値を保持するために使用されることがありますが、呼び出し規約によっては保存・復元が必要です。
  • R9 (SB): Goランタイムでは、通常、現在のgoroutineのG構造体へのポインタを保持するために使用されます(SBはStatic Baseの略で、Goのアセンブリでは特殊な意味を持つことがあります)。
  • R10 (g): Goランタイムでは、通常、現在のOSスレッドのM構造体へのポインタを保持するために使用されます。
  • R11: 一時的なレジスタとして使用されることがありますが、リンカが特定の命令を合成するために使用する場合があるため、注意が必要です。
  • R13 (SP): スタックポインタ。現在のスタックフレームの最上位を指します。
  • FP (Frame Pointer): フレームポインタ。現在のスタックフレームの基底を指します。Goのアセンブリでは、関数引数やローカル変数へのアクセスにFPからのオフセットがよく使われます。

レジスタの「clobber」

「clobber」とは、ある関数やルーチンが、呼び出し元が期待するレジスタの値を、その内容を保存せずに上書きしてしまうことを指します。これは、呼び出し規約(ABI)に違反する場合に問題となります。ABIでは、どのレジスタが呼び出し元によって保存されるべきか(caller-saved)、どのレジスタが呼び出し先によって保存されるべきか(callee-saved)が定められています。clobberingが発生すると、プログラムの論理が破綻し、バグにつながります。

memmove関数

memmoveは、C言語の標準ライブラリ関数の一つで、メモリブロックをコピーします。memcpyと異なり、コピー元とコピー先のメモリ領域が重なっていても正しく動作することが保証されています。これは、コピーの方向(前方または後方)を適切に選択することで実現されます。

ARMアセンブリ命令(関連するもの)

  • MOVW Rd, #imm: 即値immをレジスタRdに移動します。
  • MOVW Rd, [Rn, #offset]: メモリアドレスRn + offsetからワード(4バイト)を読み込み、レジスタRdに格納します。
  • MOVW [Rn, #offset], Rd: レジスタRdのワードをメモリアドレスRn + offsetに書き込みます。
  • MOVW.P Rd, #offset(Rn): メモリアドレスRn + offsetからワードを読み込み、レジスタRdに格納し、同時にRnレジスタの値をRn + offsetに更新します(プリインデックス付きライトバック)。
  • MOVM.DB.W Rn, {reglist}: デクリメンティング・ビフォア(Decrement Before)モードで、複数のレジスタをメモリにストアします。Rnが指すアドレスからレジスタリストの数だけワードを書き込み、Rnを更新します。
  • MOVM.IA.W Rn, {reglist}: インクリメンティング・アフター(Increment After)モードで、複数のレジスタをメモリからロードまたはストアします。Rnが指すアドレスからレジスタリストの数だけワードを読み込み/書き込み、Rnを更新します。
  • ADD Rd, Rn, #imm: Rnに即値immを加算し、結果をRdに格納します。
  • SUB Rd, Rn, #imm: Rnから即値immを減算し、結果をRdに格納します。
  • CMP Rn, Rm: RnRmを比較し、ステータスフラグを設定します。
  • Bcc label: 条件付き分岐。ccは条件コード(例: EQ等しい、NE等しくない、LS以下、HS以上)。
  • BLS label: Less than or Same (符号なし) または Lower or Same (符号なし) の場合に分岐します。
  • BHS label: Higher or Same (符号なし) の場合に分岐します。
  • BNE label: Not Equal の場合に分岐します。
  • AND Rd, Rn, #imm: Rnと即値immのビット単位AND演算を行い、結果をRdに格納します。
  • BIC Rd, Rn, #imm: Rnと即値immのビット単位クリア(AND NOT)演算を行い、結果をRdに格納します。
  • ORR Rd, Rn, #imm: Rnと即値immのビット単位OR演算を行い、結果をRdに格納します。
  • RET: 関数から戻ります。
  • TEXT symbol(SB), flags, $framesize: Goアセンブリにおける関数定義の開始。SBはシンボルベース、flagsは関数属性、$framesizeはスタックフレームサイズ。

技術的詳細

このコミットは、ARMアーキテクチャにおけるmemmove関数のアセンブリ実装において、レジスタの使用方法を根本的に見直しています。以前の実装では、memmoveがメモリブロックをコピーする際に、一時的なデータ転送のためにR4からR11までのレジスタ([R4-R11])をまとめて使用していました。このレジスタ範囲には、Goランタイムが特別な意味を持つR9とR10が含まれていました。

問題は、memmoveがこれらのレジスタの内容を保存せずに上書きしてしまったことです。Goランタイムは、R9を現在のgoroutineのG構造体へのポインタ、R10を現在のOSスレッドのM構造体へのポインタとして使用することがあります。これらのポインタが破壊されると、スケジューラやガベージコレクタなどのランタイムの重要な部分が誤動作し、深刻なバグやクラッシュにつながる可能性がありました。

この修正では、以下の主要な変更が行われています。

  1. レジスタ割り当ての変更:

    • 以前はTS = 0, TE = 1, FROM = 2, N = 3, TMP = 3, TMP1 = 4といったレジスタ番号のエイリアスが使われていました。
    • 新しい実装では、TS = 0, TE = 8, FROM = 11, N = 12, TMP = 12, TMP1 = 5など、より高位のレジスタや、R0-R7の範囲に限定されたレジスタを使用するように変更されています。特に、R9とR10が直接的なデータ転送レジスタとして使用されないように配慮されています。
    • R11はリンカによって特殊な用途で使われる可能性があるため、コメントで注意喚起されています。
  2. バルクレジスタ転送の変更:

    • 以前はMOVM.DB.W (R(FROM)), [R4-R11]MOVM.IA.W (R(FROM)), [R4-R11]のように、R4からR11までのレジスタをまとめてメモリとの間で転送していました。
    • 新しい実装では、MOVM.DB.W (R(FROM)), [R0-R7]MOVM.IA.W (R(FROM)), [R1-R8]のように、R0からR7(またはR1からR8)の範囲のレジスタのみを使用するように変更されています。これにより、R9とR10がmemmoveによって破壊されることを防ぎます。
  3. スタックへのレジスタ退避:

    • _b32loop(後方コピーの32バイトチャンク処理)と_f32loop(前方コピーの32バイトチャンク処理)のループ内で、TS(ターゲット開始アドレス)とTE(ターゲット終了アドレス)の値を一時的にスタックに退避する処理が追加されています。
    • MOVW R(TS), savedts+4(SP) および MOVW R(TE), savedte+4(SP) でスタックに保存し、ループ後に MOVW savedts+4(SP), R(TS) および MOVW savedte+4(SP), R(TE) で復元しています。これは、ループ内でTSTEが一時的に変更される可能性があるため、その値を保護するためと考えられます。
  4. アラインメント処理の改善:

    • _bunalignedおよび_funalignedセクションで、ソースアドレスのアラインメント処理がAND ~0x03, R(FROM)からBIC $3, R(FROM)に変更されています。BIC(Bit Clear)命令は、指定されたビットをクリアする(0にする)命令であり、この場合は下位2ビットをクリアして4バイトアラインメントを実現しています。これは機能的には同じですが、よりARMらしいイディオムです。

これらの変更により、memmoveはR9とR10を安全に保ちながら、効率的なメモリコピー操作を実行できるようになりました。

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

--- a/src/pkg/runtime/memmove_arm.s
+++ b/src/pkg/runtime/memmove_arm.s
@@ -23,19 +23,40 @@
 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 // THE SOFTWARE.\n
+// TE or TS are spilled to the stack during bulk register moves.
 TS = 0
-TE = 1
-FROM = 2
-N = 3
-TMP = 3			/* N and TMP don't overlap */
-TMP1 = 4
-
-// TODO(kaib): This can be done with the existing registers of LR is re-used. Same for memset.
-TEXT runtime·memmove(SB), 7, $8
-	// save g and m
-	MOVW	R9, 4(R13)
-	MOVW	R10, 8(R13)
-
+TE = 8
+
+// Warning: the linker will use R11 to synthesize certain instructions. Please
+// take care and double check with objdump.
+FROM = 11
+N = 12
+TMP = 12				/* N and TMP don't overlap */
+TMP1 = 5
+
+RSHIFT = 5
+LSHIFT = 6
+OFFSET = 7
+
+BR0 = 0					/* shared with TS */
+BW0 = 1
+BR1 = 1
+BW1 = 2
+BR2 = 2
+BW2 = 3
+BR3 = 3
+BW3 = 4
+
+FW0 = 1
+FR0 = 2
+FW1 = 2
+FR1 = 3
+FW2 = 3
+FR2 = 4
+FW3 = 4
+FR3 = 8					/* shared with TE */
+
+TEXT runtime·memmove(SB), 7, $4
 _memmove:
 	MOVW	to+0(FP), R(TS)
 	MOVW	from+4(FP), R(FROM)
@@ -64,15 +85,17 @@ _b4aligned:			/* is source now aligned? */
 	BNE	_bunaligned
 
 	ADD	$31, R(TS), R(TMP)	/* do 32-byte chunks if possible */
+\tMOVW\tR(TS), savedts+4(SP)
 _b32loop:
 	CMP	R(TMP), R(TE)
 	BLS	_b4tail
 
-\tMOVM.DB.W (R(FROM)), [R4-R11]
-\tMOVM.DB.W [R4-R11], (R(TE))
+\tMOVM.DB.W (R(FROM)), [R0-R7]
+\tMOVM.DB.W [R0-R7], (R(TE))
 	B	_b32loop
 
 _b4tail:			/* do remaining words if possible */
+\tMOVW\tsavedts+4(SP), R(TS)
 	ADD	$3, R(TS), R(TMP)
 _b4loop:
 	CMP	R(TMP), R(TE)
@@ -107,22 +130,24 @@ _f4aligned:			/* is source now aligned? */
 	BNE	_funaligned
 
 	SUB	$31, R(TE), R(TMP)	/* do 32-byte chunks if possible */
+\tMOVW\tR(TE), savedte+4(SP)
 _f32loop:
 	CMP	R(TMP), R(TS)
 	BHS	_f4tail
 
-\tMOVM.IA.W (R(FROM)), [R4-R11] 
-\tMOVM.IA.W [R4-R11], (R(TS))
+\tMOVM.IA.W (R(FROM)), [R1-R8] 
+\tMOVM.IA.W [R1-R8], (R(TS))
 	B	_f32loop
 
 _f4tail:
+\tMOVW\tsavedte+4(SP), R(TE)
 	SUB	$3, R(TE), R(TMP)	/* do remaining words if possible */
 _f4loop:
 	CMP	R(TMP), R(TS)
 	BHS	_f1tail
 
 	MOVW.P	4(R(FROM)), R(TMP1)	/* implicit write back */
-\tMOVW.P	R4, 4(R(TS))		/* implicit write back */
+\tMOVW.P	R(TMP1), 4(R(TS))	/* implicit write back */
 	B	_f4loop
 
 _f1tail:
@@ -134,25 +159,9 @@ _f1tail:
 	B	_f1tail
 
 _return:
-\t// restore g and m
-\tMOVW	4(R13), R9
-\tMOVW	8(R13), R10
 	MOVW	to+0(FP), R0
 	RET
 
-RSHIFT = 4
-LSHIFT = 5
-OFFSET = 6
-
-BR0 = 7
-BW0 = 8
-BR1 = 8
-BW1 = 9
-BR2 = 9
-BW2 = 10
-BR3 = 10
-BW3 = 11
-
 _bunaligned:
 	CMP	$2, R(TMP)		/* is R(TMP) < 2 ? */
 
@@ -172,7 +181,8 @@ _bunaligned:
 	CMP	R(TMP), R(TE)
 	BLS	_b1tail
 
-\tAND	$~0x03, R(FROM)		/* align source */
+\tBIC	$3, R(FROM)		/* align source */
+\tMOVW\tR(TS), savedts+4(SP)
 	MOVW	(R(FROM)), R(BR0)	/* prime first block register */
 
 _bu16loop:
@@ -196,18 +206,10 @@ _bu16loop:
 	B	_bu16loop
 
 _bu1tail:
+\tMOVW\tsavedts+4(SP), R(TS)
 	ADD	R(OFFSET), R(FROM)
 	B	_b1tail
 
-FW0 = 7
-FR0 = 8
-FW1 = 8
-FR1 = 9
-FW2 = 9
-FR2 = 10
-FW3 = 10
-FR3 = 11
-
 _funaligned:
 	CMP	$2, R(TMP)
 
@@ -227,7 +229,8 @@ _funaligned:
 	CMP	R(TMP), R(TS)
 	BHS	_f1tail
 
-\tAND	$~0x03, R(FROM)		/* align source */
+\tBIC	$3, R(FROM)		/* align source */
+\tMOVW\tR(TE), savedte+4(SP)
 	MOVW.P	4(R(FROM)), R(FR3)	/* prime last block register, implicit write back */
 
 _fu16loop:
@@ -235,7 +238,7 @@ _fu16loop:
 	BHS	_fu1tail
 
 	MOVW\tR(FR3)>>R(RSHIFT), R(FW0)
-\tMOVM.IA.W (R(FROM)), [R(FR0)-R(FR3)]
+\tMOVM.IA.W (R(FROM)), [R(FR0),R(FR1),R(FR2),R(FR3)]
 	ORR\tR(FR0)<<R(LSHIFT), R(FW0)
 
 	MOVW\tR(FR0)>>R(RSHIFT), R(FW1)
@@ -247,9 +250,10 @@ _fu16loop:
 	MOVW\tR(FR2)>>R(RSHIFT), R(FW3)
 	ORR\tR(FR3)<<R(LSHIFT), R(FW3)
 
-\tMOVM.IA.W [R(FW0)-R(FW3)], (R(TS))\n+\tMOVM.IA.W [R(FW0),R(FW1),R(FW2),R(FW3)], (R(TS))\n 	B\t_fu16loop
 
 _fu1tail:
+\tMOVW\tsavedte+4(SP), R(TE)
 	SUB\tR(OFFSET), R(FROM)
 	B\t_f1tail

コアとなるコードの解説

上記の差分は、memmove_arm.sにおけるレジスタ使用の変更と、それに伴うスタック操作の追加を示しています。

  1. レジスタエイリアスの再定義:

    • TS, TE, FROM, N, TMP, TMP1などのエイリアスが、R0-R7の範囲に収まるように、またはR9/R10を避けるように再定義されています。特にTE = 8FROM = 11N = 12といった変更は、以前R9やR10が割り当てられていたレジスタ番号を避けるためのものです。
    • RSHIFT, LSHIFT, OFFSETなどの定数も再定義されています。
    • BR0からBW3FW0からFR3といった、ブロック転送に使用するレジスタのエイリアスも、R0-R8の範囲に収まるように変更されています。これにより、R9やR10がバルク転送の対象から外れます。
  2. TEXT runtime·memmove(SB), 7, $8 から $4 への変更:

    • 関数のスタックフレームサイズが8バイトから4バイトに減っています。これは、以前R9とR10をスタックに保存していた(MOVW R9, 4(R13)MOVW R10, 8(R13))処理が削除されたためです。
  3. R9/R10の保存・復元処理の削除:

    • 以前のコードでは、関数の冒頭でMOVW R9, 4(R13)MOVW R10, 8(R13)によってR9とR10をスタックに保存し、関数の終わりにMOVW 4(R13), R9MOVW 8(R13), R10で復元していました。これらの行が完全に削除されています。これは、memmoveがR9とR10を破壊しないようにレジスタ割り当てを変更したため、これらのレジスタを保存・復元する必要がなくなったことを意味します。
  4. バルクレジスタ転送命令の変更:

    • _b32loop(後方コピー)では、MOVM.DB.W (R(FROM)), [R4-R11]MOVM.DB.W (R(FROM)), [R0-R7]に変更されています。
    • _f32loop(前方コピー)では、MOVM.IA.W (R(FROM)), [R4-R11]MOVM.IA.W (R(FROM)), [R1-R8]に変更されています。
    • これらの変更により、メモリからレジスタへの一括ロード、およびレジスタからメモリへの一括ストアの際に、R9とR10が使用されなくなります。
  5. TSTEのスタック退避と復元:

    • _b32loop_f32loopの開始時に、MOVW R(TS), savedts+4(SP)MOVW R(TE), savedte+4(SP)TSTEの値をスタックに保存しています。
    • 対応する_b4tail_f4tailの開始時に、MOVW savedts+4(SP), R(TS)MOVW savedte+4(SP), R(TE)でスタックから復元しています。
    • これは、ループ内でTSTEが一時的に変更される可能性があり、その変更がループの継続条件や後続の処理に影響を与えないように、元の値を保護するための措置です。
  6. アラインメント処理の変更:

    • _bunaligned_funalignedセクションで、ソースアドレスのアラインメント処理がAND ~0x03, R(FROM)からBIC $3, R(FROM)に変更されています。BIC命令は、指定されたビットをクリアする(0にする)命令であり、この場合は下位2ビットをクリアして4バイトアラインメントを実現しています。これは機能的には同じですが、よりARMらしいイディオムです。

これらの変更は、Goランタイムのmemmove関数がARMアーキテクチャ上でR9とR10レジスタを破壊しないようにするための重要な修正であり、Goプログラムの安定性と信頼性を向上させます。

関連リンク

参考にした情報源リンク