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

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

このコミットは、Go言語のリンカ (cmd/link) にMach-O(macOS/OS Xの実行可能ファイル形式)のファイルフォーマッタを追加するものです。これにより、GoプログラムがmacOS上でネイティブな実行可能ファイルを生成できるようになります。具体的には、Mach-Oヘッダ、ロードコマンド、セグメント、セクションといった構造をGoのリンカが理解し、適切に書き出すためのコードが導入されています。

コミット

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

    cmd/link: Mach-O (OS X) file formatter
    
    See CL 48870044 for basic structure.
    
    R=iant
    CC=golang-codereviews
    https://golang.org/cl/48910043

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

https://github.com/golang/go/commit/8449863d3173f59c11b477dc93d5eb00452e80e6

元コミット内容

cmd/link: Mach-O (OS X) file formatter

このコミットは、Go言語のリンカであるcmd/linkに、macOS (当時のOS X) 向けの実行可能ファイル形式であるMach-Oのフォーマッタを実装したものです。基本的な構造については、関連する変更リスト (CL 48870044) を参照するように記載されています。

変更の背景

Go言語はクロスプラットフォーム対応を重視しており、様々なオペレーティングシステムでネイティブな実行可能ファイルを生成できることが重要な目標の一つです。このコミットが作成された2014年当時、Goは既にLinuxやWindows向けの実行可能ファイルを生成できていましたが、macOS向けのネイティブバイナリ生成にはMach-O形式への対応が不可欠でした。

cmd/linkはGoコンパイラが生成したオブジェクトファイルをリンクし、最終的な実行可能ファイルを生成する役割を担っています。各プラットフォーム固有の実行可能ファイル形式(LinuxのELF、WindowsのPE、macOSのMach-Oなど)に対応するためには、リンカがそれぞれの形式の構造を理解し、適切にバイナリを書き出す機能が必要です。

このコミットは、GoプログラムがmacOS上で直接実行可能なバイナリを生成するための基盤を構築するものであり、Go言語のmacOSサポートを強化する上で非常に重要な一歩でした。これにより、Go開発者はmacOS環境でよりシームレスにGoアプリケーションを開発・配布できるようになりました。

前提知識の解説

Go言語のビルドプロセスは、主に以下のツールで構成されるツールチェインによって行われます。

  1. go tool compile: Goのソースコードをコンパイルし、プラットフォームに依存しない中間形式(Goアセンブリ)のオブジェクトファイルを生成します。
  2. go tool asm: Goアセンブリをプラットフォーム固有のマシンコードに変換します。
  3. go tool link (または cmd/link): コンパイルされたオブジェクトファイルと、Goランタイムライブラリなどを結合(リンク)し、最終的な実行可能ファイルを生成します。このリンカが、各OSの実行可能ファイル形式(ELF, PE, Mach-Oなど)の構造を理解し、それに従ってバイナリを構築します。

このコミットで変更されるcmd/linkは、Goプログラムを特定のOSで実行可能な形式に変換する最終段階を担う、非常に重要なコンポーネントです。

Mach-Oファイル形式

Mach-O (Mach Object) は、macOS (旧称 OS X)、iOS、watchOS、tvOSといったAppleのオペレーティングシステムで使用される実行可能ファイル、オブジェクトファイル、共有ライブラリ、コアダンプなどのバイナリ形式です。Mach-Oは、BSD Unixのa.out形式やSystem V UnixのELF形式とは異なる独自の構造を持っています。

Mach-Oファイルの主要な構成要素は以下の通りです。

  1. Mach-O ヘッダ (Mach-O Header): ファイルの先頭に位置し、CPUタイプ、サブCPUタイプ、ファイルタイプ(実行可能ファイル、ライブラリなど)、ロードコマンドの数とサイズ、フラグなどの基本的な情報を含みます。64ビット版と32ビット版で構造が異なります。
  2. ロードコマンド (Load Commands): ヘッダの直後に続く一連のコマンドです。これらはリンカやOSのローダに対して、ファイルのどの部分をどのようにメモリにロードするか、どのライブラリにリンクするか、エントリポイントはどこか、といった指示を与えます。主要なロードコマンドには以下のようなものがあります。
    • LC_SEGMENT / LC_SEGMENT_64: メモリセグメント(コード、データなど)の定義。セグメントは一つ以上のセクションを含みます。
    • LC_UNIXTHREAD: プログラムのエントリポイントや初期レジスタの状態を定義します。
    • LC_LOAD_DYLIB: 動的リンクするライブラリを指定します。
    • LC_SYMTAB: シンボルテーブルの場所とサイズを定義します。
  3. セグメント (Segments): ロードコマンドによって定義されるメモリ領域です。例えば、__TEXTセグメントは実行可能なコードを含み、__DATAセグメントは初期化されたデータを含みます。各セグメントは、仮想メモリ上のアドレス、サイズ、ファイル上のオフセット、サイズ、メモリ保護属性(読み取り、書き込み、実行)などを持っています。
  4. セクション (Sections): セグメントの内部に存在する、より細かい論理的なブロックです。例えば、__TEXTセグメント内には、実行可能なコードを含む__textセクションや、読み取り専用データを含む__constセクションなどがあります。

Mach-Oファイルは、これらの構造を組み合わせて、プログラムの実行に必要なすべての情報を提供します。リンカは、Goコンパイラが生成したコードとデータをこれらのMach-O構造にマッピングし、最終的なバイナリを生成します。

技術的詳細

このコミットでは、src/cmd/link/macho.goという新しいファイルが追加され、Mach-O形式の実行可能ファイルを生成するためのロジックが実装されています。

主要な構造体と関数は以下の通りです。

  1. machoFormat struct:

    • formatterインターフェース(Goリンカ内で定義されている)の実装です。
    • headerSize(p *Prog) (virt, file Addr): Mach-Oヘッダのサイズを計算し、プログラムの仮想アドレス空間とファイルオフセットを調整します。Mach-Oヘッダがファイルとメモリの先頭に配置されるため、その分のオフセットを考慮する必要があります。
    • write(w io.Writer, p *Prog): Prog構造体(Goリンカが扱うプログラムの内部表現)の内容をMach-O形式でio.Writerに書き出します。ヘッダ、ロードコマンド、セグメントの順に書き込みます。
  2. Mach-O関連のデータ構造:

    • machoArch: CPUタイプとサブCPUタイプを定義します。machoArchesマップでGOARCH(例: "amd64")から対応するmachoArchへのマッピングが定義されています。
    • machoHeader: Mach-Oファイルのヘッダ情報を表します。init(p *Prog)メソッドでProgからヘッダ情報を初期化します。これには、ファイルタイプ(実行可能ファイル)、CPUアーキテクチャ、ロードコマンド(セグメントやスレッド情報など)が含まれます。
    • machoLoad: ロードコマンドの抽象化です。TypeData(コマンド固有のデータ)を持ちます。
    • machoSegment: Mach-Oのセグメント情報を表します。名前、仮想アドレス、仮想サイズ、ファイルオフセット、ファイルサイズ、メモリ保護属性(Prot1, Prot2)、フラグ、そして含まれるセクションのリストを持ちます。
    • machoSection: Mach-Oのセクション情報を表します。名前、所属セグメント、アドレス、サイズ、オフセット、アラインメント、フラグなどを含みます。
  3. machoWriter struct:

    • Mach-Oヘッダのバイト列への書き込みを補助するユーティリティ構造体です。
    • バイトオーダー(リトルエンディアン)や64ビット/32ビットの区別を考慮して、uint32, uint64, Addrなどの値をバイト列にエンコードするencodeメソッドを提供します。
    • segmentSize(seg *machoSegment): セグメントのエンコード後のサイズを計算します。
    • zeroPad(s string, n int): 文字列を指定された長さになるようにヌルバイトでパディングまたは切り詰めます。これはMach-Oのセグメント名やセクション名が固定長であるため必要です。
  4. machoHeader.encode():

    • machoHeader構造体の内容を実際のMach-Oバイナリ形式のバイト列に変換する核心的なメソッドです。
    • マジックナンバー、CPUタイプ、サブCPUタイプ、ファイルタイプ、ロードコマンド数、ロードコマンドサイズなどを書き込みます。
    • 各セグメントとセクションの情報を、Mach-Oの規定に従って書き込みます。特に、__TEXTセグメントのメモリ保護属性(最初はRWX、その後RX)や、__PAGEZEROセグメント(NULLポインタ参照を捕捉するための領域)の扱いが定義されています。
    • LC_UNIXTHREADロードコマンドを生成し、プログラムのエントリポイント(p.Entry)をCPUレジスタ(AMD64の場合はRIPレジスタ)に設定する情報を含めます。

テストファイル (src/cmd/link/macho_test.go)

このコミットでは、macho.goで実装されたMach-Oフォーマッタの動作を検証するためのテストファイルmacho_test.goも追加されています。

  • TestMachoWrite関数:
    • machoWriteTestsというテストケースの配列を定義しています。これには、exit9(終了コード9で終了するプログラム)、hello("hello world"を出力して終了するプログラム)、helloro(読み取り専用データセクションから"hello world"を出力して終了するプログラム)の3つのテストケースが含まれています。
    • 各テストケースは、Goリンカの内部表現であるProg構造体でプログラムの内容を定義しています。
    • テストでは、machoFormat.writeメソッドを使ってProgをMach-Oバイナリに書き出し、その結果をdebug/machoパッケージ(Go標準ライブラリのMach-Oパーサ)を使って読み込み直します。
    • 読み込み直したProgと元のProgを比較し、内容が一致するかどうかを検証します。
    • さらに、生成されたバイナリがtestdataディレクトリにあるゴールデンファイル(期待される正しいバイナリ)と一致するかどうかもcheckGolden関数で確認しています。

テストデータ (src/cmd/link/testdata/)

macho.amd64.exit9, macho.amd64.hello, macho.amd64.helloroという3つのバイナリファイルが追加されています。これらは、macho_test.goで生成されるMach-Oバイナリの「正解」となるゴールデンファイルです。テスト時に、生成されたバイナリがこれらのファイルとバイト単位で一致するかどうかを比較することで、Mach-Oフォーマッタの正確性を保証しています。

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

このコミットのコアとなる変更は、以下の2つの新しいファイルの追加です。

  1. src/cmd/link/macho.go: Mach-Oファイル形式の生成ロジックを実装したGoソースファイル。
  2. src/cmd/link/macho_test.go: macho.goで実装された機能の単体テストと統合テストを含むGoソースファイル。

また、テストデータとして以下のバイナリファイルが追加されています。

  • src/cmd/link/testdata/macho.amd64.exit9
  • src/cmd/link/testdata/macho.amd64.hello
  • src/cmd/link/testdata/macho.amd64.helloro

コアとなるコードの解説

src/cmd/link/macho.go

このファイルは、GoリンカがMach-O形式の実行可能ファイルを生成するために必要なすべてのロジックを含んでいます。

  • machoFormat: formatterインターフェースを実装し、Goリンカのメイン処理から呼び出されるエントリポイントとなります。headerSizeでヘッダのオフセットを計算し、writeで実際のバイナリ書き込みを行います。
  • Mach-O構造体の定義: machoHeader, machoLoad, machoSegment, machoSectionといった構造体が、Mach-Oファイルの論理的な構成要素をGoのデータ構造として表現しています。これらの構造体は、debug/machoパッケージの定義と密接に対応しており、Mach-Oの仕様をGoのコードでモデル化しています。
  • machoHeader.init(p *Prog): Prog(Goリンカの内部表現)からMach-Oヘッダを初期化する重要な関数です。
    • machoArchesマップを使って、ターゲットアーキテクチャ(例: amd64)に対応するMach-OのCPU/SubCPUタイプを設定します。
    • macho.TypeExecを設定して、生成されるファイルが実行可能ファイルであることを示します。
    • __PAGEZEROセグメント(仮想アドレス0番地からの保護領域)を追加します。これはNULLポインタ参照を検出するために重要です。
    • p.Segments(Goプログラムのコードやデータセグメント)をMach-OのmachoSegmentに変換し、追加します。この際、セグメント名に__プレフィックスを付け、大文字に変換するなどのMach-Oの命名規則に従います。
    • LC_UNIXTHREADロードコマンドを追加し、プログラムのエントリポイント(p.Entry)をスレッドの初期レジスタ値として設定します。これにより、OSがプログラムをロードした際に、指定されたアドレスから実行が開始されます。
  • machoSegment.addSection(...): Goプログラムのセクション(例: text, data, rodata)をMach-OのmachoSectionに変換し、対応するmachoSegmentに追加します。セクションのアラインメントや、ファイルにデータを持つか(Offsetが設定される)、ゼロフィルされるか(Flags |= 1)といった属性が設定されます。特にtextセクションには実行可能コードであることを示すフラグ0x400が設定されます。
  • machoWriter: バイトオーダーの処理や、固定長文字列のパディングなど、Mach-Oバイナリの低レベルな書き込みを抽象化し、コードの可読性と保守性を高めています。
  • machoHeader.encode(): machoHeader構造体の内容を、Mach-Oのバイナリフォーマットに従ってバイト列にシリアライズします。この関数が、Mach-Oファイルのヘッダとロードコマンドの大部分を構築します。

src/cmd/link/macho_test.go

このファイルは、macho.goの正確性を保証するためのテストスイートです。

  • machoWriteTests: 異なるGoプログラムの構造(Prog)を定義し、それぞれが正しくMach-Oバイナリに変換されることを検証します。
    • exit9: 最小限のプログラムで、終了コード9を返します。
    • hello: 標準出力に"hello world"を出力し、終了コード9を返します。データセクションに文字列が含まれます。
    • helloro: 読み取り専用データセクションに"hello world"を配置し、それを出力して終了コード0を返します。これは、コードとデータが異なるセグメントに配置されるケースをテストします。
  • TestMachoWrite: 各テストケースに対して以下の検証を行います。
    1. machoFormat.headerSizeを呼び出し、ヘッダサイズを計算し、プログラムのオフセットを調整します。
    2. machoFormat.writeを呼び出し、ProgをMach-Oバイナリに書き出します。
    3. machoRead関数(テストヘルパー関数)を使って、生成されたMach-Oバイナリをdebug/machoパッケージで読み込み直します。
    4. diffProg関数(テストヘルパー関数)を使って、読み込み直したProgと元のProgを比較し、構造が一致するかどうかを確認します。これにより、書き込みと読み込みのラウンドトリップが正しく行われることを検証します。
    5. checkGolden関数を使って、生成されたバイナリが事前に用意されたゴールデンファイル(testdataディレクトリ内のバイナリ)とバイト単位で一致するかどうかを確認します。これは、Mach-Oフォーマッタの出力が期待通りであることを保証する強力なテストです。
  • machoRead: debug/machoパッケージを使用して、与えられたMach-OバイナリデータからGoリンカのProg構造体を再構築するヘルパー関数です。これにより、生成されたMach-Oファイルの内部構造をGoのリンカが理解できる形式で検証することが可能になります。

これらのテストは、macho.goがMach-Oの複雑な仕様を正確に実装していることを多角的に検証しており、Goリンカの堅牢性を高める上で不可欠な部分です。

関連リンク

参考にした情報源リンク