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

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

このコミットは、Go言語のデバッグツール群に、Plan 9オペレーティングシステムのa.out実行可能ファイルの解析機能を追加するものです。具体的には、debug/plan9objパッケージを新規に導入し、Plan 9の32ビットおよび64ビットバイナリのヘッダとシンボルテーブルを解析できるようにします。これにより、GoのnmツールがPlan 9の実行ファイルからシンボル情報を抽出できるようになります。

コミット

commit 021c11683ca28c7e01a2eca5ccbb9b8bd34e3bc1
Author: David du Colombier <0intro@gmail.com>
Date:   Wed Jan 22 23:30:52 2014 +0100

    debug/plan9obj: implement parsing of Plan 9 a.out executables
    
    It implements parsing of the header and symbol table for both
    32-bit and 64-bit Plan 9 binaries. The nm tool was updated to
    use this package.
    
    R=rsc, aram
    CC=golang-codereviews
    https://golang.org/cl/49970044

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

https://github.com/golang/go/commit/021c11683ca28c7e01a2eca5ccbb9b8bd34e3bc1

元コミット内容

debug/plan9obj: implement parsing of Plan 9 a.out executables

このコミットは、Plan 9 a.out実行可能ファイルの解析を実装します。 32ビットおよび64ビットのPlan 9バイナリのヘッダとシンボルテーブルの解析を実装します。nmツールはこのパッケージを使用するように更新されました。

変更の背景

Go言語のツールチェインは、様々なプラットフォームのバイナリ形式を理解し、デバッグや解析を可能にすることを目指しています。このコミット以前は、Goの標準ライブラリやツールはPlan 9のa.out形式の実行ファイルを直接解析する機能を持ちませんでした。Plan 9はベル研究所で開発された分散オペレーティングシステムであり、そのバイナリ形式であるa.outは、Unix系のa.outとは異なる独自の構造を持っています。

この機能追加の背景には、Go言語がPlan 9の思想や設計に影響を受けている点があります。Go言語の初期開発者の中にはPlan 9の開発に携わった人物もおり、Goの設計思想にはPlan 9のシンプルさやモジュール性が反映されています。そのため、GoのツールがPlan 9のバイナリを扱えるようにすることは、Goエコシステムの包括性を高め、Plan 9環境での開発やクロスコンパイルされたバイナリの解析を容易にする上で重要でした。

特に、nmのようなシンボル表示ツールがPlan 9バイナリに対応することで、開発者はPlan 9上で動作するGoプログラムや、Goで書かれたPlan 9向けツールなどのデバッグや解析をより効率的に行えるようになります。

前提知識の解説

a.out形式

a.outは"assembler output"の略で、Unix系システムで古くから使われてきた実行可能ファイル形式です。しかし、このコミットで言及されているPlan 9のa.outは、一般的なUnixのa.outとは異なる独自の構造を持っています。

Plan 9のa.outは、主に以下のセクションで構成されます。

  • ヘッダ (Header): ファイルのメタデータ(マジックナンバー、テキストセグメントサイズ、データセグメントサイズ、BSSセグメントサイズ、シンボルテーブルサイズ、エントリポイントなど)を含みます。
  • テキストセグメント (Text Segment): 実行可能な機械語コードが含まれます。
  • データセグメント (Data Segment): 初期化されたグローバル変数や静的変数が含まれます。
  • BSSセグメント (BSS Segment): 初期化されていないグローバル変数や静的変数の領域を定義します。実行時にはゼロで初期化されます。
  • シンボルテーブル (Symbol Table): プログラム内の関数名、変数名、それらのアドレスなどのシンボル情報が含まれます。デバッグやリンキングに利用されます。
  • PC/SPオフセットテーブル (PC/SP Offset Table): プログラムカウンタとスタックポインタのオフセット情報。
  • PC/行番号テーブル (PC/Line Number Table): プログラムカウンタとソースコードの行番号のマッピング情報。

Plan 9のa.outは、そのシンプルさと効率性で知られています。マジックナンバーによってCPUアーキテクチャ(386, AMD64, ARMなど)が識別されます。

nmツール

nmは"name mangler"または"names"の略で、オブジェクトファイル、アーカイブファイル、実行可能ファイルからシンボル(関数名、変数名など)をリストアップするために使用されるUnix系システムで一般的なコマンドラインツールです。nmは、バイナリファイル内のシンボルテーブルを読み取り、各シンボルの名前、型(関数、変数、未定義など)、アドレス、サイズなどの情報を提供します。

デバッグ時や、ライブラリのAPIを調査する際、あるいは特定の関数がバイナリに存在するかどうかを確認する際などに非常に有用です。

Go言語のdebugパッケージ

Go言語の標準ライブラリには、debugというパッケージ群があります。これらは、様々な実行可能ファイル形式(ELF, DWARF, Mach-O, PEなど)を解析するための機能を提供します。debugパッケージは、Goプログラムが他のプログラムのバイナリ構造を検査したり、デバッグ情報を抽出したりすることを可能にします。

このコミットで追加されたdebug/plan9objは、このdebugパッケージ群の一部として、Plan 9 a.out形式の解析を担当します。

技術的詳細

このコミットの主要な技術的詳細は、debug/plan9objパッケージにおけるPlan 9 a.out形式の構造解析とシンボルテーブルの読み込みにあります。

  1. マジックナンバーによる識別: Plan 9 a.outファイルは、ファイルの先頭にある4バイトのマジックナンバーによって、そのアーキテクチャと形式を識別します。src/pkg/debug/plan9obj/plan9obj.goには、_A_MAGIC (68020), _I_MAGIC (Intel 386), _S_MAGIC (AMD64) など、様々なアーキテクチャに対応するマジックナンバーが定義されています。parseMagic関数は、このマジックナンバーを読み取り、対応するExecTableエントリ(ポインタサイズやヘッダサイズなどの情報を含む)を返します。

  2. ファイル構造の定義: src/pkg/debug/plan9obj/file.goでは、Plan 9 a.outファイルの論理的な構造をGoの構造体として定義しています。

    • FileHeader: ポインタサイズ(32ビットまたは64ビット)を保持します。
    • SectionHeader: 各セクション(text, data, symsなど)の名前、サイズ、オフセットを定義します。
    • Section: SectionHeaderに加え、io.ReaderAtインターフェースを埋め込み、セクションの生データへのアクセスを提供します。
    • ProgHeader: プログラムヘッダの各フィールド(マジックナンバー、テキスト/データ/BSSサイズ、シンボルサイズ、エントリポイントなど)を定義します。
    • Prog: ProgHeaderに加え、プログラム本体のデータへのアクセスを提供します。
    • Sym: シンボルテーブルのエントリを表し、シンボルの値(アドレス)、型、名前を保持します。
  3. ファイル解析ロジック: NewFile関数(src/pkg/debug/plan9obj/file.go)が、io.ReaderAtからPlan 9 a.outファイルを読み込み、解析する主要なエントリポイントです。

    • まず、マジックナンバーを読み取り、parseMagicでファイルの種類を特定します。
    • 次に、ProgHeaderを読み込み、テキスト、データ、BSS、シンボルテーブルなどの各セクションのサイズとオフセットを計算します。
    • 各セクションはio.NewSectionReaderを使用して、元のファイル内の対応するバイト範囲にマッピングされます。これにより、各セクションのデータに効率的にアクセスできるようになります。
  4. シンボルテーブルの解析: walksymtab関数(src/pkg/debug/plan9obj/file.go)は、シンボルテーブルの生データを走査し、個々のシンボルエントリを解析します。

    • シンボルエントリは、値(アドレス)、型(バイト)、名前(バイトスライス)で構成されます。
    • シンボルの型は、'z', 'Z'のような特殊な型(ファイル名に関連するシンボル)や、一般的なシンボル型(関数、変数など)を区別します。
    • newTable関数は、walksymtabを利用して、解析されたシンボルエントリを[]Symスライスとして構築します。この際、ファイル名に関連するシンボル('f'型)を解決し、完全なパス名を構築するロジックも含まれています。
    • File構造体のSymbols()メソッドは、symsセクションからシンボルテーブルの生データを取得し、newTableを呼び出して解析済みのシンボルリストを返します。
  5. nmツールとの統合: src/cmd/nm/nm.goが更新され、parsers配列にPlan 9 a.outのマジックナンバーと、それに対応するplan9Symbols関数が追加されました。 src/cmd/nm/plan9obj.goに新しく追加されたplan9Symbols関数は、debug/plan9obj.NewFileを使用してPlan 9 a.outファイルを解析し、p.Symbols()を呼び出してシンボルリストを取得します。取得したシンボルは、nmツールが期待するSym構造体に変換され、アドレスに基づいてソートされます。これにより、nmツールはPlan 9バイナリのシンボル情報を正確に表示できるようになります。

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

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

  1. src/cmd/nm/nm.go:

    • parsers変数に、Plan 9 a.outのマジックナンバー(386, mips, arm, amd64用)と、それらを処理するためのplan9Symbols関数への参照が追加されました。これにより、nmツールがPlan 9バイナリを自動的に認識し、適切なパーサーを使用できるようになります。
  2. src/cmd/nm/plan9obj.go (新規):

    • plan9Symbols関数が定義されています。この関数は、os.Fileを受け取り、debug/plan9obj.NewFileを使ってPlan 9 a.outファイルを解析し、そのシンボルテーブルを読み取ります。
    • 読み取ったシンボルは、nmツールが使用するSym構造体に変換され、アドレス順にソートされて返されます。シンボルのサイズは、次のシンボルとのアドレス差から推測されます。
  3. src/pkg/debug/plan9obj/file.go (新規):

    • debug/plan9objパッケージの主要な実装ファイルです。
    • FileHeader, SectionHeader, Section, ProgHeader, Prog, SymといったPlan 9 a.outの構造を表現するGoの構造体が定義されています。
    • Open関数とNewFile関数は、ファイルを開き、マジックナンバーを検証し、プログラムヘッダと各セクション(テキスト、データ、シンボルなど)を解析するロジックを含みます。
    • walksymtab関数は、シンボルテーブルの生データを走査し、個々のシンボルエントリを抽出します。
    • newTable関数は、抽出されたシンボルエントリから[]Symスライスを構築し、ファイル名に関連するシンボルを解決します。
    • File構造体のSymbols()メソッドは、シンボルテーブルセクションからシンボルを読み込み、解析して返します。
    • File構造体のSection()メソッドは、指定された名前のセクションを返します。
  4. src/pkg/debug/plan9obj/file_test.go (新規):

    • debug/plan9objパッケージのテストファイルです。
    • TestOpen関数は、実際のPlan 9 a.outテストデータ(386-plan9-exec, amd64-plan9-exec)を使用して、Open関数がファイルヘッダとセクション情報を正しく解析できることを検証します。
    • TestOpenFailure関数は、無効なファイルを開こうとした場合の挙動をテストします。
  5. src/pkg/debug/plan9obj/plan9obj.go (新規):

    • Plan 9 a.out形式に関連する定数とデータ構造を定義するファイルです。
    • prog構造体は、Plan 9のプログラムヘッダのレイアウトを定義します。
    • sym構造体は、Plan 9のシンボルテーブルエントリの内部表現を定義します。
    • magic関数は、Plan 9のマジックナンバーを生成するためのヘルパー関数です。
    • _A_MAGICから_R_MAGICまでの様々なアーキテクチャに対応するマジックナンバー定数が定義されています。
    • ExecTable構造体とexectab変数は、各マジックナンバーに対応するポインタサイズやヘッダサイズなどの実行可能ファイル情報をマッピングします。
  6. src/pkg/debug/plan9obj/testdata/ (新規):

    • 386-plan9-exec: 32ビットPlan 9 a.out実行可能ファイルのテストデータ。
    • amd64-plan9-exec: 64ビットPlan 9 a.out実行可能ファイルのテストデータ。
    • hello.c: 上記のバイナリを生成するためのシンプルなC言語のソースコード。

コアとなるコードの解説

このコミットの核となるのは、src/pkg/debug/plan9obj/file.goに実装されたPlan 9 a.outファイルの解析ロジックです。

特に重要なのはNewFile関数とSymbolsメソッド、そしてそれらを支えるwalksymtab関数です。

NewFile関数 (src/pkg/debug/plan9obj/file.go)

func NewFile(r io.ReaderAt) (*File, error) {
	sr := io.NewSectionReader(r, 0, 1<<63-1)
	// Read and decode Plan 9 magic
	var magic [4]byte
	if _, err := r.ReadAt(magic[:], 0); err != nil {
		return nil, err
	}
	mp, err := parseMagic(magic) // マジックナンバーを解析し、実行可能ファイルのメタデータ(ポインタサイズ、ヘッダサイズ)を取得
	if err != nil {
		return nil, err
	}

	f := &File{FileHeader{mp.Ptrsz}, nil, nil} // File構造体を初期化、ポインタサイズを設定

	ph := new(prog)
	if err := binary.Read(sr, binary.BigEndian, ph); err != nil { // プログラムヘッダを読み込む
		return nil, err
	}

	// ... (Prog構造体の初期化と64ビット対応の調整) ...

	var sects = []struct { // 各セクションの定義
		name string
		size uint32
	}{
		{"text", ph.Text},
		{"data", ph.Data},
		{"syms", ph.Syms},
		{"spsz", ph.Spsz},
		{"pcsz", ph.Pcsz},
	}

	f.Sections = make([]*Section, 5) // セクションスライスを初期化

	off := mp.Hsize // ヘッダサイズからオフセットを開始

	for i, sect := range sects { // 各セクションを構築
		s := new(Section)
		s.SectionHeader = SectionHeader{
			Name:   sect.name,
			Size:   sect.size,
			Offset: off,
		}
		off += sect.size // 次のセクションのオフセットを更新
		s.sr = io.NewSectionReader(r, int64(s.SectionHeader.Offset), int64(s.SectionHeader.Size)) // セクションリーダーを作成
		s.ReaderAt = s.sr
		f.Sections[i] = s
	}

	return f, nil
}

この関数は、io.ReaderAtインターフェースを実装する任意のソース(通常はos.File)からPlan 9 a.outファイルを読み込み、その構造をGoのFile構造体として表現します。マジックナンバーの検証から始まり、プログラムヘッダを読み込み、テキスト、データ、シンボルなどの各セクションのオフセットとサイズを計算して、それぞれをio.SectionReaderとしてラップします。これにより、各セクションのデータに効率的かつ独立してアクセスできるようになります。

Symbolsメソッド (File構造体, src/pkg/debug/plan9obj/file.go)

func (f *File) Symbols() ([]Sym, error) {
	symtabSection := f.Section("syms") // "syms"セクション(シンボルテーブル)を取得
	if symtabSection == nil {
		return nil, errors.New("no symbol section")
	}

	symtab, err := symtabSection.Data() // シンボルテーブルの生データを読み込む
	if err != nil {
		return nil, errors.New("cannot load symbol section")
	}

	return newTable(symtab, f.Ptrsz) // 生データとポインタサイズを使ってシンボルテーブルを解析
}

このメソッドは、File構造体からシンボルテーブルセクションを見つけ出し、その生データを読み込みます。そして、newTable関数を呼び出して、その生データからGoのSym構造体のスライス([]Sym)を構築します。このSymスライスには、シンボルのアドレス、型、名前が含まれます。

walksymtab関数 (src/pkg/debug/plan9obj/file.go)

func walksymtab(data []byte, ptrsz int, fn func(sym) error) error {
	var order binary.ByteOrder = binary.BigEndian // Plan 9はビッグエンディアン
	var s sym
	p := data
	for len(p) >= 4 { // データがシンボルエントリの最小サイズ以上である限りループ
		// シンボルの値(アドレス)を読み込む (32ビットまたは64ビット)
		if len(p) < ptrsz {
			return &FormatError{len(data), "unexpected EOF", nil}
		}
		if ptrsz == 8 {
			s.value = order.Uint64(p[0:8])
			p = p[8:]
		} else {
			s.value = uint64(order.Uint32(p[0:4]))
			p = p[4:]
		}

		// シンボルの型を読み込む
		var typ byte
		typ = p[0] & 0x7F // 最上位ビットはフラグ、下位7ビットが型
		s.typ = typ
		p = p[1:]

		// シンボルの名前を読み込む
		var i int
		var nnul int
		for i = 0; i < len(p); i++ {
			if p[i] == 0 { // ヌル終端文字列
				nnul = 1
				break
			}
		}
		// ... (特殊なシンボル型 'z', 'Z' の処理) ...
		if len(p) < i+nnul {
			return &FormatError{len(data), "unexpected EOF", nil}
		}
		s.name = p[0:i] // 名前をバイトスライスとして取得
		i += nnul
		p = p[i:]

		fn(s) // 抽出したシンボルをコールバック関数に渡す
	}
	return nil
}

この関数は、Plan 9 a.outのシンボルテーブルの生バイトデータを解析するための低レベルなユーティリティです。シンボルテーブルは、シンボルの値(アドレス)、型、名前が連続して格納されています。この関数は、ポインタサイズ(32ビットまたは64ビット)に応じて値を読み取り、型バイトを解釈し、ヌル終端された名前を抽出します。抽出された各シンボルは、引数として渡されたコールバック関数fnに渡されます。これにより、シンボルテーブルの走査と処理を分離し、柔軟なシンボル解析を可能にしています。

これらのコード変更により、Go言語はPlan 9 a.outバイナリの内部構造を理解し、シンボル情報を抽出できるようになり、nmツールのような既存のデバッグツールがこの新しい機能を活用できるようになりました。

関連リンク

参考にした情報源リンク