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

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

このコミットは、Go言語のcmd/nmツールにおけるWindows PE (Portable Executable) ファイルのハンドリングに関する修正を導入しています。具体的には、シンボルのアドレス表示を相対アドレスから絶対アドレスに変更し、負のセクション番号を受け入れるように改善しています。これにより、cmd/nmがWindows環境でより正確かつ堅牢に動作するようになります。

コミット

commit 06dc4e78c4c925f0e3763241b9695e6f3a36d8d6
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Wed Apr 16 22:17:38 2014 -0400

    cmd/nm: windows pe handling fixes
    
    - output absolute addresses, not relative;
    - accept negative section numbers.
    
    Update #6936
    Fixes #7738
    
    LGTM=rsc
    R=golang-codereviews, bradfitz, ruiu, rsc
    CC=golang-codereviews
    https://golang.org/cl/85240046

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

https://github.com/golang/go/commit/06dc4e78c4c925f0e3763241b9695e6f3a36d8d6

元コミット内容

cmd/nm: windows pe handling fixes

このコミットは、Go言語のnmコマンドラインツールにおけるWindows PE (Portable Executable) ファイルの処理に関する修正を含んでいます。主な変更点は以下の2点です。

  1. 絶対アドレスの出力: シンボルのアドレスを相対アドレスではなく、絶対アドレスで出力するように変更されました。
  2. 負のセクション番号の許容: 負のセクション番号を適切に処理できるように修正されました。

この変更は、GoのIssue #6936の更新とIssue #7738の修正に関連しています。

変更の背景

cmd/nmは、Goのバイナリファイルやオブジェクトファイル内のシンボル情報を表示するためのツールです。Windows環境では、実行可能ファイルはPE (Portable Executable) フォーマットを使用します。従来のcmd/nmは、Windows PEファイルに対してシンボルアドレスを相対アドレスで表示していました。しかし、デバッグや解析の際には、メモリ上の実際の配置を示す絶対アドレスの方が有用な場合が多く、この表示方法が問題となっていました。

また、PEファイルのシンボルテーブルには、特定の意味を持つ負のセクション番号が存在します。例えば、N_UNDEF (未定義シンボル) や N_ABS (絶対シンボル) などがこれに該当します。cmd/nmがこれらの負のセクション番号を適切に解釈できない場合、シンボル情報の表示が不正確になったり、エラーが発生したりする可能性がありました。

これらの問題を解決し、cmd/nmがWindows PEファイルをより正確かつ堅牢に解析できるようにするために、本コミットによる修正が導入されました。

前提知識の解説

1. cmd/nmツール

cmd/nmは、Go言語のツールチェインに含まれるユーティリティで、Unix系のnmコマンドと同様に、Goのオブジェクトファイル、アーカイブ、または実行可能ファイルによって定義または使用されるシンボルをリスト表示します。これにより、開発者はコンパイルされたGoバイナリの内部構造を理解し、関数、変数、その他のエンティティの配置を把握することができます。

出力は通常、シンボルのアドレス(16進数)、シンボルの種類を示す1文字のコード(例: Tはグローバルなコードセグメント、Uは未定義シンボル)、およびシンボル名で構成されます。

2. Portable Executable (PE) フォーマット

PEフォーマットは、Microsoft Windowsオペレーティングシステムが32ビットおよび64ビットの実行可能ファイル(.exe)、ダイナミックリンクライブラリ(.dll)、オブジェクトコードなどに使用するファイル形式です。これは、Common Object File Format (COFF) 仕様に基づいています。

PEファイルは、Windowsローダーが実行可能ファイルをメモリにマッピングして実行するために必要な情報を提供します。PEファイルの構造は、DOSヘッダ、NTヘッダ(COFFファイルヘッダ、オプションヘッダを含む)、セクションテーブル、および実際のコードやデータを含むセクションで構成されます。

3. 仮想アドレス (VA) と相対仮想アドレス (RVA)

PEファイルにおいて、アドレスは「仮想アドレス (VA)」と「相対仮想アドレス (RVA)」の2つの方法で表現されます。

  • 仮想アドレス (VA): プロセスの仮想アドレス空間内の絶対的なメモリ位置を指します。PEファイルがメモリにロードされる際、オペレーティングシステムはベースアドレス(ImageBase)を割り当てます。ロードされたモジュール内のすべてのVAは、このベースアドレスにオフセットを加算することで計算されます。VAはCPUが実行中に使用する実際のメモリ位置です。

  • 相対仮想アドレス (RVA): モジュールがロードされるベースアドレス(ImageBase)からのオフセットを指します。PEヘッダやその他の構造体内で広く使用され、固定されたベースアドレスへの依存を避けるために利用されます。VAは VA = ImageBase + RVA の式で計算されます。RVAは、特にDLLのように異なるメモリ位置にロードされる可能性がある場合に、位置独立なコードとデータを実現するために重要です。

4. PEシンボルにおける特殊なセクション番号

COFFシンボルテーブルでは、シンボルの性質や場所を示すために、特定の意味を持つ特殊なセクション番号が使用されます。

  • N_UNDEF (未定義シンボル): 通常、値は0です。シンボルが未定義であることを示し、他のオブジェクトファイルやDLLで定義されている外部シンボルへの参照であることを意味します。
  • N_ABS (絶対シンボル): シンボルが絶対値を持つことを示し、どのセクションにも相対的ではありません。その値はリンク中に変化しません。これは通常、固定された定数やハードコードされたアドレスを持つシンボルに適用されます。
  • N_DEBUG (デバッグシンボル): シンボルが一般的な型情報やデバッグ情報を提供することを示します。実行可能コードやデータの特定のセクションには対応しません。

技術的詳細

このコミットの技術的な核心は、src/cmd/nm/pe.goファイルにおけるPEシンボル処理ロジックの変更にあります。

  1. ImageBaseの取得: PEファイルには、OptionalHeader内にImageBaseというフィールドがあります。これは、実行可能ファイルがメモリにロードされる際の推奨ベースアドレスを示します。このコミットでは、32ビット (OptionalHeader32) と64ビット (OptionalHeader64) の両方のオプションヘッダからImageBaseを正しく取得するロジックが追加されました。これにより、シンボルの相対アドレスを絶対アドレスに変換するための基準値が確立されます。

  2. シンボルアドレスの絶対化: PEファイル内のシンボルは、通常、セクション内の相対オフセットとしてValueフィールドに格納されています。これを絶対アドレスに変換するためには、シンボルが属するセクションの仮想アドレスと、PEファイルのImageBaseを加算する必要があります。 変更前は、sym.Addrs.Value(相対オフセット)が直接格納されていましたが、変更後には以下の計算が追加されました。 sym.Addr += imageBase + uint64(sect.VirtualAddress) これにより、nmコマンドの出力で表示されるアドレスが、メモリ上の実際の絶対アドレスとなります。

  3. 負のセクション番号のハンドリング: PEシンボル構造体にはSectionNumberフィールドがあり、これはシンボルが属するセクションのインデックスを示します。しかし、前述のN_UNDEF (0)、N_ABS (-1)、N_DEBUG (-2) のような特殊な値も存在します。 このコミットでは、s.SectionNumberの値に基づいてswitch文が導入され、これらの特殊な負のセクション番号が明示的に処理されるようになりました。

    • N_UNDEF (0): シンボルコードを'U' (Undefined) に設定します。
    • N_ABS (-1): シンボルコードを'C' (Constant address) に設定します。これはdoc.goにも追記されています。
    • N_DEBUG (-2): シンボルコードを'?' (不明) に設定します。
    • その他の負の値: エラーとして処理されます。 これにより、nmツールがPEファイルのシンボルテーブルをより正確に解釈し、未定義シンボルや絶対シンボルを適切に識別できるようになりました。
  4. テストケースの追加: src/cmd/nm/nm_test.goという新しいテストファイルが追加されました。このテストは、cmd/nmツールをビルドし、様々なアーキテクチャ(ELF、Mach-O、PE、Plan 9オブジェクト)のテスト実行可能ファイルに対してnmを実行します。これにより、PEファイルハンドリングの修正が他のプラットフォームのファイル処理に悪影響を与えず、期待通りに機能することを確認しています。

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

src/cmd/nm/doc.go

--- a/src/cmd/nm/doc.go
+++ b/src/cmd/nm/doc.go
@@ -19,6 +19,7 @@
 //	d	static data segment symbol
 //	B	bss segment symbol
 //	b	static bss segment symbol
+//	C	constant address
 //	U	referenced but undefined symbol
 //
 // Following established convention, the address is omitted for undefined

src/cmd/nm/nm_test.go (新規ファイル)

// Copyright 2014 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.

package main

import (
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"testing"
)

func TestNM(t *testing.T) {
	out, err := exec.Command("go", "build", "-o", "testnm.exe", "cmd/nm").CombinedOutput()
	if err != nil {
		t.Fatalf("go build -o testnm.exe cmd/nm: %v\n%s", err, string(out))
	}
	defer os.Remove("testnm.exe")

	testfiles := []string{
		"elf/testdata/gcc-386-freebsd-exec",
		"elf/testdata/gcc-amd64-linux-exec",
		"macho/testdata/gcc-386-darwin-exec",
		"macho/testdata/gcc-amd64-darwin-exec",
		"pe/testdata/gcc-amd64-mingw-exec",
		"pe/testdata/gcc-386-mingw-exec",
		"plan9obj/testdata/amd64-plan9-exec",
		"plan9obj/testdata/386-plan9-exec",
	}
	for _, f := range testfiles {
		exepath := filepath.Join(runtime.GOROOT(), "src", "pkg", "debug", f)
		cmd := exec.Command("./testnm.exe", exepath)
		out, err := cmd.CombinedOutput()
		if err != nil {
			t.Fatalf("go tool nm %v: %v\n%s", exepath, err, string(out))
		}
	}
}

src/cmd/nm/pe.go

--- a/src/cmd/nm/pe.go
+++ b/src/cmd/nm/pe.go
@@ -18,12 +18,41 @@ func peSymbols(f *os.File) []Sym {
 		return nil
 	}\n
+\tvar imageBase uint64
+\tswitch oh := p.OptionalHeader.(type) {
+\tcase *pe.OptionalHeader32:
+\t\timageBase = uint64(oh.ImageBase)
+\tcase *pe.OptionalHeader64:
+\t\timageBase = oh.ImageBase
+\tdefault:
+\t\terrorf("parsing %s: file format not recognized", f.Name())
+\t\treturn nil
+\t}
+\n \tvar syms []Sym
 \tfor _, s := range p.Symbols {
+\t\tconst (
+\t\t\tN_UNDEF = 0  // An undefined (extern) symbol
+\t\t\tN_ABS   = -1 // An absolute symbol (e_value is a constant, not an address)
+\t\t\tN_DEBUG = -2 // A debugging symbol
+\t\t)
 \t\tsym := Sym{Name: s.Name, Addr: uint64(s.Value), Code: '?'}
-\t\tif s.SectionNumber == 0 {\n+\t\tswitch s.SectionNumber {
+\t\tcase N_UNDEF:
 \t\t\tsym.Code = 'U'
-\t\t} else if int(s.SectionNumber) <= len(p.Sections) {\n+\t\tcase N_ABS:
+\t\t\tsym.Code = 'C'
+\t\tcase N_DEBUG:
+\t\t\tsym.Code = '?'
+\t\tdefault:
+\t\t\tif s.SectionNumber < 0 {
+\t\t\t\terrorf("parsing %s: invalid section number %d", f.Name(), s.SectionNumber)
+\t\t\t\treturn nil
+\t\t\t}
+\t\t\tif len(p.Sections) < int(s.SectionNumber) {
+\t\t\t\terrorf("parsing %s: section number %d is large then max %d", f.Name(), s.SectionNumber, len(p.Sections))
+\t\t\t\treturn nil
+\t\t\t}
 \t\t\tsect := p.Sections[s.SectionNumber-1]
 \t\t\tconst (
 \t\t\t\ttext  = 0x20
@@ -46,6 +75,7 @@ func peSymbols(f *os.File) []Sym {
 \t\t\tcase ch&bss != 0:
 \t\t\t\tsym.Code = 'B'
 \t\t\t}
+\t\t\tsym.Addr += imageBase + uint64(sect.VirtualAddress)
 \t\t}
 \t\tsyms = append(syms, sym)
 \t}

コアとなるコードの解説

src/cmd/nm/doc.go

このファイルはcmd/nmコマンドのドキュメントを定義しています。変更点として、シンボルタイプの説明に新たにCが追加されました。これは、N_ABS(絶対シンボル)に対応するもので、定数アドレスを持つシンボルを表します。これにより、nmコマンドの出力がより詳細になり、ユーザーがシンボルの性質を正確に理解できるようになります。

src/cmd/nm/nm_test.go

このファイルは、cmd/nmツールのテストケースを定義しています。 TestNM関数は、まずgo buildコマンドを使用してcmd/nmtestnm.exeとしてビルドします。ビルドが成功した後、defer os.Remove("testnm.exe")によりテスト終了時に生成された実行ファイルを削除するように設定されています。 次に、testfilesスライスには、ELF、Mach-O、PE、Plan 9オブジェクトといった様々なフォーマットのテスト実行可能ファイルのパスがリストされています。これらのファイルは、Goの標準ライブラリ内のsrc/pkg/debugディレクトリに存在します。 ループ内で、各テストファイルに対して./testnm.exe(ビルドしたnmツール)を実行し、その出力とエラーをチェックしています。これにより、cmd/nmが異なるプラットフォームのバイナリファイルを正しく処理できること、特にWindows PEファイルに対する変更が他のフォーマットに悪影響を与えていないことを検証しています。

src/cmd/nm/pe.go

このファイルは、Windows PEファイルのシンボルを解析する主要なロジックを含んでいます。

  1. ImageBaseの取得: peSymbols関数の冒頭に、PEファイルのオプションヘッダからImageBaseを取得するロジックが追加されました。

    var imageBase uint64
    switch oh := p.OptionalHeader.(type) {
    case *pe.OptionalHeader32:
        imageBase = uint64(oh.ImageBase)
    case *pe.OptionalHeader64:
        imageBase = oh.ImageBase
    default:
        errorf("parsing %s: file format not recognized", f.Name())
        return nil
    }
    

    これは、32ビット (pe.OptionalHeader32) と64ビット (pe.OptionalHeader64) の両方のPEファイルに対応し、それぞれの構造体からImageBaseフィールドを抽出します。これにより、後続の絶対アドレス計算の基盤が確立されます。

  2. 負のセクション番号の処理: シンボルを処理するループ内で、s.SectionNumber(シンボルのセクション番号)を評価するためのswitch文が導入されました。

    		const (
    			N_UNDEF = 0  // An undefined (extern) symbol
    			N_ABS   = -1 // An absolute symbol (e_value is a constant, not an address)
    			N_DEBUG = -2 // A debugging symbol
    		)
    		// ...
    		switch s.SectionNumber {
    		case N_UNDEF:
    			sym.Code = 'U'
    		case N_ABS:
    			sym.Code = 'C'
    		case N_DEBUG:
    			sym.Code = '?'
    		default:
    			if s.SectionNumber < 0 {
    				errorf("parsing %s: invalid section number %d", f.Name(), s.SectionNumber)
    				return nil
    			}
    			if len(p.Sections) < int(s.SectionNumber) {
    				errorf("parsing %s: section number %d is large then max %d", f.Name(), s.SectionNumber, len(p.Sections))
    				return nil
    			}
    			// ... 既存のセクション処理ロジック ...
    		}
    

    このswitch文により、N_UNDEFN_ABSN_DEBUGといった特殊なセクション番号が明示的に処理され、それぞれのシンボルコード('U', 'C', '?')が割り当てられます。これにより、nmツールはこれらの特殊なシンボルを正しく分類し、表示できるようになりました。また、負のセクション番号がN_UNDEF, N_ABS, N_DEBUGのいずれでもない場合はエラーとして扱われます。

  3. 絶対アドレスの計算: セクションに属する通常のシンボル(s.SectionNumberが正の値の場合)に対して、シンボルのアドレスを絶対アドレスに変換する計算が追加されました。

    			sym.Addr += imageBase + uint64(sect.VirtualAddress)
    

    これは、シンボルの相対オフセット(s.Valueが初期値としてsym.Addrに設定されている)に、PEファイルのImageBaseと、シンボルが属するセクションの仮想アドレス(sect.VirtualAddress)を加算することで、メモリ上の実際の絶対アドレスを算出しています。これにより、nmコマンドの出力がより実用的な情報を提供するようになりました。

これらの変更により、cmd/nmはWindows PEファイルのシンボル情報をより正確に、かつ絶対アドレスで表示できるようになり、デバッグやバイナリ解析の際に非常に有用なツールとなりました。

関連リンク

参考にした情報源リンク