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

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

コミット

このコミットは、Go言語のsync/atomicパッケージにおいて、ARMアーキテクチャ上でのアラインメントされていない64ビットアトミック操作がパニックを引き起こすように変更するものです。これにより、不正なメモリアクセスが静かに失敗するのではなく、明確なエラーとして報告されるようになります。

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

https://github.com/golang/go/commit/94b7853924f817fce39f4c1eae366e973623e12b

元コミット内容

commit 94b7853924f817fce39f4c1eae366e973623e12b
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Mon Apr 1 14:34:03 2013 -0700

    sync/atomic: make unaligned 64-bit atomic operation panic on ARM
    use MOVW.NE instead of BEQ and MOVW.
    
    R=golang-dev, dave, rsc, daniel.morsing
    CC=golang-dev
    https://golang.org/cl/7718043
---
 src/pkg/sync/atomic/asm_arm.s       | 16 ++++++++++++++++
 src/pkg/sync/atomic/asm_linux_arm.s |  8 ++++++++
 src/pkg/sync/atomic/atomic_test.go  |  6 +++---
 3 files changed, 27 insertions(+), 3 deletions(-)

変更の背景

Go言語のsync/atomicパッケージは、複数のゴルーチン(軽量スレッド)から共有データに安全にアクセスするためのアトミック操作を提供します。64ビットの値を扱うアトミック操作は、特に32ビットシステム(ARMv5, ARMv6, ARMv7など)において、メモリアラインメントの問題に直面することがあります。

多くのCPUアーキテクチャでは、特定のデータ型(例えば64ビット整数)がメモリ上で特定の境界(例えば8バイト境界)に配置されていることを要求します。これを「メモリアラインメント」と呼びます。アラインメントされていないアドレスからデータを読み書きしようとすると、以下のような問題が発生する可能性があります。

  1. パフォーマンスの低下: CPUがアラインメントされていないアクセスを処理するために追加のサイクルを必要とする場合があります。
  2. ハードウェア例外/クラッシュ: 一部のアーキテクチャでは、アラインメントされていないアクセスがハードウェア例外(アラインメントフォルト)を引き起こし、プログラムがクラッシュする可能性があります。
  3. データの破損: 特にアトミック操作の場合、アラインメントされていないアクセスが原因で、操作がアトミック性を失い、データの破損や競合状態を引き起こす可能性があります。

このコミット以前は、ARMアーキテクチャ上のGoにおいて、アラインメントされていない64ビットアトミック操作が静かに失敗したり、予期せぬ動作を引き起こしたりする可能性がありました。これはデバッグを困難にし、潜在的なデータ破損のリスクをはらんでいました。この変更の目的は、このような静かな失敗を防ぎ、アラインメント違反が発生した場合にプログラムを意図的にパニックさせることで、開発者が問題を早期に発見し、修正できるようにすることです。これにより、Goプログラムの堅牢性と信頼性が向上します。

前提知識の解説

1. アトミック操作 (Atomic Operations)

アトミック操作とは、複数の操作が不可分(アトミック)であることを保証するプログラミングの概念です。つまり、その操作が完全に実行されるか、全く実行されないかのどちらかであり、途中で他のスレッドやプロセスによって中断されることがありません。並行プログラミングにおいて、共有データへのアクセスを同期し、競合状態(Race Condition)を防ぐために不可欠です。Go言語ではsync/atomicパッケージがこれらの機能を提供します。

2. メモリアラインメント (Memory Alignment)

メモリアラインメントとは、コンピュータのメモリ上でデータが配置される際の、特定のメモリアドレスへの制約です。例えば、4バイトの整数は4の倍数のアドレスに、8バイトの整数は8の倍数のアドレスに配置されるべき、といったルールです。

  • 理由: CPUは通常、メモリからデータをブロック単位で読み書きします。データがアラインメントされていると、CPUは1回のメモリアクセスでデータを効率的に読み書きできます。アラインメントされていない場合、CPUは複数回のメモリアクセスを行う必要があったり、特別な処理が必要になったりするため、パフォーマンスが低下したり、ハードウェアによってはエラー(アラインメントフォルト)が発生したりします。
  • 64ビット値と32ビットシステム: 32ビットシステムでは、CPUのレジスタやメモリアクセスバスが32ビット幅(4バイト)であることが一般的です。この環境で64ビット(8バイト)の値を扱う場合、その値が8バイト境界にアラインメントされていることが特に重要になります。アラインメントされていない64ビット値へのアクセスは、32ビット幅のアクセスを2回に分けて行う必要があり、その間に他の操作が割り込むとアトミック性が保証されなくなる可能性があります。

3. ARMアーキテクチャ

ARM(Advanced RISC Machine)は、モバイルデバイスや組み込みシステムで広く使用されているRISC(Reduced Instruction Set Computer)ベースのCPUアーキテクチャです。

  • アラインメント要件: ARMアーキテクチャは、バージョンによってアラインメント要件が異なります。古いARMv5/v6ではアラインメントされていないアクセスがハードウェア例外を引き起こすことがありましたが、新しいARMv7以降ではアラインメントされていないアクセスをサポートするモードもあります。しかし、アトミック操作の文脈では、アラインメントが保証されないとアトミック性が破綻するリスクがあるため、厳密なアラインメントが求められます。
  • LDREXD/STREXD: ARMv6以降で導入された「ロード/ストア排他(Load/Store Exclusive)」命令です。これらは、複数のプロセッサコア間で共有されるメモリ領域に対してアトミックな読み書きを行うために使用されます。LDREXDは排他的にメモリから64ビット値をロードし、STREXDは排他的にその値をストアします。これらの命令は、比較交換(Compare-and-Swap, CAS)のようなアトミック操作を実装する際の基盤となります。

4. Goのアセンブリ言語 (Plan 9 Assembler)

Go言語は、一部の低レベルな操作(特に特定のアーキテクチャに依存するアトミック操作やシステムコール)に、Plan 9アセンブリ言語を使用します。これは一般的なGAS(GNU Assembler)構文とは異なる独自の構文を持っています。

  • TEXT: 関数の開始を宣言します。
  • MOVW: 32ビットワードを移動する命令です。
  • AND.S: 論理AND演算を行い、結果に基づいてステータスフラグ(Suffix .S)を設定します。
  • BEQ: ゼロフラグがセットされている場合(結果がゼロの場合)に分岐します。
  • PC: プログラムカウンタ(現在の命令のアドレス)を指します。2(PC)は現在の命令から2命令先に分岐することを意味します。
  • R1, R2, R3: ARMプロセッサの汎用レジスタです。
  • FP: フレームポインタ。スタックフレーム内の引数やローカル変数にアクセスするために使用されます。
  • SB: 静的ベースポインタ。グローバル変数や外部シンボルにアクセスするために使用されます。

技術的詳細

このコミットは、Goのsync/atomicパッケージ内のARMアセンブリコードに、64ビットアトミック操作が実行される前に、対象のアドレスが8バイト境界にアラインメントされているかをチェックするロジックを追加します。

具体的には、src/pkg/sync/atomic/asm_arm.ssrc/pkg/sync/atomic/asm_linux_arm.s内の以下の64ビットアトミック操作関数に修正が加えられました。

  • ·armCompareAndSwapUint64 (CompareAndSwapUint64)
  • ·armAddUint64 (AddUint64)
  • ·armLoadUint64 (LoadUint64)
  • ·armStoreUint64 (StoreUint64)
  • kernelCAS64<> (LinuxカーネルのCAS64ラッパー)
  • generalCAS64<> (一般的なCAS64ラッパー)

追加されたアセンブリコードは以下の通りです(レジスタ名は関数によって異なりますが、ロジックは同じです)。

// make unaligned atomic access panic
AND.S   $7, R1, R2  // R1 (アドレス) と 7 (バイナリで0b111) のビットANDを取る。
                    // 結果が0であれば、アドレスは8の倍数(8バイトアラインメント)である。
                    // 結果はR2に格納され、同時にステータスフラグが更新される。
BEQ     2(PC)       // 直前のAND.Sの結果がゼロ(Zフラグがセット)であれば、
                    // 次の2命令をスキップして、アラインメントチェック後の処理に進む。
MOVW    R2, (R2)    // AND.Sの結果がゼロでなかった場合(アラインメントされていない場合)、
                    // R2(アラインメントオフセット)の値をR2が指すアドレスに書き込もうとする。
                    // R2は通常、0から7の間の値になるため、これは不正なメモリアドレスへのアクセスとなり、
                    // オペレーティングシステムによってSIGSEGVなどのシグナルが発行され、
                    // Goランタイムがこれを捕捉してパニックを引き起こす。

このロジックにより、アラインメントされていないアドレスに対して64ビットアトミック操作が試みられた場合、Goプログラムは即座にパニックして終了します。これにより、静かなデータ破損や予期せぬ動作を防ぎ、開発者がアラインメントの問題を早期に特定し、修正することを強制します。

また、src/pkg/sync/atomic/atomic_test.goTestUnaligned64テスト関数も更新されました。以前は386アーキテクチャに限定されていたこのテストが、unsafe.Sizeof(int(0)) != 4という条件に変更されました。これは、int型のサイズが4バイトではない(つまり32ビットシステムではない)場合にテストをスキップするという意味です。これにより、このテストは32ビットシステム全般(ARMを含む)で実行されるようになり、アラインメントされていない64ビットアトミック操作がパニックを引き起こすという新しい動作を検証できるようになりました。

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

src/pkg/sync/atomic/asm_arm.s

·armCompareAndSwapUint64, ·armAddUint64, ·armLoadUint64, ·armStoreUint64 の各関数に、アドレスのアラインメントチェックとパニックを誘発するコードが追加されました。

--- a/src/pkg/sync/atomic/asm_arm.s
+++ b/src/pkg/sync/atomic/asm_arm.s
@@ -29,6 +29,10 @@ casfail:
 TEXT ·armCompareAndSwapUint64(SB),7,$0
  BL fastCheck64<>(SB)
  MOVW addr+0(FP), R1
+// make unaligned atomic access panic
+ AND.S $7, R1, R2
+ BEQ 2(PC)
+ MOVW R2, (R2)
  MOVW oldlo+4(FP), R2
  MOVW oldhi+8(FP), R3
  MOVW newlo+12(FP), R4
@@ -67,6 +71,10 @@ addloop:
 TEXT ·armAddUint64(SB),7,$0
  BL fastCheck64<>(SB)
  MOVW addr+0(FP), R1
+// make unaligned atomic access panic
+ AND.S $7, R1, R2
+ BEQ 2(PC)
+ MOVW R2, (R2)
  MOVW deltalo+4(FP), R2
  MOVW deltahi+8(FP), R3
 add64loop:
@@ -84,6 +92,10 @@ add64loop:
 TEXT ·armLoadUint64(SB),7,$0
  BL fastCheck64<>(SB)
  MOVW addr+0(FP), R1
+// make unaligned atomic access panic
+ AND.S $7, R1, R2
+ BEQ 2(PC)
+ MOVW R2, (R2)
 load64loop:
  LDREXD (R1), R2 // loads R2 and R3
  STREXD R2, (R1), R0 // stores R2 and R3
@@ -96,6 +108,10 @@ load64loop:
 TEXT ·armStoreUint64(SB),7,$0
  BL fastCheck64<>(SB)
  MOVW addr+0(FP), R1
+// make unaligned atomic access panic
+ AND.S $7, R1, R2
+ BEQ 2(PC)
+ MOVW R2, (R2)
  MOVW vallo+4(FP), R2
  MOVW valhi+8(FP), R3
 store64loop:

src/pkg/sync/atomic/asm_linux_arm.s

kernelCAS64<>generalCAS64<> の各関数に、同様のアラインメントチェックとパニックを誘発するコードが追加されました。

--- a/src/pkg/sync/atomic/asm_linux_arm.s
+++ b/src/pkg/sync/atomic/asm_linux_arm.s
@@ -80,6 +80,10 @@ TEXT cas64<>(SB),7,$0
 TEXT kernelCAS64<>(SB),7,$0
  // int (*__kuser_cmpxchg64_t)(const int64_t *oldval, const int64_t *newval, volatile int64_t *ptr);
  MOVW addr+0(FP), R2 // ptr
+// make unaligned atomic access panic
+ AND.S $7, R2, R1
+ BEQ 2(PC)
+ MOVW R1, (R1)
  MOVW $4(FP), R0 // oldval
  MOVW $12(FP), R1 // newval
  BL cas64<>(SB)
@@ -91,6 +95,10 @@ TEXT kernelCAS64<>(SB),7,$0
 TEXT generalCAS64<>(SB),7,$20
  // bool runtime·cas64(uint64 volatile *addr, uint64 *old, uint64 new)
  MOVW addr+0(FP), R0
+// make unaligned atomic access panic
+ AND.S $7, R0, R1
+ BEQ 2(PC)
+ MOVW R1, (R1)
  MOVW R0, 4(R13)
  MOVW $4(FP), R1 // oldval
  MOVW R1, 8(R13)

src/pkg/sync/atomic/atomic_test.go

TestUnaligned64 テスト関数のスキップ条件が変更され、32ビットシステム全般でテストが実行されるようになりました。

--- a/src/pkg/sync/atomic/atomic_test.go
+++ b/src/pkg/sync/atomic/atomic_test.go
@@ -1189,10 +1189,10 @@ func shouldPanic(t *testing.T, name string, f func()) {
 
  func TestUnaligned64(t *testing.T) {
  // Unaligned 64-bit atomics on 32-bit systems are
- // a continual source of pain. Test that on 386 they crash
+ // a continual source of pain. Test that on 32-bit systems they crash
  // instead of failing silently.
- if runtime.GOARCH != "386" {
-  t.Skip("test only runs on 386")
+ if unsafe.Sizeof(int(0)) != 4 {
+  t.Skip("test only runs on 32-bit systems")
  }
 
  x := make([]uint32, 4)

コアとなるコードの解説

追加されたアセンブリコードの目的は、64ビットアトミック操作の対象となるメモリアドレスが8バイト境界にアラインメントされているかを厳密にチェックし、アラインメントされていない場合には意図的にパニックを発生させることです。

  1. AND.S $7, R1, R2:

    • R1(またはR2)は、アトミック操作の対象となるメモリアドレスを保持しています。
    • $7は即値で、バイナリでは0b00000111です。
    • この命令は、アドレスの最下位3ビットを抽出します。
    • もしアドレスが8の倍数(つまり8バイトアラインメントされている)であれば、最下位3ビットはすべて0になります。したがって、AND演算の結果は0になります。
    • .Sサフィックスは、演算結果に基づいてCPUのステータスレジスタ(特にゼロフラグZ)を更新することを意味します。結果が0であればZフラグがセットされます。
  2. BEQ 2(PC):

    • BEQは"Branch if Equal to zero"(ゼロであれば分岐)の略です。直前の演算(AND.S)の結果がゼロであった場合(Zフラグがセットされている場合)、指定されたオフセットに分岐します。
    • 2(PC)は、現在のプログラムカウンタ(PC)から2命令先に分岐することを意味します。Goのアセンブリでは、命令のサイズが固定ではないため、2(PC)は通常、次の命令の次の命令を指します。つまり、アラインメントチェック後の正常な処理フローにジャンプします。
  3. MOVW R2, (R2):

    • この命令は、BEQ 2(PC)が実行されなかった場合、つまりAND.Sの結果がゼロでなかった場合(アドレスが8バイトアラインメントされていない場合)に実行されます。
    • R2には、アドレスの最下位3ビット(0から7の間の値)が格納されています。
    • MOVW R2, (R2)は、R2レジスタの値を、R2が指すメモリアドレスに書き込もうとします。
    • しかし、R2の値は0から7の間の小さな値であり、これらは通常、有効なメモリアドレスではありません。したがって、このメモリアクセスは不正なアクセス(セグメンテーション違反など)を引き起こし、オペレーティングシステムがシグナルを発生させます。
    • Goランタイムはこのようなシグナルを捕捉し、対応するGoのパニック(panic)として処理します。これにより、アラインメント違反が静かに見過ごされることなく、プログラムがクラッシュして開発者に問題が通知されるようになります。

この変更は、Goのsync/atomicパッケージが提供するアトミック操作の堅牢性を高め、特にARMのようなアラインメント要件が厳しいアーキテクチャでの潜在的なバグを早期に発見・修正することを目的としています。

関連リンク

参考にした情報源リンク