[インデックス 16784] ファイルの概要
このコミットは、Go言語のsyscall
パッケージ内のアセンブリ関数における引数サイズの記録方法を改善し、特にNetBSDおよびOpenBSD環境でのSyscall9
関数のバグを修正するものです。Goランタイムがスタックトレースや引数フレーム情報をより正確に扱うための基盤を強化することを目的としています。
コミット
commit e69082ffdb2a3a63ce26f69e393fec749a041bd2
Author: Russ Cox <rsc@golang.org>
Date: Tue Jul 16 16:23:53 2013 -0400
syscall: record argument size for all assembly functions
While we're here, fix Syscall9 on NetBSD and OpenBSD:
it was storing the results into the wrong memory locations.
I guess no one uses that function's results on those systems.
Part of cleaning up stack traces and argument frame information.
R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/11355044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e69082ffdb2a3a63ce26f69e393fec749a041bd2
元コミット内容
syscall: record argument size for all assembly functions
While we're here, fix Syscall9 on NetBSD and OpenBSD:
it was storing the results into the wrong memory locations.
I guess no one uses that function's results on those systems.
Part of cleaning up stack traces and argument frame information.
変更の背景
このコミットの主な背景には、Goランタイムがシステムコールを呼び出すアセンブリ関数において、より正確なスタックトレースと引数フレーム情報を取得できるようにするという目的があります。
Go言語のランタイムは、ガベージコレクション、スケジューリング、スタック管理など、様々な低レベルな処理を効率的に行うために、アセンブリコードを多用しています。特にシステムコール(syscall
)は、OSの機能にアクセスするための重要なインターフェースであり、GoプログラムからOSのサービスを利用する際に必須となります。
システムコールを呼び出すアセンブリ関数では、引数がスタック上にどのように配置されるか、そして関数がどれだけのスタック領域を使用するかが正確に定義されている必要があります。これは、デバッグ時やプロファイリング時に正確なスタックトレースを生成するため、またガベージコレクタがスタック上のポインタを正しく識別するために不可欠です。
以前のGoランタイムでは、一部のアセンブリ関数で引数サイズが明示的に記録されていなかったか、あるいは不正確であった可能性があります。これにより、特にクロスプラットフォーム環境(異なるOSやアーキテクチャ)で、スタックトレースが不完全になったり、ガベージコレクションが誤動作したりするリスクがありました。
また、NetBSDおよびOpenBSDにおけるSyscall9
関数の特定のバグは、システムコールの結果が誤ったメモリ位置に格納されるというものでした。これは、これらのシステムにおけるSyscall9
の利用頻度が低かったため、これまで見過ごされてきた可能性があります。このバグは、スタックフレームの管理が不適切であったことに起因すると考えられます。
このコミットは、これらの問題を解決し、Goランタイムの堅牢性とデバッグ可能性を向上させるための重要なステップでした。
前提知識の解説
このコミットを理解するためには、以下の概念について理解しておく必要があります。
-
Goアセンブリ (Plan 9 Assembly): Go言語は、独自のPlan 9アセンブリ言語を使用しています。これは一般的なx86アセンブリとは異なる構文を持ち、Goのツールチェインと密接に統合されています。Goアセンブリでは、関数は
TEXT
ディレクティブで定義され、その後にシンボル名、フラグ、そしてスタックフレームサイズが記述されます。 例:TEXT ·Syscall(SB),7,$0-32
TEXT
: 関数定義を示すディレクティブ。·Syscall(SB)
: 関数シンボル。·
はパッケージローカルなシンボルであることを示し、SB
は静的ベースポインタ(Static Base pointer)で、グローバルシンボルや関数への参照に使われます。7
: フラグ。この場合、NOSPLIT
フラグ(値は4)とRODATA
フラグ(値は2)の組み合わせ(4+2=6)に加えて、ABIInternal
フラグ(値は1)が設定されている可能性があります。NOSPLIT
は、この関数がスタックの拡張を必要としないことを示します。$0-32
: スタックフレームサイズと引数サイズ。$0
: この関数自身のスタックフレームサイズ(ローカル変数などが使用する領域)。この場合は0バイト。-32
: 引数サイズ。この関数が呼び出し元から受け取る引数の合計サイズをバイト単位で示します。負の値で表現されるのは、Goアセンブリの慣習です。この値が正確であることは、スタックトレースの生成やガベージコレクションにおいて非常に重要です。
-
システムコール (Syscall): システムコールは、ユーザー空間のプログラムがオペレーティングシステムカーネルのサービスを要求するためのメカニズムです。ファイルI/O、ネットワーク通信、メモリ管理、プロセス制御など、OSが提供するほとんどの機能はシステムコールを通じて利用されます。Go言語では、
syscall
パッケージを通じてこれらのOS機能にアクセスします。 -
スタックトレース (Stack Trace): プログラムの実行中にエラーやパニックが発生した際に、その時点での関数呼び出しの履歴(コールスタック)を順に表示したものです。各関数呼び出しの際に、その関数がどこから呼び出されたか(リターンアドレス)や、その関数に渡された引数、ローカル変数などがスタック上に積まれます。正確なスタックトレースは、デバッグにおいて問題の原因を特定するために不可欠です。
-
引数フレーム情報 (Argument Frame Information): 関数が呼び出された際に、その関数に渡される引数がメモリ(通常はスタック)上にどのように配置されるかに関する情報です。Goランタイムは、この情報を使用して、スタック上の引数を正確に識別し、ガベージコレクタがポインタをスキャンしたり、デバッガが引数の値を表示したりできるようにします。
-
ガベージコレクション (Garbage Collection): Go言語のランタイムが自動的にメモリを管理する仕組みです。不要になったメモリ領域を自動的に解放し、メモリリークを防ぎます。ガベージコレクタは、プログラムが使用しているすべてのポインタを正確に追跡する必要があります。これには、スタック上に存在するポインタも含まれます。引数フレーム情報が不正確だと、ガベージコレクタがスタック上のポインタを誤って解釈し、メモリが早期に解放されたり、逆に解放されずにリークしたりする可能性があります。
-
クロスプラットフォーム開発: Go言語は、様々なオペレーティングシステム(Linux, macOS, Windows, FreeBSD, NetBSD, OpenBSD, Plan 9など)やCPUアーキテクチャ(x86, AMD64, ARMなど)をサポートしています。システムコールの実装はOSやアーキテクチャによって異なるため、
syscall
パッケージは各プラットフォーム向けに個別のアセンブリコードを持っています。
技術的詳細
このコミットの核心は、Goアセンブリ関数定義におけるスタックフレームサイズの指定方法の変更にあります。具体的には、TEXT
ディレクティブの$framesize-argsize
という形式において、argsize
(引数サイズ)をすべてのsyscall
関連アセンブリ関数で明示的に、かつ正確に記録するように修正しています。
以前は、多くのアセンブリ関数で$0
のように引数サイズが省略されていたか、あるいは不正確な値が設定されていた可能性があります。$0
は、関数自身のスタックフレームサイズが0バイトであることを示しますが、引数サイズについては明示していません。Goのツールチェインは、この情報を使用して、スタックトレースの生成やガベージコレクションのためのスタックスキャンを行います。
TEXT func(SB),7,$0-N
という形式でN
を明示的に指定することで、その関数が呼び出し元から受け取る引数の合計バイト数を正確にランタイムに伝えます。例えば、TEXT ·Syscall(SB),7,$0-32
は、Syscall
関数が自身のローカル変数に0バイトを使用し、呼び出し元から32バイトの引数を受け取ることを意味します。
この変更がもたらす技術的な利点は以下の通りです。
-
正確なスタックトレース: デバッガやプロファイラがスタックトレースを生成する際、各関数のスタックフレームの開始位置とサイズを正確に知る必要があります。引数サイズが正確に記録されることで、関数呼び出しの境界が明確になり、スタックトレースがより信頼性の高いものになります。これにより、デバッグ時の問題特定が容易になります。
-
堅牢なガベージコレクション: Goのガベージコレクタは、スタックをスキャンして到達可能なポインタを識別します。引数の中にはポインタが含まれる可能性があるため、ガベージコレクタは引数領域を正確に認識し、その中のポインタを追跡する必要があります。引数サイズが不明確だと、ガベージコレクタが誤ってポインタではないメモリをポインタとして解釈したり、逆にポインタを見落としたりするリスクがあります。この修正により、ガベージコレクタのスタックスキャンがより正確になり、メモリ管理の堅牢性が向上します。
-
クロスプラットフォームの一貫性: Goは多様なプラットフォームをサポートしており、各プラットフォームのシステムコールインターフェースは異なります。このコミットは、Darwin (macOS), FreeBSD, Linux, NetBSD, OpenBSD, Plan 9といった複数のOSおよび386, AMD64, ARMといった複数のアーキテクチャ向けのアセンブリファイルにわたって変更を加えています。これにより、異なるプラットフォーム間でのスタックフレーム情報の扱いに一貫性がもたらされ、移植性と信頼性が向上します。
-
NetBSDおよびOpenBSDにおける
Syscall9
のバグ修正: このコミットの副次的な、しかし重要な修正として、NetBSDおよびOpenBSDのAMD64アーキテクチャにおけるSyscall9
関数のバグが修正されました。元の実装では、Syscall9
の戻り値(r1
,r2
,err
)がスタック上の誤ったメモリ位置に格納されていました。 具体的には、Syscall9
は9つの引数を取り、3つの戻り値を返します。AMD64アーキテクチャでは、引数はレジスタとスタックを組み合わせて渡され、戻り値もレジスタ(AX, DX)とスタックに格納されます。このバグは、戻り値をスタックに格納する際のオフセット計算が誤っていたために発生しました。 修正前は、MOVQ $-1, 64(SP)
のように、スタックポインタSP
からのオフセットが64
バイトの位置にr1
を格納しようとしていました。しかし、正しいオフセットは88
バイトでした。これは、Syscall9
が受け取る引数のサイズと、その後のスタックフレームのレイアウトを考慮すると、戻り値が格納されるべき位置がずれていたためです。 この修正により、MOVQ $-1, 88(SP)
のようにオフセットが調整され、戻り値が正しいメモリ位置に格納されるようになりました。これにより、Syscall9
の戻り値が正しく利用できるようになり、これらのOS上でのGoプログラムの信頼性が向上しました。
これらの変更は、Goランタイムの低レベルな部分における正確性と堅牢性を高めるための重要な改善であり、Goプログラム全体の安定性とデバッグ体験に寄与します。
コアとなるコードの変更箇所
このコミットでは、src/pkg/syscall/
ディレクトリ以下の、各OSおよびアーキテクチャ向けのアセンブリファイルが変更されています。主な変更は、TEXT
ディレクティブにおける関数のスタックフレームサイズ指定です。
変更前:
TEXT ·FunctionName(SB),7,$0
変更後:
TEXT ·FunctionName(SB),7,$0-N
ここでN
は、その関数が受け取る引数の合計バイト数を示します。
具体的な変更例(src/pkg/syscall/asm_darwin_386.s
より抜粋):
--- a/src/pkg/syscall/asm_darwin_386.s
+++ b/src/pkg/syscall/asm_darwin_386.s
@@ -10,7 +10,7 @@
// func Syscall(trap int32, a1, a2, a3 int32) (r1, r2, err int32);
// Trap # in AX, args on stack above caller pc.
-TEXT ·Syscall(SB),7,$0
+TEXT ·Syscall(SB),7,$0-32
CALL runtime·entersyscall(SB)
MOVL 4(SP), AX // syscall entry
// slide args down on top of system call number
@@ -34,7 +34,7 @@ ok:
CALL runtime·exitsyscall(SB)
RET
-TEXT ·Syscall6(SB),7,$0
+TEXT ·Syscall6(SB),7,$0-44
CALL runtime·entersyscall(SB)
MOVL 4(SP), AX // syscall entry
// slide args down on top of system call number
@@ -61,7 +61,7 @@ ok6:
CALL runtime·exitsyscall(SB)
RET
-TEXT ·Syscall9(SB),7,$0
+TEXT ·Syscall9(SB),7,$0-56
CALL runtime·entersyscall(SB)
MOVL 4(SP), AX // syscall entry
// slide args down on top of system call number
@@ -91,7 +91,7 @@ ok9:
CALL runtime·exitsyscall(SB)
RET
-TEXT ·RawSyscall(SB),7,$0
+TEXT ·RawSyscall(SB),7,$0-32
MOVL 4(SP), AX // syscall entry
// slide args down on top of system call number
LEAL 8(SP), SI
@@ -112,7 +112,7 @@ ok1:
MOVL $0, 28(SP) // errno
RET
-TEXT ·RawSyscall6(SB),7,$0
+TEXT ·RawSyscall6(SB),7,$0-44
MOVL 4(SP), AX // syscall entry
// slide args down on top of system call number
LEAL 8(SP), SI
NetBSDおよびOpenBSDのAMD64アーキテクチャにおけるSyscall9
のバグ修正箇所(src/pkg/syscall/asm_netbsd_amd64.s
およびsrc/pkg/syscall/asm_openbsd_amd64.s
より抜粋):
--- a/src/pkg/syscall/asm_netbsd_amd64.s
+++ b/src/pkg/syscall/asm_netbsd_amd64.s
@@ -76,20 +76,20 @@ TEXT ·Syscall9(SB),7,$0
SYSCALL
JCC ok9
ADDQ $32, SP
- MOVQ $-1, 64(SP) // r1
- MOVQ $0, 72(SP) // r2
- MOVQ AX, 80(SP) // errno
+ MOVQ $-1, 88(SP) // r1
+ MOVQ $0, 96(SP) // r2
+ MOVQ AX, 104(SP) // errno
CALL runtime·exitsyscall(SB)
RET
ok9:
ADDQ $32, SP
- MOVQ AX, 64(SP) // r1
- MOVQ DX, 72(SP) // r2
- MOVQ $0, 80(SP) // errno
+ MOVQ AX, 88(SP) // r1
+ MOVQ DX, 96(SP) // r2
+ MOVQ $0, 104(SP) // errno
CALL runtime·exitsyscall(SB)
RET
-TEXT ·RawSyscall(SB),7,$0
+TEXT ·RawSyscall(SB),7,$0-64
MOVQ 16(SP), DI
MOVQ 24(SP), SI
MOVQ 32(SP), DX
コアとなるコードの解説
引数サイズの記録 ($0-N
)
GoアセンブリにおけるTEXT
ディレクティブの$framesize-argsize
形式は、Goランタイムが関数呼び出しのスタックフレームを管理するために非常に重要です。
$framesize
: その関数自身のローカル変数や一時的なデータが使用するスタック領域のサイズ(バイト単位)です。このコミットでは、ほとんどのsyscall
アセンブリ関数でこの値は0
のままです。これは、これらの関数が主にレジスタを使用して処理を行い、自身のスタックフレームをほとんど必要としないためです。-argsize
: その関数が呼び出し元から受け取る引数の合計サイズ(バイト単位)です。この値が負数で表現されるのは、Goアセンブリの慣習です。このargsize
を正確に指定することが、このコミットの主要な目的です。
例えば、TEXT ·Syscall(SB),7,$0-32
という変更は、Syscall
関数が32バイトの引数を受け取ることをランタイムに明示的に伝えます。Goの関数呼び出し規約では、引数はスタック上に積まれるか、レジスタを通じて渡されます。このargsize
は、スタック上に積まれる引数の合計サイズを正確に反映している必要があります。
この情報が正確であることで、Goランタイムは以下の処理を適切に行うことができます。
-
スタックトレースの生成: Goのデバッガやプロファイラは、実行中のプログラムのスタックを遡って、どの関数がどの関数を呼び出したかを特定します。各関数のスタックフレームの正確なサイズ(引数領域を含む)を知ることで、次の呼び出し元のスタックフレームの開始位置を正確に計算し、完全で正確なスタックトレースを生成できます。
-
ガベージコレクションのスタックスキャン: Goのガベージコレクタは、プログラムが使用しているすべてのメモリをスキャンし、到達可能なオブジェクトをマークします。これには、スタック上に存在するポインタも含まれます。ガベージコレクタは、各関数のスタックフレーム内で、どの領域が引数であり、その中にポインタが含まれる可能性があるかを正確に知る必要があります。
argsize
が正確であれば、ガベージコレクタはスタック上のポインタを確実に識別し、誤ったメモリ解放やメモリリークを防ぐことができます。
NetBSDおよびOpenBSDにおけるSyscall9
のバグ修正
NetBSDおよびOpenBSDのAMD64アーキテクチャにおけるSyscall9
関数の修正は、スタック上の戻り値の格納位置のオフセットを修正するものです。
Syscall9
は、9つの引数を取り、3つの戻り値(r1
, r2
, err
)を返すシステムコールラッパーです。AMD64のGo呼び出し規約では、最初のいくつかの引数はレジスタ(DI, SI, DX, CX, R8, R9)で渡され、残りの引数と戻り値はスタックに格納されます。
元のコードでは、Syscall9
の戻り値をスタックに格納する際に、SP
(スタックポインタ)からのオフセットが誤っていました。
MOVQ $-1, 64(SP) // r1
MOVQ $0, 72(SP) // r2
MOVQ AX, 80(SP) // errno
このオフセット64
, 72
, 80
は、Syscall9
が受け取る引数のサイズと、runtime·entersyscall
などの呼び出しによってスタックがどのように変化するかを考慮すると、正しくありませんでした。
修正後のコードでは、オフセットが88
, 96
, 104
に調整されています。
MOVQ $-1, 88(SP) // r1
MOVQ $0, 96(SP) // r2
MOVQ AX, 104(SP) // errno
この変更により、Syscall9
の戻り値がスタック上の正しいメモリ位置に格納されるようになり、GoプログラムがこれらのシステムでSyscall9
の戻り値を正しく利用できるようになりました。コミットメッセージにある「I guess no one uses that function's results on those systems.」というコメントは、このバグが長期間見過ごされてきたことを示唆しています。
これらの変更は、Goランタイムの低レベルな部分における正確性と堅牢性を高め、特にクロスプラットフォーム環境での安定性を向上させるための重要な改善です。
関連リンク
- Go言語のシステムコールパッケージ: https://pkg.go.dev/syscall
- Goアセンブリのドキュメント (Go 1.20以降): https://go.dev/doc/asm (このコミットがなされた2013年当時のドキュメントとは異なる可能性がありますが、基本的な概念は共通です。)
- Goのスタック管理に関する議論 (Go開発者ブログなど): Goのスタック管理は進化しており、このコミット当時の情報を見つけるのは難しいかもしれませんが、一般的な概念はGoの公式ブログや設計ドキュメントで触れられています。
参考にした情報源リンク
- Goのコミット履歴: https://github.com/golang/go/commits/master
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている
https://golang.org/cl/11355044
は、このGerritの変更リストへのリンクです。) - Goアセンブリの構文とセマンティクスに関する一般的な情報源 (例: Goのソースコード内の
src/cmd/asm/doc.go
や、Goアセンブリに関するチュートリアルやブログ記事) - オペレーティングシステムのシステムコールに関する一般的な知識 (例: Linux man pages, BSD man pages)
- x86-64 (AMD64) アーキテクチャの呼び出し規約に関する一般的な知識