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

[インデックス 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関数を定義できます。これは、パッケージがインポートされた際に、そのパッケージ内の他のどの関数よりも先に自動的に実行される特殊な関数です。プログラムの初期化処理(例: グローバル変数の設定、リソースの初期化)によく使用されます。

技術的詳細

このコミットの技術的な核心は、リンカ6lがライブラリからオブジェクトファイルを取り込む際の「ヒューリスティック」の削除と、その役割をデッドコードエリミネーションに委ねるという方針転換にあります。

元々、6lにはignoreoptfuncsという関数が存在し、これは「オプションの関数」への呼び出しを無効化する役割を担っていました。ここでいう「オプションの関数」とは、ライブラリから取り込まれなかった関数、特にinit関数やデータのみを含む.6ファイルに関連するシンボルを指していたと考えられます。リンカは、これらのシンボルがSOPT(Symbol Optional)という特殊なタイプを持つと判断した場合、そのシンボルを含むオブジェクトファイルをリンクしない、あるいはそのシンボルへの呼び出しをnop(何もしない命令)に置き換えることで、バイナリサイズを削減しようとしていました。

しかし、このヒューリスティックは、.6ファイルがデータやinit関数のみを含んでいる場合でも、それらが実際にプログラムの動作に必要である場合に問題を引き起こしました。リンカが誤ってこれらの必要な部分をスキップしてしまうと、実行時エラーや予期せぬ動作につながる可能性がありました。

このコミットでは、以下の変更が行われ、この問題が解決されます。

  1. ignoreoptfuncs関数の削除: src/cmd/6l/go.cからignoreoptfuncs関数が完全に削除されます。これにより、「オプションの関数」を特別扱いして無効化するロジックがなくなります。
  2. SOPTシンボルタイプの削除: src/cmd/6l/l.hからSOPTというシンボルタイプが削除されます。これは、リンカが「オプションの関数」を識別するために使用していた内部的な分類が不要になったことを意味します。
  3. obj.cにおける関連ロジックの変更:
    • main関数内でのignoreoptfuncs()の呼び出しが削除されます。
    • シンボルのタイプをSOPTに設定するロジック(if(isinitfunc(s)) s->type = SOPT;)が削除されます。
    • シンボルタイプをチェックする箇所(例: s->type != SOPT)からSOPTのチェックが削除され、s->type != SXREF(外部参照シンボル)のみが考慮されるようになります。

これらの変更により、リンカは.aアーカイブから必要なシンボルをより積極的に取り込むようになります。そして、最終的にプログラムで実際に使用されないコードやデータは、リンカの後のフェーズで実行される「デッドコード/データエリミネーション」によって除去されることになります。このアプローチは、リンカの初期段階での複雑な推論を排除し、より汎用的な最適化フェーズに依存することで、リンカの堅牢性と正確性を向上させます。

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

このコミットによる主要なコード変更は以下の3つのファイルにわたります。

  1. 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;
    
    
  2. 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);
    
  3. 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がオブジェクトファイルを取り込む際の「オプション関数」に関する特殊な処理を完全に排除した点にあります。

  1. ignoreoptfuncs関数の削除:

    • この関数は、リンカがライブラリから取り込まなかった(つまり、参照されなかった)「オプションの関数」への呼び出しを、実行時に何もしないnop命令に置き換えることを目的としていました。これは、バイナリサイズを削減するための試みでしたが、init関数やデータのみを含むファイルの場合に誤動作する可能性がありました。この関数の削除により、リンカはこのような特殊な最適化を初期段階で行わなくなります。
  2. SOPTシンボルタイプの削除:

    • SOPTは、リンカが「オプションの関数」と見なしたシンボルに割り当てていた内部的なタイプでした。このタイプが削除されたことで、リンカは特定の関数を「オプション」として特別扱いする概念自体を放棄しました。これにより、シンボル処理のロジックが簡素化され、より一般的なシンボル解決の枠組みに統合されます。
  3. obj.cにおけるシンボル処理の変更:

    • 以前は、isinitfunc(s)(シンボルsinit関数であるかどうかのチェック)が真の場合、そのシンボルのタイプを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言語の初期のツールチェインに関する一般的な知識