[インデックス 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): スタックの現在のトップを指すレジスタ。
memset
とmemclr
memset
: メモリブロックを指定された値で埋める標準Cライブラリ関数に相当するものです。Goランタイムでは、メモリの初期化やゼロクリアなどに使用されます。memclr
:memset
の特殊なケースで、メモリブロックをゼロで埋めるために使用されます。Goランタイムでは、特に新しいメモリを割り当てた際に、その内容をゼロクリアするために頻繁に利用されます。
レジスタの「clobbering」(上書き)
「clobbering」とは、関数が呼び出された際に、その関数が呼び出し元が期待するレジスタの値を、呼び出し元に通知することなく変更してしまうことを指します。これは、特にアセンブリ言語で書かれた低レベル関数で問題となります。Goランタイムのように、特定のレジスタに重要なポインタ(m
やg
)を保持している場合、これらのレジスタがclobberされると、ランタイムの状態が破壊され、プログラムがクラッシュする原因となります。
Goランタイムとアセンブリ
Goランタイムは、Goプログラムの実行を管理する非常に重要な部分です。スケジューラ、ガベージコレクタ、メモリ管理、システムコールなどが含まれます。これらの機能の一部は、パフォーマンスやハードウェアとの直接的な対話のために、アセンブリ言語で実装されています。アセンブリコードは、Goのコンパイラによって生成されるコードと連携して動作するため、レジスタの使用規約(calling convention)を厳密に守る必要があります。
技術的詳細
このコミットは、主にsrc/pkg/runtime/asm_arm.s
とsrc/pkg/runtime/memset_arm.s
の2つのファイルに変更を加えています。
src/pkg/runtime/asm_arm.s
の変更
このファイルでは、runtime·memclr
関数の定義が変更されています。
変更前は、runtime·memclr
内でm
とg
レジスタの値をスタックに保存し、runtime·memset
呼び出し後に復元していました。これは、memset
がこれらのレジスタをclobberする可能性を考慮した防御的なコードでした。
変更後は、runtime·memclr
からm
とg
の保存・復元処理が削除されています。これは、memset
自体がR9/R10をclobberしないように修正されたため、memclr
側での冗長な処理が不要になったことを意味します。また、スタックフレームサイズも$20
から$0
に削減されています。
src/pkg/runtime/memset_arm.s
の変更
このファイルは、ARMアーキテクチャ向けのmemset
関数のアセンブリ実装です。主要な変更点は以下の通りです。
-
レジスタエイリアスの変更:
TO
,TOE
,N
,TMP
といった内部で使用するレジスタのエイリアスが変更されています。- 変更前:
TO = 1
,TOE = 2
,N = 3
,TMP = 3
- 変更後:
TO = 8
,TOE = 11
,N = 12
,TMP = 12
- これにより、
memset
関数内で使用されるレジスタが、Goランタイムでm
とg
を保持するために使用されるR9/R10レジスタと衝突しないように再配置されています。特に、TO
がR8、TOE
がR11、N
とTMP
がR12に割り当てられています。これにより、R9とR10がmemset
の内部処理で直接使用されることがなくなります。
-
データレジスタの変更:
MOVW data+4(FP), R(4)
がMOVW data+4(FP), R(0)
に変更されています。これは、memset
で埋めるバイト値が格納されているレジスタをR4からR0に変更しています。R0は通常、関数の引数として使用されるため、より標準的なレジスタの使用方法に沿っています。
-
レジスタの複製ロジックの変更:
_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ランタイムのm
とg
ポインタが安全に保持されるようになります。
コアとなるコードの変更箇所
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
はスタックフレームのサイズを示していました。以前はm
とg
レジスタをスタックに保存するために20バイトのスペースが必要でしたが、memset
がR9/R10をclobberしなくなったため、この保存・復元処理が不要になり、スタックフレームサイズが0に削減されました。これにより、関数呼び出しのオーバーヘッドがわずかに減少します。
MOVW m, -8(SP)
とMOVW g, -4(SP)
の削除:m
とg
レジスタの値をスタックに保存する命令が削除されました。
MOVW -8(SP), m
とMOVW -4(SP), g
の削除:memset
呼び出し後にm
とg
レジスタの値をスタックから復元する命令が削除されました。- これらの削除は、
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ランタイムがm
とg
ポインタを保持する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にロードする)と一貫性を保つためのものです。
- 埋めるバイト値をワード(4バイト)に複製する処理において、ソースレジスタがR4から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に変更されています。
- 32バイト単位でメモリを埋めるループ(
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アーキテクチャ上で実行される際に、m
とg
ポインタを保持する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検索で得られた情報源のリンク)