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

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

このコミットは、Goのデバッグ情報およびオブジェクトファイル読み込みに関するdebug/goobjパッケージにおいて、パッケージプレフィックスの展開処理を修正し、より正確に行うようにしたものです。具体的には、コンパイラ(gc)とリンカ(ld)が内部的に使用するパスからプレフィックスを生成するロジックと、debug/goobjがオブジェクトファイルからシンボルを読み込む際にそのプレフィックスを解釈するロジックとの整合性を取るための変更が含まれています。

コミット

  • コミットハッシュ: 2404b7f16866b302efb19083dae155e0f4764144
  • Author: Russ Cox rsc@golang.org
  • Date: Wed Dec 18 19:00:52 2013 -0500
  • コミットメッセージ:
    debug/goobj: expand package prefix correctly
    
    R=r, bradfitz
    CC=golang-dev
    https://golang.org/cl/43480049
    

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

https://github.com/golang/go/commit/2404b7f16866b302efb19083dae155e0f4764144

元コミット内容

debug/goobj: expand package prefix correctly

R=r, bradfitz
CC=golang-dev
https://golang.org/cl/43480049

変更の背景

Goのコンパイラ(gc)やリンカ(ld)は、コンパイルされたコード内のシンボル名に、そのシンボルが属するパッケージを示すプレフィックスを付加します。このプレフィックスは、パッケージのインポートパスから生成されます。例えば、fmtパッケージのPrintln関数は、内部的にはfmt.Printlnのような形で表現されます。しかし、このプレフィックスの生成ロジックは、特定の文字(例: .%、制御文字、非ASCII文字)をエスケープする処理を含んでいました。

src/cmd/gc/subr.csrc/cmd/ld/lib.cには、このプレフィックス生成ロジックを担うpathtoprefix関数が存在し、両者で同じロジックが使われるべきであるというコメントがありました。しかし、debug/goobjパッケージがオブジェクトファイルを読み込み、シンボル情報を解釈する際に、このエスケープされたプレフィックスを正しく「展開」または「逆変換」するロジックが不足していました。

この不整合により、debug/goobjがオブジェクトファイル内のシンボル名を正確に解釈できず、デバッグツールや分析ツールが誤ったシンボル情報を表示する可能性がありました。このコミットは、debug/goobjgcldと同じルールでプレフィックスを処理できるようにすることで、この問題を解決することを目的としています。特に、シンボル名に含まれる"".という特殊なプレフィックス(オブジェクトファイル内で現在のパッケージを示すために使われる)を、実際のパッケージプレフィックスに正しく置き換える必要がありました。

前提知識の解説

  1. Goのコンパイルプロセス: Goのソースコードは、まずコンパイラ(gc)によってオブジェクトファイル(.oファイル)にコンパイルされます。これらのオブジェクトファイルには、機械語コード、データ、そしてシンボル情報が含まれます。その後、リンカ(ld)がこれらのオブジェクトファイルを結合し、実行可能なバイナリを生成します。
  2. シンボルテーブル: 実行可能バイナリやオブジェクトファイルには、プログラム内の関数、グローバル変数、型などの名前と、それらがメモリ上のどこに配置されているかを示す情報(アドレスなど)をマッピングした「シンボルテーブル」が含まれています。デバッガやプロファイラなどのツールは、このシンボルテーブルを利用して、人間が理解できる形でプログラムの状態を表示します。
  3. パッケージプレフィックス: Goでは、異なるパッケージに属する同じ名前のシンボルを区別するために、シンボル名にパッケージのインポートパスに基づくプレフィックスを付加することが一般的です。例えば、fmtパッケージのPrintln関数は、内部的にはfmt.Printlnとして扱われます。
  4. debug/goobjパッケージ: このパッケージは、Goのオブジェクトファイル(.oファイル)を読み込み、その内容(特にシンボル情報)を解析するためのライブラリです。デバッグツールや、Goのツールチェインの一部として、オブジェクトファイルの詳細を検査するために使用されます。
  5. pathtoprefix関数: Goのコンパイラ(gc)とリンカ(ld)の内部で使用される関数で、パッケージのインポートパスを、シンボル名に付加されるプレフィックス形式に変換します。この変換には、特定の文字のエスケープ処理が含まれます。例えば、インポートパスに.が含まれる場合、シンボル名での衝突を避けるためにエスケープされることがあります。

技術的詳細

このコミットの核心は、Goのツールチェイン内で一貫したパッケージプレフィックスの処理を保証することにあります。

Goのコンパイラとリンカは、パッケージのインポートパスをシンボル名の一部として埋め込む際に、特定の文字をエスケープします。これは、シンボル名がファイルシステムパスや他の識別子と衝突するのを防ぐため、また、シンボル名が特定の形式に従う必要があるためです。エスケープされる文字には、%"、制御文字、非ASCIIバイト、そして最後のスラッシュ(/)以降の.(ドット)が含まれます。エスケープは、%xxxxは16進数)の形式で行われます。

debug/goobjパッケージは、これらのオブジェクトファイルを読み込み、シンボル名を解釈する必要があります。以前は、debug/goobjには、コンパイラやリンカが行ったエスケープ処理を逆変換するロジックがありませんでした。そのため、エスケープされた文字を含むパッケージパスを持つシンボルが正しく認識されない可能性がありました。

このコミットでは、debug/goobj/read.goimportPathToPrefixという新しい関数が導入されました。この関数は、src/cmd/gc/subr.csrc/cmd/ld/lib.cにあるpathtoprefix関数と同じロジックを実装しています。これにより、debug/goobjは、オブジェクトファイル内のシンボル名に埋め込まれたパッケージプレフィックスが、コンパイラによってどのようにエスケープされたかを正確に理解し、それを正しく展開できるようになります。

さらに、オブジェクトファイル内のシンボル名には、現在のパッケージを示すための特別なプレフィックス"".(空文字列のインポートパスを表す)が使用されることがあります。debug/goobjは、この"".を、実際に読み込んでいるオブジェクトファイルのパッケージプレフィックス(importPathToPrefixによって生成されたもの)に置き換える必要があります。これにより、例えばmainパッケージのシンボルが"".mainではなく、main.mainのように正しく解釈されるようになります。

テストファイルsrc/pkg/debug/goobj/read_test.goが新規追加され、importPathToPrefix関数の動作が、様々なエスケープケース(ドット、パーセント、制御文字、非ASCII文字など)に対して正しく行われることを検証しています。

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

  1. src/cmd/gc/subr.csrc/cmd/ld/lib.c:

    • pathtoprefix関数のコメントが更新され、debug/goobj/read.goimportPathToPrefixも同様のロジックを持つべきであることが明示されました。これは、Goツールチェイン全体での一貫性を強調するものです。
  2. src/pkg/debug/goobj/read.go:

    • objReader構造体にpkgprefix stringフィールドが追加されました。これは、現在読み込んでいるオブジェクトファイルのパッケージプレフィックスを保持します。
    • importPathToPrefix(s string) string関数が新規追加されました。この関数は、Goのコンパイラとリンカがパッケージパスをシンボルプレフィックスに変換する際に使用するエスケープロジックを正確に再現します。具体的には、%"、制御文字、非ASCIIバイト、および最後のスラッシュ以降の.%xx形式でエスケープします。
    • objReader.initメソッド内で、r.pkgprefix = importPathToPrefix(p.ImportPath) + "."が呼び出され、現在のパッケージのプレフィックスが初期化されるようになりました。
    • objReader.readSymIDメソッド内で、シンボル名を読み込んだ後、name = strings.Replace(name, ""., r.pkgprefix, -1)という行が追加されました。これにより、オブジェクトファイル内の特殊なプレフィックス"".が、実際のパッケージプレフィックスに置き換えられます。
    • Parse関数内で、pkgpathが空文字列の場合に""を設定するロジックが追加されました。これは、ルートパッケージ(例: mainパッケージ)のオブジェクトファイルを処理する際の堅牢性を高めるためです。
  3. src/pkg/debug/goobj/read_test.go:

    • importPathToPrefix関数の単体テストが追加されました。様々な入力パス(通常のパス、エスケープが必要な文字を含むパス、空文字列など)に対して、期待されるプレフィックスが生成されることを検証しています。

コアとなるコードの解説

importPathToPrefix関数 (src/pkg/debug/goobj/read.go)

func importPathToPrefix(s string) string {
	// find index of last slash, if any, or else -1.
	// used for determining whether an index is after the last slash.
	slash := strings.LastIndex(s, "/")

	// check for chars that need escaping
	n := 0
	for r := 0; r < len(s); r++ {
		if c := s[r]; c <= ' ' || (c == '.' && r > slash) || c == '%' || c == '"' || c >= 0x7F {
			n++
		}
	}

	// quick exit
	if n == 0 {
		return s
	}

	// escape
	const hex = "0123456789abcdef"
	p := make([]byte, 0, len(s)+2*n)
	for r := 0; r < len(s); r++ {
		if c := s[r]; c <= ' ' || (c == '.' && r > slash) || c == '%' || c == '"' || c >= 0x7F {
			p = append(p, '%', hex[c>>4], hex[c&0xF])
		} else {
			p = append(p, c)
		}
	}

	return string(p)
}

この関数は、Goのコンパイラとリンカがパッケージのインポートパスをシンボルプレフィックスに変換する際に適用するエスケープルールを実装しています。

  1. 最後のスラッシュの検出: strings.LastIndex(s, "/")で、インポートパス内の最後のスラッシュの位置を見つけます。これは、ドット(.)のエスケープルール(最後のスラッシュ以降のドットのみエスケープ対象)を適用するために必要です。
  2. エスケープが必要な文字のカウント: ループで文字列sを走査し、以下の条件に合致する文字の数をnにカウントします。
    • c <= ' ': 制御文字(スペースを含む)
    • (c == '.' && r > slash): 最後のスラッシュ以降のドット
    • c == '%'
    • c == '"'
    • c >= 0x7F: 非ASCII文字
  3. 高速パス: nが0の場合(エスケープが必要な文字がない場合)、元の文字列sをそのまま返します。これにより、不要な処理をスキップします。
  4. エスケープ処理: nが0でない場合、新しいバイトスライスpを作成し、エスケープが必要な文字を%xx形式(xxは16進数)に変換して追加します。それ以外の文字はそのまま追加します。
  5. 文字列として返す: 最終的に、構築されたバイトスライスpを文字列に変換して返します。

objReader.readSymID内の変更 (src/pkg/debug/goobj/read.go)

 	name, vers := r.readString(), r.readInt()

 	// In a symbol name in an object file, "". denotes the
 	// prefix for the package in which the object file has been found.
 	// Expand it.
 	name = strings.Replace(name, `"".`, r.pkgprefix, -1)

この部分が、オブジェクトファイルから読み込んだシンボル名を「展開」する重要なロジックです。

  • r.readString()でシンボル名(name)を読み込みます。
  • Goのオブジェクトファイルでは、現在のパッケージに属するシンボルに対して、パッケージプレフィックスの代わりに特別な文字列"".が使用されることがあります。これは、オブジェクトファイルがまだ最終的なバイナリにリンクされる前の段階で、そのシンボルがどのパッケージに属するかを一時的に示すためのものです。
  • strings.Replace(name, ""., r.pkgprefix, -1)は、読み込んだシンボル名nameの中に"".という文字列があれば、それをobjReaderpkgprefixフィールドに格納されている実際のパッケージプレフィックス(importPathToPrefixによって計算されたもの)に置き換えます。-1は、すべての出現箇所を置き換えることを意味します。

この変更により、debug/goobjは、コンパイラやリンカが生成したシンボル名を、エスケープされたプレフィックスや特殊な"".プレフィックスを含めて、正確に解釈し、デバッグツールなどが利用しやすい形式に変換できるようになりました。

関連リンク

参考にした情報源リンク