[インデックス 15641] ファイルの概要
このコミットは、Go言語のランタイムにおける386アーキテクチャの起動規約を変更するものです。具体的には、プログラムのエントリポイントを_rt0_386_$GOOS
($GOOS
はオペレーティングシステムを示す)とし、これが標準Cライブラリのmain(argc, argv)
関数と同様のセマンティクスを持つように変更されました。これにより、Goプログラムが標準Cライブラリとリンクする際の互換性が向上します。
コミット
commit dfc22e29ec2f687375d32c9f7662416d0c9f97d3
Author: Russ Cox <rsc@golang.org>
Date: Thu Mar 7 19:57:10 2013 -0800
runtime: change 386 startup convention
Now the default startup is that the program begins at _rt0_386_$GOOS,
which behaves as if calling main(argc, argv). Main jumps to _rt0_386.
This makes the _rt0_386 entry match the expected semantics for
the standard C "main" function, which we can now provide for use when
linking against a standard C library.
386 analogue of https://golang.org/cl/7525043
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/7551045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/dfc22e29ec2f687375d32c9f7662416d0c9f97d3
元コミット内容
このコミットは、Goランタイムの386アーキテクチャにおけるプログラム起動時の規約を変更することを目的としています。変更の核心は、プログラムの初期エントリポイントを_rt0_386_$GOOS
(ここで$GOOS
はターゲットOS、例えばdarwin
, linux
, windows
など)に設定し、このエントリポイントがC言語の標準的なmain(argc, argv)
関数の呼び出しと同様の振る舞いをするようにすることです。この_rt0_386_$GOOS
は、最終的にGoランタイムの主要な初期化ルーチンである_rt0_386
にジャンプします。
この変更の主な動機は、Goプログラムが標準Cライブラリとリンクする際に、_rt0_386
のエントリポイントがCのmain
関数の期待されるセマンティクスと一致するようにすることです。これにより、GoとCの相互運用性が向上し、特にCライブラリがプログラムの起動時に特定の引数(argc
とargv
)の配置を期待する場合に問題なく連携できるようになります。
このコミットは、以前に行われた同様の変更(https://golang.org/cl/7525043
)の386アーキテクチャ版であると明記されています。
変更の背景
Goプログラムは、その実行環境を完全に制御するために、独自のランタイムと起動プロセスを持っています。しかし、既存のCライブラリやシステムコールと連携する必要がある場合、Goの起動規約がCの標準的な規約と異なることが問題となることがあります。特に、Cのプログラムは通常、main
関数がargc
(引数の数)とargv
(引数の文字列配列)をスタックまたはレジスタ経由で受け取ることを期待します。
このコミット以前は、Goの386アーキテクチャにおける起動規約がCのそれと完全に一致していなかった可能性があります。これにより、GoプログラムがCライブラリを呼び出す際に、引数の渡し方やスタックの状態に関する不整合が生じ、予期せぬ動作やクラッシュを引き起こす可能性がありました。
この変更は、Goプログラムがより広範なCライブラリやシステム環境とシームレスに連携できるようにするためのものです。_rt0_386
のエントリポイントがCのmain
関数のセマンティクスに合わせられることで、GoプログラムはCライブラリが期待する環境で起動し、より安定した相互運用性を実現できます。これは、Goがシステムプログラミング言語としての地位を確立し、既存のシステムコンポーネントと共存していく上で重要なステップです。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
-
Goランタイム (Go Runtime): Goプログラムは、C/C++のような言語とは異なり、実行時にGoランタイムと呼ばれる独自の実行環境を必要とします。このランタイムは、ガベージコレクション、スケジューラ(ゴルーチンの管理)、メモリ管理、システムコールインターフェースなど、Goプログラムの実行に必要な多くの低レベルな機能を提供します。プログラムの起動時、OSから最初に制御が渡されるのは、Goランタイムの初期化コードです。
-
386アーキテクチャ (Intel 80386 Architecture): Intel 80386は、1985年にリリースされた32ビットのマイクロプロセッサです。このアーキテクチャは、x86命令セットの32ビット拡張であり、現代のx86-64(64ビット)アーキテクチャの基礎となっています。このコミットは、特に32ビット版のGoランタイム(
GOARCH=386
)に適用される変更です。32ビットアーキテクチャでは、レジスタのサイズやスタックの配置、呼び出し規約などが64ビットアーキテクチャとは異なります。 -
プログラムの起動規約 (Startup Convention): オペレーティングシステムがプログラムを起動する際、OSはプログラムの最初のエントリポイントに制御を渡します。このとき、プログラムにコマンドライン引数や環境変数などの情報が渡されます。これらの情報がどのようにスタックに配置されるか、あるいはどのレジスタに格納されるかといった取り決めが「起動規約」です。C言語では、通常
main(int argc, char *argv[])
という形式で引数が渡されます。 -
argc
とargv
:argc
(argument count): コマンドライン引数の数を表す整数です。プログラム名自体も1つの引数として数えられます。argv
(argument vector): コマンドライン引数の文字列へのポインタの配列です。argv[0]
は通常プログラム名、argv[1]
以降がユーザーが指定した引数になります。argv
配列の最後はNULLポインタで終端されます。
-
アセンブリ言語 (
.s
ファイル): Goのランタイムの一部は、パフォーマンスや低レベルなOSとの連携のためにアセンブリ言語で記述されています。Goのアセンブリ言語は、Plan 9アセンブラの構文に基づいています。TEXT
ディレクティブ: 関数の開始を宣言します。例:TEXT _rt0_386(SB),7,$0
SB
(Static Base): グローバルシンボルを参照するための擬似レジスタ。7
: フラグ(ここではスタックフレームのサイズに関する情報など)。$0
: スタックフレームのサイズ(ローカル変数などに使用される領域)。
MOVL
: 32ビットの値を移動する命令(Move Long)。LEAL
: アドレスをロードする命令(Load Effective Address Long)。ポインタの計算によく使われます。SUBL
: 減算命令(Subtract Long)。スタックポインタ(SP
)を減らすことでスタックフレームを確保します。ANDL
: 論理AND命令。スタックポインタを特定のバイト境界にアラインするために使われます。CALL
: 関数呼び出し命令。JMP
: 無条件ジャンプ命令。INT $3
: ソフトウェア割り込み命令。デバッグ目的でブレークポイントを設定するためによく使われます。INT $0x80
: Linux/Unix系システムにおけるシステムコールを呼び出すためのソフトウェア割り込み。
-
VDSO (Virtual Dynamic Shared Object): Linuxカーネルがユーザー空間に提供する、システムコールを高速化するためのメカニズムです。一部のシステムコール(例:
gettimeofday
)は、カーネルモードへの切り替えなしにユーザー空間から直接実行できるように、VDSOを通じて提供されます。runtime·linux_setup_vdso
関数は、このVDSOのセットアップに関連する処理を行います。 -
呼び出し規約 (Calling Convention): 関数が呼び出される際に、引数がどのように渡され(レジスタ経由かスタック経由か)、戻り値がどのように返され、スタックがどのようにクリーンアップされるかといった取り決めです。C言語の標準的な呼び出し規約とGoの呼び出し規約は異なる場合があります。
技術的詳細
このコミットの技術的な核心は、Goランタイムの386アーキテクチャにおける初期起動プロセスを、C言語の標準的なmain(argc, argv)
関数のセマンティクスに合わせることにあります。
従来のGoの386起動プロセスでは、OSから制御が渡された後、直接_rt0_386
にジャンプし、そこで引数(argc
, argv
)をスタックから取得していました。しかし、OSやCライブラリが期待するスタックのレイアウトや引数の渡し方と、Goランタイムが直接_rt0_386
で期待するそれとが完全に一致しない場合がありました。
この変更では、各OS(Darwin, FreeBSD, Linux, NetBSD, OpenBSD, Windows)の386アーキテクチャ向けに、新しいエントリポイント_rt0_386_$GOOS
が導入されました。例えば、Linuxの場合は_rt0_386_linux
です。
この新しいエントリポイントの役割は以下の通りです:
-
OSからの引数受け取り: OSがプログラムを起動する際にスタックに配置する
argc
とargv
を、まず_rt0_386_$GOOS
が受け取ります。- 多くのOSでは、スタックの特定のオフセットに
argc
とargv
が配置されます。例えば、8(SP)
からargc
、12(SP)
からargv
(の先頭アドレス)を取得しています。 - これらの値は、一時的にレジスタ(
AX
,BX
)に格納されます。
- 多くのOSでは、スタックの特定のオフセットに
-
C言語の
main
関数セマンティクスへの変換: 取得したargc
とargv
を、C言語のmain
関数が期待する形式でスタックにプッシュします。MOVL AX, 0(SP)
:argc
をスタックの先頭(0(SP)
)にプッシュします。MOVL BX, 4(SP)
:argv
をスタックの次の位置(4(SP)
)にプッシュします。- これにより、スタック上には
argv
、argc
の順で引数が配置され、これはC言語の呼び出し規約に合致します。
-
main
関数(Goランタイム内部のラッパー)の呼び出し: 準備された引数でmain(SB)
関数を呼び出します。- ここでいう
main(SB)
は、Goのユーザープログラムのmain
関数ではなく、Goランタイム内部で定義されたラッパー関数です。このラッパー関数は、C言語のmain
関数と同様のインターフェースを提供します。
- ここでいう
-
_rt0_386
へのジャンプ: このランタイム内部のmain
関数は、最終的にGoランタイムの主要な初期化ルーチンである_rt0_386
にジャンプします。_rt0_386
は、Goランタイムの初期化、スケジューラの起動、そして最終的にユーザープログラムのmain
関数への制御の移行を行います。_rt0_386
自体も変更されており、引数をスタックから直接取得するのではなく、呼び出し元(この場合はランタイム内部のmain
関数)から渡されることを期待するように変更されています。具体的には、MOVL argc+0(FP), AX
とMOVL argv+4(FP), BX
のように、フレームポインタ(FP
)からの相対アドレスで引数を取得しています。これは、関数呼び出し規約に従って引数がスタックフレームに配置されていることを前提としています。
この二段階の起動プロセスにより、GoランタイムはOSから受け取った引数をC言語のmain
関数が期待する形式に変換し、その上でGo独自の初期化プロセスに進むことができます。これにより、GoプログラムがCライブラリとリンクする際に、Cライブラリが期待するmain
関数のセマンティクス(特にargc
とargv
の配置)が満たされるようになり、互換性が大幅に向上します。
また、src/pkg/runtime/signal_linux_386.c
の変更は、runtime·linux_setup_vdso
関数の引数リストの型定義をより正確にするものです。以前はvoid *argv_list
として受け取り、内部でbyte **argv = &argv_list;
としてキャストしていましたが、直接byte **argv
として受け取るように変更されています。これは、引数の型安全性を高め、コードの意図をより明確にするための改善です。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルと、その中でのコアとなる変更箇所は以下の通りです。
-
src/pkg/runtime/asm_386.s
:_rt0_386
関数の引数取得方法が変更されました。- 変更前:
MOVL 0(SP), AX
(argc),LEAL 4(SP), BX
(argv) - 変更後:
MOVL argc+0(FP), AX
,MOVL argv+4(FP), BX
- これは、
_rt0_386
が直接OSから引数を受け取るのではなく、呼び出し元(新しいmain
ラッパー関数)からスタックフレーム経由で引数を受け取るようになったことを示します。
- 変更前:
-
src/pkg/runtime/rt0_darwin_386.s
,src/pkg/runtime/rt0_freebsd_386.s
,src/pkg/runtime/rt0_linux_386.s
,src/pkg/runtime/rt0_netbsd_386.s
,src/pkg/runtime/rt0_openbsd_386.s
,src/pkg/runtime/rt0_windows_386.s
:- 各OS固有の386起動ファイルに、新しいエントリポイント
_rt0_386_$GOOS
が追加されました。 - これらのエントリポイントは、OSから渡される
argc
とargv
をスタックから取得し、それをC言語のmain
関数が期待する形式でスタックにプッシュした後、Goランタイム内部のmain(SB)
関数を呼び出します。 - 既存の
_rt0_386_$GOOS
(変更前は直接_rt0_386
にジャンプしていた)は、main(SB)
という新しい関数にリネームされ、そのmain(SB)
が_rt0_386
にジャンプするように変更されました。 _rt0_386_linux.s
では、runtime·linux_setup_vdso(SB)
の呼び出しがmain(SB)
の呼び出しの前に移動しています。
- 各OS固有の386起動ファイルに、新しいエントリポイント
-
src/pkg/runtime/rt0_plan9_386.s
:- Plan 9固有の起動ファイルにも、
_rt0_386
へのジャンプ前にargc
とargv
をスタックにプッシュする処理が追加されました。これは他のOSとは異なる方法で引数を準備しています。
- Plan 9固有の起動ファイルにも、
-
src/pkg/runtime/signal_linux_386.c
:runtime·linux_setup_vdso
関数の引数リストの型定義がint32 argc, void *argv_list
からint32 argc, byte **argv
に変更されました。- これにより、関数内部での
byte **argv = &argv_list;
というキャストが不要になりました。
コアとなるコードの解説
各OSのrt0_*.s
ファイルにおける変更は、Goプログラムの起動シーケンスを標準Cのmain
関数呼び出し規約に合わせるためのものです。
例として、src/pkg/runtime/rt0_darwin_386.s
の変更を見てみましょう。
変更前:
TEXT _rt0_386_darwin(SB),7,$0
JMP _rt0_386(SB)
これは、OSが_rt0_386_darwin
に制御を渡すと、すぐにGoランタイムの主要な初期化ルーチンである_rt0_386
にジャンプしていたことを示します。この場合、_rt0_386
が直接OSから渡された引数を処理する必要がありました。
変更後:
TEXT _rt0_386_darwin(SB),7,$8
MOVL 8(SP), AX
LEAL 12(SP), BX
MOVL AX, 0(SP)
MOVL BX, 4(SP)
CALL main(SB)
INT $3
TEXT main(SB),7,$0
JMP _rt0_386(SB)
TEXT _rt0_386_darwin(SB),7,$8
:- 新しいエントリポイント
_rt0_386_darwin
を定義します。$8
は、この関数のスタックフレームサイズが8バイトであることを示唆しています(引数をプッシュするためのスペース)。
- 新しいエントリポイント
MOVL 8(SP), AX
:- スタックポインタ(
SP
)から8バイトオフセットの位置にある値(OSが配置したargc
)をAX
レジスタに移動します。
- スタックポインタ(
LEAL 12(SP), BX
:- スタックポインタ(
SP
)から12バイトオフセットの位置にあるアドレス(OSが配置したargv
の先頭アドレス)をBX
レジスタにロードします。LEAL
はアドレスを計算してレジスタに格納する命令です。
- スタックポインタ(
MOVL AX, 0(SP)
:AX
レジスタ(argc
の値)を現在のスタックポインタの先頭(0(SP)
)にプッシュします。
MOVL BX, 4(SP)
:BX
レジスタ(argv
の先頭アドレス)をスタックポインタから4バイトオフセットの位置(4(SP)
)にプッシュします。- これにより、スタック上には
argv
、argc
の順で引数が配置され、これはC言語の呼び出し規約(通常、引数は右から左へプッシュされるため、スタック上では左から右へ並ぶ)に合致します。
CALL main(SB)
:- Goランタイム内部で定義された
main(SB)
関数を呼び出します。このmain
関数は、C言語のmain
関数と同様のインターフェース(argc
,argv
をスタック経由で受け取る)を持つように設計されています。
- Goランタイム内部で定義された
INT $3
:- デバッグ用のソフトウェア割り込みです。通常、プログラムが予期せぬ終了をした場合などに、デバッガが介入できるようにするためのものです。
TEXT main(SB),7,$0
:- Goランタイム内部の
main
関数を定義します。
- Goランタイム内部の
JMP _rt0_386(SB)
:- この
main
関数は、最終的にGoランタイムの主要な初期化ルーチンである_rt0_386
にジャンプします。
- この
このシーケンスにより、OSから渡された引数は、まず_rt0_386_$GOOS
でC言語の呼び出し規約に合うように再配置され、その上でGoランタイムの初期化プロセスに引き渡されます。
src/pkg/runtime/asm_386.s
の_rt0_386
関数の変更も重要です。
変更前:
MOVL 0(SP), AX // argc
LEAL 4(SP), BX // argv
変更後:
MOVL argc+0(FP), AX
MOVL argv+4(FP), BX
変更前は、_rt0_386
がスタックポインタ(SP
)からの相対アドレスで直接argc
とargv
を取得していました。これは、OSが直接これらの値をスタックの特定のオフセットに配置することを期待していました。
変更後は、フレームポインタ(FP
)からの相対アドレスでargc
とargv
を取得しています。これは、_rt0_386
が関数として呼び出され、その呼び出し規約に従って引数がスタックフレームに配置されていることを前提としています。つまり、新しいmain(SB)
ラッパー関数が_rt0_386
を呼び出す際に、引数を適切にスタックにプッシュしていることを期待しています。
これらの変更により、Goランタイムの起動プロセスはよりモジュール化され、OS固有の初期エントリポイントがC言語のmain
関数セマンティクスへの変換を担当し、その後のGoランタイムの初期化は共通の_rt0_386
で行われるようになりました。これにより、GoとCの相互運用性が向上し、より柔軟なシステム統合が可能になります。
関連リンク
- 386 analogue of https://golang.org/cl/7525043
- https://golang.org/cl/7551045
参考にした情報源リンク
- Go Assembly Language: https://go.dev/doc/asm
- Go Runtime Source Code: https://github.com/golang/go/tree/master/src/runtime
- Intel 80386: https://en.wikipedia.org/wiki/Intel_80386
- Program startup (C and C++): https://en.wikipedia.org/wiki/Program_startup
- Virtual Dynamic Shared Object (VDSO): https://en.wikipedia.org/wiki/VDSO
- Calling convention: https://en.wikipedia.org/wiki/Calling_convention