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

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

このコミットは、Go言語のツールチェインに含まれる addr2line コマンドをC言語からGo言語に再実装するものです。これにより、新しいオブジェクトファイル形式に対応し、addr2line の機能が修正されます。

コミット

commit 8efb5e7d638684bcfc5e1aed1b352886b48f421b
Author: Russ Cox <rsc@golang.org>
Date:   Wed Feb 19 14:33:11 2014 -0500

    cmd/addr2line: reimplement in Go
    
    We never updated libmach for the new object file format,
    so it the existing 'go tool addr2line' is broken.
    Reimplement in Go to fix.
    
    LGTM=r
    R=golang-codereviews, r
    CC=golang-codereviews
    https://golang.org/cl/66020043

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

https://github.com/golang/go/commit/8efb5e7d638684bcfc5e1aed1b352886b48f421b

元コミット内容

cmd/addr2line: reimplement in Go

libmach が新しいオブジェクトファイル形式に対応していなかったため、既存の go tool addr2line が壊れていました。これを修正するためにGo言語で再実装します。

変更の背景

Go言語のツールチェインには、実行バイナリ内のアドレスをソースコードのファイル名と行番号に変換する addr2line というツールが含まれています。これは、特にプロファイリングツール pprof などが生成するプロファイルデータ(実行時のアドレス情報)を人間が読める形式(どの関数のどの行で時間がかかっているかなど)に変換するために不可欠なツールです。

このコミット以前の addr2line はC言語で実装されており、libmach というライブラリを使用してオブジェクトファイル(実行バイナリ)の情報を解析していました。しかし、Go言語のオブジェクトファイル形式が更新された際、libmach がこの新しい形式に対応するように更新されませんでした。その結果、既存の go tool addr2line は新しいGoバイナリに対して正しく機能しなくなり、壊れた状態となっていました。

この問題を解決し、addr2line の機能を回復させることが、このコミットの主要な背景です。Go言語で再実装することで、Goの標準ライブラリが提供するオブジェクトファイル解析機能(debug/elf, debug/macho, debug/pe など)を活用し、将来的なGoのオブジェクトファイル形式の変更にも対応しやすくなるという利点もあります。

前提知識の解説

addr2line とは

addr2line は、プログラムの実行アドレス(メモリ上の番地)を、そのアドレスに対応するソースコードのファイル名、行番号、および関数名に変換するデバッグツールです。主に、クラッシュダンプの解析、プロファイリングデータの可読化、デバッガの補助などに利用されます。Go言語においては、go tool pprof が生成するプロファイルデータを解析する際に内部的に利用されます。

pprof とは

pprof はGo言語の標準的なプロファイリングツールです。CPU使用率、メモリ割り当て、ゴルーチン、ブロック、ミューテックス競合などのプロファイルデータを収集し、視覚化することができます。pprof が収集するデータには実行アドレスが含まれており、これらのアドレスをソースコードの行にマッピングするために addr2line が必要となります。

オブジェクトファイル形式 (ELF, Mach-O, PE)

プログラムの実行バイナリは、オペレーティングシステムによって異なる形式で格納されます。

  • ELF (Executable and Linkable Format): LinuxやUnix系システムで広く使われる標準的な実行可能ファイル、オブジェクトファイル、共有ライブラリの形式です。
  • Mach-O (Mach Object): macOS (旧OS X) やiOSで使われる実行可能ファイル、オブジェクトファイル、共有ライブラリの形式です。
  • PE (Portable Executable): Windowsで使われる実行可能ファイル、オブジェクトファイル、DLL (Dynamic Link Library) の形式です。

これらのファイル形式には、プログラムコード、データ、シンボルテーブル(関数名や変数名とアドレスのマッピング)、デバッグ情報(ソースコードのファイル名と行番号とアドレスのマッピング)などが含まれています。addr2line はこれらのデバッグ情報を読み取ってアドレス変換を行います。

libmach

libmach は、Plan 9オペレーティングシステムに由来するライブラリで、様々なCPUアーキテクチャやオブジェクトファイル形式(a.out, ELF, Mach-Oなど)を抽象化してアクセスするためのものです。Go言語の初期のツールチェインでは、クロスプラットフォームなオブジェクトファイル解析のためにこのライブラリが利用されていました。しかし、Goの進化に伴い、Go自身の標準ライブラリでオブジェクトファイル形式を直接扱う debug/elfdebug/macho といったパッケージが提供されるようになり、libmach の必要性は薄れていきました。

gosym パッケージ

debug/gosym パッケージは、Goの実行バイナリに含まれるGo固有のシンボルテーブルとPC-Lineテーブル(プログラムカウンタと行番号のマッピング)を解析するためのGo標準ライブラリです。Goのコンパイラは、通常のELF/Mach-O/PE形式のデバッグ情報とは別に、Goランタイムが利用する独自のシンボル情報(.gosymtab セクション)とPC-Line情報(.gopclntab セクション)をバイナリに埋め込みます。debug/gosym はこれらのセクションを読み取り、アドレスから関数名やファイル・行番号への変換を効率的に行います。

技術的詳細

このコミットの技術的な核心は、addr2line の実装をC言語からGo言語へ完全に移行した点にあります。

  1. C言語実装の削除: 既存の src/cmd/addr2line/main.c ファイルが削除されました。これは、libmach に依存していた古い実装が完全に破棄されたことを意味します。

  2. Go言語実装の導入: src/cmd/addr2line/main.go という新しいファイルが追加され、Go言語による addr2line の機能が実装されました。

    • オブジェクトファイル解析の抽象化: 新しいGo実装では、loadTables 関数が導入されています。この関数は、入力されたバイナリファイルがELF、Mach-O、PEのいずれの形式であるかを自動的に判別し、それぞれの形式に対応するGo標準ライブラリのパッケージ(debug/elf, debug/macho, debug/pe)を使用してバイナリを解析します。
    • Go固有のデバッグ情報の抽出: 各オブジェクトファイル形式から、Goランタイムが使用するシンボルテーブル (.gosymtab または __gosymtab) とPC-Lineテーブル (.gopclntab または __gopclntab) の生データを抽出します。これらのセクションは、Goのコンパイラが生成するバイナリに埋め込まれるGo固有のデバッグ情報です。
    • debug/gosym の活用: 抽出された生データは、debug/gosym パッケージの gosym.NewLineTablegosym.NewTable 関数に渡されます。これにより、Goのシンボル情報とPC-Line情報がメモリ上にロードされ、効率的なアドレス変換が可能になります。
    • アドレスからファイル・行・関数への変換: main 関数では、標準入力からアドレス(16進数)を読み込み、gosym.Table オブジェクトの PCToLine メソッドを使用して、そのアドレスに対応するファイル名、行番号、関数名を取得します。結果は標準出力に 関数名\nファイル:行番号\n の形式で出力されます。
    • 逆変換機能の非実装: 以前のC言語版 addr2line には、ファイル:行番号 からアドレスを逆引きする機能がありましたが、新しいGo言語版ではこの機能は実装されていません。コミットメッセージにも「probably not used by anyone」とあり、重要度が低いと判断されたようです。
  3. ビルドシステムの変更:

    • src/cmd/dist/build.c: Goのビルドシステムを定義するファイルから、cmd/addr2line がC言語でビルドされるリストから削除されました。
    • src/cmd/go/pkg.go: goTools マップに cmd/addr2line が追加されました。これにより、addr2line は他のGoツール(cmd/api, cmd/cgo など)と同様に、Go言語でビルドされ、Goのツールチェインの一部として扱われるようになります。

この再実装により、addr2line はGoの新しいオブジェクトファイル形式に完全に準拠し、クロスプラットフォームで安定して動作するようになりました。

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

src/cmd/addr2line/main.c (削除)

--- a/src/cmd/addr2line/main.c
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright 2012 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-/*
- * addr2line simulation - only enough to make pprof work on Macs
- */
-
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <mach.h>
-
-// ... (rest of C code) ...

src/cmd/addr2line/main.go (新規追加)

// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// addr2line simulation - only enough to make pprof work on Macs

package main

import (
	"bufio"
	"debug/elf"
	"debug/gosym"
	"debug/macho"
	"debug/pe"
	"flag"
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"
)

// ... (printUsage, usage functions) ...

func main() {
	log.SetFlags(0)
	log.SetPrefix("addr2line: ")

	if len(os.Args) > 1 && os.Args[1] == "--help" {
		printUsage(os.Stdout)
		os.Exit(0)
	}

	flag.Usage = usage
	flag.Parse()
	if flag.NArg() != 1 {
		usage()
	}

	f, err := os.Open(flag.Arg(0))
	if err != nil {
		log.Fatal(err)
	}

	// textStart, symtab, pclntab をロード
	textStart, symtab, pclntab, err := loadTables(f)
	if err != nil {
		log.Fatalf("reading %s: %v", flag.Arg(0), err)
	}

	// gosym パッケージを使ってシンボルテーブルとPC-Lineテーブルを構築
	pcln := gosym.NewLineTable(pclntab, textStart)
	tab, err := gosym.NewTable(symtab, pcln)
	if err != nil {
		log.Fatalf("reading %s: %v", flag.Arg(0), err)
	}

	stdin := bufio.NewScanner(os.Stdin)
	stdout := bufio.NewWriter(os.Stdout)

	for stdin.Scan() {
		p := stdin.Text()
		if strings.Contains(p, ":") {
			// 逆変換は未実装
			fmt.Fprintf(stdout, "!reverse translation not implemented\\n")
			continue
		}
		pc, _ := strconv.ParseUint(p, 16, 64)
		// アドレスからファイル、行、関数名を取得
		file, line, fn := tab.PCToLine(pc)
		name := "?"
		if fn != nil {
			name = fn.Name
		} else {
			file = "?"
			line = 0
		}
		fmt.Fprintf(stdout, "%s\\n%s:%d\\n", name, file, line)
	}
	stdout.Flush()
}

// オブジェクトファイルから必要なテーブルをロードする関数
func loadTables(f *os.File) (textStart uint64, symtab, pclntab []byte, err error) {
	// ELF形式の解析
	if obj, err := elf.NewFile(f); err == nil {
		if sect := obj.Section(".text"); sect != nil {
			textStart = sect.Addr
		}
		if sect := obj.Section(".gosymtab"); sect != nil {
			if symtab, err = sect.Data(); 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
			}
		}
		return textStart, symtab, pclntab, nil
	}

	// Mach-O形式の解析
	if obj, err := macho.NewFile(f); err == nil {
		if sect := obj.Section("__text"); sect != nil {
			textStart = sect.Addr
		}
		if sect := obj.Section("__gosymtab"); sect != nil {
			if symtab, err = sect.Data(); 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
			}
		}
		return textStart, symtab, pclntab, nil
	}

	// PE形式の解析
	if obj, err := pe.NewFile(f); err == nil {
		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 sect := obj.Section(".gopclntab"); sect != nil {
			if pclntab, err = sect.Data(); err != nil {
				return 0, nil, nil, err
			}
		}
		return textStart, symtab, pclntab, nil
	}

	return 0, nil, nil, fmt.Errorf("unrecognized binary format")
}

src/cmd/dist/build.c (変更)

--- a/src/cmd/dist/build.c
+++ b/src/cmd/dist/build.c
@@ -1297,7 +1297,6 @@ static char *buildorder[] = {
 
 	"misc/pprof",
 
-\t"cmd/addr2line",
 	"cmd/objdump",
 	"cmd/prof",
 
@@ -1372,7 +1371,6 @@ static char *cleantab[] = {
 	"cmd/8c",
 	"cmd/8g",
 	"cmd/8l",
-\t"cmd/addr2line",
 	"cmd/cc",
 	"cmd/gc",
 	"cmd/go",

src/cmd/go/pkg.go (変更)

--- a/src/cmd/go/pkg.go
+++ b/src/cmd/go/pkg.go
@@ -307,13 +307,14 @@ const (
 
 // goTools is a map of Go program import path to install target directory.
 var goTools = map[string]targetDir{
-\t"cmd/api":  toTool,\n-\t"cmd/cgo":  toTool,\n-\t"cmd/fix":  toTool,\n-\t"cmd/link": toTool,\n-\t"cmd/nm":   toTool,\n-\t"cmd/pack": toTool,\n-\t"cmd/yacc": toTool,\n+\t"cmd/addr2line": toTool,\n+\t"cmd/api":       toTool,\n+\t"cmd/cgo":       toTool,\n+\t"cmd/fix":       toTool,\n+\t"cmd/link":      toTool,\n+\t"cmd/nm":        toTool,\n+\t"cmd/pack":      toTool,\n+\t"cmd/yacc":      toTool,\n \t"code.google.com/p/go.tools/cmd/benchcmp": toTool,\n \t"code.google.com/p/go.tools/cmd/cover":    toTool,\n \t"code.google.com/p/go.tools/cmd/godoc":    toBin,\n```

## コアとなるコードの解説

### `src/cmd/addr2line/main.go`

このファイルは、`addr2line` ツールの新しいGo言語実装です。

-   **`import` ステートメント**: 必要な標準ライブラリパッケージをインポートしています。特に注目すべきは、`debug/elf`, `debug/macho`, `debug/pe` で、これらがそれぞれELF、Mach-O、PE形式のバイナリを解析するために使用されます。また、`debug/gosym` はGo固有のシンボル情報とPC-Line情報を扱うためのパッケージです。
-   **`main` 関数**:
    -   コマンドライン引数を解析し、バイナリファイルのパスを取得します。
    -   `os.Open` で指定されたバイナリファイルを開きます。
    -   `loadTables` 関数を呼び出して、バイナリから必要なデバッグ情報(テキストセクションの開始アドレス、Goシンボルテーブル、Go PC-Lineテーブル)をロードします。
    -   `gosym.NewLineTable` と `gosym.NewTable` を使用して、ロードした生データから `gosym.Table` オブジェクトを構築します。この `gosym.Table` がアドレス変換の主要なインターフェースとなります。
    -   標準入力 (`os.Stdin`) からアドレスを1行ずつ読み込みます。
    -   読み込んだアドレスが `ファイル:行番号` の形式である場合(C言語版にあった逆変換の構文)、Go版では未実装であることを示すメッセージを出力します。
    -   アドレス(16進数)を `strconv.ParseUint` で `uint64` に変換します。
    -   `tab.PCToLine(pc)` を呼び出し、アドレス `pc` に対応するファイル名、行番号、関数名を取得します。
    -   取得した情報を `fmt.Fprintf` で標準出力 (`os.Stdout`) に `関数名\nファイル:行番号\n` の形式で出力します。
-   **`loadTables` 関数**:
    -   この関数は、与えられたファイル (`*os.File`) をELF、Mach-O、PEのいずれかの形式として順に解析を試みます。
    -   各形式に対応する `debug/elf.NewFile`, `debug/macho.NewFile`, `debug/pe.NewFile` を使用して、オブジェクトファイル構造を読み込みます。
    -   成功した場合、それぞれのオブジェクトファイルから、`.text` (または `__text`) セクションの開始アドレス、`.gosymtab` (または `__gosymtab`) セクションのデータ、`.gopclntab` (または `__gopclntab`) セクションのデータを抽出します。これらのセクションはGoのデバッグ情報が格納されている場所です。
    -   必要なデータがすべて抽出できたら、それらを返します。
    -   どの形式としても認識できなかった場合は、エラーを返します。

### `src/cmd/dist/build.c`

このファイルはGoのビルドシステムの一部であり、どのツールがどのようにビルドされるかを定義しています。
-   `buildorder` 配列から `"cmd/addr2line"` が削除されました。これは、`addr2line` がC言語のソースからビルドされるリストから除外されたことを意味します。
-   `cleantab` 配列からも `"cmd/addr2line"` が削除されました。これは、ビルドクリーンアップの対象からC言語版の `addr2line` が除外されたことを意味します。

### `src/cmd/go/pkg.go`

このファイルは `go` コマンドが認識するGoツールに関する情報を含んでいます。
-   `goTools` マップに `"cmd/addr2line": toTool` が追加されました。これにより、`go install cmd/addr2line` のようなコマンドで、新しいGo言語版の `addr2line` がGoのツールとしてビルド・インストールされるようになります。

これらの変更により、`addr2line` はGoのツールチェインに完全に統合され、Goの標準ライブラリのオブジェクトファイル解析機能を利用することで、将来のGoバイナリ形式の変更にも柔軟に対応できるようになりました。

## 関連リンク

-   Go言語の `debug/gosym` パッケージ: [https://pkg.go.dev/debug/gosym](https://pkg.go.dev/debug/gosym)
-   Go言語の `debug/elf` パッケージ: [https://pkg.go.dev/debug/elf](https://pkg.go.dev/debug/elf)
-   Go言語の `debug/macho` パッケージ: [https://pkg.go.dev/debug/macho](https://pkg.go.dev/debug/macho)
-   Go言語の `debug/pe` パッケージ: [https://pkg.go.dev/debug/pe](https://pkg.go.dev/debug/pe)
-   `go tool pprof` のドキュメント: [https://pkg.go.dev/cmd/pprof](https://pkg.go.dev/cmd/pprof)

## 参考にした情報源リンク

-   Go言語の公式ドキュメント
-   Go言語のソースコード
-   `addr2line` の一般的な概念に関する情報
-   ELF, Mach-O, PE オブジェクトファイル形式に関する情報