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

[インデックス 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 #7406Fixes #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ランタイムの pclntabsymtab

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バイナリの場合、pclntabsymtab のデータは、特定のセクション内に配置され、その開始と終了はシンボルによって示されることがあります。このコミットでは、PEファイル内のシンボルを検索し、それらのシンボルが指し示すセクションから必要なデータを抽出する処理が導入されています。

技術的詳細

このコミットの主要な技術的変更点は、cmd/addr2line/main.go において、PE形式の実行ファイルから pclntabsymtab のデータを正しく読み出すためのロジックが追加されたことです。

従来の loadTables 関数は、ELFやMach-O形式のバイナリを想定しており、特定のセクション名(例: .gopclntab, .gosymtab)を直接検索してデータを取得していました。しかし、PE形式ではこれらのデータが異なる方法で配置されるため、新しい処理が必要となります。

具体的には、以下の新しい関数が導入され、loadTables 関数内でPEファイルの場合に呼び出されるようになりました。

  1. findPESymbol(f *pe.File, name string) (*pe.Symbol, error): この関数は、PEファイル (*pe.File) とシンボル名 (name) を引数に取り、指定されたシンボル名を持つPEシンボル (*pe.Symbol) を検索して返します。PEファイル内のシンボルリストを走査し、一致するシンボルを見つけます。シンボルが見つからない場合や、セクション番号が無効な場合はエラーを返します。

  2. loadPETable(f *pe.File, sname, ename string) ([]byte, error): この関数は、PEファイル (*pe.File) と、テーブルの開始を示すシンボル名 (sname)、終了を示すシンボル名 (ename) を引数に取ります。

    • まず、findPESymbol を使用して snameename のシンボルを検索します。
    • 次に、これら二つのシンボルが同じセクションに存在するかどうかを確認します。Goの pclntabsymtab は、通常、特定のセクション内で連続したデータとして格納されるため、このチェックは重要です。
    • 対象のセクションのデータを取得し、sname シンボルの値(オフセット)から ename シンボルの値(オフセット)までの範囲をスライスして、テーブルのバイトデータを抽出します。

loadTables 関数内では、PEファイルが検出された場合、loadPETable を使用して pclntabsymtab のデータを読み込むように変更されました。GoのPEバイナリでは、pclntabpclntabepclntab というシンボルによって、symtabsymtabesymtab というシンボルによってその範囲が示されます。これらのシンボルは、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 関数は、実行ファイルの形式を判別し、それに応じて pclntabsymtab のデータを読み込む役割を担っています。このコミットでは、PEファイル (obj.(*pe.File)) であると判別された場合に、新しい loadPETable 関数を呼び出すように変更されました。

    • 以前は、PEファイルの場合でも .gosymtab.gopclntab といったセクションを直接探していましたが、これはPE形式のGoバイナリにおける pclntabsymtab の実際の配置方法と一致しない場合がありました。
    • 変更後は、loadPETable(obj, "pclntab", "epclntab")loadPETable(obj, "symtab", "esymtab") を呼び出すことで、GoコンパイラがPEバイナリに埋め込む pclntab/epclntab および symtab/esymtab というシンボルペアを利用して、それぞれのテーブルの開始と終了位置を特定し、データを抽出するようになりました。
  • findPESymbol 関数の追加: この関数は、PEファイルオブジェクト (*pe.File) とシンボル名を受け取り、そのシンボルに対応する *pe.Symbol オブジェクトを返します。

    • PEファイルの Symbols リストをイテレートし、名前が一致するシンボルを探します。
    • シンボルの SectionNumber が有効であること(0より大きいこと)と、そのセクション番号がファイルのセクション数を超えていないことを検証します。これにより、無効なシンボル参照によるパニックを防ぎます。
  • loadPETable 関数の追加: この関数は、PEファイルオブジェクトと、テーブルの開始シンボル名 (sname)、終了シンボル名 (ename) を受け取り、その範囲のバイトスライスを返します。

    • findPESymbol を使って開始シンボルと終了シンボルを検索します。
    • 重要なチェック: 開始シンボルと終了シンボルが同じセクションに属していることを確認します。これは、Goの pclntabsymtab が単一の連続したデータブロックとして格納されているという前提に基づいています。異なるセクションにある場合はエラーを返します。
    • 対象のセクション (sect := f.Sections[ssym.SectionNumber-1]) の全データを取得します。
    • 取得したセクションデータから、開始シンボルの値 (ssym.Value) から終了シンボルの値 (esym.Value) までの範囲をスライスして、目的のテーブルデータ (data[ssym.Value:esym.Value]) を抽出します。ssym.Valueesym.Value は、セクション内でのオフセットを示します。

これらの変更により、addr2line はWindows PE形式のGoバイナリから pclntabsymtab の情報を正確に読み取れるようになり、Windows環境でのデバッグやプロファイリングの精度が向上しました。

関連リンク

参考にした情報源リンク