[インデックス 1620] ファイルの概要
このコミットは、Go言語のリンカである6l
における、オブジェクトファイル(.6
)の取り込みに関するヒューリスティックの修正を目的としています。具体的には、ライブラリアーカイブ(.a
)から.6
ファイルをリンクする際の挙動を変更し、不要なコードやデータを除去する役割を「デッドコード/データエリミネーター」に完全に委ねるように改善しています。
コミット
commit fc8dca9dacb68eae277ba37a6356ea57398f96da
Author: Russ Cox <rsc@golang.org>
Date: Thu Feb 5 13:58:43 2009 -0800
heuristic to stop pulling .6 in from .a
is not right if the .6 is only for data and
the init function.
instead of that, pick up everything and
let the dead code/data eliminator throw
away the parts that weren't useful.
R=r
DELTA=25 (0 added, 22 deleted, 3 changed)
OCL=24446
CL=24446
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fc8dca9dacb68eae277ba37a6356ea57398f96da
元コミット内容
このコミットは、Go言語のリンカ6l
における、ライブラリアーカイブ(.a
)からオブジェクトファイル(.6
)を取り込む際のヒューリスティックが不適切であるという問題を修正します。特に、.6
ファイルがデータやinit
関数のみを含んでいる場合に、そのヒューリスティックが正しく機能しないことが指摘されています。
この問題に対処するため、コミットは、特定の条件で.6
ファイルの取り込みを停止するヒューリスティックを削除します。代わりに、リンカはすべての必要なシンボルを取り込み、その後の「デッドコード/データエリミネーター」が不要な部分を破棄するように変更されます。これにより、リンカの挙動がより堅牢になり、不必要なコードの除去がより効率的に行われるようになります。
変更の背景
Go言語の初期のリンカ6l
は、生成されるバイナリのサイズを最適化するために、ライブラリからオブジェクトファイルを取り込む際に特定のヒューリスティックを使用していました。このヒューリスティックは、.6
ファイルが「オプションの関数」(例えば、特定の条件でのみ呼び出される関数や、パッケージのinit
関数など)やデータのみを含んでいる場合、そのファイル全体をリンクしないように試みていました。これは、最終的な実行ファイルに不要なコードを含めないための試みでした。
しかし、このヒューリスティックには問題がありました。特に、.6
ファイルが実際に必要とされるデータや、プログラムの初期化に不可欠なinit
関数を含んでいる場合でも、ヒューリスティックが誤ってそのファイルの取り込みをスキップしてしまう可能性がありました。これにより、リンクエラーが発生したり、期待される動作が実現されなかったりするバグにつながる可能性がありました。
このコミットは、このような不正確なヒューリスティックを排除し、リンカの動作をよりシンプルかつ堅牢にすることを目的としています。つまり、リンカは必要なシンボルをすべて取り込み、その後の最適化フェーズである「デッドコード/データエリミネーション」に、実際に使用されないコードやデータを最終的に除去する役割を完全に委ねるという方針転換が行われました。これにより、リンカの複雑性が軽減され、より信頼性の高いバイナリ生成が可能になります。
前提知識の解説
このコミットを理解するためには、以下の概念が重要です。
- Go言語のツールチェイン(初期):
6g
: Go言語のコンパイラ(64ビットアーキテクチャ向け)。Goソースコードをオブジェクトファイル(.6
ファイル)にコンパイルします。6a
: Go言語のアセンブラ(64ビットアーキテクチャ向け)。アセンブリコードをオブジェクトファイル(.6
ファイル)にアセンブルします。6l
: Go言語のリンカ(64ビットアーキテクチャ向け)。複数のオブジェクトファイル(.6
)やライブラリアーカイブ(.a
)を結合し、実行可能なバイナリを生成します。このコミットの主要な変更対象です。
- オブジェクトファイル(
.6
)とライブラリアーカイブ(.a
):.6
ファイル: コンパイラやアセンブラによって生成される中間ファイルで、機械語コード、データ、シンボル情報などが含まれます。.a
ファイル(アーカイブ): 複数のオブジェクトファイル(.6
)を一つにまとめたライブラリファイルです。リンカは、プログラムが参照するシンボルを解決するために、これらのアーカイブから必要なオブジェクトファイルを抽出します。
- リンカの役割:
- リンカは、コンパイルされた複数のオブジェクトファイルやライブラリを結合し、最終的な実行可能ファイルや共有ライブラリを生成するプログラムです。
- 主な機能には、シンボル解決(未解決の関数や変数の参照を、定義されている場所と結びつけること)や、セクションの結合(コード、データなどの異なる種類の情報をメモリ上の適切な位置に配置すること)があります。
- デッドコードエリミネーション(Dead Code Elimination, DCE):
- コンパイラやリンカが行う最適化の一種で、プログラムの実行に影響を与えない、到達不能なコードや未使用のデータを最終的なバイナリから除去するプロセスです。これにより、バイナリサイズが削減され、ロード時間やメモリ使用量が改善されます。
init
関数:- Go言語のパッケージには、
init
関数を定義できます。これは、パッケージがインポートされた際に、そのパッケージ内の他のどの関数よりも先に自動的に実行される特殊な関数です。プログラムの初期化処理(例: グローバル変数の設定、リソースの初期化)によく使用されます。
- Go言語のパッケージには、
技術的詳細
このコミットの技術的な核心は、リンカ6l
がライブラリからオブジェクトファイルを取り込む際の「ヒューリスティック」の削除と、その役割をデッドコードエリミネーションに委ねるという方針転換にあります。
元々、6l
にはignoreoptfuncs
という関数が存在し、これは「オプションの関数」への呼び出しを無効化する役割を担っていました。ここでいう「オプションの関数」とは、ライブラリから取り込まれなかった関数、特にinit
関数やデータのみを含む.6
ファイルに関連するシンボルを指していたと考えられます。リンカは、これらのシンボルがSOPT
(Symbol Optional)という特殊なタイプを持つと判断した場合、そのシンボルを含むオブジェクトファイルをリンクしない、あるいはそのシンボルへの呼び出しをnop
(何もしない命令)に置き換えることで、バイナリサイズを削減しようとしていました。
しかし、このヒューリスティックは、.6
ファイルがデータやinit
関数のみを含んでいる場合でも、それらが実際にプログラムの動作に必要である場合に問題を引き起こしました。リンカが誤ってこれらの必要な部分をスキップしてしまうと、実行時エラーや予期せぬ動作につながる可能性がありました。
このコミットでは、以下の変更が行われ、この問題が解決されます。
ignoreoptfuncs
関数の削除:src/cmd/6l/go.c
からignoreoptfuncs
関数が完全に削除されます。これにより、「オプションの関数」を特別扱いして無効化するロジックがなくなります。SOPT
シンボルタイプの削除:src/cmd/6l/l.h
からSOPT
というシンボルタイプが削除されます。これは、リンカが「オプションの関数」を識別するために使用していた内部的な分類が不要になったことを意味します。obj.c
における関連ロジックの変更:main
関数内でのignoreoptfuncs()
の呼び出しが削除されます。- シンボルのタイプを
SOPT
に設定するロジック(if(isinitfunc(s)) s->type = SOPT;
)が削除されます。 - シンボルタイプをチェックする箇所(例:
s->type != SOPT
)からSOPT
のチェックが削除され、s->type != SXREF
(外部参照シンボル)のみが考慮されるようになります。
これらの変更により、リンカは.a
アーカイブから必要なシンボルをより積極的に取り込むようになります。そして、最終的にプログラムで実際に使用されないコードやデータは、リンカの後のフェーズで実行される「デッドコード/データエリミネーション」によって除去されることになります。このアプローチは、リンカの初期段階での複雑な推論を排除し、より汎用的な最適化フェーズに依存することで、リンカの堅牢性と正確性を向上させます。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下の3つのファイルにわたります。
-
src/cmd/6l/go.c
:ignoreoptfuncs
関数が完全に削除されました。この関数は、ライブラリから取り込まれなかったオプションの関数への呼び出しを無効化する役割を担っていました。
--- a/src/cmd/6l/go.c +++ b/src/cmd/6l/go.c @@ -503,22 +503,6 @@ isinitfunc(Sym *s) return 0; } -void -ignoreoptfuncs(void) -{ - Prog *p; - - // nop out calls to optional functions - // that were not pulled in from libraries. - for(p=firstp; p != P; p=p->link) { - if(p->to.sym != S && p->to.sym->type == SOPT) { - if(p->as != ACALL) - diag("bad use of optional function: %P", p); - nopout(p); - } - } -} - static void mark(Sym*); static int markdepth;
-
src/cmd/6l/l.h
:enum
定義からSOPT
(Symbol Optional)が削除されました。ignoreoptfuncs
関数のプロトタイプ宣言が削除されました。
--- a/src/cmd/6l/l.h +++ b/src/cmd/6l/l.h @@ -150,7 +150,6 @@ enum SFILE, SCONST, SUNDEF, - SOPT, SIMPORT, SEXPORT, @@ -386,7 +385,6 @@ void ckoff(Sym*, int32); Prog* copyp(Prog*); double cputime(void); void datblk(int32, int32); -void ignoreoptfuncs(void); void deadcode(void); void definetypestrings(void); void definetypesigs(void);
-
src/cmd/6l/obj.c
:main
関数内でのignoreoptfuncs()
の呼び出しが削除されました。- シンボルタイプを
SOPT
に設定するロジックが削除されました。 - シンボルタイプをチェックする条件式から
s->type != SOPT
が削除され、s->type != SXREF
のみが残されました。
--- a/src/cmd/6l/obj.c +++ b/src/cmd/6l/obj.c @@ -368,7 +368,6 @@ main(int argc, char *argv[]) sprint(a, "%s/lib/lib_%s_%s.a", goroot, goarch, goos); objfile(a); } - ignoreoptfuncs(); definetypestrings(); definetypesigs(); deadcode(); @@ -950,11 +949,8 @@ loop: if(debug['W']) print("\tANAME\t%s\n", s->name); h[o] = s; - if((v == D_EXTERN || v == D_STATIC) && s->type == 0) { + if((v == D_EXTERN || v == D_STATIC) && s->type == 0) s->type = SXREF; - if(isinitfunc(s)) - s->type = SOPT; // optional function; don't pull in an object file just for s. - } if(v == D_FILE) { if(s->type != SFILE) { histgen++; @@ -1096,7 +1092,7 @@ loop: case ATEXT: s = p->from.sym; - if(ntext++ == 0 && s->type != 0 && s->type != SXREF && s->type != SOPT) { + if(ntext++ == 0 && s->type != 0 && s->type != SXREF) { /* redefinition, so file has probably been seen before */ if(debug['v']) Bprint(&bso, "skipping: %s: redefinition: %s", pn, s->name); @@ -1113,7 +1109,7 @@ loop: diag("%s: no TEXT symbol: %P", pn, p); errorexit(); } - if(s->type != 0 && s->type != SXREF && s->type != SOPT) { + if(s->type != 0 && s->type != SXREF) { if(p->from.scale & DUPOK) { skip = 1; goto casdef;
コアとなるコードの解説
このコミットの核心は、Goリンカ6l
がオブジェクトファイルを取り込む際の「オプション関数」に関する特殊な処理を完全に排除した点にあります。
-
ignoreoptfuncs
関数の削除:- この関数は、リンカがライブラリから取り込まなかった(つまり、参照されなかった)「オプションの関数」への呼び出しを、実行時に何もしない
nop
命令に置き換えることを目的としていました。これは、バイナリサイズを削減するための試みでしたが、init
関数やデータのみを含むファイルの場合に誤動作する可能性がありました。この関数の削除により、リンカはこのような特殊な最適化を初期段階で行わなくなります。
- この関数は、リンカがライブラリから取り込まなかった(つまり、参照されなかった)「オプションの関数」への呼び出しを、実行時に何もしない
-
SOPT
シンボルタイプの削除:SOPT
は、リンカが「オプションの関数」と見なしたシンボルに割り当てていた内部的なタイプでした。このタイプが削除されたことで、リンカは特定の関数を「オプション」として特別扱いする概念自体を放棄しました。これにより、シンボル処理のロジックが簡素化され、より一般的なシンボル解決の枠組みに統合されます。
-
obj.c
におけるシンボル処理の変更:- 以前は、
isinitfunc(s)
(シンボルs
がinit
関数であるかどうかのチェック)が真の場合、そのシンボルのタイプをSOPT
に設定していました。このロジックが削除されたことで、init
関数も他の通常の関数と同様に扱われるようになります。 - また、コード内で
s->type != SOPT
という条件でシンボルをフィルタリングしていた箇所が、s->type != SXREF
(外部参照シンボルではない)のみをチェックするように変更されました。これは、リンカがシンボルを処理する際に、「オプション」であるかどうかを考慮しなくなったことを明確に示しています。
- 以前は、
これらの変更は、リンカの設計思想における重要な転換を示しています。以前は、リンカが「賢く」どのコードが必要でどのコードが不要かを推測しようとしていましたが、その推測が不正確な場合に問題が発生しました。このコミット以降は、リンカはよりシンプルに、参照されているすべてのシンボルとそれに関連するコード・データを取り込みます。そして、最終的なバイナリから実際に使用されない部分を除去する役割は、より洗練された「デッドコード/データエリミネーション」のフェーズに完全に委ねられることになります。これにより、リンカの堅牢性が向上し、予期せぬリンクエラーやバイナリの欠陥が減少することが期待されます。
関連リンク
- Go言語の初期のリンカに関する議論やドキュメントは、Goプロジェクトのメーリングリストや初期の設計ドキュメントに散見されますが、特定の直接的な関連リンクを見つけるのは困難です。
- Go言語の
init
関数に関する公式ドキュメント: https://go.dev/doc/effective_go#init - リンカの一般的な概念(デッドコードエリミネーションなど)に関する情報:
参考にした情報源リンク
- Go言語の公式Gitリポジトリ: https://github.com/golang/go
- コミットメッセージとコード差分
- Go言語の初期のツールチェインに関する一般的な知識