[インデックス 19283] ファイルの概要
このコミットは、Go言語のツールチェインに含まれる cmd/addr2line
ツールが、WindowsのPE (Portable Executable) 形式の実行ファイルに対応できるようにするための変更です。これにより、Windows環境で生成されたGoバイナリに対しても、アドレスからソースコードの行情報を解決できるようになります。
コミット
commit b211d0601420dbd26ef98f2c7de8167e3fdea865
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Wed May 7 10:16:55 2014 +1000
cmd/addr2line: works with windows pe executables now
Update #7406
Fixes #7899
LGTM=bradfitz
R=golang-codereviews, rsc, bradfitz
CC=golang-codereviews
https://golang.org/cl/96960043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b211d0601420dbd26ef98f2c7de8167e3fdea865
元コミット内容
cmd/addr2line: works with windows pe executables now
このコミットは、cmd/addr2line
ツールがWindowsのPE実行ファイルで動作するように修正するものです。コミットメッセージには、内部的な追跡番号である Update #7406
と Fixes #7899
が含まれています。これらはGoプロジェクトのコードレビューシステム(Gerrit)における変更セットや課題に関連する可能性があり、一般的なGitHubのIssue番号とは異なる場合があります。
変更の背景
cmd/addr2line
は、Go言語のプロファイリングツール pprof
などで利用される重要なユーティリティです。その主な機能は、実行ファイルのメモリアドレスを対応するソースコードのファイル名、行番号、関数名に変換することです。これにより、クラッシュダンプの解析やパフォーマンスプロファイリングにおいて、どのコードが問題を引き起こしているのかを特定するのに役立ちます。
しかし、このツールは元々、LinuxやmacOSなどのELF (Executable and Linkable Format) やMach-O形式の実行ファイルを主に想定して設計されていました。Windows環境で生成されたGoバイナリはPE形式であり、その内部構造がELFやMach-Oとは異なります。特に、Goランタイムがデバッグ情報やシンボル情報を格納する pclntab
(Program Counter Line Table) や symtab
(Symbol Table) といったセクションの配置や読み出し方法がPE形式では異なるため、既存の addr2line
はWindowsバイナリに対して正しく機能しませんでした。
このコミットは、WindowsユーザーがGo言語のデバッグやプロファイリングをより効果的に行えるようにするため、addr2line
がPE形式の実行ファイルから必要な情報を正しく抽出できるようにするための対応として導入されました。
前提知識の解説
1. cmd/addr2line
ツール
cmd/addr2line
は、Go言語の標準ツールチェインに含まれるコマンドラインユーティリティです。その名の通り、「アドレスから行番号へ」変換する機能を提供します。具体的には、Goの実行ファイルと1つ以上のメモリアドレス(通常は16進数)を標準入力から受け取り、そのアドレスがどの関数内の、どのソースファイルの何行目に該当するかを標準出力に出力します。これは、pprof
ツールが生成するプロファイルデータや、クラッシュ時のスタックトレースを人間が読める形式に変換する際に不可欠です。
2. Windows PE (Portable Executable) 形式
PE形式は、Microsoft Windowsオペレーティングシステムで使用される実行可能ファイル、DLL (Dynamic Link Library)、オブジェクトファイルなどの標準ファイル形式です。PEファイルは、DOSヘッダ、PEヘッダ(NTヘッダ)、セクションテーブル、そして実際のコードやデータを含む複数のセクション(例: .text
, .data
, .rdata
など)で構成されています。Go言語でWindows向けにコンパイルされたバイナリもこのPE形式に従います。
3. Goランタイムの pclntab
と symtab
Go言語のバイナリには、デバッグやプロファイリング、スタックトレースの生成に不可欠なメタデータが埋め込まれています。
-
pclntab
(Program Counter Line Table): これは「プログラムカウンタ行テーブル」の略で、Goバイナリに埋め込まれた最も重要なメタデータ構造の一つです。pclntab
は、プログラムカウンタ(PC、命令のアドレス)を対応するソースコードのファイル名、行番号、関数名にマッピングする情報を含んでいます。Goのランタイムは、パニック発生時のスタックトレース生成や、runtime.Caller
のような関数が呼び出された際に、このpclntab
を利用してアドレスをソースコード情報に変換します。pclntab
は、バイナリが「ストリップ」されても(デバッグシンボルが除去されても)残ることが多く、Goのデバッグやプロファイリングの基盤となっています。ELFやMach-O形式では通常.gopclntab
セクションに格納されます。 -
symtab
(Symbol Table): 一般的なシンボルテーブルは、識別子(シンボル)をその属性(メモリアドレス、型など)にマッピングするデータ構造です。Goの文脈では、かつては.gosymtab
というGo固有のシンボルテーブルが存在しましたが、Go 1.3以降、このテーブルは積極的に利用されなくなりました。しかし、PEファイルにおいては、runtime.symtab
というシンボルがpclntab
の位置を示すために利用されることがあります。このコミットでは、PEファイルからこれらのテーブルを正しく読み出すためのロジックが追加されています。
4. PEファイルのセクションとシンボル
PEファイルでは、コードやデータは論理的な「セクション」に分割されて格納されます。例えば、実行可能なコードは通常 .text
セクションに、初期化されたデータは .data
セクションに配置されます。Goバイナリの場合、pclntab
や symtab
のデータは、特定のセクション内に配置され、その開始と終了はシンボルによって示されることがあります。このコミットでは、PEファイル内のシンボルを検索し、それらのシンボルが指し示すセクションから必要なデータを抽出する処理が導入されています。
技術的詳細
このコミットの主要な技術的変更点は、cmd/addr2line/main.go
において、PE形式の実行ファイルから pclntab
と symtab
のデータを正しく読み出すためのロジックが追加されたことです。
従来の loadTables
関数は、ELFやMach-O形式のバイナリを想定しており、特定のセクション名(例: .gopclntab
, .gosymtab
)を直接検索してデータを取得していました。しかし、PE形式ではこれらのデータが異なる方法で配置されるため、新しい処理が必要となります。
具体的には、以下の新しい関数が導入され、loadTables
関数内でPEファイルの場合に呼び出されるようになりました。
-
findPESymbol(f *pe.File, name string) (*pe.Symbol, error)
: この関数は、PEファイル (*pe.File
) とシンボル名 (name
) を引数に取り、指定されたシンボル名を持つPEシンボル (*pe.Symbol
) を検索して返します。PEファイル内のシンボルリストを走査し、一致するシンボルを見つけます。シンボルが見つからない場合や、セクション番号が無効な場合はエラーを返します。 -
loadPETable(f *pe.File, sname, ename string) ([]byte, error)
: この関数は、PEファイル (*pe.File
) と、テーブルの開始を示すシンボル名 (sname
)、終了を示すシンボル名 (ename
) を引数に取ります。- まず、
findPESymbol
を使用してsname
とename
のシンボルを検索します。 - 次に、これら二つのシンボルが同じセクションに存在するかどうかを確認します。Goの
pclntab
やsymtab
は、通常、特定のセクション内で連続したデータとして格納されるため、このチェックは重要です。 - 対象のセクションのデータを取得し、
sname
シンボルの値(オフセット)からename
シンボルの値(オフセット)までの範囲をスライスして、テーブルのバイトデータを抽出します。
- まず、
loadTables
関数内では、PEファイルが検出された場合、loadPETable
を使用して pclntab
と symtab
のデータを読み込むように変更されました。GoのPEバイナリでは、pclntab
は pclntab
と epclntab
というシンボルによって、symtab
は symtab
と esymtab
というシンボルによってその範囲が示されます。これらのシンボルは、GoコンパイラがPEバイナリを生成する際に埋め込むものです。
また、addr2line_test.go
という新しいテストファイルが追加され、Windows PE実行ファイルに対する addr2line
の動作を検証するテストケースが記述されています。これにより、変更が正しく機能し、将来のリグレッションを防ぐための基盤が確立されました。
コアとなるコードの変更箇所
src/cmd/addr2line/addr2line_test.go
(新規ファイル)
このファイルは、addr2line
ツールがWindows PE実行ファイルに対して正しく動作するかを検証するためのテストケースを含んでいます。
loadSyms
関数:go tool nm
を使用して実行ファイルからシンボル情報をロードします。runAddr2Line
関数: ビルドしたaddr2line
ツールを実行し、指定されたアドレスに対する関数名、ファイルパス、行番号を取得します。Windowsの場合のパスの解析ロジックが追加されています。TestAddr2Line
関数: 実際のテストケースで、cmd/addr2line
をビルドし、自身のシンボル情報を使ってaddr2line
を実行し、結果が期待通りであることを検証します。特に、Windows環境でのパスの比較 (os.SameFile
) や行番号の検証が含まれています。
src/cmd/addr2line/main.go
(変更)
loadTables
関数が変更され、PE形式の実行ファイルに対応するためのロジックが追加されました。
--- a/src/cmd/addr2line/main.go
+++ b/src/cmd/addr2line/main.go
@@ -141,18 +141,50 @@ func loadTables(f *os.File) (textStart uint64, symtab, pclntab []byte, err error)
if sect := obj.Section(".text"); sect != nil {
textStart = uint64(sect.VirtualAddress)
}
- if sect := obj.Section(".gosymtab"); sect != nil {
- if symtab, err = sect.Data(); err != nil {
- return 0, nil, nil, err
- }
+ if pclntab, err = loadPETable(obj, "pclntab", "epclntab"); err != nil {
+ return 0, nil, nil, err
}
- if sect := obj.Section(".gopclntab"); sect != nil {
- if pclntab, err = sect.Data(); err != nil {
- return 0, nil, nil, err
- }
+ if symtab, err = loadPETable(obj, "symtab", "esymtab"); err != nil {
+ return 0, nil, nil, err
}
return textStart, symtab, pclntab, nil
}
return 0, nil, nil, fmt.Errorf("unrecognized binary format")
}
+
+func findPESymbol(f *pe.File, name string) (*pe.Symbol, error) {
+ for _, s := range f.Symbols {
+ if s.Name != name {
+ continue
+ }
+ if s.SectionNumber <= 0 {
+ return nil, fmt.Errorf("symbol %s: invalid section number %d", name, s.SectionNumber)
+ }
+ if len(f.Sections) < int(s.SectionNumber) {
+ return nil, fmt.Errorf("symbol %s: section number %d is larger than max %d", name, s.SectionNumber, len(f.Sections))
+ }
+ return s, nil
+ }
+ return nil, fmt.Errorf("no %s symbol found", name)
+}
+
+func loadPETable(f *pe.File, sname, ename string) ([]byte, error) {
+ ssym, err := findPESymbol(f, sname)
+ if err != nil {
+ return nil, err
+ }
+ esym, err := findPESymbol(f, ename)
+ if err != nil {
+ return nil, err
+ }
+ if ssym.SectionNumber != esym.SectionNumber {
+ return nil, fmt.Errorf("%s and %s symbols must be in the same section", sname, ename)
+ }
+ sect := f.Sections[ssym.SectionNumber-1]
+ data, err := sect.Data()
+ if err != nil {
+ return nil, err
+ }
+ return data[ssym.Value:esym.Value], nil
+}
コアとなるコードの解説
src/cmd/addr2line/main.go
の変更点
-
loadTables
関数のPE形式対応:loadTables
関数は、実行ファイルの形式を判別し、それに応じてpclntab
とsymtab
のデータを読み込む役割を担っています。このコミットでは、PEファイル (obj.(*pe.File)
) であると判別された場合に、新しいloadPETable
関数を呼び出すように変更されました。- 以前は、PEファイルの場合でも
.gosymtab
や.gopclntab
といったセクションを直接探していましたが、これはPE形式のGoバイナリにおけるpclntab
やsymtab
の実際の配置方法と一致しない場合がありました。 - 変更後は、
loadPETable(obj, "pclntab", "epclntab")
とloadPETable(obj, "symtab", "esymtab")
を呼び出すことで、GoコンパイラがPEバイナリに埋め込むpclntab
/epclntab
およびsymtab
/esymtab
というシンボルペアを利用して、それぞれのテーブルの開始と終了位置を特定し、データを抽出するようになりました。
- 以前は、PEファイルの場合でも
-
findPESymbol
関数の追加: この関数は、PEファイルオブジェクト (*pe.File
) とシンボル名を受け取り、そのシンボルに対応する*pe.Symbol
オブジェクトを返します。- PEファイルの
Symbols
リストをイテレートし、名前が一致するシンボルを探します。 - シンボルの
SectionNumber
が有効であること(0より大きいこと)と、そのセクション番号がファイルのセクション数を超えていないことを検証します。これにより、無効なシンボル参照によるパニックを防ぎます。
- PEファイルの
-
loadPETable
関数の追加: この関数は、PEファイルオブジェクトと、テーブルの開始シンボル名 (sname
)、終了シンボル名 (ename
) を受け取り、その範囲のバイトスライスを返します。findPESymbol
を使って開始シンボルと終了シンボルを検索します。- 重要なチェック: 開始シンボルと終了シンボルが同じセクションに属していることを確認します。これは、Goの
pclntab
やsymtab
が単一の連続したデータブロックとして格納されているという前提に基づいています。異なるセクションにある場合はエラーを返します。 - 対象のセクション (
sect := f.Sections[ssym.SectionNumber-1]
) の全データを取得します。 - 取得したセクションデータから、開始シンボルの値 (
ssym.Value
) から終了シンボルの値 (esym.Value
) までの範囲をスライスして、目的のテーブルデータ (data[ssym.Value:esym.Value]
) を抽出します。ssym.Value
とesym.Value
は、セクション内でのオフセットを示します。
これらの変更により、addr2line
はWindows PE形式のGoバイナリから pclntab
と symtab
の情報を正確に読み取れるようになり、Windows環境でのデバッグやプロファイリングの精度が向上しました。
関連リンク
- Go言語の
addr2line
ツールに関する公式ドキュメントやソースコード: - Go言語のコードレビューシステム (Gerrit) の変更セット:
参考にした情報源リンク
- Go言語の
cmd/addr2line
の目的: - Windows PE (Portable Executable) 形式:
- Goランタイムの
pclntab
とsymtab
について: - Go issue #7406 および #7899 については、公開されているGoプロジェクトのIssueトラッカーでは直接関連する情報が見つかりませんでした。これらはGoの内部的なコードレビューやバグトラッキングシステムにおける参照である可能性が高いです。