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

[インデックス 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は、その関数が受け取る引数の合計バイト数です。例えば、CompareAndSwapUint32addr, old, newの3つのuint32引数と、戻り値のswappedbool)を受け取ります。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 Assembly Language (Plan 9 Assembly):
  • Go Garbage Collection:
  • Go sync/atomic package documentation:
  • Stack frames in Go:
  • Discussion on Go Issue #6098 (for context on the problem):
  • 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.gocmd/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アーキテクチャのコード生成における引数と戻り値の扱いに関するヒント)
  • Plan 9 Assembly TEXT directive:
  • 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):
  • Goのソースコード:
    • Goのソースコード自体が最も正確で詳細な情報源です。特にcmd/ディレクトリ内のコンパイラ、リンカ、アセンブラのコードは、TEXTディレクティブやスタックフレームの処理に関する深い洞察を提供します。
    • src/runtime/ディレクトリ内のコードは、ガベージコレクタやスケジューラなど、Goランタイムの内部動作を理解するのに役立ちます。
    • src/pkg/sync/atomic/ディレクトリ内のアセンブリファイルは、このコミットの直接の対象であり、アトミック操作の低レベルな実装を理解するのに役立ちます。
  • 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は、その関数が受け取る引数の合計バイト数です。例えば、CompareAndSwapUint32addr, old, newの3つのuint32引数と、戻り値のswappedbool)を受け取ります。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 Assembly Language (Plan 9 Assembly):
  • Go Garbage Collection:
  • Go sync/atomic package documentation:
  • Stack frames in Go:
  • Discussion on Go Issue #6098 (for context on the problem):
  • 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.gocmd/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アーキテクチャのコード生成における引数と戻り値の扱いに関するヒント)
  • Plan 9 Assembly TEXT directive:
  • 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):
  • Goのソースコード:
    • Goのソースコード自体が最も正確で詳細な情報源です。特にcmd/ディレクトリ内のコンパイラ、リンカ、アセンブラのコードは、TEXTディレクティブやスタックフレームの処理に関する深い洞察を提供します。
    • src/runtime/ディレクトリ内のコードは、ガベージコレクタやスケジューラなど、Goランタイムの内部動作を理解するのに役立ちます。
    • src/pkg/sync/atomic/ディレクトリ内のアセンブリファイルは、このコミットの直接の対象であり、アトミック操作の低レベルな実装を理解するのに役立ちます。
  • Goのリリースノート:
    • Goの各バージョンのリリースノートには、重要な変更点や修正されたバグに関する情報が含まれています。このコミットがGoのどのバージョンでリリースされたかを確認すると、その時点でのGoの進化の文脈を理解できます。
    • https://go.dev/doc/devel/release (Goのリリース履歴)
  • Goのドキュメント:
    • https://go.dev/doc/ (Goの公式ドキュメントは、Goの基本的な概念から高度なトピックまでをカバーしています)
  • Goのツールチェーンに関する書籍や記事:
    • Goのコンパイラ、リンカ、アセンブラの内部動作について解説している専門的な書籍やブログ記事も、理解を深めるのに役立ちます。