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

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

このコミットは、Goコンパイラ(cmd/gc)が生成するエクスポートデータにスペースを追加することで、リンカ(cmd/ld)がそのデータを正しく解析できるようにする修正です。具体的には、関数シグネチャのエクスポート時に、関数名と型情報の間にスペースがないと、リンカが誤って次の定義の一部を関数名として解釈してしまう問題を解決します。

コミット

commit 8fff2525cb37a71703b2e54efd3cfc74f7b96414
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Wed Jan 9 22:02:53 2013 +0100

    cmd/gc: add space to export data to match linker expectations
    
    The linker split PKGDEF into (prefix, name, def) pairs,
    and defines def to begin after a space following the identifier.
    This is totally wrong for the following export data:
    
            func "".FunctionName()
            var SomethingCompletelyUnrelated int
    
    The linker would parse
        name=`"".FunctionName()\n\tvar`
        def=`SomethingCompletelyUnrelated int`
    since there is no space after FunctionName.
    
    R=minux.ma, rsc
    CC=golang-dev
    https://golang.org/cl/7068051
---
 src/cmd/gc/export.c             |  9 +++++----\n test/fixedbugs/bug472.dir/p1.go | 17 +++++++++++++++++\n test/fixedbugs/bug472.dir/p2.go | 17 +++++++++++++++++\n test/fixedbugs/bug472.dir/z.go  | 13 +++++++++++++\n test/fixedbugs/bug472.go        | 10 ++++++++++\n 5 files changed, 62 insertions(+), 4 deletions(-)

diff --git a/src/cmd/gc/export.c b/src/cmd/gc/export.c
index 4d0368ef09..b235f676cd 100644
--- a/src/cmd/gc/export.c
+++ b/src/cmd/gc/export.c
@@ -220,10 +220,11 @@ dumpexportvar(Sym *s)
 			// currently that can leave unresolved ONONAMEs in import-dot-ed packages in the wrong package
 			if(debug['l'] < 2)
 				typecheckinl(n);
-			Bprint(bout, "\tfunc %#S%#hT { %#H }\n", s, t, n->inl);
+			// NOTE: The space after %#S here is necessary for ld's export data parser.
+			Bprint(bout, "\tfunc %#S %#hT { %#H }\n", s, t, n->inl);
 			reexportdeplist(n->inl);
 		} else
-			Bprint(bout, "\tfunc %#S%#hT\n", s, t);
+			Bprint(bout, "\tfunc %#S %#hT\n", s, t);
 	} else
 		Bprint(bout, "\tvar %#S %#T\n", s, t);
 }
@@ -282,10 +283,10 @@ dumpexporttype(Type *t)
 			// currently that can leave unresolved ONONAMEs in import-dot-ed packages in the wrong package
 			if(debug['l'] < 2)
 				typecheckinl(f->type->nname);
-			Bprint(bout, "\tfunc (%#T) %#hhS%#hT { %#H }\n", getthisx(f->type)->type, f->sym, f->type, f->type->nname->inl);
+			Bprint(bout, "\tfunc (%#T) %#hhS %#hT { %#H }\n", getthisx(f->type)->type, f->sym, f->type, f->type->nname->inl);
 			reexportdeplist(f->type->nname->inl);
 		} else
-			Bprint(bout, "\tfunc (%#T) %#hhS%#hT\n", getthisx(f->type)->type, f->sym, f->type);
+			Bprint(bout, "\tfunc (%#T) %#hhS %#hT\n", getthisx(f->type)->type, f->sym, f->type);
 	}\n }\n 
diff --git a/test/fixedbugs/bug472.dir/p1.go b/test/fixedbugs/bug472.dir/p1.go
new file mode 100644
index 0000000000..9d47fd84a7
--- /dev/null
+++ b/test/fixedbugs/bug472.dir/p1.go
@@ -0,0 +1,17 @@
+// Copyright 2013 The Go Authors.  All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n+\n+package p1\n+\n+import \"runtime\"\n+\n+func E() func() int { return runtime.NumCPU }\n+\n+func F() func() { return runtime.Gosched }\n+\n+func G() func() string { return runtime.GOROOT }\n+\n+func H() func() { return runtime.GC }\n+\n+func I() func() string { return runtime.Version }\ndiff --git a/test/fixedbugs/bug472.dir/p2.go b/test/fixedbugs/bug472.dir/p2.go
new file mode 100644
index 0000000000..34a3f0487a
--- /dev/null
+++ b/test/fixedbugs/bug472.dir/p2.go
@@ -0,0 +1,17 @@
+// Copyright 2013 The Go Authors.  All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n+\n+package p2\n+\n+import \"runtime\"\n+\n+func E() func() int { return runtime.NumCPU }\n+\n+func F() func() { return runtime.GC }\n+\n+func G() func() string { return runtime.GOROOT }\n+\n+func H() func() { return runtime.Gosched }\n+\n+func I() func() string { return runtime.Version }\ndiff --git a/test/fixedbugs/bug472.dir/z.go b/test/fixedbugs/bug472.dir/z.go
new file mode 100644
index 0000000000..6c29dd08c6
--- /dev/null
+++ b/test/fixedbugs/bug472.dir/z.go
@@ -0,0 +1,13 @@
+// Copyright 2013 The Go Authors.  All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n+\n+package main\n+\n+import (\n+\t_ \"./p1\"\n+\t_ \"./p2\"\n+)\n+\n+func main() {\n+}\ndiff --git a/test/fixedbugs/bug472.go b/test/fixedbugs/bug472.go
new file mode 100644
index 0000000000..c79c64ca1f
--- /dev/null
+++ b/test/fixedbugs/bug472.go
@@ -0,0 +1,10 @@
+// rundir\n+\n+// Copyright 2013 The Go Authors.  All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+// Linker would incorrectly parse export data and think\n+// definitions are inconsistent.\n+\n+package ignored

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

https://github.com/golang/go/commit/8fff2525cb37a71703b2e54efd3cfc74f7b96414

元コミット内容

Goコンパイラ(cmd/gc)が生成するエクスポートデータにスペースを追加し、リンカ(cmd/ld)の期待する形式に合わせる。リンカはPKGDEFデータを(prefix, name, def)のペアに分割する際、識別子の後に続くスペースの後にdefが始まると定義している。しかし、エクスポートデータがfunc "".FunctionName()のように、関数名の後にスペースがない場合、リンカはname"".FunctionName()\n\tvardefSomethingCompletelyUnrelated intと誤って解析してしまう。このコミットは、この誤った解析を修正するために、関数名と型情報の間にスペースを追加する。

変更の背景

Go言語のコンパイルプロセスにおいて、コンパイラ(cmd/gc)は、他のパッケージから参照される可能性のあるシンボル(関数、変数、型など)の情報を「エクスポートデータ」として生成します。このエクスポートデータは、リンカ(cmd/ld)が複数のコンパイル済みオブジェクトファイルを結合して実行可能ファイルを生成する際に利用されます。

このコミットが修正しようとしている問題は、cmd/gcが生成するエクスポートデータのフォーマットと、cmd/ldがそのデータを解析する際の期待値との間に不整合があったことです。具体的には、リンカはエクスポートデータ内の各定義を(prefix, name, def)という3つの要素のペアとして解釈しようとします。ここでdefは、識別子(name)の後に続くスペースから始まると期待されていました。

しかし、cmd/gcが関数をエクスポートする際、例えばfunc "".FunctionName()のように、関数名("".FunctionName)の直後に引数リストや戻り値の型が続き、その間にスペースが挿入されていませんでした。このため、リンカは関数名の後にスペースがないことを検知できず、次の行のvar SomethingCompletelyUnrelated intのような別の定義の一部を、誤って前の関数のnameの一部として取り込んでしまっていました。結果として、リンカはエクスポートデータを正しく解析できず、シンボルの定義が矛盾していると判断してしまう可能性がありました。

この問題は、特に複数のパッケージが相互に参照し合うような複雑なプロジェクトにおいて、ビルドエラーや予期せぬ動作を引き起こす可能性がありました。このコミットは、このリンカの解析ロジックの期待値に合わせて、コンパイラがエクスポートデータに明示的にスペースを挿入することで、この不整合を解消することを目的としています。

前提知識の解説

Goコンパイラ (cmd/gc) とリンカ (cmd/ld)

  • Goコンパイラ (cmd/gc): Go言語のソースコードを機械語に変換する主要なコンパイラです。各Goパッケージは独立してコンパイルされ、オブジェクトファイル(通常は.oまたは.a拡張子)が生成されます。この際、他のパッケージから参照されるシンボル(関数、変数、型など)の情報は「エクスポートデータ」としてオブジェクトファイル内に埋め込まれます。
  • Goリンカ (cmd/ld): コンパイラによって生成された複数のオブジェクトファイルやライブラリを結合し、最終的な実行可能ファイルや共有ライブラリを生成するツールです。リンカは、オブジェクトファイル内のエクスポートデータを読み取り、シンボル解決(どのシンボルがどこで定義されているかを特定するプロセス)や、コードとデータの配置を行います。

エクスポートデータ (PKGDEF)

Goのエクスポートデータは、パッケージが外部に公開するシンボルに関するメタデータです。これは、Goのコンパイルシステムにおいて、異なるパッケージ間で型情報や関数シグネチャなどを共有するために不可欠です。このデータは、Goの内部的な形式で表現され、リンカがパッケージ間の依存関係を解決するために利用します。コミットメッセージにあるPKGDEFは、このエクスポートデータの内部的な表現形式の一つを指していると考えられます。

Bprint 関数

src/cmd/gc/export.cファイル内で使用されているBprintは、Goコンパイラの内部で使われるバッファリングされた出力関数です。C言語のfprintfに似ていますが、Goコンパイラの特定のニーズに合わせて最適化されています。この関数は、エクスポートデータをファイルやメモリバッファに書き出すために使用されます。%#S%#hT%#Hなどは、BprintがGoのシンボル、型、インライン関数などを特定のフォーマットで文字列に変換するためのフォーマット指定子です。

  • %#S: シンボル(Sym構造体)をフォーマットします。
  • %#hT: 型(Type構造体)をフォーマットします。
  • %#H: インライン関数(Node構造体)をフォーマットします。

リンカの解析ロジック

リンカがエクスポートデータを解析する際、特定の区切り文字やパターンに基づいて情報を抽出します。このコミットの文脈では、リンカはエクスポートデータ内の各エントリを「識別子」とそれに続く「定義」に分割しようとします。リンカの期待では、識別子の直後にスペースが続くことで、そのスペースの後に定義が始まると解釈されていました。

技術的詳細

この問題の核心は、Goコンパイラ(cmd/gc)がエクスポートデータを生成する際の出力フォーマットと、Goリンカ(cmd/ld)がそのデータを解析する際の入力期待フォーマットとの間の不一致にありました。

Goコンパイラは、src/cmd/gc/export.c内のdumpexportvarおよびdumpexporttype関数を使用して、変数や関数のエクスポートデータを生成します。これらの関数は、Bprintという内部関数を使って、エクスポートデータを特定の文字列形式で出力します。

問題が発生していたのは、関数シグネチャをエクスポートする行でした。修正前のコードでは、以下のようにBprintが呼び出されていました。

Bprint(bout, "\tfunc %#S%#hT { %#H }\n", s, t, n->inl);
Bprint(bout, "\tfunc %#S%#hT\n", s, t);
Bprint(bout, "\tfunc (%#T) %#hhS%#hT { %#H }\n", getthisx(f->type)->type, f->sym, f->type, f->type->nname->inl);
Bprint(bout, "\tfunc (%#T) %#hhS%#hT\n", getthisx(f->type)->type, f->sym, f->type);

ここで、%#S(関数名)の直後に%#hT(型情報、引数リストや戻り値の型など)が続いており、両者の間にスペースがありませんでした。例えば、func ".FunctionName()`のような出力が生成されます。

一方、リンカはエクスポートデータを解析する際に、PKGDEFという内部形式で定義を読み込みます。リンカのパーサーは、識別子(この場合は関数名)の後にスペースが続くことを期待し、そのスペースの直後から定義本体が始まると解釈するように設計されていました。

この不一致により、リンカはfunc "".FunctionName()という文字列を見たときに、"".FunctionName()を識別子として認識した後、その直後にスペースがないため、次の行のデータ(例: var SomethingCompletelyUnrelated int)までを誤って前の識別子の一部として取り込んでしまいました。

具体的には、コミットメッセージの例にあるように、リンカは以下のように誤解釈していました。

エクスポートデータ:
        func "".FunctionName()
        var SomethingCompletelyUnrelated int

リンカの誤った解析:
    name=`"".FunctionName()\n\tvar`  // 関数名と次の行の'var'までを誤って結合
    def=`SomethingCompletelyUnrelated int` // 'var'の後の部分を定義として解釈

この誤った解析は、リンカがシンボルテーブルを構築する際に、関数名が不正確になったり、別のシンボルの定義が欠落したりする原因となります。結果として、リンカはシンボル定義の不整合を検出し、ビルドエラーを引き起こす可能性がありました。

このコミットの修正は、Bprintのフォーマット文字列に単純にスペースを追加することで、この問題を解決します。これにより、コンパイラはリンカが期待するフォーマットでエクスポートデータを生成し、リンカは正しく識別子と定義を分離できるようになります。

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

変更は主に src/cmd/gc/export.c ファイルに集中しています。

--- a/src/cmd/gc/export.c
+++ b/src/cmd/gc/export.c
@@ -220,10 +220,11 @@ dumpexportvar(Sym *s)
 			// currently that can leave unresolved ONONAMEs in import-dot-ed packages in the wrong package
 			if(debug['l'] < 2)
 				typecheckinl(n);
-			Bprint(bout, "\tfunc %#S%#hT { %#H }\n", s, t, n->inl);
+			// NOTE: The space after %#S here is necessary for ld's export data parser.
+			Bprint(bout, "\tfunc %#S %#hT { %#H }\n", s, t, n->inl);
 			reexportdeplist(n->inl);
 		} else
-			Bprint(bout, "\tfunc %#S%#hT\n", s, t);
+			Bprint(bout, "\tfunc %#S %#hT\n", s, t);
 	} else
 		Bprint(bout, "\tvar %#S %#T\n", s, t);
 }
@@ -282,10 +283,10 @@ dumpexporttype(Type *t)
 			// currently that can leave unresolved ONONAMEs in import-dot-ed packages in the wrong package
 			if(debug['l'] < 2)
 				typecheckinl(f->type->nname);
-			Bprint(bout, "\tfunc (%#T) %#hhS%#hT { %#H }\n", getthisx(f->type)->type, f->sym, f->type, f->type->nname->inl);
+			Bprint(bout, "\tfunc (%#T) %#hhS %#hT { %#H }\n", getthisx(f->type)->type, f->sym, f->type, f->type->nname->inl);
 			reexportdeplist(f->type->nname->inl);
 		} else
-			Bprint(bout, "\tfunc (%#T) %#hhS%#hT\n", getthisx(f->type)->type, f->sym, f->type);
+			Bprint(bout, "\tfunc (%#T) %#hhS %#hT\n", getthisx(f->type)->type, f->sym, f->type);
 	}
 }

この変更では、Bprint関数のフォーマット文字列に、%#S(シンボル名)または%#hhS(メソッド名)の直後にスペースが追加されています。

また、この修正を検証するための新しいテストケースがtest/fixedbugs/bug472.dir/ディレクトリに追加されています。

  • test/fixedbugs/bug472.dir/p1.go
  • test/fixedbugs/bug472.dir/p2.go
  • test/fixedbugs/bug472.dir/z.go
  • test/fixedbugs/bug472.go

これらのテストファイルは、問題が再現するような特定の関数エクスポートパターンを持つパッケージを定義し、それらをインポートすることで、リンカの誤解析が修正されたことを確認します。

コアとなるコードの解説

src/cmd/gc/export.cファイルは、Goコンパイラがパッケージのエクスポートデータを生成するロジックを含んでいます。このファイル内のdumpexportvar関数とdumpexporttype関数が、それぞれ変数と型のエクスポート処理を担当しています。

このコミットで変更されたのは、これらの関数内でBprintを使って関数シグネチャをエクスポートする部分です。

変更前:

Bprint(bout, "\tfunc %#S%#hT { %#H }\n", s, t, n->inl);
Bprint(bout, "\tfunc %#S%#hT\n", s, t);
Bprint(bout, "\tfunc (%#T) %#hhS%#hT { %#H }\n", getthisx(f->type)->type, f->sym, f->type, f->type->nname->inl);
Bprint(bout, "\tfunc (%#T) %#hhS%#hT\n", getthisx(f->type)->type, f->sym, f->type);

これらの行では、関数名を表す%#Sまたは%#hhSの直後に、型情報を表す%#hTが続いていました。これにより、例えばfunc MyFunc(int) intのようなエクスポートデータが生成される際、MyFunc(int) intの間にスペースがありませんでした。

変更後:

Bprint(bout, "\tfunc %#S %#hT { %#H }\n", s, t, n->inl); // %#S の後にスペースを追加
// NOTE: The space after %#S here is necessary for ld's export data parser.
Bprint(bout, "\tfunc %#S %#hT\n", s, t); // %#S の後にスペースを追加
Bprint(bout, "\tfunc (%#T) %#hhS %#hT { %#H }\n", getthisx(f->type)->type, f->sym, f->type, f->type->nname->inl); // %#hhS の後にスペースを追加
Bprint(bout, "\tfunc (%#T) %#hhS %#hT\n", getthisx(f->type)->type, f->sym, f->type); // %#hhS の後にスペースを追加

変更後のコードでは、%#Sまたは%#hhSの直後に明示的にスペース( )が追加されています。この小さな変更により、生成されるエクスポートデータは、リンカが期待する「識別子の後にスペースが続く」というフォーマットに合致するようになります。

例えば、func MyFunc(int) intという関数は、変更前はfunc MyFunc(int) intとエクスポートされていたのが、変更後はfunc MyFunc (int) intのように、関数名と引数リストの間にスペースが挿入されるようになります。これにより、リンカはMyFuncを識別子として正しく認識し、その後のスペースから型情報が始まることを理解できるようになります。

この修正は、Goコンパイラとリンカ間の内部的なプロトコル(エクスポートデータのフォーマット)の整合性を保つための重要な変更であり、Goのビルドシステムの堅牢性を向上させます。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特に src/cmd/gc/export.c および src/cmd/ld 関連のファイル)
  • Go言語のコンパイルプロセスに関する一般的なドキュメント
  • Go言語のリンカの動作に関する情報