[インデックス 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
フィールドの多重利用は、以下のような問題を引き起こす可能性がありました。
- 意図しない重複検出:
LSym
構造体を直接操作するような低レベルのコードや、リンカの内部処理を理解せずにLSym
を手動で構築しようとするクライアントコードが、シンボルの実際のサイズをsize
フィールドに設定した場合、その値がゼロでなければ、意図せず重複シンボルとして扱われてしまう可能性がありました。これは、リンカの挙動を予測不能にし、デバッグを困難にする原因となります。 - コードの可読性と保守性: 一つのフィールドが複数の意味を持つことは、コードの可読性を低下させ、将来的な変更や拡張を困難にします。
size
フィールドが「サイズ」と「重複検出フラグ」という異なる概念を表しているため、コードを読んだ際にその意図を正確に把握するのが難しくなります。 - 堅牢性の欠如:
size
フィールドの本来の目的はシンボルのサイズを示すことであり、重複検出フラグとしての利用は副作用的なものでした。このような設計は、リンカの堅牢性を損ない、予期せぬバグを生み出す温床となります。
これらの問題を解決し、リンカのコードベースをより明確で堅牢なものにするために、グローバルシンボルの重複検出専用の明示的なフィールドを導入する必要がありました。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
-
リンカ (Linker): コンパイラによって生成された複数のオブジェクトファイル(
.o
ファイルなど)やライブラリファイルを結合し、最終的な実行可能ファイルや共有ライブラリを生成するプログラムです。リンカの主な役割は、シンボル解決(未解決のシンボル参照を実際の定義に結びつけること)と、コードおよびデータの再配置(メモリ上の適切なアドレスに配置すること)です。 -
シンボル (Symbol): プログラム内の関数、グローバル変数、静的変数などの名前付きエンティティを指します。リンカはこれらのシンボルを使って、異なるオブジェクトファイル間で参照されるコードやデータを結びつけます。シンボルには、その名前、アドレス、サイズ、型などの情報が含まれます。
-
グローバルシンボル (Global Symbol): プログラム全体から参照可能なシンボルです。通常、関数名やグローバル変数名がこれに該当します。リンカは、複数のオブジェクトファイルに同じ名前のグローバルシンボルが定義されていないかを確認し、重複があれば警告またはエラーを発生させます。
-
liblink
: Go言語のツールチェインの一部であるリンカライブラリです。Goのコンパイラは、最終的なバイナリを生成するためにliblink
を利用します。liblink
は、Go特有のリンキング要件(例えば、Goランタイムの特殊なシンボル処理や、スタックフレームの管理など)に対応しています。 -
LSym
構造体:liblink
内でシンボル情報を表現するために使用されるC言語の構造体です。この構造体には、シンボルの名前、型、アドレス、サイズ、セクション情報、そしてリンキングプロセスに関連する様々なフラグやメタデータが含まれます。 -
AGLOBL
: Go言語のアセンブラ(go tool asm
)やリンカの文脈で使われるアセンブリ命令の一種で、グローバルシンボルを定義するために使用されます。p->as == ctxt->arch->AGLOBL
という条件は、現在の処理対象がグローバルシンボルであることを示しています。 -
print("duplicate %P\\n", p)
: これは、Goのリンカが重複するグローバルシンボルを検出した際に、そのシンボルに関する情報を標準出力に表示するためのデバッグまたは警告メッセージです。%P
は、シンボルp
の情報を整形して出力するためのフォーマット指定子です。
技術的詳細
このコミットの技術的な核心は、LSym
構造体の設計改善と、それに基づくグローバルシンボル重複検出ロジックの変更にあります。
LSym
構造体の変更
以前のLSym
構造体では、シンボルの実際のサイズを格納するsize
フィールドが、グローバルシンボルが既に処理されたかどうかを示すフラグとしても利用されていました。このコミットでは、この多重利用を解消するために、include/link.h
ファイル内のLSym
構造体に新しいフィールドseenglobl
(uchar
型)が追加されました。
// 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);
: 以前の重複検出ロジックが削除されました。この行では、LSym
のsize
フィールドが非ゼロである場合に、そのグローバルシンボルが既に処理されたものと見なし、重複メッセージを出力していました。この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 CL 44140043: https://golang.org/cl/44140043
- Go言語の公式GitHubリポジトリ: https://github.com/golang/go
参考にした情報源リンク
- Go言語のリンカに関するドキュメントやソースコード(特に
src/cmd/link
およびsrc/liblink
ディレクトリ) - C言語のポインタと構造体に関する一般的な知識
- 後置インクリメント演算子(
++
)の動作に関する一般的な知識 - リンカのシンボル解決と重複検出に関する一般的なコンピュータサイエンスの知識