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

[インデックス 1906] ファイルの概要

このコミットは、Goランタイムの386アーキテクチャ向けサポートを拡張し、小さなCプログラムを実行できるようにすることを目的としています。具体的には、386固有のアセンブリコード、クロージャのハンドリング、トレースバック機能、64ビット演算のためのヘルパー関数、メモリ管理の改善、およびLinuxシグナルハンドリングの更新が含まれています。これにより、Goプログラムが386環境でより堅牢に動作し、C言語で書かれたコードとの基本的な連携が可能になります。

コミット

commit 0d3a043de9b544ee3fca10fd1070a58f97161c4
Author: Russ Cox <rsc@golang.org>
Date:   Mon Mar 30 00:01:07 2009 -0700

    more 386 runtime - can run tiny c programs.
    
    R=r
    DELTA=1926  (1727 added, 168 deleted, 31 changed)
    OCL=26876
    CL=26878

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/0d3a043de9b544ee3fca10fd1070a58f973161c4

元コミット内容

more 386 runtime - can run tiny c programs.

変更の背景

このコミットは、Go言語の初期開発段階、特に2009年3月という非常に早い時期に行われました。当時のGoランタイムはまだ成熟しておらず、特定のアーキテクチャ(この場合はIntel 386互換プロセッサ)上での基本的な動作を確立することが重要な課題でした。

変更の主な背景は以下の点にあります。

  1. 386アーキテクチャのサポート強化: Goはクロスプラットフォームを志向していますが、初期段階では特定のアーキテクチャでの動作を安定させることが優先されました。386は当時の一般的な32ビットシステムであり、その上でGoランタイムが適切に機能することが求められました。
  2. Cプログラムとの連携: Go言語はC言語との相互運用性(cgo)を重視しており、既存のCライブラリやシステムコールをGoから利用できることが設計目標の一つでした。このコミットは、「tiny c programs」を実行できるようになったと明記されており、GoランタイムがCコードを呼び出し、その実行環境をサポートするための基盤を構築していることを示唆しています。これは、Goが単独で動作するだけでなく、既存のシステムと連携するための重要なステップでした。
  3. ランタイムの機能拡充: Goランタイムは、ガベージコレクション、スケジューラ、メモリ管理、スタック管理など、Goプログラムの実行に必要な低レベルな機能を提供します。このコミットでは、これらの機能が386アーキテクチャに合わせて実装または改善されており、特にスタックトレース、クロージャ、64ビット演算といった、より複雑な言語機能のサポートに向けた準備が進められています。
  4. Inferno OSからの影響: 変更されたファイルの一部(vlop.s, vlrt.c)がInferno OSのlibkernに由来することが明記されています。Go言語の設計者の一部はInferno OSの開発にも関わっており、そのランタイムやシステムプログラミングの知見がGoの初期ランタイムに影響を与えたと考えられます。特に、32ビット環境で64ビット整数演算を効率的に行うためのルーチンは、Infernoの経験が活かされた例と言えるでしょう。

これらの背景から、このコミットはGoランタイムが386アーキテクチャ上でより実用的なレベルに達し、C言語との連携の道を開くための重要なマイルストーンであったと評価できます。

前提知識の解説

このコミットの技術的詳細を理解するためには、以下の前提知識が役立ちます。

1. Intel 386アーキテクチャ (IA-32)

  • 32ビットアーキテクチャ: 386は32ビットの汎用レジスタ、32ビットのアドレスバス、32ビットのデータバスを持つプロセッサアーキテクチャです。これにより、最大4GBのメモリを直接アドレス指定できます。
  • セグメンテーションとページング: 386はメモリ管理のためにセグメンテーションとページングの両方をサポートします。特に、スレッドローカルストレージ (TLS) の実装には、セグメントレジスタ(例: FSGS)が利用されることがあります。このコミットのasm.sには%fsレジスタの設定が含まれており、これはTLSや特定のランタイムデータへの高速アクセスに利用される可能性があります。
  • アセンブリ言語: Goランタイムの低レベル部分は、パフォーマンスやOSとのインタフェースのためにアセンブリ言語で書かれています。386のアセンブリは、レジスタ(EAX, EBX, ECX, EDX, EBP, ESP, ESI, EDIなど)、命令(MOVL, ADDL, SUBL, CALL, JMP, RETなど)、スタック操作(PUSH, POP)を理解する必要があります。
  • システムコール: OSの機能を利用するために、Goランタイムはシステムコールを発行します。386 Linuxでは、int $0x80命令を使ってシステムコールを呼び出すのが一般的ですが、現代のLinuxではsysenter/sysexitsyscall/sysret命令が使われます。このコミットの時代では、int $0x80がまだ一般的だった可能性があります。

2. Goランタイムの基本概念

  • Goroutine (G): Goの軽量スレッドです。Goスケジューラによって管理され、OSスレッド(M)上で実行されます。各Goroutineは独自のスタックを持ちます。
  • Machine (M): OSスレッドを表します。GoスケジューラはM上でGを実行します。
  • Processor (P): GoスケジューラがGをMに割り当てるための論理プロセッサです。
  • スタック: Goの関数呼び出しはスタックフレームを使用します。Goランタイムは、スタックの動的な拡張(stack growth)をサポートしており、関数呼び出し時にスタックガードをチェックし、必要に応じてスタックを拡張します。
  • ヒープとガベージコレクション (GC): Goは自動メモリ管理(ガベージコレクション)を行います。オブジェクトはヒープに割り当てられ、GCによって不要になったメモリが解放されます。mheapmapはヒープのページを管理するためのデータ構造です。
  • クロージャ: Goのクロージャは、関数が定義された環境(レキシカルスコープ)の変数を「キャプチャ」して保持する関数です。ランタイムは、これらのキャプチャされた変数を適切に管理し、クロージャが呼び出されたときにアクセスできるようにする必要があります。

3. cgo (GoとCの相互運用)

  • GoからCの呼び出し: GoプログラムからC関数を呼び出すためのメカニズムです。GoとCの間でデータ型を変換し、呼び出し規約を合わせる必要があります。
  • CからGoの呼び出し: CコードからGo関数を呼び出すことも可能ですが、より複雑です。
  • ランタイムの役割: cgoは、GoランタイムがCスタックとGoスタックの切り替え、シグナルハンドリングの調整、メモリ管理の連携などを行う必要があります。このコミットの「can run tiny c programs」という記述は、この基本的な連携機能が実装されたことを示しています。

4. Inferno OSとlibkern

  • Inferno OS: ベル研究所で開発された分散オペレーティングシステムです。Plan 9の思想を受け継ぎ、Limbo言語とDis仮想マシンを特徴とします。Go言語の設計者の一部(Rob Pike, Ken Thompson, Russ Coxなど)はInfernoの開発にも関わっていました。
  • libkern: Inferno OSのカーネルライブラリで、低レベルなユーティリティ関数やデータ構造を提供します。このコミットで追加されたvlop.svlrt.cは、Infernoのlibkernから移植されたものであり、特に32ビットシステム上での64ビット整数演算(乗算、除算、シフトなど)を効率的に行うためのルーチンが含まれています。これは、Goが64ビット整数型(int64, uint64)をサポートするために不可欠な要素でした。

5. シグナルハンドリング

  • OSシグナル: プロセスに非同期的に通知されるイベント(例: セグメンテーション違反、割り込み、タイマー)。
  • シグナルハンドラ: シグナルが発生したときに実行される関数です。Goランタイムは、クラッシュ時のスタックトレース出力や、特定のシグナルに対するカスタム動作のためにシグナルハンドラを設定します。
  • sigaction: POSIXシステムでシグナルハンドラを設定するためのシステムコール。SA_ONSTACKSA_SIGINFOSA_RESTORERなどのフラグがあります。

これらの知識を持つことで、このコミットがGoランタイムの初期段階でどのような技術的課題に取り組んでいたか、そしてどのように解決策を実装していったかをより深く理解できます。

技術的詳細

このコミットは、Goランタイムの386アーキテクチャサポートを大幅に強化し、Cプログラムとの基本的な連携を可能にするための多岐にわたる変更を含んでいます。主要な変更点をファイルごとに詳細に見ていきます。

src/runtime/386/asm.s (新規追加)

このファイルは、386アーキテクチャ向けのGoランタイムのブートストラップコードと、低レベルなヘルパー関数を定義するアセンブリファイルです。

  • _rt0_386: Goプログラムのエントリポイントです。
    • OSから渡された引数(argc, argv)をスタックにコピーし、ランタイムがアクセスできるようにします。
    • スタックを128バイト減らし、8バイトアラインメントを保証します(ANDL $~7, SP)。
    • ldt0setupを呼び出し、スレッドローカルストレージ (TLS) のためのLocal Descriptor Table (LDT) エントリを設定します。これは、g (goroutine) と m (machine) ポインタを%fsセグメントレジスタを介して高速にアクセスできるようにするためです。0(FS)g4(FS)mを格納しています。
    • g0 (システムGoroutine) と m0 (初期Machine) を設定し、m0->g0g0に設定します。
    • OSスタックから初期スタック(istack)を作成します。
    • check, args, osinit, schedinitなどのランタイム初期化関数を呼び出します。
    • mainstart関数を新しいGoroutineとしてsys·newprocで起動します。
    • mstartを呼び出し、現在のM(OSスレッド)のスケジューリングループを開始します。
  • sys·Breakpoint: デバッグ用のブレークポイント命令 (INT $3) を提供します。
  • gogo / gosave: Goroutineのコンテキストスイッチ(スタックポインタとプログラムカウンタの保存/復元)のための関数です。gobuf構造体を使ってレジスタの状態を保存・復元します。
  • retfromnewstack: スタック拡張後に新しいスタックから戻る際のエントリポイントです。
  • gogoret: gogoに似ていますが、2番目の引数を返します。
  • setspgoto: スタックポインタを設定し、指定された関数にジャンプします。
  • cas (Compare And Swap): アトミックな比較交換操作を実装します。マルチスレッド環境での同期プリミティブとして重要です。LOCK CMPXCHGL命令を使用しています。
  • jmpdefer: 遅延呼び出し(defer)の実装に関連するジャンプ操作です。呼び出し元のリターンアドレスを調整して、指定された関数にジャンプします。
  • sys·memclr: メモリ領域をゼロクリアする関数です。REP STOSL命令を使って高速に処理します。
  • sys·getcallerpc / sys·setcallerpc: 呼び出し元のプログラムカウンタ (PC) を取得/設定する関数です。スタックトレースやデバッグに利用されます。
  • ldt0setup: LDTエントリ7を設定し、tls0(スレッドローカルストレージのデータ領域)を指すようにします。これはsetldtシステムコールを呼び出します。
  • m0, g0, tls0: グローバル変数として定義される初期のMachine、Goroutine、およびTLSデータ領域です。

src/runtime/386/closure.c (新規追加)

このファイルは、Goのクロージャを386アーキテクチャ上で動的に生成するためのCコードです。

  • sys·closure: クロージャを生成するGoランタイム関数です。
    • 引数としてクロージャのサイズ (siz)、元の関数 (fn)、キャプチャされる引数 (arg0, arg1, arg2など) を取ります。
    • 新しいクロージャのコードとキャプチャされた引数を格納するためのメモリをmal(Goのメモリ割り当て関数)で確保します。
    • 生成されるクロージャのコードは、キャプチャされた引数をスタックにコピーし、元の関数を呼び出すアセンブリ命令のシーケンスです。
    • 具体的には、SUBL $siz, SP(スタックを減らす)、MOVL $q, SI(キャプチャされた引数のアドレスをSIにロード)、MOVL SP, DI(現在のスタックポインタをDIにロード)、MOVSLまたはREP; MOVSL(引数をコピー)、CALL fn(元の関数を呼び出す)、ADDL $siz, SP(スタックを元に戻す)、RET(リターン)といった命令を動的に生成しています。
    • sizが大きすぎる場合(100バイト超)はthrow("closure too big")でエラーを発生させています。これは初期実装の制限と考えられます。

src/runtime/386/traceback.c (新規追加)

このファイルは、386アーキテクチャ向けのスタックトレース(トレースバック)機能を実装しています。

  • traceback: 指定されたPC (Program Counter) とSP (Stack Pointer) からスタックを遡り、関数呼び出しの履歴(スタックフレーム)を解析して出力します。
    • findfunc(pc)を使ってPCに対応する関数情報(Func構造体)を取得します。
    • スタックフレームのサイズ (f->frame) を使ってSPを調整し、次のフレームに進みます。
    • retfromnewstackのような特殊なPC値(スタック拡張からの戻り点)を検出した場合、古いスタックブロックに切り替えてトレースを続行します。
    • クロージャのスタックフレームを識別し、適切にスキップするロジックも含まれています(ADDL $xxx, SP; RETパターンを検出)。
    • 関数名、オフセット、ソースファイル、行番号、引数などを整形して出力します。
  • sys·Caller: 指定されたレベルnの呼び出し元のPC、ファイル名、行番号を取得するGoランタイム関数です。tracebackと同様にスタックを遡るロジックを使用します。

src/runtime/386/vlop.s (新規追加)

このファイルは、Inferno OSのlibkern/vlop-386.sから移植された、386アーキテクチャ向けの64ビット整数演算のアセンブリルーチンです。

  • _mul64by32: 64ビット整数と32ビット整数の乗算を行います。結果は64ビットです。
  • _div64by32: 64ビット整数を32ビット整数で除算します。結果は32ビットの商と32ビットの剰余です。
    • これらのルーチンは、32ビットプロセッサ上で64ビット整数型(Goのint64, uint64)を効率的に扱うために不可欠です。

src/runtime/386/vlrt.c (新規追加)

このファイルは、Inferno OSのlibkern/vlrt-386.cから移植された、64ビット整数演算や浮動小数点数と64ビット整数の変換、その他のユーティリティ関数を含むCコードです。

  • Vlong構造体: 64ビット整数をlo(下位32ビット)とhi(上位32ビット)の2つのulongで表現する共用体を含む構造体です。
  • _d2v / _f2v: double / floatVlong(64ビット整数)に変換します。浮動小数点数の内部表現(IEEE 754)を解析し、ビットシフトとマスク操作で整数部分を抽出します。
  • _v2d / _v2f: Vlongdouble / floatに変換します。
  • dodiv / slowdodiv: 64ビット整数同士の除算と剰余演算のコアロジックです。slowdodivはビットごとの減算とシフトを繰り返す汎用的なアルゴリズムで、dodivは特定のケース(例えば、除数が32ビットに収まる場合)で最適化された_div64by32アセンブリルーチンを利用します。
  • _divvu / _modvu: 符号なし64ビット整数同士の除算と剰余。
  • _divv / _modv: 符号付き64ビット整数同士の除算と剰余。符号を考慮してvneg(符号反転)を適用します。
  • _rshav / _rshlv / _lshv: 64ビット整数の算術右シフト、論理右シフト、左シフト。
  • _andv / _orv / _xorv: 64ビット整数のビット論理演算。
  • _vpp / _vmm / _ppv / _mmv: 64ビット整数のインクリメント/デクリメント(前置/後置)。
  • _vasop: 複合代入演算子(+=, -=など)をサポートするための汎用関数。様々なデータ型(schar, uchar, short, ushort, int, uint, long, ulong, vlong, uvlong)に対して、指定された演算 (fn) を適用します。
  • 型変換関数: _p2v (ポインタからVlong), _sl2v (longからVlong), _ul2v (ulongからVlong) など、様々な基本型からVlongへの変換関数。
  • 比較関数: _eqv, _nev, _ltv, _lev, _gtv, _gev, _lov, _lsv, _hiv, _hsvなど、64ビット整数同士の比較を行う関数。

src/runtime/Makefile

ビルドシステムに関する変更です。

  • 386固有のオブジェクトファイル追加: OFILES_386変数にvlop.$Ovlrt.$Oが追加され、OFILES$(OFILES_$(GOARCH))として組み込まれるようになりました。これにより、386ビルド時にこれらのファイルがコンパイル・リンクされます。
  • SIZE変数の導入: SIZE_386=32, SIZE_amd64=64が定義され、CFLAGS-D_64BITが64ビットプラットフォームでのみ追加されるようになりました。これは、コンパイル時に32ビット/64ビットの区別を行うためのマクロ定義です。
  • O変数の導入: コンパイラ(8c for 386, 6c for amd64)を決定するためのO_386=8, O_amd64=6が定義されました。
  • ARツールの明示: AR=6arが明示的に定義されました。
  • mheapmapの動的選択: mheapmap64.$Omheapmap$(SIZE).$Oに変更され、32ビット/64ビットに応じて適切なmheapmap実装が選択されるようになりました。
  • クリーンアップコマンドの変更: rm -f *.[68] *.aとなり、386 (8) とamd64 (6) の両方のオブジェクトファイルをクリーンアップ対象に含めるようになりました。

src/runtime/amd64/asm.s

  • FLUSH関数の削除: この関数は単にRETするだけの空の関数であり、おそらくデバッグ目的で一時的に存在していたか、不要になったため削除されました。

src/runtime/iface.c

  • Sigt構造体のパディングに関するコメント追加: amd64での32ビットパディングに関するコメントが追加されました。
  • sys·ifaceT2I関数でのアラインメント計算の変更: rnd(wid, 8)rnd(wid, sizeof(uintptr))に変更されました。これは、ポインタのサイズ(32ビットシステムでは4バイト、64ビットシステムでは8バイト)に基づいてアラインメントを行うように修正されたことを意味します。これにより、クロスプラットフォームでの互換性が向上します。

src/runtime/linux/386/signal.c (新規追加)

このファイルは、Linux 386アーキテクチャ向けのシグナルハンドリングを実装しています。

  • dumpregs: シグナルコンテキスト(レジスタの状態)をダンプする関数です。デバッグ時にレジスタの値を確認するために使用されます。
  • sighandler: シグナルが発生したときに呼び出されるメインのシグナルハンドラです。
    • panickingフラグをチェックし、既にパニック状態であれば終了します。
    • シグナル番号とシグナル情報 (Siginfo)、コンテキスト (Ucontext) を受け取ります。
    • シグナル名、フォルトアドレス、PCなどを出力します。
    • gotraceback()が真の場合、traceback関数を呼び出してスタックトレースを出力します。
    • sys·Breakpoint()を呼び出し、デバッガがアタッチされていれば停止します。
    • 最終的にsys_Exit(2)でプロセスを終了します。
  • signalstack: シグナルハンドラが使用する代替スタックを設定します。
  • initsig: シグナルハンドラを初期化し、様々なシグナルに対してsigtramp(Goのシグナルハンドラへのエントリポイント)またはsigignoreを設定します。SA_ONSTACK, SA_SIGINFO, SA_RESTORERフラグを使用しています。

src/runtime/linux/{ => amd64}/signal.c (ファイル名変更と修正)

src/runtime/linux/signal.csrc/runtime/linux/amd64/signal.cにリネームされ、amd64固有のシグナルハンドリングコードとなりました。変更内容は主にコメントの修正と、sighandler関数内でpanicking = 1;が追加された点です。これは、パニック状態であることを明示的に設定し、二重パニックを防ぐためのものです。

src/runtime/linux/amd64/sys.s

  • sys·mmap関数から不要なレジスタ操作が削除されました。これは、MAP_ANONフラグの処理に関する古いロジックが不要になったためと考えられます。

src/runtime/linux/defs.c / src/runtime/linux/defs1.c

  • godefsツールに関するコメントから、386アーキテクチャ向けのdefs.h生成に関する記述が削除されました。これは、defs2.cが386向けの定義を生成するようになったためと考えられます。

src/runtime/linux/defs2.c (新規追加)

このファイルは、Linux 386アーキテクチャ向けのシステム定義(構造体、定数など)をgodefsツールで生成するためのCコードです。

  • PROT_NONE, PROT_READ, PROT_WRITE, PROT_EXEC (メモリ保護フラグ)
  • MAP_ANON, MAP_PRIVATE (mmapフラグ)
  • SA_RESTART, SA_ONSTACK, SA_RESTORER, SA_SIGINFO (sigactionフラグ)
  • Fpreg, Fpxreg, Xmmreg, Fpstate, Timespec, Timeval, Sigaction, Siginfo, Sigaltstack, Sigcontext, Ucontextなどの構造体定義
    • これらの定義は、GoランタイムがLinuxカーネルとインタフェースするために必要です。

src/runtime/malloc.c

  • defs.hのインクルード: malloc.cdefs.hをインクルードするようになりました。
  • mlookup関数でのprintfのフォーマット文字列の修正: nobj, n, npagesなどの変数を%D(Goのuint64フォーマット)で出力するように修正されました。これは、これらの変数が32ビット環境でも大きな値を取りうるため、正確な表示のためと考えられます。
  • SysAlloc関数でのsys_mmap呼び出しの修正: MAP_ANONフラグがLinuxで正しく扱われるように、fd引数が-1に設定されました。
  • stackalloc関数でのprintfのフォーマット文字列の修正: stacks.size%Dで出力するように修正されました。
  • 不要なenum定義の削除: PROT_NONE, MAP_ANONなどの定数定義がdefs.hに移動されたため、malloc.cから削除されました。

src/runtime/malloc.h

  • mheapmapの条件付きインクルード: #ifdef _64BITディレクティブが追加され、64ビット環境ではmheapmap64.hを、それ以外(32ビット)ではmheapmap32.hをインクルードするように変更されました。これにより、アーキテクチャに応じた適切なヒープマップ実装が選択されます。

src/runtime/mem.c

  • defs.hのインクルード: mem.cdefs.hをインクルードするようになりました。
  • 不要なenum定義の削除: PROT_NONE, MAP_ANONなどの定数定義がdefs.hに移動されたため、mem.cから削除されました。

src/runtime/mgc0.c

  • sweepspan関数でのprintfのフォーマット文字列の修正: s->npages<<PageShift%Dで出力するように修正されました。

src/runtime/mheapmap32.c (新規追加)

このファイルは、32ビットアーキテクチャ向けのヒープマップ(MHeapMap)の実装です。

  • MHeapMap: ページIDからMSpan(メモリ領域の管理単位)へのマッピングを管理する3レベルのラディックスツリーです。
  • MHeapMap_Init: ヒープマップを初期化します。
  • MHeapMap_Get / MHeapMap_GetMaybe: ページIDから対応するMSpanを取得します。
  • MHeapMap_Set: ページIDにMSpanを設定します。
  • MHeapMap_Preallocate: 指定されたページ範囲のストレージを事前に割り当てます。これにより、GetSet呼び出し時にnilポインタチェックが不要になります。
    • 32ビットアドレス空間を効率的にマッピングするために、MHeapMap_Level1BitsMHeapMap_Level2Bitsがそれぞれ10ビットに設定されています。

src/runtime/mheapmap32.h (新規追加)

このファイルは、32ビットアーキテクチャ向けのヒープマップのデータ構造と定数を定義します。

  • MHeapMap_Level1Bits, MHeapMap_Level2Bits, MHeapMap_TotalBitsなどの定数: ページIDを3レベルのラディックスツリーでマッピングするためのビット数とマスクを定義します。
  • MHeapMap構造体: allocator関数ポインタと、レベル1のノードポインタの配列pを含みます。
  • MHeapMapNode2構造体: レベル2のノードで、MSpanポインタの配列sを含みます。

その他のファイル

  • src/runtime/print.c, src/runtime/proc.c, src/runtime/runtime.c, src/runtime/runtime.h, src/runtime/symtab.cにも小さな変更(主にprintfのフォーマット修正や、defs.hのインクルードなど)が含まれています。

これらの変更は、Goランタイムが386アーキテクチャ上でより堅牢に動作し、C言語との連携を可能にするための基盤を構築する上で不可欠なものでした。特に、低レベルなアセンブリコード、メモリ管理、シグナルハンドリング、そしてInferno OSからの64ビット演算ルーチンの移植は、Goの初期の機能拡張において重要な役割を果たしました。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更箇所は多岐にわたりますが、特にGoランタイムの386アーキテクチャへの適応とC言語との連携、そして64ビット演算のサポートを示す以下のファイルとコードスニペットが挙げられます。

1. src/runtime/386/asm.s - ランタイムのブートストラップとTLS設定

// src/runtime/386/asm.s (新規追加)
TEXT _rt0_386(SB),7,$0
    // copy arguments forward on an even stack
    MOVL    0(SP), AX        // argc
    LEAL    4(SP), BX        // argv
    SUBL    $128, SP        // plenty of scratch
    ANDL    $~7, SP

    // ... (argument saving) ...

    CALL    ldt0setup(SB)

    // set up %fs to refer to that ldt entry
    MOVL    $(7*8+7), AX
    MOVW    AX, FS

    // store through it, to make sure it works
    MOVL    $0x123, 0(FS)
    MOVL    tls0(SB), AX
    CMPL    AX, $0x123
    JEQ ok
    MOVL    AX, 0
ok:

    // set up m and g "registers"
    // g is 0(FS), m is 4(FS)
    LEAL    g0(SB), CX
    MOVL    CX, 0(FS)
    LEAL    m0(SB), AX
    MOVL    AX, 4(FS)

    // save m->g0 = g0
    MOVL    CX, 0(AX)

    // ... (stack setup, runtime initialization calls) ...

    CALL    schedinit(SB)

    // create a new goroutine to start program
    PUSHL    $mainstart(SB)    // entry
    PUSHL    $8    // arg size
    CALL    sys·newproc(SB)
    POPL    AX
    POPL    AX

    // start this M
    CALL    mstart(SB)

    INT $3
    RET

2. src/runtime/386/closure.c - クロージャの動的生成

// src/runtime/386/closure.c (新規追加)
void
sys·closure(int32 siz, byte *fn, byte *arg0)
{
    byte *p, *q, **ret;
    int32 i, n;
    int32 pcrel;

    if(siz < 0 || siz%4 != 0)
        throw("bad closure size");

    ret = (byte**)((byte*)&arg0 + siz);

    if(siz > 100) {
        // TODO(rsc): implement stack growth preamble?
        throw("closure too big");
    }

    // compute size of new fn.
    // must match code laid out below.
    n = 6+5+2;    // SUBL MOVL MOVL
    if(siz <= 4*4)
        n += 1*siz/4;    // MOVSL MOVSL...
    else
        n += 6+2;    // MOVL REP MOVSL
    n += 5;    // CALL
    n += 6+1;    // ADDL RET

    // store args aligned after code, so gc can find them.
    n += siz;
    if(n%4)
        n += 4 - n%4;

    p = mal(n);
    *ret = p;
    q = p + n - siz;
    mcpy(q, (byte*)&arg0, siz);

    // SUBL $siz, SP
    *p++ = 0x81;
    *p++ = 0xec;
    *(uint32*)p = siz;
    p += 4;

    // MOVL $q, SI
    *p++ = 0xbe;
    *(byte**)p = q;
    p += 4;

    // MOVL SP, DI
    *p++ = 0x89;
    *p++ = 0xe7;

    if(siz <= 4*4) {
        for(i=0; i<siz; i+=4) {
            // MOVSL
            *p++ = 0xa5;
        }
    } else {
        // MOVL $(siz/8), CX  [32-bit immediate siz/4]
        *p++ = 0xc7;
        *p++ = 0xc1;
        *(uint32*)p = siz/4;
        p += 4;

        // REP; MOVSL
        *p++ = 0xf3;
        *p++ = 0xa5;
    }

    // call fn
    pcrel = fn - (p+5);
    // direct call with pc-relative offset
    // CALL fn
    *p++ = 0xe8;
    *(int32*)p = pcrel;
    p += 4;

    // ADDL $siz, SP
    *p++ = 0x81;
    *p++ = 0xc4;
    *(uint32*)p = siz;
    p += 4;

    // RET
    *p++ = 0xc3;

    if(p > q)
        throw("bad math in sys.closure");
}

3. src/runtime/386/vlrt.c - 64ビット整数演算のソフトウェア実装

// src/runtime/386/vlrt.c (新規追加)
typedef    struct    Vlong    Vlong;
struct    Vlong
{
    union
    {
        struct
        {
            ulong    lo;
            ulong    hi;
        };
        struct
        {
            ushort    lols;
            ushort    loms;
            ushort    hils;
            ushort    hims;
        };
    };
};

// ... (various conversion functions like _d2v, _f2v, _v2d, _v2f) ...

static void
dodiv(Vlong num, Vlong den, Vlong *qp, Vlong *rp)
{
    ulong n;
    Vlong x, q, r;

    // ... (handle cases where den > num) ...

    if(den.hi != 0){
        // ... (optimized division for non-zero den.hi) ...
    } else {
        if(num.hi >= den.lo){
            q.hi = n = num.hi/den.lo;
            num.hi -= den.lo*n;
        } else {
            q.hi = 0;
        }
        q.lo = _div64by32(num, den.lo, &r.lo); // Calls assembly helper
        r.hi = 0;
    }
    // ... (assign results to qp and rp) ...
}

// ... (various arithmetic and comparison functions for Vlong) ...

4. src/runtime/malloc.h - 32/64ビットヒープマップの条件付き選択

// src/runtime/malloc.h (変更)
#ifdef _64BIT
#include "mheapmap64.h"
#else
#include "mheapmap32.h"
#endif

5. src/runtime/mheapmap32.h - 32ビットヒープマップの定義

// src/runtime/mheapmap32.h (新規追加)
// Free(v) must be able to determine the MSpan containing v.
// The MHeapMap is a 2-level radix tree mapping page numbers to MSpans.

typedef struct MHeapMapNode2 MHeapMapNode2;

enum
{
    // 32 bit address - 12 bit page size = 20 bits to map
    MHeapMap_Level1Bits = 10,
    MHeapMap_Level2Bits = 10,

    MHeapMap_TotalBits =
        MHeapMap_Level1Bits +
        MHeapMap_Level2Bits,

    MHeapMap_Level1Mask = (1<<MHeapMap_Level1Bits) - 1,
    MHeapMap_Level2Mask = (1<<MHeapMap_Level2Bits) - 1,
};

struct MHeapMap
{
    void *(*allocator)(uintptr);
    MHeapMapNode2 *p[1<<MHeapMap_Level1Bits];
};

struct MHeapMapNode2
{
    MSpan *s[1<<MHeapMap_Level2Bits];
};

コアとなるコードの解説

1. src/runtime/386/asm.s の解説

このアセンブリコードは、Goプログラムが386システム上で起動する際の初期化プロセスを定義しています。

  • _rt0_386: これはGoランタイムの「ゼロランタイム」と呼ばれる部分で、OSがプログラムをロードした後に最初に実行されるコードです。
    • argcargvの処理: C言語のmain関数と同様に、コマンドライン引数を取得し、Goランタイムがアクセスできる場所に保存します。
    • スタックアラインメント: SUBL $128, SPANDL $~7, SPは、スタックポインタを128バイト減らし、さらに8バイト境界にアラインメントしています。これは、Goの関数呼び出し規約や特定の命令(例: SSE命令)がスタックアラインメントを要求するためです。
    • TLS (Thread Local Storage) の設定: ldt0setupの呼び出しと%fsレジスタの設定は、386アーキテクチャにおけるTLSの重要な側面です。Goランタイムは、現在のGoroutine (g) とMachine (m) のポインタをTLSに格納することで、これらの重要なランタイムデータ構造に非常に高速にアクセスできるようにします。0(FS)4(FS)は、%fsセグメントレジスタをベースとしたオフセットで、それぞれgmのポインタを指します。これは、Goのスケジューラが頻繁にGoroutineのコンテキストを切り替える際に、現在のGoroutineとMachineの情報を効率的に取得するために不可欠です。
    • 初期GoroutineとMachine: g0m0は、Goランタイムが起動する際に使用される特別なGoroutineとMachineです。g0はシステムコールやGCなどのランタイム内部処理を実行するためのスタックを持ち、m0は初期のOSスレッドを表します。
    • mainstartの起動: sys·newprocは、Goのスケジューラを使って新しいGoroutineを起動するランタイム関数です。ここでは、Goプログラムのユーザーコードのエントリポイントであるmainstart関数を新しいGoroutineとして起動しています。これにより、Goプログラムの並行実行が開始されます。
    • mstart: これは現在のOSスレッド(M)をGoスケジューラのループに参加させる関数です。mstartが呼び出されると、OSスレッドはGoスケジューラによって管理され、Goroutineの実行を開始します。

2. src/runtime/386/closure.c の解説

このCコードは、Goのクロージャを386アーキテクチャ上で動的に生成するメカニズムを実装しています。

  • 動的なコード生成: Goのクロージャは、その定義されたスコープの外部変数を「キャプチャ」します。このコミットでは、クロージャが呼び出されたときにこれらのキャプチャされた変数を元の関数に渡すための小さなアセンブリコードスニペットをメモリ上に動的に生成しています。
  • スタック操作: 生成されるアセンブリコードは、以下の主要なステップを実行します。
    • SUBL $siz, SP: クロージャがキャプチャした変数をスタックに配置するために、スタックポインタをsizバイト分減らします。
    • MOVL $q, SIMOVL SP, DI: キャプチャされた変数の元の場所(q)と、それらをコピーする先のスタック上の場所(SP、DIレジスタにロード)を設定します。
    • MOVSL または REP; MOVSL: sizのサイズに応じて、キャプチャされた変数をSIからDIへコピーします。REP; MOVSLは、大きなデータブロックを効率的にコピーするためのアセンブリ命令です。
    • CALL fn: キャプチャされた変数をスタックに配置した後、元の関数 (fn) を呼び出します。
    • ADDL $siz, SP: 関数呼び出しが完了した後、スタックポインタを元に戻し、クロージャが使用したスタック領域を解放します。
    • RET: 呼び出し元に戻ります。
  • siz > 100 の制限: 初期実装のため、キャプチャされる変数の合計サイズが100バイトを超えるクロージャはサポートされていません。これは、動的に生成されるコードの複雑さや、スタック拡張との連携がまだ完全ではなかったことを示唆しています。
  • pcrel = fn - (p+5): CALL命令は通常、相対アドレス指定を使用します。pcrelは、CALL命令の次の命令のアドレスからターゲット関数fnまでの相対オフセットを計算しています。

3. src/runtime/386/vlrt.c の解説

このCコードは、32ビットシステム上で64ビット整数演算をソフトウェアでエミュレートするためのルーチンを提供します。これは、Goがint64uint64のような64ビット整数型をサポートするために不可欠です。

  • Vlong構造体: 32ビットシステムでは、64ビット整数は2つの32ビット整数(hilo)のペアとして扱われます。Vlong構造体はこの表現をカプセル化しています。
  • dodiv関数: 64ビット整数同士の除算と剰余の主要なロジックを含みます。
    • 最適化されたケース: den.hiがゼロの場合(つまり、除数が32ビットに収まる場合)、_div64by32というアセンブリヘルパー関数を呼び出しています。これは、アセンブリレベルで最適化された32ビット除算命令を利用することで、パフォーマンスを向上させています。
    • 汎用的なケース: den.hiがゼロでない場合(除数が64ビットである場合)、slowdodivというより汎用的なソフトウェア実装にフォールバックします。この実装は、ビットごとの減算とシフトを繰り返すことで除算を行います。これは、ハードウェアが直接64ビット除算をサポートしない32ビットシステムで、64ビット除算を実現するための標準的な手法です。
  • 型変換とビット操作: _d2v, _f2v, _v2d, _v2fなどの関数は、浮動小数点数と64ビット整数の間の変換を行います。これらはIEEE 754浮動小数点数の内部表現を理解し、ビットシフトやマスク操作を駆使して変換を実現しています。
  • その他の演算: _rshav (算術右シフト), _lshv (左シフト), _andv (ビットAND) など、様々な64ビット整数に対するビット操作や算術演算がソフトウェアで実装されています。これらは、Goの言語仕様で定義されている64ビット整数演算を、32ビットハードウェア上で正しく実行するために必要です。

4. src/runtime/malloc.h の解説

この変更は、Goランタイムのメモリ管理システムが32ビットと64ビットのアーキテクチャ間で異なるヒープマップ実装を動的に選択できるようにするためのものです。

  • 条件付きコンパイル: #ifdef _64BITプリプロセッサディレクティブを使用することで、コンパイル時にターゲットアーキテクチャが64ビットであるかどうかに応じて、異なるヘッダーファイル(mheapmap64.hまたはmheapmap32.h)をインクルードします。
  • MHeapMapの役割: MHeapMapは、Goのガベージコレクタがメモリを効率的に管理するために使用する重要なデータ構造です。これは、仮想メモリのページ番号から、そのページが属するMSpan(メモリ領域の管理単位)へのマッピングを提供します。このマッピングは、GCがオブジェクトのポインタをスキャンする際に、そのポインタがどのメモリ領域に属し、どのようなオブジェクトであるかを迅速に判断するために使用されます。
  • 32ビットと64ビットの違い: 32ビットシステムと64ビットシステムでは、アドレス空間のサイズが大きく異なります。そのため、ヒープマップの実装も、アドレス空間の大きさに合わせて最適化される必要があります。このコミットでは、32ビットシステム向けに特化したmheapmap32.hmheapmap32.cが導入され、より効率的なメモリマッピングが可能になりました。

5. src/runtime/mheapmap32.h の解説

このヘッダーファイルは、32ビットシステム向けのMHeapMapの構造と定数を定義しています。

  • ラディックスツリー: MHeapMapは、ページ番号をMSpanにマッピングするために2レベルのラディックスツリー(基数木)を使用します。
    • MHeapMap_Level1Bits = 10: ページIDの上位10ビットがレベル1のインデックスとして使用されます。
    • MHeapMap_Level2Bits = 10: ページIDの次の10ビットがレベル2のインデックスとして使用されます。
    • MHeapMap_TotalBits = 20: 32ビットアドレス空間から12ビットのページサイズを引くと、20ビットのページIDが残ります。この20ビットが2つのレベル(10ビット + 10ビット)に分割されます。
  • MHeapMap構造体: p配列は、レベル1のノード(MHeapMapNode2へのポインタ)を格納します。配列のサイズは1 << MHeapMap_Level1Bits、つまり2^10 = 1024です。
  • MHeapMapNode2構造体: s配列は、MSpanへのポインタを格納します。配列のサイズは1 << MHeapMap_Level2Bits、つまり2^10 = 1024です。
  • 効率的なマッピング: この2レベルのラディックスツリー構造により、Goランタイムは32ビットアドレス空間内の任意のページIDに対応するMSpanを、少ないメモリフットプリントと高速なルックアップで効率的に管理できます。

これらのコアとなるコードの変更は、Go言語が初期段階で直面した低レベルなシステムプログラミングの課題を解決し、386アーキテクチャ上での実用的なランタイムを構築するための重要なステップでした。特に、C言語との連携、64ビット演算のサポート、そして効率的なメモリ管理は、Goのその後の発展の基盤となりました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード: この解説の主要な情報源は、GitHub上のGo言語リポジトリにある実際のコミットと関連ファイルです。
  • Go言語の初期開発に関するブログ記事やメーリングリストのアーカイブ: Go言語の初期の設計決定や実装の背景について言及している可能性があります。
  • Inferno OSのドキュメントとソースコード: vlop.svlrt.cの元のコンテキストを理解するために参照しました。
  • x86アセンブリ言語の資料: 386アセンブリコードの理解のために一般的なx86アセンブリの知識を使用しました。
  • オペレーティングシステムの概念に関する一般的な知識: シグナルハンドリング、メモリ管理、プロセス/スレッド管理などのOSの基本概念を理解するために使用しました。
  • C言語の知識: クロージャのC言語での実装や、Cgoの概念を理解するために使用しました。
  • Google検索: 特定のキーワード(例: "Inferno OS libkern", "Go runtime 386", "Go cgo early history")で追加情報を検索しました。
    • google_web_searchツールを使用しました。

[インデックス 1906] ファイルの概要

このコミットは、Goランタイムの386アーキテクチャ向けサポートを拡張し、小さなCプログラムを実行できるようにすることを目的としています。具体的には、386固有のアセンブリコード、クロージャのハンドリング、トレースバック機能、64ビット演算のためのヘルパー関数、メモリ管理の改善、およびLinuxシグナルハンドリングの更新が含まれています。これにより、Goプログラムが386環境でより堅牢に動作し、C言語で書かれたコードとの基本的な連携が可能になります。

コミット

commit 0d3a043de9b544ee3fca10fd1070a58f97161c4
Author: Russ Cox <rsc@golang.org>
Date:   Mon Mar 30 00:01:07 2009 -0700

    more 386 runtime - can run tiny c programs.
    
    R=r
    DELTA=1926  (1727 added, 168 deleted, 31 changed)
    OCL=26876
    CL=26878

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/0d3a043de9b544ee3fca10fd1070a58f973161c4

元コミット内容

more 386 runtime - can run tiny c programs.

変更の背景

このコミットは、Go言語の初期開発段階、特に2009年3月という非常に早い時期に行われました。当時のGoランタイムはまだ成熟しておらず、特定のアーキテクチャ(この場合はIntel 386互換プロセッサ)上での基本的な動作を確立することが重要な課題でした。

変更の主な背景は以下の点にあります。

  1. 386アーキテクチャのサポート強化: Goはクロスプラットフォームを志向していますが、初期段階では特定のアーキテクチャでの動作を安定させることが優先されました。386は当時の一般的な32ビットシステムであり、その上でGoランタイムが適切に機能することが求められました。
  2. Cプログラムとの連携: Go言語はC言語との相互運用性(cgo)を重視しており、既存のCライブラリやシステムコールをGoから利用できることが設計目標の一つでした。このコミットは、「tiny c programs」を実行できるようになったと明記されており、GoランタイムがCコードを呼び出し、その実行環境をサポートするための基盤を構築していることを示唆しています。これは、Goが単独で動作するだけでなく、既存のシステムと連携するための重要なステップでした。
  3. ランタイムの機能拡充: Goランタイムは、ガベージコレクション、スケジューラ、メモリ管理、スタック管理など、Goプログラムの実行に必要な低レベルな機能を提供します。このコミットでは、これらの機能が386アーキテクチャに合わせて実装または改善されており、特にスタックトレース、クロージャ、64ビット演算といった、より複雑な言語機能のサポートに向けた準備が進められています。
  4. Inferno OSからの影響: 変更されたファイルの一部(vlop.s, vlrt.c)がInferno OSのlibkernに由来することが明記されています。Go言語の設計者の一部はInferno OSの開発にも関わっており、そのランタイムやシステムプログラミングの知見がGoの初期ランタイムに影響を与えたと考えられます。特に、32ビット環境で64ビット整数演算を効率的に行うためのルーチンは、Infernoの経験が活かされた例と言えるでしょう。

これらの背景から、このコミットはGoランタイムが386アーキテクチャ上でより実用的なレベルに達し、C言語との連携の道を開くための重要なマイルストーンであったと評価できます。

前提知識の解説

このコミットの技術的詳細を理解するためには、以下の前提知識が役立ちます。

1. Intel 386アーキテクチャ (IA-32)

  • 32ビットアーキテクチャ: 386は32ビットの汎用レジスタ、32ビットのアドレスバス、32ビットのデータバスを持つプロセッサアーキテクチャです。これにより、最大4GBのメモリを直接アドレス指定できます。
  • セグメンテーションとページング: 386はメモリ管理のためにセグメンテーションとページングの両方をサポートします。特に、スレッドローカルストレージ (TLS) の実装には、セグメントレジスタ(例: FSGS)が利用されることがあります。このコミットのasm.sには%fsレジスタの設定が含まれており、これはTLSや特定のランタイムデータへの高速アクセスに利用される可能性があります。
  • アセンブリ言語: Goランタイムの低レベル部分は、パフォーマンスやOSとのインタフェースのためにアセンブリ言語で書かれています。386のアセンブリは、レジスタ(EAX, EBX, ECX, EDX, EBP, ESP, ESI, EDIなど)、命令(MOVL, ADDL, SUBL, CALL, JMP, RETなど)、スタック操作(PUSH, POP)を理解する必要があります。
  • システムコール: OSの機能を利用するために、Goランタイムはシステムコールを発行します。386 Linuxでは、int $0x80命令を使ってシステムコールを呼び出すのが一般的ですが、現代のLinuxではsysenter/sysexitsyscall/sysret命令が使われます。このコミットの時代では、int $0x80がまだ一般的だった可能性があります。

2. Goランタイムの基本概念

  • Goroutine (G): Goの軽量スレッドです。Goスケジューラによって管理され、OSスレッド(M)上で実行されます。各Goroutineは独自のスタックを持ちます。
  • Machine (M): OSスレッドを表します。GoスケジューラはM上でGを実行します。
  • Processor (P): GoスケジューラがGをMに割り当てるための論理プロセッサです。
  • スタック: Goの関数呼び出しはスタックフレームを使用します。Goランタイムは、スタックの動的な拡張(stack growth)をサポートしており、関数呼び出し時にスタックガードをチェックし、必要に応じてスタックを拡張します。
  • ヒープとガベージコレクション (GC): Goは自動メモリ管理(ガベージコレクション)を行います。オブジェクトはヒープに割り当てられ、GCによって不要になったメモリが解放されます。mheapmapはヒープのページを管理するためのデータ構造です。
  • クロージャ: Goのクロージャは、関数が定義された環境(レキシカルスコープ)の変数を「キャプチャ」して保持する関数です。ランタイムは、これらのキャプチャされた変数を適切に管理し、クロージャが呼び出されたときにアクセスできるようにする必要があります。

3. cgo (GoとCの相互運用)

  • GoからCの呼び出し: GoプログラムからC関数を呼び出すためのメカニズムです。GoとCの間でデータ型を変換し、呼び出し規約を合わせる必要があります。
  • CからGoの呼び出し: CコードからGo関数を呼び出すことも可能ですが、より複雑です。
  • ランタイムの役割: cgoは、GoランタイムがCスタックとGoスタックの切り替え、シグナルハンドリングの調整、メモリ管理の連携などを行う必要があります。このコミットの「can run tiny c programs」という記述は、この基本的な連携機能が実装されたことを示しています。

4. Inferno OSとlibkern

  • Inferno OS: ベル研究所で開発された分散オペレーティングシステムです。Plan 9の思想を受け継ぎ、Limbo言語とDis仮想マシンを特徴とします。Go言語の設計者の一部(Rob Pike, Ken Thompson, Russ Coxなど)はInfernoの開発にも関わっていました。
  • libkern: Inferno OSのカーネルライブラリで、低レベルなユーティリティ関数やデータ構造を提供します。このコミットで追加されたvlop.svlrt.cは、Infernoのlibkernから移植されたものであり、特に32ビットシステム上での64ビット整数演算(乗算、除算、シフトなど)を効率的に行うためのルーチンが含まれています。これは、Goが64ビット整数型(int64, uint64)をサポートするために不可欠な要素でした。

5. シグナルハンドリング

  • OSシグナル: プロセスに非同期的に通知されるイベント(例: セグメンテーション違反、割り込み、タイマー)。
  • シグナルハンドラ: シグナルが発生したときに実行される関数です。Goランタイムは、クラッシュ時のスタックトレース出力や、特定のシグナルに対するカスタム動作のためにシグナルハンドラを設定します。
  • sigaction: POSIXシステムでシグナルハンドラを設定するためのシステムコール。SA_ONSTACKSA_SIGINFOSA_RESTORERなどのフラグがあります。

これらの知識を持つことで、このコミットがGoランタイムの初期段階でどのような技術的課題に取り組んでいたか、そしてどのように解決策を実装していったかをより深く理解できます。

技術的詳細

このコミットは、Goランタイムの386アーキテクチャサポートを大幅に強化し、Cプログラムとの基本的な連携を可能にするための多岐にわたる変更を含んでいます。主要な変更点をファイルごとに詳細に見ていきます。

src/runtime/386/asm.s (新規追加)

このファイルは、386アーキテクチャ向けのGoランタイムのブートストラップコードと、低レベルなヘルパー関数を定義するアセンブリファイルです。

  • _rt0_386: Goプログラムのエントリポイントです。
    • OSから渡された引数(argc, argv)をスタックにコピーし、ランタイムがアクセスできるようにします。
    • スタックを128バイト減らし、8バイトアラインメントを保証します(ANDL $~7, SP)。
    • ldt0setupを呼び出し、スレッドローカルストレージ (TLS) のためのLocal Descriptor Table (LDT) エントリを設定します。これは、g (goroutine) と m (machine) ポインタを%fsセグメントレジスタを介して高速にアクセスできるようにするためです。0(FS)g4(FS)mを格納しています。
    • g0 (システムGoroutine) と m0 (初期Machine) を設定し、m0->g0g0に設定します。
    • OSスタックから初期スタック(istack)を作成します。
    • check, args, osinit, schedinitなどのランタイム初期化関数を呼び出します。
    • mainstart関数を新しいGoroutineとしてsys·newprocで起動します。
    • mstartを呼び出し、現在のM(OSスレッド)のスケジューリングループを開始します。
  • sys·Breakpoint: デバッグ用のブレークポイント命令 (INT $3) を提供します。
  • gogo / gosave: Goroutineのコンテキストスイッチ(スタックポインタとプログラムカウンタの保存/復元)のための関数です。gobuf構造体を使ってレジスタの状態を保存・復元します。
  • retfromnewstack: スタック拡張後に新しいスタックから戻る際のエントリポイントです。
  • gogoret: gogoに似ていますが、2番目の引数を返します。
  • setspgoto: スタックポインタを設定し、指定された関数にジャンプします。
  • cas (Compare And Swap): アトミックな比較交換操作を実装します。マルチスレッド環境での同期プリミティブとして重要です。LOCK CMPXCHGL命令を使用しています。
  • jmpdefer: 遅延呼び出し(defer)の実装に関連するジャンプ操作です。呼び出し元のリターンアドレスを調整して、指定された関数にジャンプします。
  • sys·memclr: メモリ領域をゼロクリアする関数です。REP STOSL命令を使って高速に処理します。
  • sys·getcallerpc / sys·setcallerpc: 呼び出し元のプログラムカウンタ (PC) を取得/設定する関数です。スタックトレースやデバッグに利用されます。
  • ldt0setup: LDTエントリ7を設定し、tls0(スレッドローカルストレージのデータ領域)を指すようにします。これはsetldtシステムコールを呼び出します。
  • m0, g0, tls0: グローバル変数として定義される初期のMachine、Goroutine、およびTLSデータ領域です。

src/runtime/386/closure.c (新規追加)

このファイルは、Goのクロージャを386アーキテクチャ上で動的に生成するためのCコードです。

  • sys·closure: クロージャを生成するGoランタイム関数です。
    • 引数としてクロージャのサイズ (siz)、元の関数 (fn)、キャプチャされる引数 (arg0, arg1, arg2など) を取ります。
    • 新しいクロージャのコードとキャプチャされた引数を格納するためのメモリをmal(Goのメモリ割り当て関数)で確保します。
    • 生成されるクロージャのコードは、キャプチャされた引数をスタックにコピーし、元の関数を呼び出すアセンブリ命令のシーケンスです。
    • 具体的には、SUBL $siz, SP(スタックを減らす)、MOVL $q, SI(キャプチャされた引数のアドレスをSIにロード)、MOVL SP, DI(現在のスタックポインタをDIにロード)、MOVSLまたはREP; MOVSL(引数をコピー)、CALL fn(元の関数を呼び出す)、ADDL $siz, SP(スタックを元に戻す)、RET(リターン)といった命令を動的に生成しています。
    • sizが大きすぎる場合(100バイト超)はthrow("closure too big")でエラーを発生させています。これは初期実装の制限と考えられます。

src/runtime/386/traceback.c (新規追加)

このファイルは、386アーキテクチャ向けのスタックトレース(トレースバック)機能を実装しています。

  • traceback: 指定されたPC (Program Counter) とSP (Stack Pointer) からスタックを遡り、関数呼び出しの履歴(スタックフレーム)を解析して出力します。
    • findfunc(pc)を使ってPCに対応する関数情報(Func構造体)を取得します。
    • スタックフレームのサイズ (f->frame) を使ってSPを調整し、次のフレームに進みます。
    • retfromnewstackのような特殊なPC値(スタック拡張からの戻り点)を検出した場合、古いスタックブロックに切り替えてトレースを続行します。
    • クロージャのスタックフレームを識別し、適切にスキップするロジックも含まれています(ADDL $xxx, SP; RETパターンを検出)。
    • 関数名、オフセット、ソースファイル、行番号、引数などを整形して出力します。
  • sys·Caller: 指定されたレベルnの呼び出し元のPC、ファイル名、行番号を取得するGoランタイム関数です。tracebackと同様にスタックを遡るロジックを使用します。

src/runtime/386/vlop.s (新規追加)

このファイルは、Inferno OSのlibkern/vlop-386.sから移植された、386アーキテクチャ向けの64ビット整数演算のアセンブリルーチンです。

  • _mul64by32: 64ビット整数と32ビット整数の乗算を行います。結果は64ビットです。
  • _div64by32: 64ビット整数を32ビット整数で除算します。結果は32ビットの商と32ビットの剰余です。
    • これらのルーチンは、32ビットプロセッサ上で64ビット整数型(Goのint64, uint64)を効率的に扱うために不可欠です。

src/runtime/386/vlrt.c (新規追加)

このファイルは、Inferno OSのlibkern/vlrt-386.cから移植された、64ビット整数演算や浮動小数点数と64ビット整数の変換、その他のユーティリティ関数を含むCコードです。

  • Vlong構造体: 64ビット整数をlo(下位32ビット)とhi(上位32ビット)の2つのulongで表現する共用体を含む構造体です。
  • _d2v / _f2v: double / floatVlong(64ビット整数)に変換します。浮動小数点数の内部表現(IEEE 754)を解析し、ビットシフトとマスク操作で整数部分を抽出します。
  • _v2d / _v2f: Vlongdouble / floatに変換します。
  • dodiv / slowdodiv: 64ビット整数同士の除算と剰余演算のコアロジックです。slowdodivはビットごとの減算とシフトを繰り返す汎用的なアルゴリズムで、dodivは特定のケース(例えば、除数が32ビットに収まる場合)で最適化された_div64by32アセンブリルーチンを利用します。
  • _divvu / _modvu: 符号なし64ビット整数同士の除算と剰余。
  • _divv / _modv: 符号付き64ビット整数同士の除算と剰余。符号を考慮してvneg(符号反転)を適用します。
  • _rshav / _rshlv / _lshv: 64ビット整数の算術右シフト、論理右シフト、左シフト。
  • _andv / _orv / _xorv: 64ビット整数のビット論理演算。
  • _vpp / _vmm / _ppv / _mmv: 64ビット整数のインクリメント/デクリメント(前置/後置)。
  • _vasop: 複合代入演算子(+=, -=など)をサポートするための汎用関数。様々なデータ型(schar, uchar, short, ushort, int, uint, long, ulong, vlong, uvlong)に対して、指定された演算 (fn) を適用します。
  • 型変換関数: _p2v (ポインタからVlong), _sl2v (longからVlong), _ul2v (ulongからVlong) など、様々な基本型からVlongへの変換関数。
  • 比較関数: _eqv, _nev, _ltv, _lev, _gtv, _gev, _lov, _lsv, _hiv, _hsvなど、64ビット整数同士の比較を行う関数。

src/runtime/Makefile

ビルドシステムに関する変更です。

  • 386固有のオブジェクトファイル追加: OFILES_386変数にvlop.$Ovlrt.$Oが追加され、OFILES$(OFILES_$(GOARCH))として組み込まれるようになりました。これにより、386ビルド時にこれらのファイルがコンパイル・リンクされます。
  • SIZE変数の導入: SIZE_386=32, SIZE_amd64=64が定義され、CFLAGS-D_64BITが64ビットプラットフォームでのみ追加されるようになりました。これは、コンパイル時に32ビット/64ビットの区別を行うためのマクロ定義です。
  • O変数の導入: コンパイラ(8c for 386, 6c for amd64)を決定するためのO_386=8, O_amd64=6が定義されました。
  • ARツールの明示: AR=6arが明示的に定義されました。
  • mheapmapの動的選択: mheapmap64.$Omheapmap$(SIZE).$Oに変更され、32ビット/64ビットに応じて適切なmheapmap実装が選択されるようになりました。
  • クリーンアップコマンドの変更: rm -f *.[68] *.aとなり、386 (8) とamd64 (6) の両方のオブジェクトファイルをクリーンアップ対象に含めるようになりました。

src/runtime/amd64/asm.s

  • FLUSH関数の削除: この関数は単にRETするだけの空の関数であり、おそらくデバッグ目的で一時的に存在していたか、不要になったため削除されました。

src/runtime/iface.c

  • Sigt構造体のパディングに関するコメント追加: amd64での32ビットパディングに関するコメントが追加されました。
  • sys·ifaceT2I関数でのアラインメント計算の変更: rnd(wid, 8)rnd(wid, sizeof(uintptr))に変更されました。これは、ポインタのサイズ(32ビットシステムでは4バイト、64ビットシステムでは8バイト)に基づいてアラインメントを行うように修正されたことを意味します。これにより、クロスプラットフォームでの互換性が向上します。

src/runtime/linux/386/signal.c (新規追加)

このファイルは、Linux 386アーキテクチャ向けのシグナルハンドリングを実装しています。

  • dumpregs: シグナルコンテキスト(レジスタの状態)をダンプする関数です。デバッグ時にレジスタの値を確認するために使用されます。
  • sighandler: シグナルが発生したときに呼び出されるメインのシグナルハンドラです。
    • panickingフラグをチェックし、既にパニック状態であれば終了します。
    • シグナル番号とシグナル情報 (Siginfo)、コンテキスト (Ucontext) を受け取ります。
    • シグナル名、フォルトアドレス、PCなどを出力します。
    • gotraceback()が真の場合、traceback関数を呼び出してスタックトレースを出力します。
    • sys·Breakpoint()を呼び出し、デバッガがアタッチされていれば停止します。
    • 最終的にsys_Exit(2)でプロセスを終了します。
  • signalstack: シグナルハンドラが使用する代替スタックを設定します。
  • initsig: シグナルハンドラを初期化し、様々なシグナルに対してsigtramp(Goのシグナルハンドラへのエントリポイント)またはsigignoreを設定します。SA_ONSTACK, SA_SIGINFO, SA_RESTORERフラグを使用しています。

src/runtime/linux/{ => amd64}/signal.c (ファイル名変更と修正)

src/runtime/linux/signal.csrc/runtime/linux/amd64/signal.cにリネームされ、amd64固有のシグナルハンドリングコードとなりました。変更内容は主にコメントの修正と、sighandler関数内でpanicking = 1;が追加された点です。これは、パニック状態であることを明示的に設定し、二重パニックを防ぐためのものです。

src/runtime/linux/amd64/sys.s

  • sys·mmap関数から不要なレジスタ操作が削除されました。これは、MAP_ANONフラグの処理に関する古いロジックが不要になったためと考えられます。

src/runtime/linux/defs.c / src/runtime/linux/defs1.c

  • godefsツールに関するコメントから、386アーキテクチャ向けのdefs.h生成に関する記述が削除されました。これは、defs2.cが386向けの定義を生成するようになったためと考えられます。

src/runtime/linux/defs2.c (新規追加)

このファイルは、Linux 386アーキテクチャ向けのシステム定義(構造体、定数など)をgodefsツールで生成するためのCコードです。

  • PROT_NONE, PROT_READ, PROT_WRITE, PROT_EXEC (メモリ保護フラグ)
  • MAP_ANON, MAP_PRIVATE (mmapフラグ)
  • SA_RESTART, SA_ONSTACK, SA_RESTORER, SA_SIGINFO (sigactionフラグ)
  • Fpreg, Fpxreg, Xmmreg, Fpstate, Timespec, Timeval, Sigaction, Siginfo, Sigaltstack, Sigcontext, Ucontextなどの構造体定義
    • これらの定義は、GoランタイムがLinuxカーネルとインタフェースするために必要です。

src/runtime/malloc.c

  • defs.hのインクルード: malloc.cdefs.hをインクルードするようになりました。
  • mlookup関数でのprintfのフォーマット文字列の修正: nobj, n, npagesなどの変数を%D(Goのuint64フォーマット)で出力するように修正されました。これは、これらの変数が32ビット環境でも大きな値を取りうるため、正確な表示のためと考えられます。
  • SysAlloc関数でのsys_mmap呼び出しの修正: MAP_ANONフラグがLinuxで正しく扱われるように、fd引数が-1に設定されました。
  • stackalloc関数でのprintfのフォーマット文字列の修正: stacks.size%Dで出力するように修正されました。
  • 不要なenum定義の削除: PROT_NONE, MAP_ANONなどの定数定義がdefs.hに移動されたため、malloc.cから削除されました。

src/runtime/malloc.h

  • mheapmapの条件付きインクルード: #ifdef _64BITディレクティブが追加され、64ビット環境ではmheapmap64.hを、それ以外(32ビット)ではmheapmap32.hをインクルードするように変更されました。これにより、アーキテクチャに応じた適切なヒープマップ実装が選択されます。

src/runtime/mem.c

  • defs.hのインクルード: mem.cdefs.hをインクルードするようになりました。
  • 不要なenum定義の削除: PROT_NONE, MAP_ANONなどの定数定義がdefs.hに移動されたため、mem.cから削除されました。

src/runtime/mgc0.c

  • sweepspan関数でのprintfのフォーマット文字列の修正: s->npages<<PageShift%Dで出力するように修正されました。

src/runtime/mheapmap32.c (新規追加)

このファイルは、32ビットアーキテクチャ向けのヒープマップ(MHeapMap)の実装です。

  • MHeapMap: ページIDからMSpan(メモリ領域の管理単位)へのマッピングを管理する3レベルのラディックスツリーです。
  • MHeapMap_Init: ヒープマップを初期化します。
  • MHeapMap_Get / MHeapMap_GetMaybe: ページIDから対応するMSpanを取得します。
  • MHeapMap_Set: ページIDにMSpanを設定します。
  • MHeapMap_Preallocate: 指定されたページ範囲のストレージを事前に割り当てます。これにより、GetSet呼び出し時にnilポインタチェックが不要になります。
    • 32ビットアドレス空間を効率的にマッピングするために、MHeapMap_Level1BitsMHeapMap_Level2Bitsがそれぞれ10ビットに設定されています。

src/runtime/mheapmap32.h (新規追加)

このファイルは、32ビットアーキテクチャ向けのヒープマップのデータ構造と定数を定義します。

  • MHeapMap_Level1Bits, MHeapMap_Level2Bits, MHeapMap_TotalBitsなどの定数: ページIDを3レベルのラディックスツリーでマッピングするためのビット数とマスクを定義します。
  • MHeapMap構造体: allocator関数ポインタと、レベル1のノードポインタの配列pを含みます。
  • MHeapMapNode2構造体: レベル2のノードで、MSpanへのポインタを格納します。配列のサイズは1 << MHeapMap_Level2Bits、つまり2^10 = 1024です。
  • 効率的なマッピング: この2レベルのラディックスツリー構造により、Goランタイムは32ビットアドレス空間内の任意のページIDに対応するMSpanを、少ないメモリフットプリントと高速なルックアップで効率的に管理できます。

その他のファイル

  • src/runtime/print.c, src/runtime/proc.c, src/runtime/runtime.c, src/runtime/runtime.h, src/runtime/symtab.cにも小さな変更(主にprintfのフォーマット修正や、defs.hのインクルードなど)が含まれています。

これらの変更は、Goランタイムが386アーキテクチャ上でより堅牢に動作し、C言語との連携を可能にするための基盤を構築する上で不可欠なものでした。特に、低レベルなアセンブリコード、メモリ管理、シグナルハンドリング、そしてInferno OSからの64ビット演算ルーチンの移植は、Goの初期の機能拡張において重要な役割を果たしました。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更箇所は多岐にわたりますが、特にGoランタイムの386アーキテクチャへの適応とC言語との連携、そして64ビット演算のサポートを示す以下のファイルとコードスニペットが挙げられます。

1. src/runtime/386/asm.s - ランタイムのブートストラップとTLS設定

// src/runtime/386/asm.s (新規追加)
TEXT _rt0_386(SB),7,$0
    // copy arguments forward on an even stack
    MOVL    0(SP), AX        // argc
    LEAL    4(SP), BX        // argv
    SUBL    $128, SP        // plenty of scratch
    ANDL    $~7, SP

    // ... (argument saving) ...

    CALL    ldt0setup(SB)

    // set up %fs to refer to that ldt entry
    MOVL    $(7*8+7), AX
    MOVW    AX, FS

    // store through it, to make sure it works
    MOVL    $0x123, 0(FS)
    MOVL    tls0(SB), AX
    CMPL    AX, $0x123
    JEQ ok
    MOVL    AX, 0
ok:

    // set up m and g "registers"
    // g is 0(FS), m is 4(FS)
    LEAL    g0(SB), CX
    MOVL    CX, 0(FS)
    LEAL    m0(SB), AX
    MOVL    AX, 4(FS)

    // save m->g0 = g0
    MOVL    CX, 0(AX)

    // ... (stack setup, runtime initialization calls) ...

    CALL    schedinit(SB)

    // create a new goroutine to start program
    PUSHL    $mainstart(SB)    // entry
    PUSHL    $8    // arg size
    CALL    sys·newproc(SB)
    POPL    AX
    POPL    AX

    // start this M
    CALL    mstart(SB)

    INT $3
    RET

2. src/runtime/386/closure.c - クロージャの動的生成

// src/runtime/386/closure.c (新規追加)
void
sys·closure(int32 siz, byte *fn, byte *arg0)
{
    byte *p, *q, **ret;
    int32 i, n;
    int32 pcrel;

    if(siz < 0 || siz%4 != 0)
        throw("bad closure size");

    ret = (byte**)((byte*)&arg0 + siz);

    if(siz > 100) {
        // TODO(rsc): implement stack growth preamble?
        throw("closure too big");
    }

    // compute size of new fn.
    // must match code laid out below.
    n = 6+5+2;    // SUBL MOVL MOVL
    if(siz <= 4*4)
        n += 1*siz/4;    // MOVSL MOVSL...
    else
        n += 6+2;    // MOVL REP MOVSL
    n += 5;    // CALL
    n += 6+1;    // ADDL RET

    // store args aligned after code, so gc can find them.
    n += siz;
    if(n%4)
        n += 4 - n%4;

    p = mal(n);
    *ret = p;
    q = p + n - siz;
    mcpy(q, (byte*)&arg0, siz);

    // SUBL $siz, SP
    *p++ = 0x81;
    *p++ = 0xec;
    *(uint32*)p = siz;
    p += 4;

    // MOVL $q, SI
    *p++ = 0xbe;
    *(byte**)p = q;
    p += 4;

    // MOVL SP, DI
    *p++ = 0x89;
    *p++ = 0xe7;

    if(siz <= 4*4) {
        for(i=0; i<siz; i+=4) {
            // MOVSL
            *p++ = 0xa5;
        }
    } else {
        // MOVL $(siz/8), CX  [32-bit immediate siz/4]
        *p++ = 0xc7;
        *p++ = 0xc1;
        *(uint32*)p = siz/4;
        p += 4;

        // REP; MOVSL
        *p++ = 0xf3;
        *p++ = 0xa5;
    }

    // call fn
    pcrel = fn - (p+5);
    // direct call with pc-relative offset
    // CALL fn
    *p++ = 0xe8;
    *(int32*)p = pcrel;
    p += 4;

    // ADDL $siz, SP
    *p++ = 0x81;
    *p++ = 0xc4;
    *(uint32*)p = siz;
    p += 4;

    // RET
    *p++ = 0xc3;

    if(p > q)
        throw("bad math in sys.closure");
}

3. src/runtime/386/vlrt.c - 64ビット整数演算のソフトウェア実装

// src/runtime/386/vlrt.c (新規追加)
typedef    struct    Vlong    Vlong;
struct    Vlong
{
    union
    {
        struct
        {
            ulong    lo;
            ulong    hi;
        };
        struct
        {
            ushort    lols;
            ushort    loms;
            ushort    hils;
            ushort    hims;
        };
    };
};

// ... (various conversion functions like _d2v, _f2v, _v2d, _v2f) ...

static void
dodiv(Vlong num, Vlong den, Vlong *qp, Vlong *rp)
{
    ulong n;
    Vlong x, q, r;

    // ... (handle cases where den > num) ...

    if(den.hi != 0){
        // ... (optimized division for non-zero den.hi) ...
    } else {
        if(num.hi >= den.lo){
            q.hi = n = num.hi/den.lo;
            num.hi -= den.lo*n;
        } else {
            q.hi = 0;
        }
        q.lo = _div64by32(num, den.lo, &r.lo); // Calls assembly helper
        r.hi = 0;
    }
    // ... (assign results to qp and rp) ...
}

// ... (various arithmetic and comparison functions for Vlong) ...

4. src/runtime/malloc.h - 32/64ビットヒープマップの条件付き選択

// src/runtime/malloc.h (変更)
#ifdef _64BIT
#include "mheapmap64.h"
#else
#include "mheapmap32.h"
#endif

5. src/runtime/mheapmap32.h - 32ビットヒープマップの定義

// src/runtime/mheapmap32.h (新規追加)
// Free(v) must be able to determine the MSpan containing v.
// The MHeapMap is a 2-level radix tree mapping page numbers to MSpans.

typedef struct MHeapMapNode2 MHeapMapNode2;

enum
{
    // 32 bit address - 12 bit page size = 20 bits to map
    MHeapMap_Level1Bits = 10,
    MHeapMap_Level2Bits = 10,

    MHeapMap_TotalBits =
        MHeapMap_Level1Bits +
        MHeapMap_Level2Bits,

    MHeapMap_Level1Mask = (1<<MHeapMap_Level1Bits) - 1,
    MHeapMap_Level2Mask = (1<<MHeapMap_Level2Bits) - 1,
};

struct MHeapMap
{
    void *(*allocator)(uintptr);
    MHeapMapNode2 *p[1<<MHeapMap_Level1Bits];
};

struct MHeapMapNode2
{
    MSpan *s[1<<MHeapMap_Level2Bits];
};

コアとなるコードの解説

1. src/runtime/386/asm.s の解説

このアセンブリコードは、Goプログラムが386システム上で起動する際の初期化プロセスを定義しています。

  • _rt0_386: これはGoランタイムの「ゼロランタイム」と呼ばれる部分で、OSがプログラムをロードした後に最初に実行されるコードです。
    • argcargvの処理: C言語のmain関数と同様に、コマンドライン引数を取得し、Goランタイムがアクセスできる場所に保存します。
    • スタックアラインメント: SUBL $128, SPANDL $~7, SPは、スタックポインタを128バイト減らし、さらに8バイト境界にアラインメントしています。これは、Goの関数呼び出し規約や特定の命令(例: SSE命令)がスタックアラインメントを要求するためです。
    • TLS (Thread Local Storage) の設定: ldt0setupの呼び出しと%fsレジスタの設定は、386アーキテクチャにおけるTLSの重要な側面です。Goランタイムは、現在のGoroutine (g) とMachine (m) のポインタをTLSに格納することで、これらの重要なランタイムデータ構造に非常に高速にアクセスできるようにします。0(FS)4(FS)は、%fsセグメントレジスタをベースとしたオフセットで、それぞれgmのポインタを指します。これは、Goのスケジューラが頻繁にGoroutineのコンテキストを切り替える際に、現在のGoroutineとMachineの情報を効率的に取得するために不可欠です。
    • 初期GoroutineとMachine: g0m0は、Goランタイムが起動する際に使用される特別なGoroutineとMachineです。g0はシステムコールやGCなどのランタイム内部処理を実行するためのスタックを持ち、m0は初期のOSスレッドを表します。
    • mainstartの起動: sys·newprocは、Goのスケジューラを使って新しいGoroutineを起動するランタイム関数です。ここでは、Goプログラムのユーザーコードのエントリポイントであるmainstart関数を新しいGoroutineとして起動しています。これにより、Goプログラムの並行実行が開始されます。
    • mstart: これは現在のOSスレッド(M)をGoスケジューラのループに参加させる関数です。mstartが呼び出されると、OSスレッドはGoスケジューラによって管理され、Goroutineの実行を開始します。

2. src/runtime/386/closure.c の解説

このCコードは、Goのクロージャを386アーキテクチャ上で動的に生成するメカニズムを実装しています。

  • 動的なコード生成: Goのクロージャは、その定義されたスコープの外部変数を「キャプチャ」します。このコミットでは、クロージャが呼び出されたときにこれらのキャプチャされた変数を元の関数に渡すための小さなアセンブリコードスニペットをメモリ上に動的に生成しています。
  • スタック操作: 生成されるアセンブリコードは、以下の主要なステップを実行します。
    • SUBL $siz, SP: クロージャがキャプチャした変数をスタックに配置するために、スタックポインタをsizバイト分減らします。
    • MOVL $q, SIMOVL SP, DI: キャプチャされた変数の元の場所(q)と、それらをコピーする先のスタック上の場所(SP、DIレジスタにロード)を設定します。
    • MOVSL または REP; MOVSL: sizのサイズに応じて、キャプチャされた変数をSIからDIへコピーします。REP; MOVSLは、大きなデータブロックを効率的にコピーするためのアセンブリ命令です。
    • CALL fn: キャプチャされた変数をスタックに配置した後、元の関数 (fn) を呼び出します。
    • ADDL $siz, SP: 関数呼び出しが完了した後、スタックポインタを元に戻し、クロージャが使用したスタック領域を解放します。
    • RET: 呼び出し元に戻ります。
  • siz > 100 の制限: 初期実装のため、キャプチャされる変数の合計サイズが100バイトを超えるクロージャはサポートされていません。これは、動的に生成されるコードの複雑さや、スタック拡張との連携がまだ完全ではなかったことを示唆しています。
  • pcrel = fn - (p+5): CALL命令は通常、相対アドレス指定を使用します。pcrelは、CALL命令の次の命令のアドレスからターゲット関数fnまでの相対オフセットを計算しています。

3. src/runtime/386/vlrt.c の解説

このCコードは、32ビットシステム上で64ビット整数演算をソフトウェアでエミュレートするためのルーチンを提供します。これは、Goがint64uint64のような64ビット整数型をサポートするために不可欠です。

  • Vlong構造体: 32ビットシステムでは、64ビット整数は2つの32ビット整数(hilo)のペアとして扱われます。Vlong構造体はこの表現をカプセル化しています。
  • dodiv関数: 64ビット整数同士の除算と剰余の主要なロジックを含みます。
    • 最適化されたケース: den.hiがゼロの場合(つまり、除数が32ビットに収まる場合)、_div64by32というアセンブリヘルパー関数を呼び出しています。これは、アセンブリレベルで最適化された32ビット除算命令を利用することで、パフォーマンスを向上させています。
    • 汎用的なケース: den.hiがゼロでない場合(除数が64ビットである場合)、slowdodivというより汎用的なソフトウェア実装にフォールバックします。この実装は、ビットごとの減算とシフトを繰り返すことで除算を行います。これは、ハードウェアが直接64ビット除算をサポートしない32ビットシステムで、64ビット除算を実現するための標準的な手法です。
  • 型変換とビット操作: _d2v, _f2v, _v2d, _v2fなどの関数は、浮動小数点数と64ビット整数の間の変換を行います。これらはIEEE 754浮動小数点数の内部表現を理解し、ビットシフトやマスク操作を駆使して変換を実現しています。
  • その他の演算: _rshav (算術右シフト), _lshv (左シフト), _andv (ビットAND) など、様々な64ビット整数に対するビット操作や算術演算がソフトウェアで実装されています。これらは、Goの言語仕様で定義されている64ビット整数演算を、32ビットハードウェア上で正しく実行するために必要です。

4. src/runtime/malloc.h の解説

この変更は、Goランタイムのメモリ管理システムが32ビットと64ビットのアーキテクチャ間で異なるヒープマップ実装を動的に選択できるようにするためのものです。

  • 条件付きコンパイル: #ifdef _64BITプリプロセッサディレクティブを使用することで、コンパイル時にターゲットアーキテクチャが64ビットであるかどうかに応じて、異なるヘッダーファイル(mheapmap64.hまたはmheapmap32.h)をインクルードします。
  • MHeapMapの役割: MHeapMapは、Goのガベージコレクタがメモリを効率的に管理するために使用する重要なデータ構造です。これは、仮想メモリのページ番号から、そのページが属するMSpan(メモリ領域の管理単位)へのマッピングを提供します。このマッピングは、GCがオブジェクトのポインタをスキャンする際に、そのポインタがどのメモリ領域に属し、どのようなオブジェクトであるかを迅速に判断するために使用されます。
  • 32ビットと64ビットの違い: 32ビットシステムと64ビットシステムでは、アドレス空間のサイズが大きく異なります。そのため、ヒープマップの実装も、アドレス空間の大きさに合わせて最適化される必要があります。このコミットでは、32ビットシステム向けに特化したmheapmap32.hmheapmap32.cが導入され、より効率的なメモリマッピングが可能になりました。

5. src/runtime/mheapmap32.h の解説

このヘッダーファイルは、32ビットシステム向けのMHeapMapの構造と定数を定義しています。

  • ラディックスツリー: MHeapMapは、ページ番号をMSpanにマッピングするために2レベルのラディックスツリー(基数木)を使用します。
    • MHeapMap_Level1Bits = 10: ページIDの上位10ビットがレベル1のインデックスとして使用されます。
    • MHeapMap_Level2Bits = 10: ページIDの次の10ビットがレベル2のインデックスとして使用されます。
    • MHeapMap_TotalBits = 20: 32ビットアドレス空間から12ビットのページサイズを引くと、20ビットのページIDが残ります。この20ビットが2つのレベル(10ビット + 10ビット)に分割されます。
  • MHeapMap構造体: p配列は、レベル1のノード(MHeapMapNode2へのポインタ)を格納します。配列のサイズは1 << MHeapMap_Level1Bits、つまり2^10 = 1024です。
  • MHeapMapNode2構造体: s配列は、MSpanへのポインタを格納します。配列のサイズは1 << MHeapMap_Level2Bits、つまり2^10 = 1024です。
  • 効率的なマッピング: この2レベルのラディックスツリー構造により、Goランタイムは32ビットアドレス空間内の任意のページIDに対応するMSpanを、少ないメモリフットプリントと高速なルックアップで効率的に管理できます。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード: この解説の主要な情報源は、GitHub上のGo言語リポジトリにある実際のコミットと関連ファイルです。
  • Go言語の初期開発に関するブログ記事やメーリングリストのアーカイブ: Go言語の初期の設計決定や実装の背景について言及している可能性があります。
  • Inferno OSのドキュメントとソースコード: vlop.svlrt.cの元のコンテキストを理解するために参照しました。
  • x86アセンブリ言語の資料: 386アセンブリコードの理解のために一般的なx86アセンブリの知識を使用しました。
  • オペレーティングシステムの概念に関する一般的な知識: シグナルハンドリング、メモリ管理、プロセス/スレッド管理などのOSの基本概念を理解するために使用しました。
  • C言語の知識: クロージャのC言語での実装や、Cgoの概念を理解するために使用しました。
  • Google検索: 特定のキーワード(例: "Inferno OS libkern", "Go runtime 386", "Go cgo early history")で追加情報を検索しました。
    • google_web_searchツールを使用しました。