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

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

このコミットは、Go言語のリンカであるliblinkにおけるグローバルシンボルの重複検出メカニズムの改善に関するものです。具体的には、シンボルの重複を検出するためにLSym構造体のsizeフィールドを流用していた問題を解決し、専用のseengloblフィールドを導入することで、コードの明確性と堅牢性を向上させています。

コミット

liblink: use explicit field for globl duplicate detection

Overloading size leads to problems if clients
try to set up an LSym by hand.

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

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

https://github.com/golang/go/commit/4890502af647b3df6995dda55cff3345836c7d67

元コミット内容

Go言語のリンカであるliblinkにおいて、グローバルシンボルの重複検出にLSym構造体のsizeフィールドを使用していた。このsizeフィールドの多重利用(オーバーロード)は、クライアントが手動でLSymを設定しようとした際に問題を引き起こす可能性があった。このコミットでは、この問題を解決するために、グローバルシンボルの重複検出専用の明示的なフィールドを導入する。

変更の背景

Go言語のビルドプロセスにおいて、リンカは複数のオブジェクトファイルやライブラリを結合し、実行可能なバイナリを生成する重要な役割を担っています。この過程で、同じ名前のグローバルシンボルが複数回定義されている場合(重複定義)、リンカは通常エラーを報告するか、特定のルールに基づいていずれか一つを選択する必要があります。

以前のliblinkの実装では、LSym(Linker Symbol)構造体内のsizeフィールドが、シンボルの実際のサイズを示すだけでなく、そのグローバルシンボルが既に処理されたかどうか(つまり重複しているかどうか)を検出するためのフラグとしても使用されていました。具体的には、sizeフィールドがゼロでない場合に、そのシンボルが既に「見られた」ものとして扱われ、重複と判断されていました。

このsizeフィールドの多重利用は、以下のような問題を引き起こす可能性がありました。

  1. 意図しない重複検出: LSym構造体を直接操作するような低レベルのコードや、リンカの内部処理を理解せずにLSymを手動で構築しようとするクライアントコードが、シンボルの実際のサイズをsizeフィールドに設定した場合、その値がゼロでなければ、意図せず重複シンボルとして扱われてしまう可能性がありました。これは、リンカの挙動を予測不能にし、デバッグを困難にする原因となります。
  2. コードの可読性と保守性: 一つのフィールドが複数の意味を持つことは、コードの可読性を低下させ、将来的な変更や拡張を困難にします。sizeフィールドが「サイズ」と「重複検出フラグ」という異なる概念を表しているため、コードを読んだ際にその意図を正確に把握するのが難しくなります。
  3. 堅牢性の欠如: sizeフィールドの本来の目的はシンボルのサイズを示すことであり、重複検出フラグとしての利用は副作用的なものでした。このような設計は、リンカの堅牢性を損ない、予期せぬバグを生み出す温床となります。

これらの問題を解決し、リンカのコードベースをより明確で堅牢なものにするために、グローバルシンボルの重複検出専用の明示的なフィールドを導入する必要がありました。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

  1. リンカ (Linker): コンパイラによって生成された複数のオブジェクトファイル(.oファイルなど)やライブラリファイルを結合し、最終的な実行可能ファイルや共有ライブラリを生成するプログラムです。リンカの主な役割は、シンボル解決(未解決のシンボル参照を実際の定義に結びつけること)と、コードおよびデータの再配置(メモリ上の適切なアドレスに配置すること)です。

  2. シンボル (Symbol): プログラム内の関数、グローバル変数、静的変数などの名前付きエンティティを指します。リンカはこれらのシンボルを使って、異なるオブジェクトファイル間で参照されるコードやデータを結びつけます。シンボルには、その名前、アドレス、サイズ、型などの情報が含まれます。

  3. グローバルシンボル (Global Symbol): プログラム全体から参照可能なシンボルです。通常、関数名やグローバル変数名がこれに該当します。リンカは、複数のオブジェクトファイルに同じ名前のグローバルシンボルが定義されていないかを確認し、重複があれば警告またはエラーを発生させます。

  4. liblink: Go言語のツールチェインの一部であるリンカライブラリです。Goのコンパイラは、最終的なバイナリを生成するためにliblinkを利用します。liblinkは、Go特有のリンキング要件(例えば、Goランタイムの特殊なシンボル処理や、スタックフレームの管理など)に対応しています。

  5. LSym 構造体: liblink内でシンボル情報を表現するために使用されるC言語の構造体です。この構造体には、シンボルの名前、型、アドレス、サイズ、セクション情報、そしてリンキングプロセスに関連する様々なフラグやメタデータが含まれます。

  6. AGLOBL: Go言語のアセンブラ(go tool asm)やリンカの文脈で使われるアセンブリ命令の一種で、グローバルシンボルを定義するために使用されます。p->as == ctxt->arch->AGLOBLという条件は、現在の処理対象がグローバルシンボルであることを示しています。

  7. print("duplicate %P\\n", p): これは、Goのリンカが重複するグローバルシンボルを検出した際に、そのシンボルに関する情報を標準出力に表示するためのデバッグまたは警告メッセージです。%Pは、シンボルpの情報を整形して出力するためのフォーマット指定子です。

技術的詳細

このコミットの技術的な核心は、LSym構造体の設計改善と、それに基づくグローバルシンボル重複検出ロジックの変更にあります。

LSym構造体の変更

以前のLSym構造体では、シンボルの実際のサイズを格納するsizeフィールドが、グローバルシンボルが既に処理されたかどうかを示すフラグとしても利用されていました。このコミットでは、この多重利用を解消するために、include/link.hファイル内のLSym構造体に新しいフィールドseenglobluchar型)が追加されました。

// include/link.h の変更
struct	LSym
{
	// ... 既存のフィールド ...
	uchar	hide;
	uchar	leaf;	// arm only
	uchar	fnptr;	// arm only
	uchar	seenglobl; // 新しく追加されたフィールド
	int16	symid;	// for writing .5/.6/.8 files
	int32	dynid;
	int32	sig;
	// ... 既存のフィールド ...
};
  • uchar seenglobl;: このフィールドはunsigned char型であり、通常は1バイトのメモリを占有します。この型は、ブール値(真/偽)や小さなカウンタとして使用するのに適しています。ここでは、特定のグローバルシンボルがリンカによって既に「見られた」(処理された)回数を追跡するために使用されます。

グローバルシンボル重複検出ロジックの変更

src/liblink/objfile.cファイル内のlinkwriteobj関数(オブジェクトファイルを書き出す処理の一部)には、グローバルシンボルを処理するロジックが含まれています。この関数内で、AGLOBLタイプのシンボルが検出された際に、そのシンボルが重複しているかどうかをチェックしていました。

変更前は、以下のロジックが使用されていました。

// 変更前 (src/liblink/objfile.c)
if(p->as == ctxt->arch->AGLOBL) {
    s = p->from.sym;
    if(s->size) print("duplicate %P\\n", p); // sizeフィールドを重複検出に使用
    // ...
}

このコードでは、s->sizeがゼロでない場合に「重複」と判断し、警告メッセージを出力していました。これは、sizeフィールドがシンボルの実際のサイズを表すため、サイズがゼロでないシンボルは全て「既に処理済み」と見なされるという暗黙の前提に基づいています。しかし、これはsizeフィールドの本来の目的とは異なる利用方法であり、前述の問題を引き起こしていました。

変更後、このロジックはseengloblフィールドを使用するように修正されました。

// 変更後 (src/liblink/objfile.c)
if(p->as == ctxt->arch->AGLOBL) {
    s = p->from.sym;
    if(s->seenglobl++) // seengloblフィールドを重複検出に使用
        print("duplicate %P\\n", p);
    // ...
}
  • if(s->seenglobl++): この行が変更の核心です。
    • s->seenglobl++は、後置インクリメント演算子です。これは、まずs->seengloblの現在の値が評価され、その後にs->seengloblの値が1増加するという動作をします。
    • C言語では、if文の条件式において、非ゼロの値は真(true)と評価され、ゼロは偽(false)と評価されます。
    • したがって、あるグローバルシンボルsが初めてlinkwriteobj関数で処理される際、s->seengloblの初期値は通常ゼロです。このとき、s->seenglobl++はゼロと評価されるため、if文の条件は偽となり、重複メッセージは出力されません。その後、s->seengloblは1にインクリメントされます。
    • もし同じグローバルシンボルsが二度目以降に処理される場合、s->seengloblの値は既に1以上になっています。このとき、s->seenglobl++は非ゼロの値(1以上)と評価されるため、if文の条件は真となり、「duplicate %P」という警告メッセージが出力されます。その後、s->seengloblはさらにインクリメントされます。

この変更により、sizeフィールドはシンボルの実際のサイズのみを表現するようになり、重複検出のロジックはseengloblという専用のフィールドによって明示的に管理されるようになりました。これにより、コードの意図が明確になり、LSym構造体を扱う際の混乱が解消され、リンカの堅牢性が向上しました。

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

include/link.h

--- a/include/link.h
+++ b/include/link.h
@@ -131,6 +131,7 @@ struct	LSym
 	uchar	hide;
 	uchar	leaf;	// arm only
 	uchar	fnptr;	// arm only
+	uchar	seenglobl;
 	int16	symid;	// for writing .5/.6/.8 files
 	int32	dynid;
 	int32	sig;

src/liblink/objfile.c

--- a/src/liblink/objfile.c
+++ b/src/liblink/objfile.c
@@ -167,7 +167,8 @@ linkwriteobj(Link *ctxt, Biobuf *b)
 
 			if(p->as == ctxt->arch->AGLOBL) {
 				s = p->from.sym;
-				if(s->size) print("duplicate %P\\n", p);
+				if(s->seenglobl++)
+					print("duplicate %P\\n", p);
 				if(data == nil)
 					data = s;
 				else

コアとなるコードの解説

include/link.h の変更

  • + uchar seenglobl;: LSym構造体にseengloblという新しいフィールドが追加されました。このフィールドはuchar(符号なし文字型、通常1バイト)であり、グローバルシンボルがリンカによって処理された回数を記録するために使用されます。これにより、シンボルの「サイズ」と「重複検出フラグ」という異なる概念が分離され、LSym構造体のセマンティクスがより明確になりました。

src/liblink/objfile.c の変更

  • - if(s->size) print("duplicate %P\\n", p);: 以前の重複検出ロジックが削除されました。この行では、LSymsizeフィールドが非ゼロである場合に、そのグローバルシンボルが既に処理されたものと見なし、重複メッセージを出力していました。このsizeフィールドの多重利用が問題の原因でした。

  • + if(s->seenglobl++): 新しい重複検出ロジックが導入されました。

    • s->seenglobl++は、まずs->seengloblの現在の値を評価し、その値が非ゼロであればif文の条件が真となります。その後、s->seengloblの値が1増加します。
    • これにより、あるグローバルシンボルが初めて現れた際にはs->seengloblは0であるため、条件は偽となり重複メッセージは出力されません。
    • しかし、同じグローバルシンボルが2回目以降に現れた際にはs->seengloblは1以上になっているため、条件は真となり、print("duplicate %P\\n", p);が実行されて重複メッセージが出力されます。
    • この変更により、重複検出の目的がseengloblという専用のフィールドに集約され、コードの意図が明確になり、sizeフィールドの誤用による問題が解消されました。

これらの変更は、Goリンカの内部構造を改善し、より堅牢で理解しやすいコードベースに貢献しています。

関連リンク

参考にした情報源リンク

  • Go言語のリンカに関するドキュメントやソースコード(特にsrc/cmd/linkおよびsrc/liblinkディレクトリ)
  • C言語のポインタと構造体に関する一般的な知識
  • 後置インクリメント演算子(++)の動作に関する一般的な知識
  • リンカのシンボル解決と重複検出に関する一般的なコンピュータサイエンスの知識