[インデックス 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互換プロセッサ)上での基本的な動作を確立することが重要な課題でした。
変更の主な背景は以下の点にあります。
- 386アーキテクチャのサポート強化: Goはクロスプラットフォームを志向していますが、初期段階では特定のアーキテクチャでの動作を安定させることが優先されました。386は当時の一般的な32ビットシステムであり、その上でGoランタイムが適切に機能することが求められました。
- Cプログラムとの連携: Go言語はC言語との相互運用性(cgo)を重視しており、既存のCライブラリやシステムコールをGoから利用できることが設計目標の一つでした。このコミットは、「tiny c programs」を実行できるようになったと明記されており、GoランタイムがCコードを呼び出し、その実行環境をサポートするための基盤を構築していることを示唆しています。これは、Goが単独で動作するだけでなく、既存のシステムと連携するための重要なステップでした。
- ランタイムの機能拡充: Goランタイムは、ガベージコレクション、スケジューラ、メモリ管理、スタック管理など、Goプログラムの実行に必要な低レベルな機能を提供します。このコミットでは、これらの機能が386アーキテクチャに合わせて実装または改善されており、特にスタックトレース、クロージャ、64ビット演算といった、より複雑な言語機能のサポートに向けた準備が進められています。
- 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) の実装には、セグメントレジスタ(例:
FS
、GS
)が利用されることがあります。このコミットの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
/sysexit
やsyscall
/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.s
とvlrt.c
は、Infernoのlibkern
から移植されたものであり、特に32ビットシステム上での64ビット整数演算(乗算、除算、シフトなど)を効率的に行うためのルーチンが含まれています。これは、Goが64ビット整数型(int64
,uint64
)をサポートするために不可欠な要素でした。
5. シグナルハンドリング
- OSシグナル: プロセスに非同期的に通知されるイベント(例: セグメンテーション違反、割り込み、タイマー)。
- シグナルハンドラ: シグナルが発生したときに実行される関数です。Goランタイムは、クラッシュ時のスタックトレース出力や、特定のシグナルに対するカスタム動作のためにシグナルハンドラを設定します。
sigaction
: POSIXシステムでシグナルハンドラを設定するためのシステムコール。SA_ONSTACK
、SA_SIGINFO
、SA_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)
にg
、4(FS)
にm
を格納しています。g0
(システムGoroutine) とm0
(初期Machine) を設定し、m0->g0
をg0
に設定します。- OSスタックから初期スタック(
istack
)を作成します。 check
,args
,osinit
,schedinit
などのランタイム初期化関数を呼び出します。mainstart
関数を新しいGoroutineとしてsys·newproc
で起動します。mstart
を呼び出し、現在のM(OSスレッド)のスケジューリングループを開始します。
- 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
)を効率的に扱うために不可欠です。
- これらのルーチンは、32ビットプロセッサ上で64ビット整数型(Goの
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
/float
をVlong
(64ビット整数)に変換します。浮動小数点数の内部表現(IEEE 754)を解析し、ビットシフトとマスク操作で整数部分を抽出します。_v2d
/_v2f
:Vlong
をdouble
/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.$O
とvlrt.$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.$O
がmheapmap$(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.c
がsrc/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.c
がdefs.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.c
がdefs.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
: 指定されたページ範囲のストレージを事前に割り当てます。これにより、Get
やSet
呼び出し時にnil
ポインタチェックが不要になります。- 32ビットアドレス空間を効率的にマッピングするために、
MHeapMap_Level1Bits
とMHeapMap_Level2Bits
がそれぞれ10ビットに設定されています。
- 32ビットアドレス空間を効率的にマッピングするために、
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がプログラムをロードした後に最初に実行されるコードです。argc
とargv
の処理: C言語のmain
関数と同様に、コマンドライン引数を取得し、Goランタイムがアクセスできる場所に保存します。- スタックアラインメント:
SUBL $128, SP
とANDL $~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
セグメントレジスタをベースとしたオフセットで、それぞれg
とm
のポインタを指します。これは、Goのスケジューラが頻繁にGoroutineのコンテキストを切り替える際に、現在のGoroutineとMachineの情報を効率的に取得するために不可欠です。 - 初期GoroutineとMachine:
g0
とm0
は、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, SI
とMOVL 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がint64
やuint64
のような64ビット整数型をサポートするために不可欠です。
Vlong
構造体: 32ビットシステムでは、64ビット整数は2つの32ビット整数(hi
とlo
)のペアとして扱われます。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.h
とmheapmap32.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言語公式ドキュメント: Go言語のランタイム、メモリ管理、cgoに関する公式ドキュメントは、これらの概念をより深く理解するための出発点となります。
- Inferno Operating System:
vlop.s
とvlrt.c
の起源であるInferno OSに関する情報。- Inferno Operating System
- Inferno OS Source Code (Google Code Archive) - このコミットで参照されている
libkern
のソースコードを見つけることができるかもしれません。
- Intel 386 Architecture: 386プロセッサのアーキテクチャに関する詳細情報。
参考にした情報源リンク
- Go言語のソースコード: この解説の主要な情報源は、GitHub上のGo言語リポジトリにある実際のコミットと関連ファイルです。
- Go言語の初期開発に関するブログ記事やメーリングリストのアーカイブ: Go言語の初期の設計決定や実装の背景について言及している可能性があります。
- Inferno OSのドキュメントとソースコード:
vlop.s
とvlrt.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互換プロセッサ)上での基本的な動作を確立することが重要な課題でした。
変更の主な背景は以下の点にあります。
- 386アーキテクチャのサポート強化: Goはクロスプラットフォームを志向していますが、初期段階では特定のアーキテクチャでの動作を安定させることが優先されました。386は当時の一般的な32ビットシステムであり、その上でGoランタイムが適切に機能することが求められました。
- Cプログラムとの連携: Go言語はC言語との相互運用性(cgo)を重視しており、既存のCライブラリやシステムコールをGoから利用できることが設計目標の一つでした。このコミットは、「tiny c programs」を実行できるようになったと明記されており、GoランタイムがCコードを呼び出し、その実行環境をサポートするための基盤を構築していることを示唆しています。これは、Goが単独で動作するだけでなく、既存のシステムと連携するための重要なステップでした。
- ランタイムの機能拡充: Goランタイムは、ガベージコレクション、スケジューラ、メモリ管理、スタック管理など、Goプログラムの実行に必要な低レベルな機能を提供します。このコミットでは、これらの機能が386アーキテクチャに合わせて実装または改善されており、特にスタックトレース、クロージャ、64ビット演算といった、より複雑な言語機能のサポートに向けた準備が進められています。
- 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) の実装には、セグメントレジスタ(例:
FS
、GS
)が利用されることがあります。このコミットの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
/sysexit
やsyscall
/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.s
とvlrt.c
は、Infernoのlibkern
から移植されたものであり、特に32ビットシステム上での64ビット整数演算(乗算、除算、シフトなど)を効率的に行うためのルーチンが含まれています。これは、Goが64ビット整数型(int64
,uint64
)をサポートするために不可欠な要素でした。
5. シグナルハンドリング
- OSシグナル: プロセスに非同期的に通知されるイベント(例: セグメンテーション違反、割り込み、タイマー)。
- シグナルハンドラ: シグナルが発生したときに実行される関数です。Goランタイムは、クラッシュ時のスタックトレース出力や、特定のシグナルに対するカスタム動作のためにシグナルハンドラを設定します。
sigaction
: POSIXシステムでシグナルハンドラを設定するためのシステムコール。SA_ONSTACK
、SA_SIGINFO
、SA_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)
にg
、4(FS)
にm
を格納しています。g0
(システムGoroutine) とm0
(初期Machine) を設定し、m0->g0
をg0
に設定します。- OSスタックから初期スタック(
istack
)を作成します。 check
,args
,osinit
,schedinit
などのランタイム初期化関数を呼び出します。mainstart
関数を新しいGoroutineとしてsys·newproc
で起動します。mstart
を呼び出し、現在のM(OSスレッド)のスケジューリングループを開始します。
- 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
)を効率的に扱うために不可欠です。
- これらのルーチンは、32ビットプロセッサ上で64ビット整数型(Goの
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
/float
をVlong
(64ビット整数)に変換します。浮動小数点数の内部表現(IEEE 754)を解析し、ビットシフトとマスク操作で整数部分を抽出します。_v2d
/_v2f
:Vlong
をdouble
/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.$O
とvlrt.$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.$O
がmheapmap$(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.c
がsrc/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.c
がdefs.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.c
がdefs.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
: 指定されたページ範囲のストレージを事前に割り当てます。これにより、Get
やSet
呼び出し時にnil
ポインタチェックが不要になります。- 32ビットアドレス空間を効率的にマッピングするために、
MHeapMap_Level1Bits
とMHeapMap_Level2Bits
がそれぞれ10ビットに設定されています。
- 32ビットアドレス空間を効率的にマッピングするために、
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がプログラムをロードした後に最初に実行されるコードです。argc
とargv
の処理: C言語のmain
関数と同様に、コマンドライン引数を取得し、Goランタイムがアクセスできる場所に保存します。- スタックアラインメント:
SUBL $128, SP
とANDL $~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
セグメントレジスタをベースとしたオフセットで、それぞれg
とm
のポインタを指します。これは、Goのスケジューラが頻繁にGoroutineのコンテキストを切り替える際に、現在のGoroutineとMachineの情報を効率的に取得するために不可欠です。 - 初期GoroutineとMachine:
g0
とm0
は、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, SI
とMOVL 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がint64
やuint64
のような64ビット整数型をサポートするために不可欠です。
Vlong
構造体: 32ビットシステムでは、64ビット整数は2つの32ビット整数(hi
とlo
)のペアとして扱われます。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.h
とmheapmap32.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言語公式ドキュメント: Go言語のランタイム、メモリ管理、cgoに関する公式ドキュメントは、これらの概念をより深く理解するための出発点となります。
- Inferno Operating System:
vlop.s
とvlrt.c
の起源であるInferno OSに関する情報。- Inferno Operating System
- Inferno OS Source Code (Google Code Archive) - このコミットで参照されている
libkern
のソースコードを見つけることができるかもしれません。
- Intel 386 Architecture: 386プロセッサのアーキテクチャに関する詳細情報。
参考にした情報源リンク
- Go言語のソースコード: この解説の主要な情報源は、GitHub上のGo言語リポジトリにある実際のコミットと関連ファイルです。
- Go言語の初期開発に関するブログ記事やメーリングリストのアーカイブ: Go言語の初期の設計決定や実装の背景について言及している可能性があります。
- Inferno OSのドキュメントとソースコード:
vlop.s
とvlrt.c
の元のコンテキストを理解するために参照しました。 - x86アセンブリ言語の資料: 386アセンブリコードの理解のために一般的なx86アセンブリの知識を使用しました。
- オペレーティングシステムの概念に関する一般的な知識: シグナルハンドリング、メモリ管理、プロセス/スレッド管理などのOSの基本概念を理解するために使用しました。
- C言語の知識: クロージャのC言語での実装や、Cgoの概念を理解するために使用しました。
- Google検索: 特定のキーワード(例: "Inferno OS libkern", "Go runtime 386", "Go cgo early history")で追加情報を検索しました。
google_web_search
ツールを使用しました。