[インデックス 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ビットのデータ型(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ビットのアトミック操作を行うアセンブリ関数に以下のコードが追加されました。
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)