[インデックス 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/elf
や debug/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言語へ完全に移行した点にあります。
-
C言語実装の削除: 既存の
src/cmd/addr2line/main.c
ファイルが削除されました。これは、libmach
に依存していた古い実装が完全に破棄されたことを意味します。 -
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.NewLineTable
とgosym.NewTable
関数に渡されます。これにより、Goのシンボル情報とPC-Line情報がメモリ上にロードされ、効率的なアドレス変換が可能になります。- アドレスからファイル・行・関数への変換:
main
関数では、標準入力からアドレス(16進数)を読み込み、gosym.Table
オブジェクトのPCToLine
メソッドを使用して、そのアドレスに対応するファイル名、行番号、関数名を取得します。結果は標準出力に関数名\nファイル:行番号\n
の形式で出力されます。 - 逆変換機能の非実装: 以前のC言語版
addr2line
には、ファイル:行番号
からアドレスを逆引きする機能がありましたが、新しいGo言語版ではこの機能は実装されていません。コミットメッセージにも「probably not used by anyone」とあり、重要度が低いと判断されたようです。
- オブジェクトファイル解析の抽象化: 新しいGo実装では、
-
ビルドシステムの変更:
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 オブジェクトファイル形式に関する情報