[インデックス 15653] ファイルの概要
このコミットは、Go言語のランタイムにおけるPlan 9オペレーティングシステム向けのerrstr
関数の修正に関するものです。具体的には、C言語で実装されたruntime.findnull()
関数を呼び出す際の引数渡し規約の不一致を解消し、スタックポインタ(SP)のオフセット0(SP)
に引数が正しく配置されるように修正しています。
コミット
commit ef7705f6dd1dacdc3d3cf97893dd942b37b61744
Author: Akshat Kumar <seed@mail.nanosouffle.net>
Date: Sat Mar 9 05:39:15 2013 +0100
runtime: Plan 9: fix errstr
The call to the C function runtime.findnull() requires
that we provide the argument at 0(SP).
R=rsc, rminnich, ality
CC=golang-dev
https://golang.org/cl/7559047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ef7705f6dd1dacdc3d3cf97893dd942b37b61744
元コミット内容
Goランタイムのerrstr
関数において、C言語で実装されたruntime.findnull()
関数を呼び出す際に、引数が0(SP)
(スタックポインタの最上位)に配置される必要があるという要件を満たしていなかった問題を修正します。
変更の背景
Go言語は、様々なオペレーティングシステムやアーキテクチャをサポートするように設計されています。その中には、ベル研究所で開発された分散オペレーティングシステムであるPlan 9も含まれます。Goランタイムは、OSの機能を利用するために、C言語で書かれた低レベルの関数(システムコールラッパーやユーティリティ関数など)を呼び出すことがあります。
このコミットの背景には、GoランタイムがPlan 9上でruntime.findnull()
というC関数を呼び出す際の、**呼び出し規約(Calling Convention)**の不一致がありました。呼び出し規約とは、関数が呼び出される際に、引数をどのように渡し、戻り値をどのように受け取るか、レジスタをどのように保存・復元するかといった、関数呼び出しに関する取り決めです。
特に、アセンブリ言語で書かれたコードがC言語の関数を呼び出す場合、両者の呼び出し規約が厳密に一致している必要があります。Goのランタイムコードの一部はパフォーマンスや低レベルな操作のためにアセンブリ言語で書かれており、これがC関数とのインターフェースで問題を引き起こすことがありました。
runtime.findnull()
は、おそらく文字列の終端(ヌル文字)を見つけるためのユーティリティ関数であり、errstr
(エラー文字列を取得する関数)の処理の一部として使用されていたと考えられます。この関数が期待する引数の位置(0(SP)
)と、Goランタイムのアセンブリコードが実際に引数を配置していた位置が異なっていたため、正しく動作しない、あるいは未定義の動作を引き起こす可能性がありました。
また、コメントにある// syscall requires caller-save
は、システムコールを行う際に特定のレジスタ(この場合はCX
)が呼び出し元によって保存される必要があることを示唆しています。これは、システムコールがこれらのレジスタを破壊する可能性があるため、呼び出し元がその値を保持したい場合に、事前にスタックに退避させる必要があるという一般的なプログラミングの慣習です。
前提知識の解説
- Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルな部分です。ガベージコレクション、スケジューラ、システムコールインターフェースなどが含まれます。Goプログラムは、OSと直接やり取りするのではなく、ランタイムを介してOSサービスを利用します。
- Plan 9 from Bell Labs: ベル研究所で開発された分散オペレーティングシステムです。Go言語の開発者の一部はPlan 9の開発にも携わっており、Go言語の設計思想にはPlan 9の影響が見られます。Goは当初からPlan 9を含む複数のOSをサポートしていました。
- アセンブリ言語 (Assembly Language): コンピュータのプロセッサが直接実行できる機械語命令を、人間が読みやすいニーモニックで記述した低レベル言語です。Goランタイムの一部は、パフォーマンスの最適化やOSとの直接的なやり取りのためにアセンブリ言語で書かれています。
- 呼び出し規約 (Calling Convention): 関数呼び出しにおいて、引数の渡し方(レジスタ、スタック)、戻り値の受け取り方、スタックの管理、レジスタの保存・復元などのルールを定めたものです。異なる言語やコンパイラ、OS、アーキテクチャ間で呼び出し規約が異なると、関数呼び出しが正しく行われません。
- スタック (Stack): プログラム実行中に一時的なデータを格納するためのメモリ領域です。関数呼び出しの際には、引数、ローカル変数、戻りアドレスなどがスタックに積まれます(プッシュ)。関数から戻る際には、これらのデータがスタックから取り除かれます(ポップ)。
- スタックポインタ (SP): スタックの現在の最上位(または最下位、アーキテクチャによる)を指すレジスタです。
0(SP)
: スタックポインタが指すアドレス(スタックの最上位)から0バイトオフセットした位置を意味します。これは、関数に渡される最初の引数や、関数が使用する一時的な領域の開始点として使われることがあります。4(SP)
/8(SP)
: スタックポインタから4バイトまたは8バイトオフセットした位置を意味します。これは、スタック上の他の引数やローカル変数にアクセスするために使用されます。32ビットシステムでは4バイト、64ビットシステムでは8バイトが一般的なワードサイズです。
- レジスタ (Registers): CPU内部にある高速な記憶領域です。計算やデータ転送に頻繁に使用されます。
AX
/EAX
/RAX
: 汎用レジスタ。通常、関数の戻り値を格納するために使用されます。CX
/ECX
/RCX
: 汎用レジスタ。特定の操作や、呼び出し規約によっては引数として使用されます。BP
/EBP
/RBP
: ベースポインタレジスタ。スタックフレームの基準点として使用されることがあります。
PUSHL
/PUSHQ
: スタックに値をプッシュするアセンブリ命令です。L
はLong (32ビット)、Q
はQuad (64ビット) を意味します。POPL
/POPQ
: スタックから値をポップするアセンブリ命令です。MOVL
/MOVQ
: データを移動するアセンブリ命令です。CALL
: 関数を呼び出すアセンブリ命令です。RET
: 関数から戻るアセンブリ命令です。INT $64
(386) /SYSCALL
(amd64): システムコールを実行するための命令です。Plan 9では、INT $64
が386アーキテクチャで、SYSCALL
がAMD64アーキテクチャでシステムコールをトリガーします。TEXT runtime·errstr(SB),7,$0
: Goのアセンブリ言語における関数定義の構文です。TEXT
: 関数定義の開始。runtime·errstr
: 関数名。Goのアセンブリでは、パッケージ名と関数名が·
で区切られます。SB
はStatic Baseで、グローバルシンボルを参照するための基準点です。7
: Goのアセンブリにおけるフラグ。この場合は、NOSPLIT
フラグ(スタックの拡張を許可しない)とRODATA
フラグ(読み取り専用データ)の組み合わせかもしれません。$0
: 関数のスタックフレームサイズ(ローカル変数などに使用するスタック領域のサイズ)。この場合は0バイトです。
技術的詳細
このコミットは、GoランタイムがPlan 9上でC関数runtime.findnull()
を呼び出す際の引数渡しに関する問題を解決しています。
Goのランタイムは、OSの機能を利用するために、C言語で書かれた低レベルの関数を呼び出すことがあります。このようなクロス言語(GoアセンブリからC)の呼び出しでは、両者の呼び出し規約が一致していることが極めて重要です。
問題は、runtime.findnull()
が、その引数をスタックの最上位、すなわち0(SP)
に期待していたのに対し、Goのアセンブリコードがそのように引数を配置していなかった点にありました。
修正前は、runtime.errstr
関数内でシステムコール(INT $64
またはSYSCALL
)が実行された後、runtime.findnull()
が呼び出されていました。システムコールは、特定のレジスタ(特にCX
)の値を変更する可能性があります。コミットメッセージのコメント// syscall requires caller-save
が示すように、CX
レジスタは呼び出し元が保存すべきレジスタ(caller-saved register)であるため、システムコールによってその値が破壊される可能性があります。
修正では、以下の手順が追加されました。
-
CX
レジスタの退避:- 386アーキテクチャ (
sys_plan9_386.s
):MOVL 4(SP), CX
- AMD64アーキテクチャ (
sys_plan9_amd64.s
):MOVQ 8(SP), CX
これは、システムコールによって破壊される可能性のあるCX
レジスタの値を、スタック上の適切なオフセット(386では4(SP)
、AMD64では8(SP)
)から読み出してCX
にロードしています。この値がruntime.findnull()
に渡すべき引数であると考えられます。
- 386アーキテクチャ (
-
引数のスタックへのプッシュ:
- 386アーキテクチャ:
PUSHL CX
- AMD64アーキテクチャ:
PUSHQ CX
これにより、CX
レジスタに格納された引数がスタックの最上位(0(SP)
)にプッシュされます。これは、runtime.findnull()
が期待する引数の位置です。
- 386アーキテクチャ:
-
runtime.findnull()
の呼び出し:CALL runtime·findnull(SB)
引数が正しくスタックに配置された状態で、runtime.findnull()
が呼び出されます。
-
CX
レジスタの復元:- 386アーキテクチャ:
POPL CX
- AMD64アーキテクチャ:
POPQ CX
runtime.findnull()
の呼び出し後、スタックにプッシュした引数をポップしてCX
レジスタを元の状態に戻します。これは、スタックのバランスを保ち、後続のコードが正しいスタックポインタを参照できるようにするために重要です。
- 386アーキテクチャ:
この修正により、runtime.findnull()
が期待する呼び出し規約(引数が0(SP)
にあること)が満たされ、errstr
関数がPlan 9上で正しく動作するようになりました。これは、低レベルなアセンブリコードとC言語のインターフェースにおける厳密な呼び出し規約の遵守がいかに重要であるかを示す典型的な例です。
コアとなるコードの変更箇所
src/pkg/runtime/sys_plan9_386.s
--- a/src/pkg/runtime/sys_plan9_386.s
+++ b/src/pkg/runtime/sys_plan9_386.s
@@ -187,6 +187,13 @@ TEXT runtime·errstr(SB),7,$0
MOVL $ERRMAX, 8(SP)
MOVL $41, AX
INT $64
+
+ // syscall requires caller-save
+ MOVL 4(SP), CX
+
+ // push the argument
+ PUSHL CX
CALL runtime·findnull(SB)
+ POPL CX
MOVL AX, 8(SP)
RET
src/pkg/runtime/sys_plan9_amd64.s
--- a/src/pkg/runtime/sys_plan9_amd64.s
+++ b/src/pkg/runtime/sys_plan9_amd64.s
@@ -224,6 +224,13 @@ TEXT runtime·errstr(SB),7,$0
MOVQ $0x8000, AX
MOVQ $41, BP
SYSCALL
+
+ // syscall requires caller-save
+ MOVQ 8(SP), CX
+
+ // push the argument
+ PUSHQ CX
CALL runtime·findnull(SB)
+ POPQ CX
MOVQ AX, 16(SP)
RET
コアとなるコードの解説
両方のファイル(386とAMD64アーキテクチャ向け)で同様の変更が行われています。
-
// syscall requires caller-save
: このコメントは、直前のシステムコール(INT $64
またはSYSCALL
)が、CX
レジスタのような「呼び出し元が保存すべき」レジスタの値を破壊する可能性があることを示しています。そのため、CX
レジスタの値をruntime.findnull
に渡す引数として使用する前に、その値をスタックから読み出す必要があります。 -
MOVL 4(SP), CX
(386) /MOVQ 8(SP), CX
(AMD64): これは、スタック上の特定のオフセット(386では4(SP)
、AMD64では8(SP)
)に格納されている値をCX
レジスタに移動しています。この値が、runtime.findnull()
に渡すべき引数(おそらくエラー文字列へのポインタ)です。システムコールによってCX
が破壊された場合でも、スタックに保存されている元の値を取得できます。 -
// push the argument
: このコメントは、引数をスタックにプッシュする意図を示しています。 -
PUSHL CX
(386) /PUSHQ CX
(AMD64):CX
レジスタに格納されている引数の値をスタックにプッシュします。これにより、引数はスタックの最上位、すなわち0(SP)
に配置されます。これは、C関数runtime.findnull()
が引数を期待する位置です。 -
CALL runtime·findnull(SB)
: 引数が正しくスタックに配置された状態で、runtime.findnull()
関数が呼び出されます。 -
POPL CX
(386) /POPQ CX
(AMD64):runtime.findnull()
の呼び出し後、スタックにプッシュした引数をポップしてCX
レジスタを元の状態に戻します。これは、スタックのバランスを保ち、スタックポインタを元の位置に戻すために必要です。これにより、runtime.errstr
関数の残りの部分が正しく実行され、最終的にRET
で呼び出し元に戻ることができます。
この一連の変更により、GoランタイムのアセンブリコードとC言語のruntime.findnull()
関数との間の呼び出し規約の不一致が解消され、Plan 9上でのエラー文字列処理が安定しました。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
- Plan 9 from Bell Labs: https://9p.io/plan9/
- Goのアセンブリ言語に関するドキュメント (Go 1.2以降の形式): https://go.dev/doc/asm (このコミットは2013年のものであり、当時のアセンブリ構文は現在のものと若干異なる可能性がありますが、基本的な概念は共通です。)
参考にした情報源リンク
- Go言語のソースコード (GitHub): https://github.com/golang/go
- Go CL (Change List) 7559047: https://golang.org/cl/7559047 (コミットメッセージに記載されているリンク)
- Goの呼び出し規約に関する一般的な情報 (Goのバージョンやアーキテクチャによって異なるため、一般的な概念の理解に役立つ情報源):
- "Go's execution model": https://go.dev/doc/articles/go_mem.html (直接呼び出し規約を説明するものではないが、ランタイムの動作を理解する上で役立つ)
- Goのコンパイラやランタイムの内部に関するブログ記事やプレゼンテーション (例: "Go's runtime and calling conventions"などで検索)
- x86/x64アセンブリ言語と呼び出し規約に関する一般的な情報 (例: System V AMD64 ABI, Microsoft x64 calling conventionなど)
- Plan 9のシステムコールに関するドキュメント (例: Plan 9 manual pages for system calls)
- C言語の呼び出し規約に関する一般的な情報 (例: cdecl, stdcall, fastcallなど)
- Goの
runtime
パッケージのソースコード (特にsys_plan9_386.s
,sys_plan9_amd64.s
,findnull.go
など)src/runtime/sys_plan9_386.s
(現在のGoリポジトリでのパス)src/runtime/sys_plan9_amd64.s
(現在のGoリポジトリでのパス)src/runtime/string.go
(現在のGoリポジトリでfindnull
が関連する可能性のあるファイル)src/runtime/error.go
(現在のGoリポジトリでerrstr
が関連する可能性のあるファイル)src/runtime/sys_plan9.go
(現在のGoリポジトリでPlan 9関連のシステムコールやユーティリティが定義されている可能性のあるファイル)