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

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

このコミットは、Go言語のsync/atomicパッケージにおいて、32ビットアーキテクチャ(特に386)上で64ビットのアトミック操作がアラインメントされていないメモリアドレスに対して実行された場合に、サイレントに失敗するのではなく、意図的にクラッシュ(パニック)するように変更を加えるものです。これにより、開発者がアラインメントの問題を早期に発見し、デバッグできるようになります。

コミット

commit 14a295c8172aa12b19944d85e92835c7e9f904cc
Author: Russ Cox <rsc@golang.org>
Date:   Mon Mar 11 12:21:46 2013 -0400

    sync/atomic: make unaligned 64-bit atomics crash on 386
    
    R=golang-dev, bradfitz, dvyukov
    CC=golang-dev
    https://golang.org/cl/7702043
---
 src/pkg/sync/atomic/asm_386.s      | 12 ++++++++++++\n src/pkg/sync/atomic/atomic_test.go | 26 ++++++++++++++++++++++++++\n 2 files changed, 38 insertions(+)

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

https://github.com/golang/go/commit/14a295c8172aa12b19944d85e92835c7e9f904cc

元コミット内容

sync/atomic: 386アーキテクチャ上でアラインメントされていない64ビットアトミック操作をクラッシュさせる。

変更の背景

32ビットシステム(特にIntel 386アーキテクチャ)において、64ビットのデータ型(int64uint64)を扱う際、そのデータがメモリアドレスの境界に正しく配置されていない(アラインメントされていない)場合に問題が発生することが知られています。通常、64ビットのデータは8バイト境界にアラインメントされている必要があります。

Go言語のsync/atomicパッケージは、複数のゴルーチンから同時にアクセスされる共有変数に対して、不可分(アトミック)な操作を提供します。これにより、競合状態を防ぎ、データの整合性を保つことができます。しかし、32ビットシステムでアラインメントされていない64ビット変数に対してアトミック操作を実行すると、ハードウェアがその操作を正しく実行できない場合があります。

この問題は、多くの場合、サイレントに(エラーを報告せずに)不正な結果を生成するという形で現れました。サイレントな失敗は、デバッグを非常に困難にし、プログラムの信頼性を損なう原因となります。このコミットの目的は、このようなサイレントな失敗を防ぎ、アラインメントの問題が発生した際にプログラムを意図的にクラッシュさせることで、開発者が問題を早期に特定し、修正できるようにすることです。クラッシュは、不正なメモリアクセス(nilポインタのデリファレンス)を意図的に引き起こすことで実現されます。

前提知識の解説

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

アトミック操作とは、複数のCPUコアやスレッドから同時にアクセスされる共有データに対して、その操作全体が中断されることなく、単一の不可分な操作として実行されることを保証するものです。これにより、競合状態(Race Condition)を防ぎ、データの整合性を保ちます。Go言語では、sync/atomicパッケージがこれらの操作を提供します。例えば、AddInt64は64ビット整数に値をアトミックに加算し、LoadUint64は64ビット符号なし整数をアトミックに読み込みます。

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

メモリアラインメントとは、データがメモリ上で特定のバイト境界に配置されることを指します。CPUは、特定のデータ型(例えば、32ビット整数や64ビット整数)を効率的に読み書きするために、そのデータが特定のメモリアドレスの倍数に配置されていることを期待します。

  • 32ビットシステム (例: 386): 32ビットシステムでは、通常、32ビット(4バイト)のデータは4バイト境界に、64ビット(8バイト)のデータは8バイト境界にアラインメントされていることが望ましいです。
  • アラインメントされていないアクセス: データが期待される境界に配置されていない場合、CPUはデータを読み書きするために複数のメモリアクセスを行う必要があったり、パフォーマンスが低下したり、最悪の場合、ハードウェア例外(クラッシュ)を引き起こしたりすることがあります。特に、アトミック操作のような低レベルの操作では、アラインメントが厳密に要求されることがあります。

3. Intel 386アーキテクチャ

Intel 386(i386)は、1985年にリリースされた32ビットのx86マイクロプロセッサです。このアーキテクチャは、現代の64ビットx86プロセッサの基礎となりましたが、64ビットのデータ型をネイティブに扱うための命令セットやメモリアクセス機構には制約がありました。特に、64ビットのアトミック操作は、386のような32ビットアーキテクチャでは、より複雑な実装(例えば、CMPXCHG8B命令の使用)が必要となり、アラインメントの要件が厳しくなります。

4. CMPXCHG8B命令

CMPXCHG8B (Compare and Exchange 8 Bytes) は、Intel x86アーキテクチャの命令セットの一部で、8バイト(64ビット)の値をアトミックに比較し、交換するために使用されます。これは、32ビットプロセッサ上で64ビットのアトミック操作を実現するために不可欠な命令です。この命令は、メモリ上の64ビット値とEDX:EAXレジスタペアの値を比較し、一致すればECX:EBXレジスタペアの値をメモリに書き込みます。この操作全体がアトミックに実行されます。

5. unsafe.Pointer in Go

Go言語のunsafeパッケージは、Goの型システムをバイパスして、低レベルのメモリアクセスを可能にする機能を提供します。unsafe.Pointerは、任意の型のポインタを保持できる特殊なポインタ型で、C言語のvoid*に似ています。これを使用することで、型安全性を犠牲にして、メモリレイアウトを直接操作したり、アラインメントされていないアドレスにアクセスしたりすることが可能になります。しかし、その名の通り「unsafe」であり、誤用するとプログラムのクラッシュや未定義の動作を引き起こす可能性があります。このコミットのテストコードでは、意図的にアラインメントされていないuint64ポインタを作成するためにunsafe.Pointerが使用されています。

6. Goにおけるパニック (Panic) とクラッシュ

Go言語では、プログラムの実行中に回復不可能なエラーが発生した場合、panicメカニズムが使用されます。パニックが発生すると、通常の実行フローは中断され、遅延関数(defer)が実行され、最終的にプログラムがクラッシュします。このコミットでは、アラインメントされていないアトミック操作が検出された際に、意図的にnilポインタのデリファレンスを引き起こすことでパニックを発生させ、プログラムをクラッシュさせています。これにより、サイレントな失敗を防ぎ、開発者に問題の存在を明確に通知します。

技術的詳細

このコミットの技術的な核心は、386アーキテクチャのGoアセンブリコード(src/pkg/sync/atomic/asm_386.s)にアラインメントチェックを追加し、アラインメントされていないアドレスが渡された場合に意図的にパニックを発生させる点にあります。

具体的には、CompareAndSwapInt64, CompareAndSwapUint64, AddInt64, AddUint64, LoadInt64, LoadUint64, StoreInt64, StoreUint64といった64ビットのアトミック操作を行うアセンブリ関数に以下のコードが追加されました。

	TESTL	$7, BP
	JZ	2(PC)
	MOVL	0, AX // crash with nil ptr deref

または

	TESTL	$7, AX
	JZ	2(PC)
	MOVL	0, AX // crash with nil ptr deref
  • TESTL $7, BP (または TESTL $7, AX):

    • BPまたはAXレジスタには、アトミック操作の対象となるメモリアドレスが格納されています。
    • TESTL命令は、2つのオペランドのビットごとのAND演算を行い、結果を破棄しますが、フラグレジスタ(特にゼロフラグZとパリティフラグP)を設定します。
    • $7はバイナリで00000111です。メモリアドレスと7のビットごとのAND演算を行うことで、アドレスの下位3ビットをチェックしています。
    • もしアドレスが8の倍数(8バイト境界にアラインメントされている)であれば、下位3ビットはすべて0になります。この場合、TESTLの結果も0になり、ゼロフラグ(ZF)がセットされます。
    • もしアドレスが8の倍数でなければ、下位3ビットの少なくとも1つが1になり、TESTLの結果は非ゼロになります。この場合、ゼロフラグ(ZF)はクリアされます。
  • JZ 2(PC):

    • JZ (Jump if Zero) 命令は、ゼロフラグ(ZF)がセットされている場合(つまり、TESTLの結果がゼロだった場合、アドレスが8バイト境界にアラインメントされている場合)に、指定されたオフセット(2(PC)は現在のプログラムカウンタから2バイト先)にジャンプします。
    • これにより、アラインメントが正しい場合は、アトミック操作の本来の処理に進みます。
  • MOVL 0, AX // crash with nil ptr deref:

    • JZ命令が実行されなかった場合(つまり、ゼロフラグがクリアされている場合、アドレスがアラインメントされていない場合)、この命令が実行されます。
    • MOVL 0, AXは、AXレジスタに0(nil)をロードします。
    • この直後に続くアトミック操作の命令は、AXレジスタに格納されたアドレス(この場合は0)を間接参照しようとします。
    • 0番地へのアクセスは、オペレーティングシステムによって保護されており、通常はセグメンテーション違反やページフォルトといったハードウェア例外を引き起こします。Goランタイムはこれを捕捉し、nilポインタデリファレンスとしてパニックを発生させます。

この変更により、386アーキテクチャでアラインメントされていない64ビットアトミック操作が実行されると、即座にプログラムがクラッシュし、開発者は問題の根本原因(不正なメモリアラインメント)を特定できるようになります。

また、src/pkg/sync/atomic/atomic_test.goには、この新しいクラッシュ動作を検証するためのテストケースTestUnaligned64が追加されました。このテストは、unsafe.Pointerを使用して意図的にアラインメントされていないuint64ポインタを作成し、それに対して各種アトミック操作を実行することで、期待通りにパニックが発生するかどうかを確認します。shouldPanicヘルパー関数は、指定された関数がパニックを引き起こすことを検証するために使用されます。

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

src/pkg/sync/atomic/asm_386.s

以下のコードが、CompareAndSwapInt64, CompareAndSwapUint64, AddInt64, AddUint64, LoadInt64, LoadUint64, StoreInt64, StoreUint64の各関数の冒頭付近に追加されました。

--- a/src/pkg/sync/atomic/asm_386.s
+++ b/src/pkg/sync/atomic/asm_386.s
@@ -28,6 +28,9 @@ TEXT ·CompareAndSwapInt64(SB),7,$0
 
 TEXT ·CompareAndSwapUint64(SB),7,$0
 	MOVL	addr+0(FP), BP
+	TESTL	$7, BP
+	JZ	2(PC)
+	MOVL	0, AX // crash with nil ptr deref
 	MOVL	old+4(FP), AX
 	MOVL	old+8(FP), DX
 	MOVL	new+12(FP), BX
@@ -61,6 +64,9 @@ TEXT ·AddInt64(SB),7,$0
 TEXT ·AddUint64(SB),7,$0
 	// no XADDQ so use CMPXCHG8B loop
 	MOVL	addr+0(FP), BP
+	TESTL	$7, BP
+	JZ	2(PC)
+	MOVL	0, AX // crash with nil ptr deref
 	// DI:SI = delta
 	MOVL	delta+4(FP), SI
 	MOVL	delta+8(FP), DI
@@ -105,6 +111,9 @@ TEXT ·LoadInt64(SB),7,$0
 
 TEXT ·LoadUint64(SB),7,$0
 	MOVL	addr+0(FP), AX
+	TESTL	$7, AX
+	JZ	2(PC)
+	MOVL	0, AX // crash with nil ptr deref
 	// MOVQ and EMMS were introduced on the Pentium MMX.
 	// MOVQ (%EAX), %MM0
 	BYTE $0x0f; BYTE $0x6f; BYTE $0x00
@@ -133,6 +142,9 @@ TEXT ·StoreInt64(SB),7,$0
 
 TEXT ·StoreUint64(SB),7,$0
 	MOVL	addr+0(FP), AX
+	TESTL	$7, AX
+	JZ	2(PC)
+	MOVL	0, AX // crash with nil ptr deref
 	// MOVQ and EMMS were introduced on the Pentium MMX.
 	// MOVQ 0x8(%ESP), %MM0
 	BYTE $0x0f; BYTE $0x6f; BYTE $0x44; BYTE $0x24; BYTE $0x08

src/pkg/sync/atomic/atomic_test.go

以下のテストコードが追加されました。

--- a/src/pkg/sync/atomic/atomic_test.go
+++ b/src/pkg/sync/atomic/atomic_test.go
@@ -1177,3 +1177,29 @@ func TestStoreLoadRelAcq64(t *testing.T) {\n 	<-c\n 	<-c\n }\n+\n+func shouldPanic(t *testing.T, name string, f func()) {\n+\tdefer func() {\n+\t\tif recover() == nil {\n+\t\t\tt.Errorf(\"%s did not panic\", name)\n+\t\t}\n+\t}()\n+\tf()\n+}\n+\n+func TestUnaligned64(t *testing.T) {\n+\t// Unaligned 64-bit atomics on 32-bit systems are\n+\t// a continual source of pain. Test that on 386 they crash\n+\t// instead of failing silently.\n+\tif runtime.GOARCH != \"386\" {\n+\t\tt.Skip(\"test only runs on 386\")\n+\t}\n+\n+\tx := make([]uint32, 4)\n+\tp := (*uint64)(unsafe.Pointer(&x[1])) // misaligned\n+\n+\tshouldPanic(t, \"LoadUint64\", func() { LoadUint64(p) })\n+\tshouldPanic(t, \"StoreUint64\", func() { StoreUint64(p, 1) })\n+\tshouldPanic(t, \"CompareAndSwapUint64\", func() { CompareAndSwapUint64(p, 1, 2) })\n+\tshouldPanic(t, \"AddUint64\", func() { AddUint64(p, 3) })\n+}\n```

## コアとなるコードの解説

### `src/pkg/sync/atomic/asm_386.s` の変更点

各64ビットアトミック操作のアセンブリ関数において、操作対象のアドレス(`BP`または`AX`レジスタに格納されている)が8バイト境界にアラインメントされているかどうかがチェックされます。

1.  `TESTL $7, BP` (または `TESTL $7, AX`):
    *   これは、アドレスの下位3ビットがすべてゼロであるか(つまり、アドレスが8の倍数であるか)を効率的にチェックするアセンブリ命令です。
    *   `$7`はバイナリで`00000111`であり、アドレスと`7`のビットごとのAND演算を行うことで、アドレスの最下位3ビットを分離します。
    *   もしアドレスが8バイト境界にアラインメントされていれば、下位3ビットは`000`となり、`TESTL`の結果はゼロになります。この場合、CPUのゼロフラグ(ZF)がセットされます。

2.  `JZ 2(PC)`:
    *   ゼロフラグ(ZF)がセットされている場合(アラインメントが正しい場合)、プログラムカウンタ(PC)から2バイト先にジャンプします。これにより、アラインメントチェックの後の本来のアトミック操作のコードにスキップします。

3.  `MOVL 0, AX // crash with nil ptr deref`:
    *   `JZ`命令が実行されなかった場合(アラインメントが正しくない場合)、この命令が実行されます。
    *   `AX`レジスタに`0`(nilポインタ)をロードします。
    *   この直後に続くアトミック操作の命令は、`AX`レジスタに格納されたアドレスを間接参照しようとします。しかし、`AX`には`0`がロードされているため、`0`番地へのメモリアクセスが発生し、これがGoランタイムによってnilポインタデリファレンスとして捕捉され、パニックを引き起こします。

このメカニズムにより、386アーキテクチャでアラインメントされていない64ビットアトミック操作が実行されると、サイレントな失敗ではなく、明確なクラッシュが発生し、デバッグが容易になります。

### `src/pkg/sync/atomic/atomic_test.go` の変更点

1.  `shouldPanic`関数:
    *   これはヘルパー関数で、引数として渡された関数`f`がパニックを引き起こすことを検証します。
    *   `defer func() { if recover() == nil { ... } }()`ブロックを使用しています。`recover()`はパニックから回復するために使用され、パニックが発生しなかった場合は`nil`を返します。
    *   もし`f()`がパニックを引き起こさなかった場合(`recover()`が`nil`を返した場合)、テストはエラーを報告します。

2.  `TestUnaligned64`関数:
    *   このテストは、`runtime.GOARCH != "386"`の場合にスキップされます。これは、このアラインメントチェックとクラッシュ動作が386アーキテクチャに特化しているためです。
    *   `x := make([]uint32, 4)`: 4つの`uint32`要素を持つスライスを作成します。`uint32`は4バイトです。
    *   `p := (*uint64)(unsafe.Pointer(&x[1]))`: ここが重要な部分です。
        *   `&x[1]`は、スライス`x`の2番目の要素(インデックス1)のアドレスを取得します。
        *   `x[0]`が4バイト、`x[1]`が4バイトなので、`&x[1]`のアドレスは、スライスの開始アドレスから4バイトオフセットされた位置になります。
        *   `unsafe.Pointer(&x[1])`は、このアドレスを`unsafe.Pointer`型に変換します。
        *   `(*uint64)(...)`は、`unsafe.Pointer`を`*uint64`型にキャストします。
        *   結果として、`p`は`uint64`へのポインタですが、その指すアドレスは8バイト境界にアラインメントされていません(4バイト境界にアラインメントされています)。
    *   `shouldPanic(t, "LoadUint64", func() { LoadUint64(p) })`など:
        *   作成されたアラインメントされていないポインタ`p`に対して、`LoadUint64`, `StoreUint64`, `CompareAndSwapUint64`, `AddUint64`といった64ビットアトミック操作をそれぞれ実行します。
        *   `shouldPanic`関数を使用することで、これらの操作が期待通りにパニックを引き起こすことを検証します。

このテストは、アセンブリコードの変更が意図した通りに機能し、アラインメントされていない64ビットアトミック操作が386上で確実にクラッシュすることを保証します。

## 関連リンク

*   Go Gerrit Change-ID: [https://golang.org/cl/7702043](https://golang.org/cl/7702043)

## 参考にした情報源リンク

*   Go言語の`sync/atomic`パッケージのドキュメント
*   x86アセンブリ言語の命令セットリファレンス(特に`TEST`、`JZ`、`MOVL`、`CMPXCHG8B`命令について)
*   メモリアラインメントに関する一般的なコンピュータアーキテクチャの資料
*   Go言語の`unsafe`パッケージに関するドキュメント
*   Go言語のパニックと回復に関するドキュメント
*   [https://go.dev/doc/asm](https://go.dev/doc/asm) (Go Assembly Language)
*   [https://go.dev/src/sync/atomic/asm_386.s](https://go.dev/src/sync/atomic/asm_386.s) (Go source code for 386 atomic assembly)
*   [https://go.dev/src/sync/atomic/atomic_test.go](https://go.dev/src/sync/atomic/atomic_test.go) (Go source code for atomic tests)
*   [https://en.wikipedia.org/wiki/Memory_alignment](https://en.wikipedia.org/wiki/Memory_alignment)
*   [https://en.wikipedia.org/wiki/CMPXCHG8B](https://en.wikipedia.org/wiki/CMPXCHG8B)
*   [https://en.wikipedia.org/wiki/Intel_80386](https://en.wikipedia.org/wiki/Intel_80386)
*   [https://go.dev/blog/go1.1-release](https://go.dev/blog/go1.1-release) (Go 1.1 Release Notes, which might mention this change as it was committed in 2013)
*   [https://go.dev/doc/effective_go#recover](https://go.dev/doc/effective_go#recover) (Effective Go - Defer, Panic, and Recover)
*   [https://go.dev/doc/articles/go_mem.html](https://go.dev/doc/articles/go_mem.html) (The Go Memory Model)
*   [https://go.dev/doc/articles/unsafe.html](https://go.dev/doc/articles/unsafe.html) (The Go unsafe package)```markdown
# [インデックス 15680] ファイルの概要

このコミットは、Go言語の`sync/atomic`パッケージにおいて、32ビットアーキテクチャ(特に386)上で64ビットのアトミック操作がアラインメントされていないメモリアドレスに対して実行された場合に、サイレントに失敗するのではなく、意図的にクラッシュ(パニック)するように変更を加えるものです。これにより、開発者がアラインメントの問題を早期に発見し、デバッグできるようになります。

## コミット

commit 14a295c8172aa12b19944d85e92835c7e9f904cc Author: Russ Cox rsc@golang.org Date: Mon Mar 11 12:21:46 2013 -0400

sync/atomic: make unaligned 64-bit atomics crash on 386

R=golang-dev, bradfitz, dvyukov
CC=golang-dev
https://golang.org/cl/7702043

src/pkg/sync/atomic/asm_386.s | 12 ++++++++++++\n src/pkg/sync/atomic/atomic_test.go | 26 ++++++++++++++++++++++++++\n 2 files changed, 38 insertions(+)


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

[https://github.com/golang/go/commit/14a295c8172aa12b19944d85e92835c7e9f904cc](https://github.com/golang/go/commit/14a295c8172aa12b19944d85e92835c7e9f904cc)

## 元コミット内容

`sync/atomic`: 386アーキテクチャ上でアラインメントされていない64ビットアトミック操作をクラッシュさせる。

## 変更の背景

32ビットシステム(特にIntel 386アーキテクチャ)において、64ビットのデータ型(`int64`や`uint64`)を扱う際、そのデータがメモリアドレスの境界に正しく配置されていない(アラインメントされていない)場合に問題が発生することが知られています。通常、64ビットのデータは8バイト境界にアラインメントされている必要があります。

Go言語の`sync/atomic`パッケージは、複数のゴルーチンから同時にアクセスされる共有変数に対して、不可分(アトミック)な操作を提供します。これにより、競合状態を防ぎ、データの整合性を保つことができます。しかし、32ビットシステムでアラインメントされていない64ビット変数に対してアトミック操作を実行すると、ハードウェアがその操作を正しく実行できない場合があります。

この問題は、多くの場合、サイレントに(エラーを報告せずに)不正な結果を生成するという形で現れました。サイレントな失敗は、デバッグを非常に困難にし、プログラムの信頼性を損なう原因となります。このコミットの目的は、このようなサイレントな失敗を防ぎ、アラインメントの問題が発生した際にプログラムを意図的にクラッシュさせることで、開発者が問題を早期に特定し、修正できるようにすることです。クラッシュは、不正なメモリアクセス(nilポインタのデリファレンス)を意図的に引き起こすことで実現されます。

## 前提知識の解説

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

アトミック操作とは、複数のCPUコアやスレッドから同時にアクセスされる共有データに対して、その操作全体が中断されることなく、単一の不可分な操作として実行されることを保証するものです。これにより、競合状態(Race Condition)を防ぎ、データの整合性を保ちます。Go言語では、`sync/atomic`パッケージがこれらの操作を提供します。例えば、`AddInt64`は64ビット整数に値をアトミックに加算し、`LoadUint64`は64ビット符号なし整数をアトミックに読み込みます。

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

メモリアラインメントとは、データがメモリ上で特定のバイト境界に配置されることを指します。CPUは、特定のデータ型(例えば、32ビット整数や64ビット整数)を効率的に読み書きするために、そのデータが特定のメモリアドレスの倍数に配置されていることを期待します。

*   **32ビットシステム (例: 386)**: 32ビットシステムでは、通常、32ビット(4バイト)のデータは4バイト境界に、64ビット(8バイト)のデータは8バイト境界にアラインメントされていることが望ましいです。
*   **アラインメントされていないアクセス**: データが期待される境界に配置されていない場合、CPUはデータを読み書きするために複数のメモリアクセスを行う必要があったり、パフォーマンスが低下したり、最悪の場合、ハードウェア例外(クラッシュ)を引き起こしたりすることがあります。特に、アトミック操作のような低レベルの操作では、アラインメントが厳密に要求されることがあります。

### 3. Intel 386アーキテクチャ

Intel 386(i386)は、1985年にリリースされた32ビットのx86マイクロプロセッサです。このアーキテクチャは、現代の64ビットx86プロセッサの基礎となりましたが、64ビットのデータ型をネイティブに扱うための命令セットやメモリアクセス機構には制約がありました。特に、64ビットのアトミック操作は、386のような32ビットアーキテクチャでは、より複雑な実装(例えば、`CMPXCHG8B`命令の使用)が必要となり、アラインメントの要件が厳しくなります。

### 4. `CMPXCHG8B`命令

`CMPXCHG8B` (Compare and Exchange 8 Bytes) は、Intel x86アーキテクチャの命令セットの一部で、8バイト(64ビット)の値をアトミックに比較し、交換するために使用されます。これは、32ビットプロセッサ上で64ビットのアトミック操作を実現するために不可欠な命令です。この命令は、メモリ上の64ビット値とEDX:EAXレジスタペアの値を比較し、一致すればECX:EBXレジスタペアの値をメモリに書き込みます。この操作全体がアトミックに実行されます。

### 5. `unsafe.Pointer` in Go

Go言語の`unsafe`パッケージは、Goの型システムをバイパスして、低レベルのメモリアクセスを可能にする機能を提供します。`unsafe.Pointer`は、任意の型のポインタを保持できる特殊なポインタ型で、C言語の`void*`に似ています。これを使用することで、型安全性を犠牲にして、メモリレイアウトを直接操作したり、アラインメントされていないアドレスにアクセスしたりすることが可能になります。しかし、その名の通り「unsafe」であり、誤用するとプログラムのクラッシュや未定義の動作を引き起こす可能性があります。このコミットのテストコードでは、意図的にアラインメントされていない`uint64`ポインタを作成するために`unsafe.Pointer`が使用されています。

### 6. Goにおけるパニック (Panic) とクラッシュ

Go言語では、プログラムの実行中に回復不可能なエラーが発生した場合、`panic`メカニズムが使用されます。パニックが発生すると、通常の実行フローは中断され、遅延関数(`defer`)が実行され、最終的にプログラムがクラッシュします。このコミットでは、アラインメントされていないアトミック操作が検出された際に、意図的にnilポインタのデリファレンスを引き起こすことでパニックを発生させ、プログラムをクラッシュさせています。これにより、サイレントな失敗を防ぎ、開発者に問題の存在を明確に通知します。

## 技術的詳細

このコミットの技術的な核心は、386アーキテクチャのGoアセンブリコード(`src/pkg/sync/atomic/asm_386.s`)にアラインメントチェックを追加し、アラインメントされていないアドレスが渡された場合に意図的にパニックを発生させる点にあります。

具体的には、`CompareAndSwapInt64`, `CompareAndSwapUint64`, `AddInt64`, `AddUint64`, `LoadInt64`, `LoadUint64`, `StoreInt64`, `StoreUint64`といった64ビットのアトミック操作を行うアセンブリ関数に以下のコードが追加されました。

```assembly
	TESTL	$7, BP
	JZ	2(PC)
	MOVL	0, AX // crash with nil ptr deref

または

	TESTL	$7, AX
	JZ	2(PC)
	MOVL	0, AX // crash with nil ptr deref
  • TESTL $7, BP (または TESTL $7, AX):

    • BPまたはAXレジスタには、アトミック操作の対象となるメモリアドレスが格納されています。
    • TESTL命令は、2つのオペランドのビットごとのAND演算を行い、結果を破棄しますが、フラグレジスタ(特にゼロフラグZとパリティフラグP)を設定します。
    • $7はバイナリで00000111です。メモリアドレスと7のビットごとのAND演算を行うことで、アドレスの下位3ビットをチェックしています。
    • もしアドレスが8の倍数(8バイト境界にアラインメントされている)であれば、下位3ビットはすべて0になります。この場合、TESTLの結果も0になり、ゼロフラグ(ZF)がセットされます。
    • もしアドレスが8の倍数でなければ、下位3ビットの少なくとも1つが1になり、TESTLの結果は非ゼロになります。この場合、ゼロフラグ(ZF)はクリアされます。
  • JZ 2(PC):

    • JZ (Jump if Zero) 命令は、ゼロフラグ(ZF)がセットされている場合(つまり、TESTLの結果がゼロだった場合、アドレスが8バイト境界にアラインメントされている場合)に、指定されたオフセット(2(PC)は現在のプログラムカウンタから2バイト先)にジャンプします。
    • これにより、アラインメントが正しい場合は、アトミック操作の本来の処理に進みます。
  • MOVL 0, AX // crash with nil ptr deref:

    • JZ命令が実行されなかった場合(つまり、ゼロフラグがクリアされている場合、アドレスがアラインメントされていない場合)、この命令が実行されます。
    • MOVL 0, AXは、AXレジスタに0(nil)をロードします。
    • この直後に続くアトミック操作の命令は、AXレジスタに格納されたアドレス(この場合は0)を間接参照しようとします。
    • 0番地へのアクセスは、オペレーティングシステムによって保護されており、通常はセグメンテーション違反やページフォルトといったハードウェア例外を引き起こします。Goランタイムはこれを捕捉し、nilポインタのデリファレンスとしてパニックを発生させます。

この変更により、386アーキテクチャでアラインメントされていない64ビットアトミック操作が実行されると、即座にプログラムがクラッシュし、開発者は問題の根本原因(不正なメモリアラインメント)を特定できるようになります。

また、src/pkg/sync/atomic/atomic_test.goには、この新しいクラッシュ動作を検証するためのテストケースTestUnaligned64が追加されました。このテストは、unsafe.Pointerを使用して意図的にアラインメントされていないuint64ポインタを作成し、それに対して各種アトミック操作を実行することで、期待通りにパニックが発生するかどうかを確認します。shouldPanicヘルパー関数は、指定された関数がパニックを引き起こすことを検証するために使用されます。

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

src/pkg/sync/atomic/asm_386.s

以下のコードが、CompareAndSwapInt64, CompareAndSwapUint64, AddInt64, AddUint64, LoadInt64, LoadUint64, StoreInt64, StoreUint64の各関数の冒頭付近に追加されました。

--- a/src/pkg/sync/atomic/asm_386.s
+++ b/src/pkg/sync/atomic/asm_386.s
@@ -28,6 +28,9 @@ TEXT ·CompareAndSwapInt64(SB),7,$0
 
 TEXT ·CompareAndSwapUint64(SB),7,$0
 	MOVL	addr+0(FP), BP
+	TESTL	$7, BP
+	JZ	2(PC)
+	MOVL	0, AX // crash with nil ptr deref
 	MOVL	old+4(FP), AX
 	MOVL	old+8(FP), DX
 	MOVL	new+12(FP), BX
@@ -61,6 +64,9 @@ TEXT ·AddInt64(SB),7,$0
 TEXT ·AddUint64(SB),7,$0
 	// no XADDQ so use CMPXCHG8B loop
 	MOVL	addr+0(FP), BP
+	TESTL	$7, BP
+	JZ	2(PC)
+	MOVL	0, AX // crash with nil ptr deref
 	// DI:SI = delta
 	MOVL	delta+4(FP), SI
 	MOVL	delta+8(FP), DI
@@ -105,6 +111,9 @@ TEXT ·LoadInt64(SB),7,$0
 
 TEXT ·LoadUint64(SB),7,$0
 	MOVL	addr+0(FP), AX
+	TESTL	$7, AX
+	JZ	2(PC)
+	MOVL	0, AX // crash with nil ptr deref
 	// MOVQ and EMMS were introduced on the Pentium MMX.
 	// MOVQ (%EAX), %MM0
 	BYTE $0x0f; BYTE $0x6f; BYTE $0x00
@@ -133,6 +142,9 @@ TEXT ·StoreInt64(SB),7,$0
 
 TEXT ·StoreUint64(SB),7,$0
 	MOVL	addr+0(FP), AX
+	TESTL	$7, AX
+	JZ	2(PC)
+	MOVL	0, AX // crash with nil ptr deref
 	// MOVQ and EMMS were introduced on the Pentium MMX.
 	// MOVQ 0x8(%ESP), %MM0
 	BYTE $0x0f; BYTE $0x6f; BYTE $0x44; BYTE $0x24; BYTE $0x08

src/pkg/sync/atomic/atomic_test.go

以下のテストコードが追加されました。

--- a/src/pkg/sync/atomic/atomic_test.go
+++ b/src/pkg/sync/atomic/atomic_test.go
@@ -1177,3 +1177,29 @@ func TestStoreLoadRelAcq64(t *testing.T) {\n 	<-c\n 	<-c\n }\n+\n+func shouldPanic(t *testing.T, name string, f func()) {\n+\tdefer func() {\n+\t\tif recover() == nil {\n+\t\t\tt.Errorf(\"%s did not panic\", name)\n+\t\t}\n+\t}()\n+\tf()\n+}\n+\n+func TestUnaligned64(t *testing.T) {\n+\t// Unaligned 64-bit atomics on 32-bit systems are\n+\t// a continual source of pain. Test that on 386 they crash\n+\t// instead of failing silently.\n+\tif runtime.GOARCH != \"386\" {\n+\t\tt.Skip(\"test only runs on 386\")\n+\t}\n+\n+\tx := make([]uint32, 4)\n+\tp := (*uint64)(unsafe.Pointer(&x[1])) // misaligned\n+\n+\tshouldPanic(t, \"LoadUint64\", func() { LoadUint64(p) })\n+\tshouldPanic(t, \"StoreUint64\", func() { StoreUint64(p, 1) })\n+\tshouldPanic(t, \"CompareAndSwapUint64\", func() { CompareAndSwapUint64(p, 1, 2) })\n+\tshouldPanic(t, \"AddUint64\", func() { AddUint64(p, 3) })\n+}\n```

## コアとなるコードの解説

### `src/pkg/sync/atomic/asm_386.s` の変更点

各64ビットアトミック操作のアセンブリ関数において、操作対象のアドレス(`BP`または`AX`レジスタに格納されている)が8バイト境界にアラインメントされているかどうかがチェックされます。

1.  `TESTL $7, BP` (または `TESTL $7, AX`):
    *   これは、アドレスの下位3ビットがすべてゼロであるか(つまり、アドレスが8の倍数であるか)を効率的にチェックするアセンブリ命令です。
    *   `$7`はバイナリで`00000111`であり、アドレスと`7`のビットごとのAND演算を行うことで、アドレスの最下位3ビットを分離します。
    *   もしアドレスが8バイト境界にアラインメントされていれば、下位3ビットは`000`となり、`TESTL`の結果はゼロになります。この場合、CPUのゼロフラグ(ZF)がセットされます。

2.  `JZ 2(PC)`:
    *   ゼロフラグ(ZF)がセットされている場合(アラインメントが正しい場合)、プログラムカウンタ(PC)から2バイト先にジャンプします。これにより、アラインメントチェックの後の本来のアトミック操作のコードにスキップします。

3.  `MOVL 0, AX // crash with nil ptr deref`:
    *   `JZ`命令が実行されなかった場合(アラインメントが正しくない場合)、この命令が実行されます。
    *   `AX`レジスタに`0`(nilポインタ)をロードします。
    *   この直後に続くアトミック操作の命令は、`AX`レジスタに格納されたアドレスを間接参照しようとします。しかし、`AX`には`0`がロードされているため、`0`番地へのメモリアクセスが発生し、これがGoランタイムによってnilポインタデリファレンスとして捕捉され、パニックを引き起こします。

このメカニズムにより、386アーキテクチャでアラインメントされていない64ビットアトミック操作が実行されると、サイレントな失敗ではなく、明確なクラッシュが発生し、デバッグが容易になります。

### `src/pkg/sync/atomic/atomic_test.go` の変更点

1.  `shouldPanic`関数:
    *   これはヘルパー関数で、引数として渡された関数`f`がパニックを引き起こすことを検証します。
    *   `defer func() { if recover() == nil { ... } }()`ブロックを使用しています。`recover()`はパニックから回復するために使用され、パニックが発生しなかった場合は`nil`を返します。
    *   もし`f()`がパニックを引き起こさなかった場合(`recover()`が`nil`を返した場合)、テストはエラーを報告します。

2.  `TestUnaligned64`関数:
    *   このテストは、`runtime.GOARCH != "386"`の場合にスキップされます。これは、このアラインメントチェックとクラッシュ動作が386アーキテクチャに特化しているためです。
    *   `x := make([]uint32, 4)`: 4つの`uint32`要素を持つスライスを作成します。`uint32`は4バイトです。
    *   `p := (*uint64)(unsafe.Pointer(&x[1]))`: ここが重要な部分です。
        *   `&x[1]`は、スライス`x`の2番目の要素(インデックス1)のアドレスを取得します。
        *   `x[0]`が4バイト、`x[1]`が4バイトなので、`&x[1]`のアドレスは、スライスの開始アドレスから4バイトオフセットされた位置になります。
        *   `unsafe.Pointer(&x[1])`は、このアドレスを`unsafe.Pointer`型に変換します。
        *   `(*uint64)(...)`は、`unsafe.Pointer`を`*uint64`型にキャストします。
        *   結果として、`p`は`uint64`へのポインタですが、その指すアドレスは8バイト境界にアラインメントされていません(4バイト境界にアラインメントされています)。
    *   `shouldPanic(t, "LoadUint64", func() { LoadUint64(p) })`など:
        *   作成されたアラインメントされていないポインタ`p`に対して、`LoadUint64`, `StoreUint64`, `CompareAndSwapUint64`, `AddUint64`といった64ビットアトミック操作をそれぞれ実行します。
        *   `shouldPanic`関数を使用することで、これらの操作が期待通りにパニックを引き起こすことを検証します。

このテストは、アセンブリコードの変更が意図した通りに機能し、アラインメントされていない64ビットアトミック操作が386上で確実にクラッシュすることを保証します。

## 関連リンク

*   Go Gerrit Change-ID: [https://golang.org/cl/7702043](https://golang.org/cl/7702043)

## 参考にした情報源リンク

*   Go言語の`sync/atomic`パッケージのドキュメント
*   x86アセンブリ言語の命令セットリファレンス(特に`TEST`、`JZ`、`MOVL`、`CMPXCHG8B`命令について)
*   メモリアラインメントに関する一般的なコンピュータアーキテクチャの資料
*   Go言語の`unsafe`パッケージに関するドキュメント
*   Go言語のパニックと回復に関するドキュメント
*   [https://go.dev/doc/asm](https://go.dev/doc/asm) (Go Assembly Language)
*   [https://go.dev/src/sync/atomic/asm_386.s](https://go.dev/src/sync/atomic/asm_386.s) (Go source code for 386 atomic assembly)
*   [https://go.dev/src/sync/atomic/atomic_test.go](https://go.dev/src/sync/atomic/atomic_test.go) (Go source code for atomic tests)
*   [https://en.wikipedia.org/wiki/Memory_alignment](https://en.wikipedia.org/wiki/Memory_alignment)
*   [https://en.wikipedia.org/wiki/CMPXCHG8B](https://en.wikipedia.org/wiki/CMPXCHG8B)
*   [https://en.wikipedia.org/wiki/Intel_80386](https://en.wikipedia.org/wiki/Intel_80386)
*   [https://go.dev/blog/go1.1-release](https://go.dev/blog/go1.1-release) (Go 1.1 Release Notes, which might mention this change as it was committed in 2013)
*   [https://go.dev/doc/effective_go#recover](https://go.dev/doc/effective_go#recover) (Effective Go - Defer, Panic, and Recover)
*   [https://go.dev/doc/articles/go_mem.html](https://go.dev/doc/articles/go_mem.html) (The Go Memory Model)
*   [https://go.dev/doc/articles/unsafe.html](https://go.dev/doc/articles/unsafe.html) (The Go unsafe package)