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

[インデックス 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ライブラリがプログラムの起動時に特定の引数(argcargv)の配置を期待する場合に問題なく連携できるようになります。

このコミットは、以前に行われた同様の変更(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がシステムプログラミング言語としての地位を確立し、既存のシステムコンポーネントと共存していく上で重要なステップです。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

  1. Goランタイム (Go Runtime): Goプログラムは、C/C++のような言語とは異なり、実行時にGoランタイムと呼ばれる独自の実行環境を必要とします。このランタイムは、ガベージコレクション、スケジューラ(ゴルーチンの管理)、メモリ管理、システムコールインターフェースなど、Goプログラムの実行に必要な多くの低レベルな機能を提供します。プログラムの起動時、OSから最初に制御が渡されるのは、Goランタイムの初期化コードです。

  2. 386アーキテクチャ (Intel 80386 Architecture): Intel 80386は、1985年にリリースされた32ビットのマイクロプロセッサです。このアーキテクチャは、x86命令セットの32ビット拡張であり、現代のx86-64(64ビット)アーキテクチャの基礎となっています。このコミットは、特に32ビット版のGoランタイム(GOARCH=386)に適用される変更です。32ビットアーキテクチャでは、レジスタのサイズやスタックの配置、呼び出し規約などが64ビットアーキテクチャとは異なります。

  3. プログラムの起動規約 (Startup Convention): オペレーティングシステムがプログラムを起動する際、OSはプログラムの最初のエントリポイントに制御を渡します。このとき、プログラムにコマンドライン引数や環境変数などの情報が渡されます。これらの情報がどのようにスタックに配置されるか、あるいはどのレジスタに格納されるかといった取り決めが「起動規約」です。C言語では、通常main(int argc, char *argv[])という形式で引数が渡されます。

  4. argcargv:

    • argc (argument count): コマンドライン引数の数を表す整数です。プログラム名自体も1つの引数として数えられます。
    • argv (argument vector): コマンドライン引数の文字列へのポインタの配列です。argv[0]は通常プログラム名、argv[1]以降がユーザーが指定した引数になります。argv配列の最後はNULLポインタで終端されます。
  5. アセンブリ言語 (.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系システムにおけるシステムコールを呼び出すためのソフトウェア割り込み。
  6. VDSO (Virtual Dynamic Shared Object): Linuxカーネルがユーザー空間に提供する、システムコールを高速化するためのメカニズムです。一部のシステムコール(例: gettimeofday)は、カーネルモードへの切り替えなしにユーザー空間から直接実行できるように、VDSOを通じて提供されます。runtime·linux_setup_vdso関数は、このVDSOのセットアップに関連する処理を行います。

  7. 呼び出し規約 (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です。

この新しいエントリポイントの役割は以下の通りです:

  1. OSからの引数受け取り: OSがプログラムを起動する際にスタックに配置するargcargvを、まず_rt0_386_$GOOSが受け取ります。

    • 多くのOSでは、スタックの特定のオフセットにargcargvが配置されます。例えば、8(SP)からargc12(SP)からargv(の先頭アドレス)を取得しています。
    • これらの値は、一時的にレジスタ(AX, BX)に格納されます。
  2. C言語のmain関数セマンティクスへの変換: 取得したargcargvを、C言語のmain関数が期待する形式でスタックにプッシュします。

    • MOVL AX, 0(SP): argcをスタックの先頭(0(SP))にプッシュします。
    • MOVL BX, 4(SP): argvをスタックの次の位置(4(SP))にプッシュします。
    • これにより、スタック上にはargvargcの順で引数が配置され、これはC言語の呼び出し規約に合致します。
  3. main関数(Goランタイム内部のラッパー)の呼び出し: 準備された引数でmain(SB)関数を呼び出します。

    • ここでいうmain(SB)は、Goのユーザープログラムのmain関数ではなく、Goランタイム内部で定義されたラッパー関数です。このラッパー関数は、C言語のmain関数と同様のインターフェースを提供します。
  4. _rt0_386へのジャンプ: このランタイム内部のmain関数は、最終的にGoランタイムの主要な初期化ルーチンである_rt0_386にジャンプします。

    • _rt0_386は、Goランタイムの初期化、スケジューラの起動、そして最終的にユーザープログラムのmain関数への制御の移行を行います。
    • _rt0_386自体も変更されており、引数をスタックから直接取得するのではなく、呼び出し元(この場合はランタイム内部のmain関数)から渡されることを期待するように変更されています。具体的には、MOVL argc+0(FP), AXMOVL argv+4(FP), BXのように、フレームポインタ(FP)からの相対アドレスで引数を取得しています。これは、関数呼び出し規約に従って引数がスタックフレームに配置されていることを前提としています。

この二段階の起動プロセスにより、GoランタイムはOSから受け取った引数をC言語のmain関数が期待する形式に変換し、その上でGo独自の初期化プロセスに進むことができます。これにより、GoプログラムがCライブラリとリンクする際に、Cライブラリが期待するmain関数のセマンティクス(特にargcargvの配置)が満たされるようになり、互換性が大幅に向上します。

また、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から渡されるargcargvをスタックから取得し、それを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)の呼び出しの前に移動しています。
  • src/pkg/runtime/rt0_plan9_386.s:

    • Plan 9固有の起動ファイルにも、_rt0_386へのジャンプ前にargcargvをスタックにプッシュする処理が追加されました。これは他のOSとは異なる方法で引数を準備しています。
  • 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)
  1. TEXT _rt0_386_darwin(SB),7,$8:
    • 新しいエントリポイント_rt0_386_darwinを定義します。$8は、この関数のスタックフレームサイズが8バイトであることを示唆しています(引数をプッシュするためのスペース)。
  2. MOVL 8(SP), AX:
    • スタックポインタ(SP)から8バイトオフセットの位置にある値(OSが配置したargc)をAXレジスタに移動します。
  3. LEAL 12(SP), BX:
    • スタックポインタ(SP)から12バイトオフセットの位置にあるアドレス(OSが配置したargvの先頭アドレス)をBXレジスタにロードします。LEALはアドレスを計算してレジスタに格納する命令です。
  4. MOVL AX, 0(SP):
    • AXレジスタ(argcの値)を現在のスタックポインタの先頭(0(SP))にプッシュします。
  5. MOVL BX, 4(SP):
    • BXレジスタ(argvの先頭アドレス)をスタックポインタから4バイトオフセットの位置(4(SP))にプッシュします。
    • これにより、スタック上にはargvargcの順で引数が配置され、これはC言語の呼び出し規約(通常、引数は右から左へプッシュされるため、スタック上では左から右へ並ぶ)に合致します。
  6. CALL main(SB):
    • Goランタイム内部で定義されたmain(SB)関数を呼び出します。このmain関数は、C言語のmain関数と同様のインターフェース(argc, argvをスタック経由で受け取る)を持つように設計されています。
  7. INT $3:
    • デバッグ用のソフトウェア割り込みです。通常、プログラムが予期せぬ終了をした場合などに、デバッガが介入できるようにするためのものです。
  8. TEXT main(SB),7,$0:
    • Goランタイム内部のmain関数を定義します。
  9. 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)からの相対アドレスで直接argcargvを取得していました。これは、OSが直接これらの値をスタックの特定のオフセットに配置することを期待していました。

変更後は、フレームポインタ(FP)からの相対アドレスでargcargvを取得しています。これは、_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

参考にした情報源リンク