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

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

このコミットは、Go言語のリンカ (cmd/ld) におけるWindows PE (Portable Executable) ファイルのシンボルテーブルで、アドレスが誤って記録される問題を修正するものです。具体的には、シンボルアドレスをセクション内のオフセットに変換するロジックを改善し、これに伴い go tool nm の出力が正しいことを検証するための新しいテストが追加されています。

コミット

commit 80e7f972067b3da542ba86f969719456139f111d
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Mon Apr 21 19:28:02 2014 +1000

    cmd/ld: correct addresses in windows pe symbol table
    
    This should have been part of 36eb4a62fbb6,
    but I later discovered that addresses are all wrong.
    Appropriate test added now.
    
    LGTM=r
    R=golang-codereviews, r
    CC=golang-codereviews
    https://golang.org/cl/89470043

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

https://github.com/golang/go/commit/80e7f972067b3da542ba86f969719456139f111d

元コミット内容

cmd/ld: correct addresses in windows pe symbol table

This should have been part of 36eb4a62fbb6,
but I later discovered that addresses are all wrong.
Appropriate test added now.

LGTM=r
R=golang-codereviews, r
CC=golang-codereviews
https://golang.org/cl/89470043

変更の背景

このコミットは、以前のコミット 36eb4a62fbb6 で導入された、または関連する変更によって発生した問題の修正です。元のコミットメッセージによると、36eb4a62fbb6 の一部として含まれるべきだった修正が漏れており、その結果、Windows PE実行ファイルのシンボルテーブルに記録されるアドレスが誤っていたことが後から判明しました。

36eb4a62fbb6 の具体的な内容は、現時点では詳細を特定できませんでしたが、このコミットの性質から、Windows PEファイルの生成、特にシンボル情報の取り扱いに関する変更であったと推測されます。本コミットは、その先行コミットによって生じたアドレスの不整合を解消し、シンボル情報が正確に反映されるようにするためのバグ修正として位置づけられます。この修正の重要性を鑑み、go tool nm の出力が正しいことを検証するテストも追加されています。

前提知識の解説

Go言語のリンカ (cmd/ld)

Go言語のビルドプロセスにおいて、cmd/ld はリンカ(linker)として機能します。リンカは、コンパイラによって生成されたオブジェクトファイル(.o ファイルなど)やアーカイブファイル(.a ファイルなど)を結合し、実行可能なバイナリファイル(Windowsでは .exe、Linuxでは実行ファイル、macOSでは .dylib や実行ファイルなど)を生成する役割を担います。この過程で、シンボル解決(関数や変数のアドレスを決定する)や、セクションの配置、外部ライブラリとのリンクなどが行われます。

Go言語の nm ツール (cmd/nm)

go tool nm は、Unix系の nm コマンドに似たツールで、Goのバイナリファイル(実行ファイルやライブラリ)に含まれるシンボル(関数名、変数名など)の情報を表示します。表示される情報には、シンボルのアドレス、サイズ、型、名前などが含まれます。開発者がバイナリの内部構造を調査したり、デバッグ情報を確認したりする際に利用されます。

Windows PE (Portable Executable) フォーマット

PE (Portable Executable) は、Microsoft Windowsオペレーティングシステムで使用される実行可能ファイル、オブジェクトコード、DLL (Dynamic Link Library) などのファイル形式です。PEファイルは、ヘッダ、セクションテーブル、および複数のセクション(コード、データ、リソースなど)で構成されます。シンボルテーブルは、デバッグ情報や動的リンクのために、ファイル内の関数や変数のアドレス情報を含む重要な部分です。

シンボルテーブル

シンボルテーブルは、プログラム内のシンボル(関数名、変数名、ラベルなど)とその対応するアドレスや型などの情報を格納するデータ構造です。リンカはシンボルテーブルを使用して、異なるモジュール間でシンボルを解決し、実行可能なプログラムを構築します。デバッガはシンボルテーブルを利用して、ソースコードの変数名や関数名と実行時のメモリ位置を関連付け、デバッグを容易にします。PEファイルの場合、シンボルテーブルはCOFF (Common Object File Format) 形式のシンボルエントリで構成されることが一般的です。

仮想アドレスとファイルオフセット

実行可能ファイルにおいて、アドレスには主に二つの種類があります。

  • 仮想アドレス (Virtual Address, VA): プログラムが実行時にメモリ上で使用するアドレスです。オペレーティングシステムによって管理され、プロセスごとに独立したアドレス空間を持ちます。
  • ファイルオフセット (File Offset): ファイルの先頭からの物理的なバイト位置です。ファイルがディスク上に保存されている際のアドレスを示します。

PEファイルでは、セクションはファイルオフセットと仮想アドレスの両方に対応付けられます。リンカは、シンボルテーブルにシンボルのアドレスを記録する際に、これらのアドレスを適切に変換する必要があります。特に、シンボルテーブルに記録されるアドレスは、通常、セクションの先頭からのオフセット(RVA: Relative Virtual Address)や、セクション内のオフセットとして表現されることが多いです。

vlong

Goのリンカのコードベースで使われている vlong は、long long 型、つまり64ビット整数を表すことが多いです。アドレスやサイズなど、大きな数値を扱うために使用されます。

技術的詳細

このコミットは、主に2つのファイルに変更を加えています。

  1. src/cmd/ld/pe.c: Windows PEファイルのシンボルテーブル生成ロジックを修正します。

    • 新しい静的関数 datoffsect(vlong addr) が追加されました。この関数は、与えられた仮想アドレス addr がどのセクション(テキストセクション segtext またはデータセクション segdata)に属するかを判断し、そのセクションの仮想アドレスの開始点からのオフセットを返します。
      • if(addr >= segdata.vaddr): アドレスがデータセクションの仮想アドレス範囲内にある場合、addr - segdata.vaddr を返します。
      • if(addr >= segtext.vaddr): アドレスがテキストセクションの仮想アドレス範囲内にある場合、addr - segtext.vaddr を返します。
      • どちらのセクションにも属さない場合は diag 関数で診断メッセージを出力し、0 を返します。
    • addsymtable 関数内で、シンボルの値 (s->sym->value) をシンボルテーブルに書き込む際に、これまでの lputl(datoff(s->sym->value))lputl(datoffsect(s->sym->value)) に変更されました。
      • datoff 関数は、おそらくファイル全体の先頭からのオフセットを計算していたのに対し、datoffsect はシンボルが属するセクションの先頭からのオフセットを正確に計算するようになりました。これにより、PEシンボルテーブルが期待する形式(セクション相対オフセット)でアドレスが記録されるようになります。
  2. src/cmd/nm/nm_test.go: go tool nm の出力が正しいことを検証するための新しいテストが追加されました。

    • checkSymbols(t *testing.T, nmoutput []byte) 関数が追加されました。この関数は go tool nm の出力(バイト配列)を解析し、特定のシンボル(cmd/nm.checkSymbolscmd/nm.testData)が期待されるアドレスで存在するかどうかを検証します。
      • bufio.NewScannerbytes.NewBuffer を使用して nm の出力を1行ずつ読み込みます。
      • 各行をスペースで分割し、シンボル名とアドレスを抽出します。
      • fmt.Sprintf("%p", checkSymbols)fmt.Sprintf("%p", &testData) を使用して、Goのランタイムが認識する実際のシンボルアドレスを取得し、nm の出力と比較します。これにより、nm が報告するアドレスがメモリ上の実際のシンボルアドレスと一致するかを確認します。
    • TestNM 関数内で、go tool nm を実行した後、新しくビルドされた testnm.exe に対して go tool nm を実行し、その出力を checkSymbols 関数に渡して検証するロジックが追加されました。これにより、リンカの変更が nm ツールの出力に正しく反映されているか、つまりシンボルアドレスが正しく解決されているかをエンドツーエンドで確認できます。

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

src/cmd/ld/pe.c

// 新規追加関数
+static vlong
+datoffsect(vlong addr)
+{
+	if(addr >= segdata.vaddr)
+		return addr - segdata.vaddr;
+	if(addr >= segtext.vaddr)
+		return addr - segtext.vaddr;
+	diag("datoff %#llx", addr);
+	return 0;
+}

// addsymtable 関数内の変更
-		lputl(datoff(s->sym->value));
+		lputl(datoffsect(s->sym->value));

src/cmd/nm/nm_test.go

// 新規追加関数
+func checkSymbols(t *testing.T, nmoutput []byte) {
+	var checkSymbolsFound, testDataFound bool
+	scanner := bufio.NewScanner(bytes.NewBuffer(nmoutput))
+	for scanner.Scan() {
+		f := strings.Fields(scanner.Text())
+		if len(f) < 3 {
+			t.Error("nm must have at least 3 columns")
+			continue
+		}
+		switch f[2] {
+		case "cmd/nm.checkSymbols":
+			checkSymbolsFound = true
+			addr := "0x" + f[0]
+			if addr != fmt.Sprintf("%p", checkSymbols) {
+				t.Errorf("nm shows wrong address %v for checkSymbols (%p)", addr, checkSymbols)
+			}
+		case "cmd/nm.testData":
+			testDataFound = true
+			addr := "0x" + f[0]
+			if addr != fmt.Sprintf("%p", &testData) {
+				t.Errorf("nm shows wrong address %v for testData (%p)", addr, &testData)
+			}
+		}
+	}
+	if err := scanner.Err(); err != nil {
+		t.Errorf("error while reading symbols: %v", err)
+		return
+	}
+	if !checkSymbolsFound {
+		t.Error("nm shows no checkSymbols symbol")
+	}
+	if !testDataFound {
+		t.Error("nm shows no testData symbol")
+	}
+}

// TestNM 関数内の追加ロジック
+	cmd := exec.Command("./testnm.exe", os.Args[0])
+	out, err = cmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("go tool nm %v: %v\\n%s", os.Args[0], err, string(out))
+	}
+	checkSymbols(t, out)

コアとなるコードの解説

src/cmd/ld/pe.c における datoffsect 関数の導入と addsymtable でのその利用は、Windows PEファイルのシンボルテーブルにシンボルアドレスを書き込む際の正確性を保証するために不可欠です。PEファイルでは、シンボルアドレスは通常、セクションの先頭からの相対オフセットとして表現されます。従来の datoff 関数がファイル全体の先頭からのオフセットを計算していた場合、これはPEフォーマットの期待する形式と異なり、シンボルアドレスが誤って解釈される原因となります。datoffsect は、シンボルが属するセクション(テキストまたはデータ)を特定し、そのセクションの仮想アドレスの開始点からのオフセットを計算することで、この問題を解決します。これにより、go tool nm のようなツールがPEファイルからシンボル情報を読み取る際に、正しいアドレスを取得できるようになります。

src/cmd/nm/nm_test.go に追加された checkSymbols 関数と TestNM 内のその呼び出しは、この修正が正しく機能することを検証するための重要なテストカバレッジを提供します。このテストは、go tool nm が出力するシンボルアドレスが、Goのランタイムが認識する実際のメモリ上のアドレスと一致するかどうかを動的に確認します。これにより、リンカが生成するPEファイルのシンボル情報が正確であり、nm ツールがその情報を正しく解析できることが保証されます。これは、デバッグやプロファイリングの際に正確なシンボル情報が不可欠であるため、Go開発者にとって非常に重要です。

関連リンク

参考にした情報源リンク

  • 36eb4a62fbb6 コミットの詳細は、GitHub APIまたは直接のWebフェッチでは取得できませんでした。