[インデックス 17164] ファイルの概要
このコミットは、Go言語のsync/atomic
パッケージ内のアセンブリルーチンにおいて、引数フレームサイズ(argsize
)を明示的に指定するように変更するものです。これにより、Goのリンカとガベージコレクタがアセンブリ関数におけるスタックレイアウトを正確に理解できるようになり、特にポインタの追跡とガベージコレクションの正確性が向上します。
コミット
commit f3c1070fa4cb02c55b47b874076fe74879288a4c
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Aug 12 21:46:33 2013 +0400
sync/atomic: specify argsize for asm routines
Fixes #6098.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/12717043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f3c1070fa4cb02c55b47b874076fe74879288a4c
元コミット内容
sync/atomic: specify argsize for asm routines
Fixes #6098.
このコミットは、sync/atomic
パッケージのアセンブリルーチンに対して、引数フレームサイズを明示的に指定するものです。これはGoのIssue #6098を修正します。
変更の背景
Goのランタイム、特にガベージコレクタ(GC)は、実行中のプログラムのスタックフレームを正確に検査し、どのメモリ領域がポインタを含んでいるかを識別する必要があります。これにより、GCは到達可能なオブジェクトを正確にマークし、不要なオブジェクトを解放できます。
アセンブリ言語で書かれた関数(Goでは通常、パフォーマンスが非常に重要な低レベルの操作、例えばアトミック操作などに用いられます)は、Goコンパイラが生成する通常のGoコードとは異なるスタックフレームの規約を持つことがあります。特に、アセンブリ関数が引数を受け取る場合、その引数がスタック上のどこに配置され、どれくらいのサイズを占めるのかをリンカとGCに正確に伝える必要があります。
このコミットが行われた2013年頃のGoのバージョンでは、アセンブリ関数における引数フレームサイズの指定が不十分であったため、ガベージコレクタがアセンブリ関数のスタックフレームを正しく処理できない可能性がありました。具体的には、アセンブリ関数がポインタ型の引数を受け取る場合、GCがそのポインタを認識できず、誤って解放してしまう(Use-After-Free)などの問題を引き起こす可能性がありました。
Issue #6098は、まさにこの問題、すなわちアセンブリ関数がポインタを引数として受け取る際に、GCがそのポインタを追跡できないというバグを報告しています。このコミットは、TEXT
ディレクティブに引数フレームサイズを明示的に追加することで、この問題を解決し、アセンブリ関数とGoランタイム間の連携を強化することを目的としています。
前提知識の解説
Goのアセンブリ言語 (Plan 9 Assembly)
Goは、一般的なx86/x64アセンブリとは異なる、Plan 9アセンブリと呼ばれる独自の構文を使用します。これは、Goのツールチェーン(コンパイラ、リンカ)と密接に統合されており、クロスコンパイルの容易さや、Goのランタイムとの連携を考慮して設計されています。
Goのアセンブリ関数は、TEXT
ディレクティブで定義されます。
TEXT symbol(SB), flags, $framesize
または
TEXT symbol(SB), flags, $framesize-argsize
symbol(SB)
: 関数のシンボル名。SB
はStatic Baseで、グローバルシンボルを参照するための擬似レジスタです。flags
: 関数の特性を示すフラグ。このコミットで重要なのはNOSPLIT
です。NOSPLIT
: この関数はスタックを拡張しないことを示します。つまり、関数内で新しいスタックフレームを割り当てたり、スタックチェックを行ったりしません。これは、非常に短い、低レベルのアセンブリ関数でよく使用されます。
$framesize
: この関数自身のスタックフレームのサイズ(ローカル変数などに使用される領域)。$argsize
: この関数が引数として受け取るバイト数。この値は、呼び出し元がスタックにプッシュする引数の合計サイズを示します。Goのリンカとガベージコレクタは、このargsize
情報を使用して、スタック上の引数領域を正確に識別し、特にポインタ引数を追跡します。
ガベージコレクション (GC) とポインタ追跡
Goはトレース型ガベージコレクタを採用しています。GCは、プログラムが使用しているメモリ(到達可能なオブジェクト)を識別し、それ以外のメモリを解放します。GCが到達可能なオブジェクトを識別するためには、スタック、レジスタ、グローバル変数など、プログラムのルートセットからポインタをたどる必要があります。
アセンブリ関数がポインタを引数として受け取る場合、そのポインタはスタック上に存在します。GCがスタックをスキャンする際、どの部分がポインタであり、どの部分が単なる整数値などの非ポインタデータであるかを正確に知る必要があります。TEXT
ディレクティブのargsize
は、この情報を提供し、GCがスタック上のポインタを正しく識別するために不可欠です。
sync/atomic
パッケージ
sync/atomic
パッケージは、Goにおいてアトミック操作(不可分操作)を提供します。アトミック操作は、複数のゴルーチンが同時に同じメモリ位置にアクセスしても、データ競合が発生しないように保証される操作です。これは、ロックを使用せずに並行処理を行うための重要なプリミティブであり、非常に高いパフォーマンスが要求されるため、多くの場合、プラットフォーム固有のアセンブリ言語で実装されています。
技術的詳細
このコミットの主要な変更点は、src/pkg/sync/atomic
ディレクトリ内の各アーキテクチャ(386, amd64, arm, freebsd_arm, linux_arm, netbsd_arm)のアセンブリファイルにおいて、TEXT
ディレクティブにargsize
を追加したことです。
変更前:
TEXT ·FunctionName(SB),NOSPLIT,$0
変更後:
TEXT ·FunctionName(SB),NOSPLIT,$0-argsize
ここでargsize
は、その関数が受け取る引数の合計バイト数です。例えば、CompareAndSwapUint32
はaddr
, old
, new
の3つのuint32
引数と、戻り値のswapped
(bool
)を受け取ります。Goの呼び出し規約では、引数はスタックにプッシュされ、戻り値もスタック経由で渡されることがあります。
CompareAndSwapUint32
:addr
(ポインタ),old
(uint32),new
(uint32),swapped
(bool)- 32-bitシステムの場合、ポインタは4バイト、uint32は4バイト、boolは1バイト。
- 引数と戻り値の合計サイズが13バイト(4+4+4+1)と計算され、
$0-13
が指定されています。
CompareAndSwapUint64
:addr
(ポインタ),old
(uint64),new
(uint64),swapped
(bool)- 64-bitシステムの場合、ポインタは8バイト、uint64は8バイト、boolは1バイト。
- 引数と戻り値の合計サイズが25バイト(8+8+8+1)と計算され、
$0-25
が指定されています。
このargsize
の追加は、Goのリンカ(cmd/5l
, cmd/6l
など)とガベージコレクタにとって非常に重要です。リンカは、この情報を使用して、アセンブリ関数が呼び出されたときにスタックフレームがどのように見えるかを正確に把握します。特に、スタック上のどのオフセットにポインタが存在する可能性があるかを識別するために使用されます。
ガベージコレクタは、スタックをスキャンする際に、このリンカによって生成された情報(スタックマップ)を利用します。argsize
が正しく指定されていないと、GCはアセンブリ関数のスタックフレーム内のポインタを誤って非ポインタデータと見なしたり、その逆を行ったりする可能性があります。これにより、GCがポインタを追跡できず、到達可能なオブジェクトを誤って回収してしまう(Use-After-Free)などの深刻なバグにつながる可能性があります。
また、atomic_test.go
にはTestNilDeref
という新しいテスト関数が追加されています。このテストは、sync/atomic
パッケージの各アトミック操作関数にnil
ポインタを渡した場合に、適切にパニックが発生するかどうかを検証します。これは、argsize
の修正によって、アセンブリ関数が不正なポインタを扱った際の挙動がより予測可能になったことを確認するため、または、修正が正しく機能していることを保証するための追加の安全策と考えられます。
コアとなるコードの変更箇所
このコミットは、主に以下のファイルに影響を与えています。
src/pkg/sync/atomic/asm_386.s
src/pkg/sync/atomic/asm_amd64.s
src/pkg/sync/atomic/asm_arm.s
src/pkg/sync/atomic/asm_freebsd_arm.s
src/pkg/sync/atomic/asm_linux_arm.s
src/pkg/sync/atomic/asm_netbsd_arm.s
src/pkg/sync/atomic/atomic_test.go
具体的な変更は、各アセンブリファイル内のTEXT
ディレクティブに-$argsize
が追加されたことです。
例: src/pkg/sync/atomic/asm_386.s
--- a/src/pkg/sync/atomic/asm_386.s
+++ b/src/pkg/sync/atomic/asm_386.s
@@ -6,10 +6,10 @@
#include "../../../cmd/ld/textflag.h"
-TEXT ·CompareAndSwapInt32(SB),NOSPLIT,$0
+TEXT ·CompareAndSwapInt32(SB),NOSPLIT,$0-13
JMP ·CompareAndSwapUint32(SB)
-TEXT ·CompareAndSwapUint32(SB),NOSPLIT,$0
+TEXT ·CompareAndSwapUint32(SB),NOSPLIT,$0-13
MOVL addr+0(FP), BP
MOVL old+4(FP), AX
MOVL new+8(FP), CX
例: src/pkg/sync/atomic/atomic_test.go
--- a/src/pkg/sync/atomic/atomic_test.go
+++ b/src/pkg/sync/atomic/atomic_test.go
@@ -1203,3 +1203,40 @@ func TestUnaligned64(t *testing.T) {
shouldPanic(t, "CompareAndSwapUint64", func() { CompareAndSwapUint64(p, 1, 2) })
shouldPanic(t, "AddUint64", func() { AddUint64(p, 3) })
}
+
+func TestNilDeref(t *testing.T) {
+ funcs := [...]func(){
+ func() { CompareAndSwapInt32(nil, 0, 0) },
+ func() { CompareAndSwapInt64(nil, 0, 0) },
+ func() { CompareAndSwapUint32(nil, 0, 0) },
+ func() { CompareAndSwapUint64(nil, 0, 0) },
+ func() { CompareAndSwapUintptr(nil, 0, 0) },
+ func() { CompareAndSwapPointer(nil, nil, nil) },
+ func() { AddInt32(nil, 0) },
+ func() { AddUint32(nil, 0) },
+ func() { AddInt64(nil, 0) },
+ func() { AddUint64(nil, 0) },
+ func() { AddUintptr(nil, 0) },
+ func() { LoadInt32(nil) },
+ func() { LoadInt64(nil) },
+ func() { LoadUint32(nil) },
+ func() { LoadUint64(nil) },
+ func() { LoadUintptr(nil) },
+ func() { LoadPointer(nil) },
+ func() { StoreInt32(nil, 0) },
+ func() { StoreInt64(nil, 0) },
+ func() { StoreUint32(nil, 0) },
+ func() { StoreUint64(nil, 0) },
+ func() { StoreUintptr(nil, 0) },
+ func() { StorePointer(nil, nil) },
+ }
+ for _, f := range funcs {
+ func() {
+ defer func() {
+ runtime.GC()
+ recover()
+ }()
+ f()
+ }()
+ }
+}
コアとなるコードの解説
このコミットの核心は、Goのアセンブリ言語におけるTEXT
ディレクティブのargsize
パラメータの正確な使用にあります。
TEXT ·FunctionName(SB),NOSPLIT,$0-argsize
ここで$0
は、このアセンブリ関数自体がローカル変数にスタック領域を必要としないことを示しています(NOSPLIT
フラグと合わせて、非常に軽量な関数であることを示唆)。重要なのはその後の-$argsize
の部分です。
argsize
は、Goのリンカに対して、このアセンブリ関数が呼び出し元から受け取る引数の合計バイト数を伝えます。この情報がなぜ重要かというと、Goのガベージコレクタがスタックをスキャンしてポインタを識別する際に、このargsize
を基にスタック上の引数領域を正確に判断するからです。
例えば、CompareAndSwapUint32
関数は、メモリのアドレス(ポインタ)、古い値、新しい値、そして操作が成功したかどうかのブール値を扱います。これらの引数や戻り値はスタック上に配置されます。もしargsize
が正しく指定されていないと、GCはスタック上のポインタ(この場合はaddr
引数)を正しく識別できず、そのポインタが指すメモリを誤って回収してしまう可能性があります。これは、Goプログラムの実行時エラーやクラッシュにつながる、非常に深刻なバグです。
この修正により、Goのツールチェーンはアセンブリ関数とGoコード間のスタックフレームの整合性をより厳密にチェックし、ガベージコレクタがアセンブリ関数を介して渡されるポインタを確実に追跡できるようになります。これにより、sync/atomic
パッケージのような低レベルで重要なコードの堅牢性と安全性が向上します。
atomic_test.go
に追加されたTestNilDeref
は、各アトミック操作関数にnil
ポインタを渡した場合の挙動をテストしています。アトミック操作は通常、有効なメモリアドレスに対して行われるため、nil
ポインタが渡された場合にはパニック(プログラムの異常終了)が発生することが期待されます。このテストは、argsize
の修正が、このような不正な入力に対するアセンブリ関数の挙動に悪影響を与えていないこと、または、むしろより正確なパニックを引き起こすようになったことを確認するためのものです。runtime.GC()
とrecover()
を使用しているのは、パニックが発生してもテストが続行できるようにするためです。
関連リンク
- Go Issue #6098: https://github.com/golang/go/issues/6098
- Gerrit Change-ID 12717043: https://golang.org/cl/12717043
参考にした情報源リンク
- Go Assembly Language (Plan 9 Assembly):
- https://go.dev/doc/asm
- https://go.dev/src/cmd/internal/obj/doc.go (特に
TEXT
ディレクティブに関する記述)
- Go Garbage Collection:
- https://go.dev/doc/gc-guide
- https://go.dev/blog/go15gc (Go 1.5以降のGCに関する記事だが、GCの基本的な概念理解に役立つ)
- Go
sync/atomic
package documentation: - Stack frames in Go:
- https://go.dev/src/runtime/stack.go (Goランタイムにおけるスタック管理のコード)
- Discussion on Go Issue #6098 (for context on the problem):
- https://github.com/golang/go/issues/6098 (Issueのコメント欄も参照)
- Go Code Review Comments (for understanding Go's development practices):
- Go's calling convention (for understanding how arguments are passed):
- Goの呼び出し規約は公式ドキュメントには詳細に記述されていませんが、Goのソースコード(特に
cmd/compile/internal/gc/ssa.go
やcmd/compile/internal/gc/walk.go
など)や、関連するGoのIssueやメーリングリストの議論から推測できます。 - 一般的には、引数はスタックにプッシュされ、戻り値もスタック経由で渡されることが多いです。
- https://go.dev/src/cmd/compile/internal/ssa/gen/ARM.go (ARMアーキテクチャのコード生成における引数と戻り値の扱いに関するヒント)
- https://go.dev/src/cmd/compile/internal/ssa/gen/AMDU64.go (AMD64アーキテクチャのコード生成における引数と戻り値の扱いに関するヒント)
- Goの呼び出し規約は公式ドキュメントには詳細に記述されていませんが、Goのソースコード(特に
- Plan 9 Assembly
TEXT
directive:- https://go.dev/src/cmd/asm/doc.go (Goのアセンブラのドキュメント)
- https://go.dev/src/cmd/ld/textflag.h (
NOSPLIT
などのフラグ定義)
- Dmitriy Vyukov's contributions to Go:
- Dmitriy VyukovはGoの並行処理とランタイムの分野で多くの重要な貢献をしている開発者です。彼の他のコミットや論文(例: Go Memory Model)も参照すると、Goの低レベルな挙動に関する理解が深まります。
- https://research.google/pubs/pub41891/ (Go Memory Model by Dmitriy Vyukov)
- https://github.com/dvyukov (GitHubプロフィール)
- Brad Fitzpatrick's contributions to Go:
- Brad FitzpatrickもGoの初期からの主要な貢献者の一人であり、特にネットワークや標準ライブラリに多くの貢献をしています。彼のレビューはGoのコード品質を保証する上で重要です。
- https://github.com/bradfitz (GitHubプロフィール)
- Goのメーリングリスト (golang-dev):
- https://groups.google.com/g/golang-dev (過去の議論を検索すると、このコミットに関する詳細な背景が見つかる可能性があります)
- Goのソースコード:
- Goのソースコード自体が最も正確で詳細な情報源です。特に
cmd/
ディレクトリ内のコンパイラ、リンカ、アセンブラのコードは、TEXT
ディレクティブやスタックフレームの処理に関する深い洞察を提供します。 src/runtime/
ディレクトリ内のコードは、ガベージコレクタやスケジューラなど、Goランタイムの内部動作を理解するのに役立ちます。src/pkg/sync/atomic/
ディレクトリ内のアセンブリファイルは、このコミットの直接の対象であり、アトミック操作の低レベルな実装を理解するのに役立ちます。
- Goのソースコード自体が最も正確で詳細な情報源です。特に
- Goのリリースノート:
- Goの各バージョンのリリースノートには、重要な変更点や修正されたバグに関する情報が含まれています。このコミットがGoのどのバージョンでリリースされたかを確認すると、その時点でのGoの進化の文脈を理解できます。
- https://go.dev/doc/devel/release (Goのリリース履歴)
- Goのドキュメント:
- https://go.dev/doc/ (Goの公式ドキュメントは、Goの基本的な概念から高度なトピックまでをカバーしています)
- Goのツールチェーンに関する書籍や記事:
- Goのコンパイラ、リンカ、アセンブラの内部動作について解説している専門的な書籍やブログ記事も、理解を深めるのに役立ちます。```markdown
[インデックス 17164] ファイルの概要
このコミットは、Go言語のsync/atomic
パッケージ内のアセンブリルーチンにおいて、引数フレームサイズ(argsize
)を明示的に指定するように変更するものです。これにより、Goのリンカとガベージコレクタがアセンブリ関数におけるスタックレイアウトを正確に理解できるようになり、特にポインタの追跡とガベージコレクションの正確性が向上します。
コミット
commit f3c1070fa4cb02c55b47b874076fe74879288a4c
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Aug 12 21:46:33 2013 +0400
sync/atomic: specify argsize for asm routines
Fixes #6098.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/12717043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f3c1070fa4cb02c55b47b874076fe74879288a4c
元コミット内容
sync/atomic: specify argsize for asm routines
Fixes #6098.
このコミットは、sync/atomic
パッケージのアセンブリルーチンに対して、引数フレームサイズを明示的に指定するものです。これはGoのIssue #6098を修正します。
変更の背景
Goのランタイム、特にガベージコレクタ(GC)は、実行中のプログラムのスタックフレームを正確に検査し、どのメモリ領域がポインタを含んでいるかを識別する必要があります。これにより、GCは到達可能なオブジェクトを正確にマークし、不要なオブジェクトを解放できます。
アセンブリ言語で書かれた関数(Goでは通常、パフォーマンスが非常に重要な低レベルの操作、例えばアトミック操作などに用いられます)は、Goコンパイラが生成する通常のGoコードとは異なるスタックフレームの規約を持つことがあります。特に、アセンブリ関数が引数を受け取る場合、その引数がスタック上のどこに配置され、どれくらいのサイズを占めるのかをリンカとGCに正確に伝える必要があります。
このコミットが行われた2013年頃のGoのバージョンでは、アセンブリ関数における引数フレームサイズの指定が不十分であったため、ガベージコレクタがアセンブリ関数のスタックフレームを正しく処理できない可能性がありました。具体的には、アセンブリ関数がポインタ型の引数を受け取る場合、GCがそのポインタを認識できず、誤って解放してしまう(Use-After-Free)などの問題を引き起こす可能性がありました。
Issue #6098は、まさにこの問題、すなわちアセンブリ関数がポインタを引数として受け取る際に、GCがそのポインタを追跡できないというバグを報告しています。このコミットは、TEXT
ディレクティブに引数フレームサイズを明示的に追加することで、この問題を解決し、アセンブリ関数とGoランタイム間の連携を強化することを目的としています。
前提知識の解説
Goのアセンブリ言語 (Plan 9 Assembly)
Goは、一般的なx86/x64アセンブリとは異なる、Plan 9アセンブリと呼ばれる独自の構文を使用します。これは、Goのツールチェーン(コンパイラ、リンカ)と密接に統合されており、クロスコンパイルの容易さや、Goのランタイムとの連携を考慮して設計されています。
Goのアセンブリ関数は、TEXT
ディレクティブで定義されます。
TEXT symbol(SB), flags, $framesize
または
TEXT symbol(SB), flags, $framesize-argsize
symbol(SB)
: 関数のシンボル名。SB
はStatic Baseで、グローバルシンボルを参照するための擬似レジスタです。flags
: 関数の特性を示すフラグ。このコミットで重要なのはNOSPLIT
です。NOSPLIT
: この関数はスタックを拡張しないことを示します。つまり、関数内で新しいスタックフレームを割り当てたり、スタックチェックを行ったりしません。これは、非常に短い、低レベルのアセンブリ関数でよく使用されます。
$framesize
: この関数自身のスタックフレームのサイズ(ローカル変数などに使用される領域)。$argsize
: この関数が引数として受け取るバイト数。この値は、呼び出し元がスタックにプッシュする引数の合計サイズを示します。Goのリンカとガベージコレクタは、このargsize
情報を使用して、スタック上の引数領域を正確に識別し、特にポインタ引数を追跡します。
ガベージコレクション (GC) とポインタ追跡
Goはトレース型ガベージコレクタを採用しています。GCは、プログラムが使用しているメモリ(到達可能なオブジェクト)を識別し、それ以外のメモリを解放します。GCが到達可能なオブジェクトを識別するためには、スタック、レジスタ、グローバル変数など、プログラムのルートセットからポインタをたどる必要があります。
アセンブリ関数がポインタを引数として受け取る場合、そのポインタはスタック上に存在します。GCがスタックをスキャンする際、どの部分がポインタであり、どの部分が単なる整数値などの非ポインタデータであるかを正確に知る必要があります。TEXT
ディレクティブのargsize
は、この情報を提供し、GCがスタック上のポインタを正しく識別するために不可欠です。
sync/atomic
パッケージ
sync/atomic
パッケージは、Goにおいてアトミック操作(不可分操作)を提供します。アトミック操作は、複数のゴルーチンが同時に同じメモリ位置にアクセスしても、データ競合が発生しないように保証される操作です。これは、ロックを使用せずに並行処理を行うための重要なプリミティブであり、非常に高いパフォーマンスが要求されるため、多くの場合、プラットフォーム固有のアセンブリ言語で実装されています。
技術的詳細
このコミットの主要な変更点は、src/pkg/sync/atomic
ディレクトリ内の各アーキテクチャ(386, amd64, arm, freebsd_arm, linux_arm, netbsd_arm)のアセンブリファイルにおいて、TEXT
ディレクティブにargsize
を追加したことです。
変更前:
TEXT ·FunctionName(SB),NOSPLIT,$0
変更後:
TEXT ·FunctionName(SB),NOSPLIT,$0-argsize
ここでargsize
は、その関数が受け取る引数の合計バイト数です。例えば、CompareAndSwapUint32
はaddr
, old
, new
の3つのuint32
引数と、戻り値のswapped
(bool
)を受け取ります。Goの呼び出し規約では、引数はスタックにプッシュされ、戻り値もスタック経由で渡されることがあります。
CompareAndSwapUint32
:addr
(ポインタ),old
(uint32),new
(uint32),swapped
(bool)- 32-bitシステムの場合、ポインタは4バイト、uint32は4バイト、boolは1バイト。
- 引数と戻り値の合計サイズが13バイト(4+4+4+1)と計算され、
$0-13
が指定されています。
CompareAndSwapUint64
:addr
(ポインタ),old
(uint64),new
(uint64),swapped
(bool)- 64-bitシステムの場合、ポインタは8バイト、uint64は8バイト、boolは1バイト。
- 引数と戻り値の合計サイズが25バイト(8+8+8+1)と計算され、
$0-25
が指定されています。
このargsize
の追加は、Goのリンカ(cmd/5l
, cmd/6l
など)とガベージコレクタにとって非常に重要です。リンカは、この情報を使用して、アセンブリ関数が呼び出されたときにスタックフレームがどのように見えるかを正確に把握します。特に、スタック上のどのオフセットにポインタが存在する可能性があるかを識別するために使用されます。
ガベージコレクタは、スタックをスキャンする際に、このリンカによって生成された情報(スタックマップ)を利用します。argsize
が正しく指定されていないと、GCはアセンブリ関数のスタックフレーム内のポインタを誤って非ポインタデータと見なしたり、その逆を行ったりする可能性があります。これにより、GCがポインタを追跡できず、到達可能なオブジェクトを誤って回収してしまう(Use-After-Free)などの深刻なバグにつながる可能性があります。
また、atomic_test.go
にはTestNilDeref
という新しいテスト関数が追加されています。このテストは、sync/atomic
パッケージの各アトミック操作関数にnil
ポインタを渡した場合に、適切にパニックが発生するかどうかを検証します。これは、argsize
の修正によって、アセンブリ関数が不正なポインタを扱った際の挙動がより予測可能になったことを確認するため、または、修正が正しく機能していることを保証するための追加の安全策と考えられます。
コアとなるコードの変更箇所
このコミットは、主に以下のファイルに影響を与えています。
src/pkg/sync/atomic/asm_386.s
src/pkg/sync/atomic/asm_amd64.s
src/pkg/sync/atomic/asm_arm.s
src/pkg/sync/atomic/asm_freebsd_arm.s
src/pkg/sync/atomic/asm_linux_arm.s
src/pkg/sync/atomic/asm_netbsd_arm.s
src/pkg/sync/atomic/atomic_test.go
具体的な変更は、各アセンブリファイル内のTEXT
ディレクティブに-$argsize
が追加されたことです。
例: src/pkg/sync/atomic/asm_386.s
--- a/src/pkg/sync/atomic/asm_386.s
+++ b/src/pkg/sync/atomic/asm_386.s
@@ -6,10 +6,10 @@
#include "../../../cmd/ld/textflag.h"
-TEXT ·CompareAndSwapInt32(SB),NOSPLIT,$0
+TEXT ·CompareAndSwapInt32(SB),NOSPLIT,$0-13
JMP ·CompareAndSwapUint32(SB)
-TEXT ·CompareAndSwapUint32(SB),NOSPLIT,$0
+TEXT ·CompareAndSwapUint32(SB),NOSPLIT,$0-13
MOVL addr+0(FP), BP
MOVL old+4(FP), AX
MOVL new+8(FP), CX
例: src/pkg/sync/atomic/atomic_test.go
--- a/src/pkg/sync/atomic/atomic_test.go
+++ b/src/pkg/sync/atomic/atomic_test.go
@@ -1203,3 +1203,40 @@ func TestUnaligned64(t *testing.T) {
shouldPanic(t, "CompareAndSwapUint64", func() { CompareAndSwapUint64(p, 1, 2) })
shouldPanic(t, "AddUint64", func() { AddUint64(p, 3) })
}
+
+func TestNilDeref(t *testing.T) {
+ funcs := [...]func(){
+ func() { CompareAndSwapInt32(nil, 0, 0) },
+ func() { CompareAndSwapInt64(nil, 0, 0) },
+ func() { CompareAndSwapUint32(nil, 0, 0) },
+ func() { CompareAndSwapUint64(nil, 0, 0) },
+ func() { CompareAndSwapUintptr(nil, 0, 0) },
+ func() { CompareAndSwapPointer(nil, nil, nil) },
+ func() { AddInt32(nil, 0) },
+ func() { AddUint32(nil, 0) },
+ func() { AddInt64(nil, 0) },
+ func() { AddUint64(nil, 0) },
+ func() { AddUintptr(nil, 0) },
+ func() { LoadInt32(nil) },
+ func() { LoadInt64(nil) },
+ func() { LoadUint32(nil) },
+ func() { LoadUint64(nil) },
+ func() { LoadUintptr(nil) },
+ func() { LoadPointer(nil) },
+ func() { StoreInt32(nil, 0) },
+ func() { StoreInt64(nil, 0) },
+ func() { StoreUint32(nil, 0) },
+ func() { StoreUint64(nil, 0) },
+ func() { StoreUintptr(nil, 0) },
+ func() { StorePointer(nil, nil) },
+ }
+ for _, f := range funcs {
+ func() {
+ defer func() {
+ runtime.GC()
+ recover()
+ }()
+ f()
+ }()
+ }
+}
コアとなるコードの解説
このコミットの核心は、Goのアセンブリ言語におけるTEXT
ディレクティブのargsize
パラメータの正確な使用にあります。
TEXT ·FunctionName(SB),NOSPLIT,$0-argsize
ここで$0
は、このアセンブリ関数自体がローカル変数にスタック領域を必要としないことを示しています(NOSPLIT
フラグと合わせて、非常に軽量な関数であることを示唆)。重要なのはその後の-$argsize
の部分です。
argsize
は、Goのリンカに対して、このアセンブリ関数が呼び出し元から受け取る引数の合計バイト数を伝えます。この情報がなぜ重要かというと、Goのガベージコレクタがスタックをスキャンしてポインタを識別する際に、このargsize
を基にスタック上の引数領域を正確に判断するからです。
例えば、CompareAndSwapUint32
関数は、メモリのアドレス(ポインタ)、古い値、新しい値、そして操作が成功したかどうかのブール値を扱います。これらの引数や戻り値はスタック上に配置されます。もしargsize
が正しく指定されていないと、GCはスタック上のポインタ(この場合はaddr
引数)を正しく識別できず、そのポインタが指すメモリを誤って回収してしまう可能性があります。これは、Goプログラムの実行時エラーやクラッシュにつながる、非常に深刻なバグです。
この修正により、Goのツールチェーンはアセンブリ関数とGoコード間のスタックフレームの整合性をより厳密にチェックし、ガベージコレクタがアセンブリ関数を介して渡されるポインタを確実に追跡できるようになります。これにより、sync/atomic
パッケージのような低レベルで重要なコードの堅牢性と安全性が向上します。
atomic_test.go
に追加されたTestNilDeref
は、各アトミック操作関数にnil
ポインタを渡した場合の挙動をテストしています。アトミック操作は通常、有効なメモリアドレスに対して行われるため、nil
ポインタが渡された場合にはパニック(プログラムの異常終了)が発生することが期待されます。このテストは、argsize
の修正が、このような不正な入力に対するアセンブリ関数の挙動に悪影響を与えていないこと、または、むしろより正確なパニックを引き起こすようになったことを確認するためのものです。runtime.GC()
とrecover()
を使用しているのは、パニックが発生してもテストが続行できるようにするためです。
関連リンク
- Go Issue #6098: https://github.com/golang/go/issues/6098
- Gerrit Change-ID 12717043: https://golang.org/cl/12717043
参考にした情報源リンク
- Go Assembly Language (Plan 9 Assembly):
- https://go.dev/doc/asm
- https://go.dev/src/cmd/internal/obj/doc.go (特に
TEXT
ディレクティブに関する記述)
- Go Garbage Collection:
- https://go.dev/doc/gc-guide
- https://go.dev/blog/go15gc (Go 1.5以降のGCに関する記事だが、GCの基本的な概念理解に役立つ)
- Go
sync/atomic
package documentation: - Stack frames in Go:
- https://go.dev/src/runtime/stack.go (Goランタイムにおけるスタック管理のコード)
- Discussion on Go Issue #6098 (for context on the problem):
- https://github.com/golang/go/issues/6098 (Issueのコメント欄も参照)
- Go Code Review Comments (for understanding Go's development practices):
- Go's calling convention (for understanding how arguments are passed):
- Goの呼び出し規約は公式ドキュメントには詳細に記述されていませんが、Goのソースコード(特に
cmd/compile/internal/gc/ssa.go
やcmd/compile/internal/gc/walk.go
など)や、関連するGoのIssueやメーリングリストの議論から推測できます。 - 一般的には、引数はスタックにプッシュされ、戻り値もスタック経由で渡されることが多いです。
- https://go.dev/src/cmd/compile/internal/ssa/gen/ARM.go (ARMアーキテクチャのコード生成における引数と戻り値の扱いに関するヒント)
- https://go.dev/src/cmd/compile/internal/ssa/gen/AMDU64.go (AMD64アーキテクチャのコード生成における引数と戻り値の扱いに関するヒント)
- Goの呼び出し規約は公式ドキュメントには詳細に記述されていませんが、Goのソースコード(特に
- Plan 9 Assembly
TEXT
directive:- https://go.dev/src/cmd/asm/doc.go (Goのアセンブラのドキュメント)
- https://go.dev/src/cmd/ld/textflag.h (
NOSPLIT
などのフラグ定義)
- Dmitriy Vyukov's contributions to Go:
- Dmitriy VyukovはGoの並行処理とランタイムの分野で多くの重要な貢献をしている開発者です。彼の他のコミットや論文(例: Go Memory Model)も参照すると、Goの低レベルな挙動に関する理解が深まります。
- https://research.google.com/pubs/pub41891/ (Go Memory Model by Dmitriy Vyukov)
- https://github.com/dvyukov (GitHubプロフィール)
- Brad Fitzpatrick's contributions to Go:
- Brad FitzpatrickもGoの初期からの主要な貢献者の一人であり、特にネットワークや標準ライブラリに多くの貢献をしています。彼のレビューはGoのコード品質を保証する上で重要です。
- https://github.com/bradfitz (GitHubプロフィール)
- Goのメーリングリスト (golang-dev):
- https://groups.google.com/g/golang-dev (過去の議論を検索すると、このコミットに関する詳細な背景が見つかる可能性があります)
- Goのソースコード:
- Goのソースコード自体が最も正確で詳細な情報源です。特に
cmd/
ディレクトリ内のコンパイラ、リンカ、アセンブラのコードは、TEXT
ディレクティブやスタックフレームの処理に関する深い洞察を提供します。 src/runtime/
ディレクトリ内のコードは、ガベージコレクタやスケジューラなど、Goランタイムの内部動作を理解するのに役立ちます。src/pkg/sync/atomic/
ディレクトリ内のアセンブリファイルは、このコミットの直接の対象であり、アトミック操作の低レベルな実装を理解するのに役立ちます。
- Goのソースコード自体が最も正確で詳細な情報源です。特に
- Goのリリースノート:
- Goの各バージョンのリリースノートには、重要な変更点や修正されたバグに関する情報が含まれています。このコミットがGoのどのバージョンでリリースされたかを確認すると、その時点でのGoの進化の文脈を理解できます。
- https://go.dev/doc/devel/release (Goのリリース履歴)
- Goのドキュメント:
- https://go.dev/doc/ (Goの公式ドキュメントは、Goの基本的な概念から高度なトピックまでをカバーしています)
- Goのツールチェーンに関する書籍や記事:
- Goのコンパイラ、リンカ、アセンブラの内部動作について解説している専門的な書籍やブログ記事も、理解を深めるのに役立ちます。