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

[インデックス 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)からのオフセット計算が誤っていたことが判明しました。具体的には、savedtssavedteといった一時変数をスタックに保存する際に、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と異なり、srcdestのメモリ領域が重なっていても正しく動作することが保証されています。これは、コピー操作が一時的なバッファを介して行われるか、コピーの方向が調整されることによって実現されます。

技術的詳細

このコミットの核心は、ARMアセンブリにおけるスタックフレームへのアクセス方法の修正です。memmove_arm.s内の複数の箇所で、一時的な値をスタックに保存したり、スタックから読み出したりする際に、savedtssavedteといった変数に対して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という変数に保存する命令です。
    • 元のコードではSP4を加算したアドレスに保存しようとしていました。しかし、ARMのフルデセンディングスタックでは、スタック上のローカル変数や一時変数はSPから負のオフセットに配置されるのが一般的です。
    • 修正後はSPから4を減算したアドレスに保存するようになり、savedtsが意図されたスタック上の正しい位置に書き込まれるようになります。
  • MOVW savedts+4(SP), R(TS)MOVW savedts-4(SP), R(TS):

    • これは、スタック上のsavedtsの値をレジスタR(TS)に復元する命令です。
    • 上記と同様に、元のコードでは誤ったアドレスから読み出そうとしていましたが、修正後はSPから4を減算した正しいアドレスから値を読み出すようになります。

savedtssavedteは、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アセンブリとスタック規約の理解のため)