[インデックス 16845] ファイルの概要
このコミットは、GoランタイムのARMアーキテクチャ向けmemmove
アセンブリコード、具体的にはsrc/pkg/runtime/memmove_arm.s
ファイルに対する修正です。memmove
はメモリブロックをコピーするための重要な関数であり、その実装はパフォーマンスと正確性の両面で極めて重要です。このファイルは、ARMプロセッサ上でのmemmove
の効率的な実行を担うアセンブリ言語で書かれたルーチンを含んでいます。
コミット
このコミットは、GoランタイムのARM版memmove
関数において、スタックフレームへのアクセス方法の誤りによりパラメータが破損する可能性があったバグを修正します。具体的には、スタックポインタ(SP)からのオフセット計算がx+(SP)
からx-(SP)
に変更され、スタック上の正しいメモリ位置にアクセスするように修正されました。これにより、memmove
の実行中に一時的に保存されるべき値が正しく扱われるようになり、データの破損が防止されます。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f8fd77baa9e57b2de6b9c0e08a3de0a7a8ad8947
元コミット内容
commit f8fd77baa9e57b2de6b9c0e08a3de0a7a8ad8947
Author: Nick Craig-Wood <nick@craig-wood.com>
Date: Tue Jul 23 09:29:25 2013 +1000
runtime: Stop arm memmove corrupting its parameters
Change use of x+(SP) to access the stack frame into x-(SP)
Fixes #5925.
R=golang-dev, bradfitz, dave, remyoudompheng, nick, rsc
CC=dave cheney <dave, golang-dev
https://golang.org/cl/11647043
変更の背景
memmove
関数は、指定されたメモリ領域から別のメモリ領域へバイト列をコピーする標準ライブラリ関数です。特に、コピー元とコピー先のメモリ領域が重なっている場合でも正しく動作することが保証されています。Goランタイムにおいて、このmemmove
はガベージコレクション、スライス操作、文字列操作など、様々な低レベルのメモリ操作で頻繁に利用されるため、その正確性と効率性はシステムの安定性とパフォーマンスに直結します。
ARMアーキテクチャ向けのアセンブリ実装において、memmove
が一時的にレジスタの値をスタックに保存し、後で復元する際に、スタックポインタ(SP)からのオフセット計算が誤っていたことが判明しました。具体的には、savedts
やsavedte
といった一時変数をスタックに保存する際に、x+(SP)
という形式でアドレス指定を行っていました。しかし、ARMのスタックは通常、高位アドレスから低位アドレスに向かって成長します(フルデセンディングスタック)。このため、スタックに新しいデータをプッシュする際にはSPを減算し、スタック上の既存のデータにアクセスする際にはSPからの負のオフセットを使用するのが一般的です。
この誤ったアドレス指定により、memmove
が自身のパラメータや他の重要なスタック上のデータを上書きしてしまう、いわゆる「パラメータの破損」が発生していました。これは、memmove
がコピー操作を行う際に、本来変更してはならないメモリ領域を破壊してしまう深刻なバグであり、プログラムのクラッシュや不正な動作を引き起こす可能性がありました。この問題はGoのIssue #5925として報告され、このコミットによって修正されました。
前提知識の解説
ARMアセンブリ言語
ARMアセンブリ言語は、ARMアーキテクチャのプロセッサが直接実行できる機械語命令を人間が読める形式で記述したものです。レジスタ、メモリ操作、条件分岐、ループなどの基本的なプログラミング構造を低レベルで制御します。
スタックとスタックポインタ (SP)
プログラムの実行中、関数呼び出しやローカル変数の保存には「スタック」と呼ばれるメモリ領域が使用されます。スタックはLIFO(Last-In, First-Out)の原則で動作し、データはスタックの「トップ」に追加(プッシュ)され、スタックのトップから削除(ポップ)されます。
「スタックポインタ (SP)」は、スタックの現在のトップを指す特別なレジスタです。ARMアーキテクチャでは、スタックは通常「フルデセンディングスタック」として実装されます。これは、スタックがメモリの高位アドレスから低位アドレスに向かって成長することを意味します。したがって、新しいデータをスタックにプッシュする際にはSPの値を減算し、スタックからデータをポップする際にはSPの値を加算します。スタック上の既存のデータにアクセスする場合、SPからのオフセットは通常負の値になります。
メモリのアドレス指定モード
ARMアセンブリでは、メモリにアクセスするための様々なアドレス指定モードがあります。このコミットで問題となったのは、ベースレジスタ(ここではSP)とオフセットを組み合わせたアドレス指定です。
x+(SP)
: これは、SPレジスタの値にx
を加算したアドレスを指します。もしスタックが低位アドレスから高位アドレスに成長する場合(アセンブリによってはそのような実装もありますが、ARMの一般的なスタック規約とは異なります)、またはSPがスタックのベースを指し、データがSPより高位アドレスに配置される場合に適切です。x-(SP)
: これは、SPレジスタの値からx
を減算したアドレスを指します。フルデセンディングスタックにおいて、SPがスタックのトップを指し、スタック上の既存のデータ(SPより低位アドレスにある)にアクセスする場合に適切です。
memmove
関数
memmove
はC言語の標準ライブラリ関数であり、Goランタイムでも同様の機能が提供されます。そのプロトタイプは通常 void *memmove(void *dest, const void *src, size_t n)
です。これは、src
が指すメモリ領域からn
バイトをdest
が指すメモリ領域にコピーします。memcpy
と異なり、src
とdest
のメモリ領域が重なっていても正しく動作することが保証されています。これは、コピー操作が一時的なバッファを介して行われるか、コピーの方向が調整されることによって実現されます。
技術的詳細
このコミットの核心は、ARMアセンブリにおけるスタックフレームへのアクセス方法の修正です。memmove_arm.s
内の複数の箇所で、一時的な値をスタックに保存したり、スタックから読み出したりする際に、savedts
やsavedte
といった変数に対してx+(SP)
という形式でアドレス指定が行われていました。
ARMの一般的なスタック規約では、スタックは高位アドレスから低位アドレスに向かって成長します。つまり、スタックにデータをプッシュするとSPの値は減少し、スタック上のデータはSPよりも低いアドレスに配置されます。したがって、SPを基準としてスタック上のデータにアクセスする場合、オフセットは負の値であるべきです。
例えば、MOVW R(TS), savedts+4(SP)
という命令は、レジスタR(TS)
の値をSP+4
のアドレスに保存しようとします。しかし、もしsavedts
がSPから負のオフセットにあるべき場所(例えばSP-4
)に割り当てられていた場合、SP+4
はスタックフレームの外部、あるいは別のスタック上のデータ領域を指してしまい、意図しないメモリ領域を上書きする結果となります。
このコミットでは、この誤りを修正するために、x+(SP)
の形式をすべてx-(SP)
に変更しています。具体的には、savedts+4(SP)
はsavedts-4(SP)
に、savedte+4(SP)
はsavedte-4(SP)
に変更されています。これにより、MOVW
命令はSPから負のオフセットにある正しいスタック上の位置にアクセスするようになり、一時的に保存されるべき値が正しく扱われ、パラメータの破損が防止されます。
この修正は、memmove
の順方向コピー(_b4aligned
, _b32loop
, _b4tail
, _bunaligned
, _bu16loop
, _bu1tail
)と逆方向コピー(_f4aligned
, _f32loop
, _f4tail
, _funaligned
, _fu16loop
, _fu1tail
)の両方のパスに適用されています。これにより、memmove
のすべての実行パスでスタックアクセスが正しく行われることが保証されます。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/memmove_arm.s
ファイルに集中しており、以下の8箇所でスタックアクセスのアドレス指定が変更されています。
--- a/src/pkg/runtime/memmove_arm.s
+++ b/src/pkg/runtime/memmove_arm.s
@@ -85,7 +85,7 @@ _b4aligned: /* is source now aligned? */
BNE _bunaligned
ADD $31, R(TS), R(TMP) /* do 32-byte chunks if possible */
- MOVW R(TS), savedts+4(SP)
+ MOVW R(TS), savedts-4(SP)
_b32loop:
CMP R(TMP), R(TE)
BLS _b4tail
@@ -95,7 +95,7 @@ _b32loop:
B _b32loop
_b4tail: /* do remaining words if possible */
- MOVW savedts+4(SP), R(TS)
+ MOVW savedts-4(SP), R(TS)
ADD $3, R(TS), R(TMP)
_b4loop:
CMP R(TMP), R(TE)
@@ -130,7 +130,7 @@ _f4aligned: /* is source now aligned? */
BNE _funaligned
SUB $31, R(TE), R(TMP) /* do 32-byte chunks if possible */
- MOVW R(TE), savedte+4(SP)
+ MOVW R(TE), savedte-4(SP)
_f32loop:
CMP R(TMP), R(TS)
BHS _f4tail
@@ -140,7 +140,7 @@ _f32loop:
B _f32loop
_f4tail:
- MOVW savedte+4(SP), R(TE)
+ MOVW savedte-4(SP), R(TE)
SUB $3, R(TE), R(TMP) /* do remaining words if possible */
_f4loop:
CMP R(TMP), R(TS)
@@ -182,7 +182,7 @@ _bunaligned:
BLS _b1tail
BIC $3, R(FROM) /* align source */
- MOVW R(TS), savedts+4(SP)
+ MOVW R(TS), savedts-4(SP)
MOVW (R(FROM)), R(BR0) /* prime first block register */
_bu16loop:
@@ -206,7 +206,7 @@ _bu16loop:
B _bu16loop
_bu1tail:
- MOVW savedts+4(SP), R(TS)
+ MOVW savedts-4(SP), R(TS)
ADD R(OFFSET), R(FROM)
B _b1tail
@@ -230,7 +230,7 @@ _funaligned:
BHS _f1tail
BIC $3, R(FROM) /* align source */
- MOVW R(TE), savedte+4(SP)
+ MOVW R(TE), savedte-4(SP)
MOVW.P 4(R(FROM)), R(FR3) /* prime last block register, implicit write back */
_fu16loop:
@@ -254,6 +254,6 @@ _fu16loop:
B _fu16loop
_fu1tail:
- MOVW savedte+4(SP), R(TE)
+ MOVW savedte-4(SP), R(TE)
SUB R(OFFSET), R(FROM)
B _f1tail
コアとなるコードの解説
変更された各行は、MOVW
命令を使用してレジスタの値をスタック上の変数(savedts
またはsavedte
)に保存したり、スタックからレジスタに復元したりする部分です。
-
MOVW R(TS), savedts+4(SP)
→MOVW R(TS), savedts-4(SP)
:- これは、レジスタ
R(TS)
(おそらく一時的なソースポインタまたはターゲットポインタ)の値を、スタック上のsavedts
という変数に保存する命令です。 - 元のコードでは
SP
に4
を加算したアドレスに保存しようとしていました。しかし、ARMのフルデセンディングスタックでは、スタック上のローカル変数や一時変数はSP
から負のオフセットに配置されるのが一般的です。 - 修正後は
SP
から4
を減算したアドレスに保存するようになり、savedts
が意図されたスタック上の正しい位置に書き込まれるようになります。
- これは、レジスタ
-
MOVW savedts+4(SP), R(TS)
→MOVW savedts-4(SP), R(TS)
:- これは、スタック上の
savedts
の値をレジスタR(TS)
に復元する命令です。 - 上記と同様に、元のコードでは誤ったアドレスから読み出そうとしていましたが、修正後は
SP
から4
を減算した正しいアドレスから値を読み出すようになります。
- これは、スタック上の
savedts
とsavedte
は、memmove
の処理中に一時的に保存されるレジスタの値(例えば、コピーの開始アドレスや終了アドレスなど)を保持するためのスタック上の変数です。これらの変数が正しく読み書きされないと、memmove
の内部状態が破損し、結果としてコピー操作が失敗したり、意図しないメモリ領域が破壊されたりする原因となります。
この修正により、memmove
がスタックを正しく利用し、一時的な値を安全に保存・復元できるようになり、パラメータの破損という深刻なバグが解消されました。
関連リンク
- Go Issue #5925: コミットメッセージに
Fixes #5925
と記載されていますが、現在のGitHubのGoリポジトリでは直接この番号のIssueは見つかりませんでした。これは、古いIssueトラッカーの番号であるか、または内部的な参照番号である可能性があります。しかし、コミットメッセージ自体が問題の内容を明確に説明しています。 - Gerrit Change List 11647043:
https://golang.org/cl/11647043
- これはGoプロジェクトのコードレビューシステムであるGerritにおけるこの変更のChange Listへのリンクです。Gerritでは、コミットがマージされる前に詳細な議論やレビューが行われます。
参考にした情報源リンク
- Goコミット: f8fd77baa9e57b2de6b9c0e08a3de0a7a8ad8947 (GitHub)
- Go Issue #22925 (Web検索結果):
https://github.com/golang/go/issues/22925
(直接関連はないが、memmove
のARM64最適化に関する議論) - Go Issue #40324 (Web検索結果):
https://github.com/golang/go/issues/40324
(直接関連はないが、memmove
のARM64におけるアラインメントに関する議論) - ARM Architecture Reference Manual (一般的なARMアセンブリとスタック規約の理解のため)