[インデックス 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
:Rn
とRm
を比較し、ステータスフラグを設定します。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構造体へのポインタとして使用することがあります。これらのポインタが破壊されると、スケジューラやガベージコレクタなどのランタイムの重要な部分が誤動作し、深刻なバグやクラッシュにつながる可能性がありました。
この修正では、以下の主要な変更が行われています。
-
レジスタ割り当ての変更:
- 以前は
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
はリンカによって特殊な用途で使われる可能性があるため、コメントで注意喚起されています。
- 以前は
-
バルクレジスタ転送の変更:
- 以前は
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
によって破壊されることを防ぎます。
- 以前は
-
スタックへのレジスタ退避:
_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)
で復元しています。これは、ループ内でTS
やTE
が一時的に変更される可能性があるため、その値を保護するためと考えられます。
-
アラインメント処理の改善:
_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
におけるレジスタ使用の変更と、それに伴うスタック操作の追加を示しています。
-
レジスタエイリアスの再定義:
TS
,TE
,FROM
,N
,TMP
,TMP1
などのエイリアスが、R0-R7の範囲に収まるように、またはR9/R10を避けるように再定義されています。特にTE = 8
、FROM = 11
、N = 12
といった変更は、以前R9やR10が割り当てられていたレジスタ番号を避けるためのものです。RSHIFT
,LSHIFT
,OFFSET
などの定数も再定義されています。BR0
からBW3
、FW0
からFR3
といった、ブロック転送に使用するレジスタのエイリアスも、R0-R8の範囲に収まるように変更されています。これにより、R9やR10がバルク転送の対象から外れます。
-
TEXT runtime·memmove(SB), 7, $8
から$4
への変更:- 関数のスタックフレームサイズが8バイトから4バイトに減っています。これは、以前R9とR10をスタックに保存していた(
MOVW R9, 4(R13)
とMOVW R10, 8(R13)
)処理が削除されたためです。
- 関数のスタックフレームサイズが8バイトから4バイトに減っています。これは、以前R9とR10をスタックに保存していた(
-
R9/R10の保存・復元処理の削除:
- 以前のコードでは、関数の冒頭で
MOVW R9, 4(R13)
とMOVW R10, 8(R13)
によってR9とR10をスタックに保存し、関数の終わりにMOVW 4(R13), R9
とMOVW 8(R13), R10
で復元していました。これらの行が完全に削除されています。これは、memmove
がR9とR10を破壊しないようにレジスタ割り当てを変更したため、これらのレジスタを保存・復元する必要がなくなったことを意味します。
- 以前のコードでは、関数の冒頭で
-
バルクレジスタ転送命令の変更:
_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が使用されなくなります。
-
TS
とTE
のスタック退避と復元:_b32loop
と_f32loop
の開始時に、MOVW R(TS), savedts+4(SP)
とMOVW R(TE), savedte+4(SP)
でTS
とTE
の値をスタックに保存しています。- 対応する
_b4tail
と_f4tail
の開始時に、MOVW savedts+4(SP), R(TS)
とMOVW savedte+4(SP), R(TE)
でスタックから復元しています。 - これは、ループ内で
TS
やTE
が一時的に変更される可能性があり、その変更がループの継続条件や後続の処理に影響を与えないように、元の値を保護するための措置です。
-
アラインメント処理の変更:
_bunaligned
と_funaligned
セクションで、ソースアドレスのアラインメント処理がAND ~0x03, R(FROM)
からBIC $3, R(FROM)
に変更されています。BIC
命令は、指定されたビットをクリアする(0にする)命令であり、この場合は下位2ビットをクリアして4バイトアラインメントを実現しています。これは機能的には同じですが、よりARMらしいイディオムです。
これらの変更は、Goランタイムのmemmove
関数がARMアーキテクチャ上でR9とR10レジスタを破壊しないようにするための重要な修正であり、Goプログラムの安定性と信頼性を向上させます。
関連リンク
- Go issue #3718: runtime: memset/memmove clobbers r9/r10 on Linux/ARM
- Go CL 6305100: https://golang.org/cl/6305100
参考にした情報源リンク
- GitHub: golang/go commit e34079bb5919e1cf66b6008a1b7e8ee36a03c487
- GitHub Issue: golang/go issue 3718
- ARM Architecture Reference Manual (ARM ARM) - アセンブリ命令の詳細については、ARMの公式ドキュメントを参照。
- Go Assembly Language Documentation - Goのアセンブリ構文と規約については、Goの公式ドキュメントを参照。