[インデックス 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.c
とsrc/cmd/ld/lib.c
には、このプレフィックス生成ロジックを担うpathtoprefix
関数が存在し、両者で同じロジックが使われるべきであるというコメントがありました。しかし、debug/goobj
パッケージがオブジェクトファイルを読み込み、シンボル情報を解釈する際に、このエスケープされたプレフィックスを正しく「展開」または「逆変換」するロジックが不足していました。
この不整合により、debug/goobj
がオブジェクトファイル内のシンボル名を正確に解釈できず、デバッグツールや分析ツールが誤ったシンボル情報を表示する可能性がありました。このコミットは、debug/goobj
がgc
やld
と同じルールでプレフィックスを処理できるようにすることで、この問題を解決することを目的としています。特に、シンボル名に含まれる"".
という特殊なプレフィックス(オブジェクトファイル内で現在のパッケージを示すために使われる)を、実際のパッケージプレフィックスに正しく置き換える必要がありました。
前提知識の解説
- Goのコンパイルプロセス: Goのソースコードは、まずコンパイラ(
gc
)によってオブジェクトファイル(.o
ファイル)にコンパイルされます。これらのオブジェクトファイルには、機械語コード、データ、そしてシンボル情報が含まれます。その後、リンカ(ld
)がこれらのオブジェクトファイルを結合し、実行可能なバイナリを生成します。 - シンボルテーブル: 実行可能バイナリやオブジェクトファイルには、プログラム内の関数、グローバル変数、型などの名前と、それらがメモリ上のどこに配置されているかを示す情報(アドレスなど)をマッピングした「シンボルテーブル」が含まれています。デバッガやプロファイラなどのツールは、このシンボルテーブルを利用して、人間が理解できる形でプログラムの状態を表示します。
- パッケージプレフィックス: Goでは、異なるパッケージに属する同じ名前のシンボルを区別するために、シンボル名にパッケージのインポートパスに基づくプレフィックスを付加することが一般的です。例えば、
fmt
パッケージのPrintln
関数は、内部的にはfmt.Println
として扱われます。 debug/goobj
パッケージ: このパッケージは、Goのオブジェクトファイル(.o
ファイル)を読み込み、その内容(特にシンボル情報)を解析するためのライブラリです。デバッグツールや、Goのツールチェインの一部として、オブジェクトファイルの詳細を検査するために使用されます。pathtoprefix
関数: Goのコンパイラ(gc
)とリンカ(ld
)の内部で使用される関数で、パッケージのインポートパスを、シンボル名に付加されるプレフィックス形式に変換します。この変換には、特定の文字のエスケープ処理が含まれます。例えば、インポートパスに.
が含まれる場合、シンボル名での衝突を避けるためにエスケープされることがあります。
技術的詳細
このコミットの核心は、Goのツールチェイン内で一貫したパッケージプレフィックスの処理を保証することにあります。
Goのコンパイラとリンカは、パッケージのインポートパスをシンボル名の一部として埋め込む際に、特定の文字をエスケープします。これは、シンボル名がファイルシステムパスや他の識別子と衝突するのを防ぐため、また、シンボル名が特定の形式に従う必要があるためです。エスケープされる文字には、%
、"
、制御文字、非ASCIIバイト、そして最後のスラッシュ(/
)以降の.
(ドット)が含まれます。エスケープは、%xx
(xx
は16進数)の形式で行われます。
debug/goobj
パッケージは、これらのオブジェクトファイルを読み込み、シンボル名を解釈する必要があります。以前は、debug/goobj
には、コンパイラやリンカが行ったエスケープ処理を逆変換するロジックがありませんでした。そのため、エスケープされた文字を含むパッケージパスを持つシンボルが正しく認識されない可能性がありました。
このコミットでは、debug/goobj/read.go
にimportPathToPrefix
という新しい関数が導入されました。この関数は、src/cmd/gc/subr.c
とsrc/cmd/ld/lib.c
にあるpathtoprefix
関数と同じロジックを実装しています。これにより、debug/goobj
は、オブジェクトファイル内のシンボル名に埋め込まれたパッケージプレフィックスが、コンパイラによってどのようにエスケープされたかを正確に理解し、それを正しく展開できるようになります。
さらに、オブジェクトファイル内のシンボル名には、現在のパッケージを示すための特別なプレフィックス"".
(空文字列のインポートパスを表す)が使用されることがあります。debug/goobj
は、この"".
を、実際に読み込んでいるオブジェクトファイルのパッケージプレフィックス(importPathToPrefix
によって生成されたもの)に置き換える必要があります。これにより、例えばmain
パッケージのシンボルが"".main
ではなく、main.main
のように正しく解釈されるようになります。
テストファイルsrc/pkg/debug/goobj/read_test.go
が新規追加され、importPathToPrefix
関数の動作が、様々なエスケープケース(ドット、パーセント、制御文字、非ASCII文字など)に対して正しく行われることを検証しています。
コアとなるコードの変更箇所
-
src/cmd/gc/subr.c
とsrc/cmd/ld/lib.c
:pathtoprefix
関数のコメントが更新され、debug/goobj/read.go
のimportPathToPrefix
も同様のロジックを持つべきであることが明示されました。これは、Goツールチェイン全体での一貫性を強調するものです。
-
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
パッケージ)のオブジェクトファイルを処理する際の堅牢性を高めるためです。
-
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のコンパイラとリンカがパッケージのインポートパスをシンボルプレフィックスに変換する際に適用するエスケープルールを実装しています。
- 最後のスラッシュの検出:
strings.LastIndex(s, "/")
で、インポートパス内の最後のスラッシュの位置を見つけます。これは、ドット(.
)のエスケープルール(最後のスラッシュ以降のドットのみエスケープ対象)を適用するために必要です。 - エスケープが必要な文字のカウント: ループで文字列
s
を走査し、以下の条件に合致する文字の数をn
にカウントします。c <= ' '
: 制御文字(スペースを含む)(c == '.' && r > slash)
: 最後のスラッシュ以降のドットc == '%'
c == '"'
c >= 0x7F
: 非ASCII文字
- 高速パス:
n
が0の場合(エスケープが必要な文字がない場合)、元の文字列s
をそのまま返します。これにより、不要な処理をスキップします。 - エスケープ処理:
n
が0でない場合、新しいバイトスライスp
を作成し、エスケープが必要な文字を%xx
形式(xx
は16進数)に変換して追加します。それ以外の文字はそのまま追加します。 - 文字列として返す: 最終的に、構築されたバイトスライス
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
の中に"".
という文字列があれば、それをobjReader
のpkgprefix
フィールドに格納されている実際のパッケージプレフィックス(importPathToPrefix
によって計算されたもの)に置き換えます。-1
は、すべての出現箇所を置き換えることを意味します。
この変更により、debug/goobj
は、コンパイラやリンカが生成したシンボル名を、エスケープされたプレフィックスや特殊な"".
プレフィックスを含めて、正確に解釈し、デバッグツールなどが利用しやすい形式に変換できるようになりました。
関連リンク
- Go CL 43480049: https://golang.org/cl/43480049
参考にした情報源リンク
- Goのシンボルテーブルに関する情報: https://medium.com/@jason_777/go-binary-analysis-part-1-symbol-table-and-line-number-table-2d2d2d2d2d2d
- Goの
-trimpath
フラグに関する情報(パスの扱いに関連): https://xnacly.me/posts/go-trimpath/ debug/gosym
パッケージに関する情報(シンボルテーブルのアクセス): https://golang.bg/blog/go-binary-analysis-part-1-symbol-table-and-line-number-table