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

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

このコミットは、Go言語のリンカをGo言語自体で書き直すプロジェクトの初期スケルトンを導入するものです。src/cmd/link ディレクトリ以下に多数の新規ファイルが追加されており、新しいリンカの基本的な構造と処理フローが定義されています。

コミット

commit 146897b031c00021fe78c0a9d76861cf5e27c5ec
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jan 9 19:29:10 2014 -0500

    cmd/link: intial skeleton of linker written in Go
    
    R=iant
    CC=golang-codereviews
    https://golang.org/cl/48870044

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

https://github.com/golang/go/commit/146897b031c00021fe78c0a9d76861cf5e27c5ec

元コミット内容

cmd/link: intial skeleton of linker written in Go

このコミットは、GoリンカのGo言語による初期スケルトンを導入します。

変更の背景

Go言語のリンカは、Go 1.3(2014年6月リリース)以前はPlan 9のCコンパイラツールチェーンの一部としてC言語で書かれていました。しかし、このC言語で書かれたリンカは、Goプログラムのビルド時間におけるボトルネックの一つとして認識されていました。ビルド時間の改善、並列処理の可能性の拡大、そしてGoツールチェーン全体の自己完結性を高める(Cコードベースを削減する)ことを目的として、リンカをGo言語で書き直すという大規模なプロジェクトが開始されました。

このコミットは、そのGo言語によるリンカの再実装の初期段階にあたります。リンカの機能をよりシンプルにするために、既存のリンカの多くの機能が liblink ライブラリに移行され、Goでの再実装が現実的なものとなりました。この再実装は、Go 1.3で本格的に始まり、Go 1.4以降も継続的に改善が加えられていくことになります。

前提知識の解説

このコミットを理解するためには、以下の概念についての基本的な知識が必要です。

  • リンカ (Linker): コンパイラによって生成されたオブジェクトファイル(機械語コードとデータを含む)を結合し、実行可能なプログラムやライブラリを生成するツールです。シンボル解決(未定義の参照を定義に結びつける)や、メモリレイアウトの決定(コードやデータをメモリ上のどこに配置するか)といった重要な役割を担います。
  • オブジェクトファイル: ソースコードがコンパイルされた後の中間形式のファイルです。通常、機械語コード、データ、シンボルテーブル(定義された関数や変数の名前とアドレス)、および再配置情報(他のオブジェクトファイルやライブラリからの参照を解決するための情報)を含みます。
  • シンボル (Symbol): プログラム内の関数や変数などの識別子です。リンカはシンボル名を使って、異なるオブジェクトファイル間で参照される関数や変数のアドレスを解決します。
  • 再配置 (Relocation): オブジェクトファイル内のコードやデータが、最終的な実行可能ファイル内のどこに配置されるかによってアドレスが変化する場合に、そのアドレスを修正するプロセスです。リンカがこの処理を行います。
  • セグメント (Segment) とセクション (Section): 実行可能ファイルは通常、論理的なブロックに分割されます。
    • セグメント: オペレーティングシステムがメモリにロードする際の最小単位であり、通常はページ境界にアラインされます。例えば、コードを含む「テキストセグメント」や、初期化されたデータを含む「データセグメント」などがあります。
    • セクション: セグメント内のより細かい論理的な区分です。例えば、テキストセグメント内には.text(実行可能コード)、.rodata(読み取り専用データ)などのセクションが含まれます。
  • debug/goobj パッケージ: Goのオブジェクトファイル形式を解析するためのパッケージです。このコミットでは、Goのオブジェクトファイルからシンボル情報などを読み込むために利用されています。
  • _rt0_go: Goプログラムのエントリポイントとなるランタイム関数です。プログラムの初期化処理を行い、main.main関数を呼び出します。

技術的詳細

このコミットは、Go言語で書かれた新しいリンカの基本的なアーキテクチャを定義しています。主要な構造体は Prog であり、これは実行可能イメージを構築するための状態を保持します。

新しいリンカの処理フローは、Prog 構造体の link メソッドに集約されており、以下のステップで構成されます。

  1. init(): リンカの初期設定を行います。ターゲットのOS (GOOS)、アーキテクチャ (GOARCH)、出力ファイル形式 (Format) を設定し、対応するフォーマッタ(例: Darwinの場合はMach-Oフォーマッタ)を初期化します。
  2. scan(mainFile): プログラムを構成するすべてのパッケージをスキャンします。mainFileで指定されたメインパッケージから開始し、インポートされたパッケージを再帰的に読み込みます。この段階で、各パッケージのシンボル情報(goobj.Package)を読み込み、Prog内のSymsマップに登録します。未定義のシンボル(Missing)も追跡されます。
    • scanFile関数は、指定されたGoオブジェクトファイルを開き、debug/goobj.Parseを使用してその内容を解析します。
    • シンボルのバージョン情報 (gs.Version) や再配置情報 (gs.Reloc) が適切に更新されます。
    • SBSSSNOPTRBSSのようなデータを持つセクションがSDATASNOPTRDATAに修正されるワークアラウンドが含まれています。これは、当時のGoオブジェクトファイル形式の課題を示唆しています。
  3. dead(): デッドコード(到達不能なコードやデータ)の削除を行います。このコミットの時点では、dead.go内のdeadメソッドは空のスケルトンですが、将来的にこの機能が実装されることを示しています。
  4. runtime(): ランタイムがアクセスするデータ構造の生成を行います。runtime.go内のruntimeメソッドもこの時点では空のスケルトンです。
  5. layout(): シンボルをセクションに、セクションをセグメントに配置し、それらに仮想アドレスとファイルオフセットを割り当てます。
    • layout変数で、Goバイナリが持つ可能性のあるセクションの固定セットと、それらが属するセグメント(text, dataなど)が定義されています。
    • layoutByKindは、goobj.SymKindからlayoutSectionへのマッピングを提供し、シンボルの種類に基づいて適切なセクションに配置できるようにします。
    • round関数は、指定されたアラインメントにアドレスを丸めるために使用されます。
    • segAlign (4096バイト) は、セグメントのアラインメントとして設定されています。
  6. load(): パッケージファイルからコードとデータの断片を最終的なイメージにロードし、再配置を適用します。
    • seg.Dataにセグメントのイメージを格納するためのバイトスライスが割り当てられます。
    • loadPackage関数は、各パッケージのオブジェクトファイルを開き、シンボルのデータ (sym.Data) を対応するセグメントのDataスライスに読み込みます。
    • relocateSym関数は、シンボルに適用される再配置処理を行います。D_ADDR(絶対アドレス)やD_PCREL(PC相対アドレス)などの再配置タイプが定義されており、ターゲットシンボルのアドレスに基づいてデータが修正されます。この時点では、再配置タイプはamd64アーキテクチャに固有の番号がハードコードされていますが、将来的にdebug/goobjからロードされるべきだとコメントされています。
  7. debug(): デバッグデータ構造の生成を行います。debug.go内のdebugメソッドもこの時点では空のスケルトンです。
  8. write(w): 最終的な実行可能ファイルを指定されたio.Writerに書き出します。p.Entry(プログラムのエントリポイント)は_rt0_goシンボルのアドレスに設定されます。

このコミットでは、リンカの各段階が独立したファイル(dead.go, debug.go, layout.go, load.go, scan.go, write.go)に分割されており、モジュール化された設計が採用されています。また、link_test.goprog_test.goにテストコードが追加されており、リンカの基本的な機能とProg構造体の動作が検証されています。

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

このコミットでは、主に以下のファイルが新規作成されています。

  • src/cmd/go/pkg.go: cmd/linkがGoツールとして認識されるようにgoToolsマップにエントリが追加されています。
  • src/cmd/link/dead.go: デッドコード削除のスケルトン。
  • src/cmd/link/debug.go: デバッグデータ生成のスケルトン。
  • src/cmd/link/layout.go: 実行可能イメージのメモリレイアウトとアドレス割り当てを定義。layout配列とlayoutByKindマップ、Prog.layout()メソッドが含まれます。
  • src/cmd/link/link_test.go: 新しいリンカの基本的なテストケース。TestLinkHello関数でhello.6オブジェクトファイルをリンクするテストが含まれます。
  • src/cmd/link/load.go: パッケージファイルからのコードとデータのロード、および再配置の適用を処理。Prog.load()loadPackage()relocateSym()メソッドが含まれます。
  • src/cmd/link/main.go: リンカのメインエントリポイントのプレースホルダー。
  • src/cmd/link/prog.go: リンカの主要な状態を保持するProg構造体と、リンカの処理フローを定義するlink()メソッド、初期化を行うinit()メソッドが含まれます。Package, Sym, Segment, Sectionなどの補助的な構造体も定義されています。
  • src/cmd/link/prog_test.go: Prog構造体のテストユーティリティ関数(shiftProg, diffProg, cloneProgなど)が含まれます。
  • src/cmd/link/runtime.go: ランタイムデータ構造生成のスケルトン。
  • src/cmd/link/scan.go: プログラムを構成するパッケージの初期スキャンを処理。Prog.scan()initScan()scanFile()scanImport()メソッドが含まれます。
  • src/cmd/link/testdata/hello.6: テスト用のGoオブジェクトファイル。
  • src/cmd/link/testdata/hello.s: テスト用のGoアセンブリファイル。
  • src/cmd/link/testdata/link.hello.darwin.amd64: テストで生成される期待されるバイナリのゴールデンファイル。
  • src/cmd/link/util.go: ユーティリティ関数(roundなど)。
  • src/cmd/link/write.go: 実行可能ファイルの書き出しを処理。Prog.write()メソッドが含まれます。

これらのファイルは、Goリンカの再実装における初期のコミットであり、リンカの各段階をGo言語でどのように表現し、連携させるかの基礎を築いています。

コアとなるコードの解説

src/cmd/link/prog.go

このファイルは、新しいGoリンカの中心となるProg構造体を定義しています。

type Prog struct {
	// Context
	GOOS      string // target operating system
	GOARCH    string // target architecture
	Format    string // desired file format ("elf", "macho", ...)
	formatter formatter
	Error     func(string) // called to report an error (if set)
	NumError  int          // number of errors printed

	// Input
	Packages   map[string]*Package  // loaded packages, by import path
	Syms       map[goobj.SymID]*Sym // defined symbols, by symbol ID
	Missing    map[goobj.SymID]bool // missing symbols, by symbol ID
	MaxVersion int                  // max SymID.Version, for generating fresh symbol IDs

	// Output
	UnmappedSize Addr       // size of unmapped region at address 0
	HeaderSize   Addr       // size of object file header
	Entry        Addr       // virtual address where execution begins
	Segments     []*Segment // loaded memory segments
}

Prog構造体は、リンカの実行に必要なすべての情報(ターゲット環境、入力パッケージとシンボル、出力バイナリの構造)をカプセル化します。linkメソッドは、リンカの主要な処理ステップを順序立てて実行する高レベルな関数です。

src/cmd/link/layout.go

このファイルは、実行可能ファイルのメモリレイアウトを決定するロジックを含んでいます。

var layout = []layoutSection{
	{Segment: "text", Section: "text", Kind: goobj.STEXT},
	{Segment: "data", Section: "data", Kind: goobj.SDATA},

	// Later:
	//	{"rodata", "type", goobj.STYPE},
	//	... (コメントアウトされた他のセクション)
}

func (p *Prog) layout() {
	// ... シンボルをセクションに割り当て ...
	// ... セクションをセグメントに割り当て ...
	// ... アドレスを割り当て ...
}

layout配列は、Goバイナリが持つべきセクションの順序と、それらが属するセグメントを定義します。layout()メソッドは、読み込まれたシンボルをこれらのセクションに配置し、最終的な実行可能ファイル内の仮想アドレスとファイルオフセットを計算します。これは、バイナリの構造を決定する上で非常に重要なステップです。

src/cmd/link/load.go

このファイルは、オブジェクトファイルから実際のコードとデータを読み込み、再配置を適用する役割を担います。

func (p *Prog) load() {
	for _, seg := range p.Segments {
		seg.Data = make([]byte, seg.FileSize)
	}
	for _, pkg := range p.Packages {
		p.loadPackage(pkg)
	}
}

func (p *Prog) relocateSym(sym *Sym, data []byte) {
	for i := range sym.Reloc {
		r := &sym.Reloc[i]
		targ := p.Syms[r.Sym]
		// ... 再配置タイプに応じたアドレス計算とデータ書き込み ...
	}
}

load()メソッドは、layout()で決定されたセグメントのサイズに基づいてメモリを確保し、各パッケージのオブジェクトファイルからシンボルのデータを読み込みます。relocateSym()は、読み込んだデータに対して再配置処理を行い、他のシンボルへの参照を正しいアドレスに修正します。これは、複数のオブジェクトファイルを結合して実行可能なバイナリを生成するリンカの核心部分です。

src/cmd/link/scan.go

このファイルは、プログラムを構成するすべてのGoパッケージをスキャンし、シンボル情報を収集します。

func (p *Prog) scan(mainfile string) {
	p.initScan()
	p.scanFile("main", mainfile)
	// ... 未定義シンボルのチェック ...
}

func (p *Prog) scanFile(pkgpath string, file string) {
	// ... ファイルを開き、debug/goobj.Parseで解析 ...
	// ... シンボルをp.Symsに登録し、p.Missingから削除 ...
	// ... インポートされたパッケージを再帰的にスキャン ...
}

scan()メソッドは、指定されたメインファイルから開始し、インポートパスをたどって必要なすべてのGoオブジェクトファイルを読み込みます。scanFile()は個々のオブジェクトファイルを解析し、その中に定義されているシンボルをProgのシンボルテーブルに登録します。この段階で、リンカはプログラム全体のシンボル定義と参照のグラフを構築します。

これらのファイルは、GoリンカがGo言語でどのように再構築され始めたかを示す重要な初期ステップであり、リンカの主要な機能(スキャン、レイアウト、ロード、再配置)がどのようにモジュール化され、実装されているかを示しています。

関連リンク

参考にした情報源リンク