[インデックス 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形式の構造解析とシンボルテーブルの読み込みにあります。
-
マジックナンバーによる識別: Plan 9 a.outファイルは、ファイルの先頭にある4バイトのマジックナンバーによって、そのアーキテクチャと形式を識別します。
src/pkg/debug/plan9obj/plan9obj.go
には、_A_MAGIC
(68020),_I_MAGIC
(Intel 386),_S_MAGIC
(AMD64) など、様々なアーキテクチャに対応するマジックナンバーが定義されています。parseMagic
関数は、このマジックナンバーを読み取り、対応するExecTable
エントリ(ポインタサイズやヘッダサイズなどの情報を含む)を返します。 -
ファイル構造の定義:
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
: シンボルテーブルのエントリを表し、シンボルの値(アドレス)、型、名前を保持します。
-
ファイル解析ロジック:
NewFile
関数(src/pkg/debug/plan9obj/file.go
)が、io.ReaderAt
からPlan 9 a.outファイルを読み込み、解析する主要なエントリポイントです。- まず、マジックナンバーを読み取り、
parseMagic
でファイルの種類を特定します。 - 次に、
ProgHeader
を読み込み、テキスト、データ、BSS、シンボルテーブルなどの各セクションのサイズとオフセットを計算します。 - 各セクションは
io.NewSectionReader
を使用して、元のファイル内の対応するバイト範囲にマッピングされます。これにより、各セクションのデータに効率的にアクセスできるようになります。
- まず、マジックナンバーを読み取り、
-
シンボルテーブルの解析:
walksymtab
関数(src/pkg/debug/plan9obj/file.go
)は、シンボルテーブルの生データを走査し、個々のシンボルエントリを解析します。- シンボルエントリは、値(アドレス)、型(バイト)、名前(バイトスライス)で構成されます。
- シンボルの型は、
'z'
,'Z'
のような特殊な型(ファイル名に関連するシンボル)や、一般的なシンボル型(関数、変数など)を区別します。 newTable
関数は、walksymtab
を利用して、解析されたシンボルエントリを[]Sym
スライスとして構築します。この際、ファイル名に関連するシンボル('f'
型)を解決し、完全なパス名を構築するロジックも含まれています。File
構造体のSymbols()
メソッドは、syms
セクションからシンボルテーブルの生データを取得し、newTable
を呼び出して解析済みのシンボルリストを返します。
-
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バイナリのシンボル情報を正確に表示できるようになります。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが新規作成または変更されています。
-
src/cmd/nm/nm.go
:parsers
変数に、Plan 9 a.outのマジックナンバー(386, mips, arm, amd64用)と、それらを処理するためのplan9Symbols
関数への参照が追加されました。これにより、nm
ツールがPlan 9バイナリを自動的に認識し、適切なパーサーを使用できるようになります。
-
src/cmd/nm/plan9obj.go
(新規):plan9Symbols
関数が定義されています。この関数は、os.File
を受け取り、debug/plan9obj.NewFile
を使ってPlan 9 a.outファイルを解析し、そのシンボルテーブルを読み取ります。- 読み取ったシンボルは、
nm
ツールが使用するSym
構造体に変換され、アドレス順にソートされて返されます。シンボルのサイズは、次のシンボルとのアドレス差から推測されます。
-
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()
メソッドは、指定された名前のセクションを返します。
-
src/pkg/debug/plan9obj/file_test.go
(新規):debug/plan9obj
パッケージのテストファイルです。TestOpen
関数は、実際のPlan 9 a.outテストデータ(386-plan9-exec, amd64-plan9-exec)を使用して、Open
関数がファイルヘッダとセクション情報を正しく解析できることを検証します。TestOpenFailure
関数は、無効なファイルを開こうとした場合の挙動をテストします。
-
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
変数は、各マジックナンバーに対応するポインタサイズやヘッダサイズなどの実行可能ファイル情報をマッピングします。
-
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
ツールのような既存のデバッグツールがこの新しい機能を活用できるようになりました。
関連リンク
- Plan 9 from Bell Labs: Plan 9オペレーティングシステムの公式ウェブサイト。
- Plan 9 a.out(5) man page: Plan 9 a.out形式に関する公式ドキュメント。
- Go言語の
debug
パッケージドキュメント: - Go言語の
cmd/nm
ツール:
参考にした情報源リンク
- Go Gerrit Change 49970044: このコミットの元のGerritレビューページ。
- Plan 9 a.out format: Plan 9 a.out形式に関する技術的な説明。
- The Go Programming Language: Go言語に関する公式ドキュメントや情報。
- Wikipedia - Plan 9 from Bell Labs: Plan 9オペレーティングシステムに関するWikipediaの記事。
- Wikipedia - a.out: a.outファイル形式に関するWikipediaの記事。