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

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

このコミットは、Go言語のリンカである6lにおけるシンボル解決の挙動を修正するものです。具体的には、ジャンプ命令(AJMP)のシンボル解決において、特定の種類のジャンプ(ローカルラベルへのジャンプ)ではシンボルを解決しないように変更しています。これにより、リンカが意図しないシンボルを拾ってしまう問題を解決し、より正確なバイナリ生成を可能にしています。

コミット

commit 4d6bccb0f13a76cbadab00c8d5f7d5053a799948
Author: Russ Cox <rsc@golang.org>
Date:   Mon Dec 8 11:33:04 2008 -0800

    pick up symbol for JMP main(SB)
    but not JMP main (label).
    
    R=ken
    OCL=20724
    CL=20724

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

https://github.com/golang/go/commit/4d6bccb0f13a76cbadab00c8d5f7d5053a799948

元コミット内容

pick up symbol for JMP main(SB)
but not JMP main (label).

R=ken
OCL=20724
CL=20724

変更の背景

このコミットが行われた2008年12月は、Go言語がまだ一般に公開される前の初期開発段階でした。Goのコンパイラとリンカは、Plan 9のツールチェインをベースに開発されており、そのアセンブラとリンカの設計思想が色濃く反映されています。

当時のリンカ6l(x86-64アーキテクチャ用リンカ)は、コード内のジャンプ命令(JMP)やコール命令(CALL)が参照するシンボルを解決する際に、すべてのJMP命令に対して一律にシンボル解決を試みていました。しかし、アセンブリコードには、グローバルなシンボル(例: main(SB))へのジャンプと、関数内部のローカルなラベル(例: mainという名前のローカルラベル)へのジャンプが存在します。

リンカがローカルラベルへのジャンプに対してもシンボル解決を試みると、意図しないシンボル(例えば、同名のグローバルシンボル)を誤って参照してしまう可能性がありました。これは、生成されるバイナリの動作に予期せぬ影響を与えたり、リンカのエラーを引き起こしたりする原因となります。

このコミットは、JMP main(SB)のように明示的にシンボルベースのアドレス指定を行うジャンプに対してはシンボル解決を行い、JMP main(ローカルラベルを指す可能性のあるもの)のようにシンボルではないローカルな分岐先を指すジャンプに対してはシンボル解決を行わないように区別することで、この問題を解決しようとしています。これにより、リンカのシンボル解決の正確性が向上し、より堅牢なバイナリ生成プロセスが実現されます。

前提知識の解説

このコミットを理解するためには、以下の概念について知っておく必要があります。

1. Go言語のツールチェインと6l

Go言語の初期のツールチェインは、Plan 9オペレーティングシステムのツールチェイン(8c, 8l, 6c, 6lなど)に強く影響を受けています。

  • 6l: x86-64アーキテクチャ(AMD64)用のGoリンカです。アセンブラによって生成されたオブジェクトファイル(.6ファイル)を結合し、実行可能なバイナリを生成する役割を担います。シンボル解決、再配置、セクションの結合などを行います。

2. Goアセンブリと擬似命令

Go言語は、独自のGoアセンブリ言語を使用します。これは、一般的なAT&T構文やIntel構文とは異なる、Plan 9アセンブリに似た構文を持っています。

  • ATEXT: 関数の開始を示す擬似命令です。リンカはこれを見て、新しいテキストセクション(コードセクション)の開始を認識します。
  • ACALL: 関数呼び出し命令(CALL)を表します。
  • AJMP: ジャンプ命令(JMP)を表します。無条件ジャンプに使用されます。
  • p->as: リンカ内部で命令の種類(オペレーションコード)を表すフィールドです。ATEXT, ACALL, AJMPなどがこれに該当します。

3. シンボルとアドレス指定

  • シンボル: プログラム内の関数、変数、ラベルなどの名前付きエンティティです。リンカはこれらのシンボルを解決し、実際のメモリ上のアドレスにマッピングします。
  • main(SB): Goアセンブリにおけるアドレス指定の一種です。mainはシンボル名、SBはStatic Base(静的ベース)レジスタを意味します。これは、mainというグローバルシンボルが静的ベースからのオフセットで参照されることを示し、通常はプログラムのエントリポイントやグローバルな関数を指します。
  • ローカルラベル: 関数内部などで定義される、そのスコープ内でのみ有効なラベルです。これらは通常、シンボルテーブルには登録されず、リンカによるグローバルなシンボル解決の対象とはなりません。

4. D_BRANCH

D_BRANCHは、Goリンカの内部構造体(Prog構造体)のto.typeフィールドで使われる定数の一つです。Prog構造体は、アセンブリ命令やデータ転送の単位を表します。

  • p->to: 命令のオペランド(特に宛先)を表す構造体です。
  • p->to.type: オペランドの型を示します。D_BRANCHは、そのオペランドがローカルな分岐先(ラベル)であることを示唆します。つまり、これはシンボル解決を必要としない、相対的なジャンプや関数内のローカルなジャンプを意味します。

リンカは、p->to.typeD_BRANCHであるかどうかをチェックすることで、そのジャンプがグローバルシンボルへのジャンプなのか、それともローカルラベルへのジャンプなのかを区別します。

技術的詳細

このコミットは、Goリンカのsrc/cmd/6l/pass.cファイル内のpatch関数を変更しています。patch関数は、リンカのパッチングフェーズの一部であり、命令のアドレス解決や再配置を行う重要な部分です。

変更前のコードでは、ACALL命令とAJMP命令の両方に対して、一律にp->to.sym(命令の宛先シンボル)を取得し、そのシンボルが存在すれば何らかの処理(デバッグ情報の出力など)を行っていました。これは、すべてのAJMP命令がシンボル解決の対象となることを意味します。

変更後のコードでは、AJMP命令に対する条件がp->as == AJMP && p->to.type != D_BRANCHに修正されています。 この変更のポイントは以下の通りです。

  1. ACALL命令の扱い: ACALL命令は常にシンボル解決の対象となります。関数呼び出しは通常、別の関数(シンボル)へのジャンプを伴うため、これは正しい挙動です。
  2. AJMP命令のフィルタリング: AJMP命令の場合、p->to.type != D_BRANCHという条件が追加されました。
    • p->to.type == D_BRANCHの場合:これは、ジャンプの宛先がローカルな分岐先(ラベル)であることを示します。このようなジャンプは、リンカによるグローバルなシンボル解決を必要としません。例えば、JMP L1のような命令で、L1が現在の関数内で定義されたラベルである場合がこれに該当します。この場合、p->to.symNULLであるか、あるいは誤ったシンボルを指す可能性があります。この条件により、リンカはこのようなローカルジャンプに対してシンボル解決を試みなくなります。
    • p->to.type != D_BRANCHの場合:これは、ジャンプの宛先がシンボル(例: JMP main(SB))である可能性が高いことを示します。この場合、リンカはp->to.symを取得し、そのシンボルを解決しようとします。

この修正により、リンカはJMP main(SB)のようなグローバルシンボルへのジャンプと、JMP L1のようなローカルラベルへのジャンプを正確に区別できるようになります。これにより、リンカが不要なシンボル解決を試みたり、誤ったシンボルを解決したりするのを防ぎ、リンカの堅牢性と正確性が向上します。

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

変更はsrc/cmd/6l/pass.cファイルのpatch関数内の一行です。

--- a/src/cmd/6l/pass.c
+++ b/src/cmd/6l/pass.c
@@ -362,7 +362,7 @@ patch(void)
 	for(p = firstp; p != P; p = p->link) {
 		if(p->as == ATEXT)
 			curtext = p;
-		if(p->as == ACALL || p->as == AJMP) {
+		if(p->as == ACALL || (p->as == AJMP && p->to.type != D_BRANCH)) {
 			s = p->to.sym;
 			if(s) {
 				if(debug['c'])

コアとなるコードの解説

変更された行は、リンカが命令を処理するループ内で、ACALLまたはAJMP命令を識別するための条件分岐です。

  • 変更前: if(p->as == ACALL || p->as == AJMP) これは、「もし現在の命令pACALL命令であるか、またはAJMP命令であるならば」という条件です。この条件では、すべてのAJMP命令がシンボル解決の対象となっていました。

  • 変更後: if(p->as == ACALL || (p->as == AJMP && p->to.type != D_BRANCH)) これは、「もし現在の命令pACALL命令であるならば、またはAJMP命令であり、かつその宛先タイプがD_BRANCHではないならば)」という条件です。 この新しい条件により、AJMP命令の中でも、その宛先がローカルな分岐(D_BRANCH)ではない場合にのみ、シンボル解決のロジックに進むようになります。これにより、JMP main(SB)のようなシンボル参照は処理され、JMP L1のようなローカルラベル参照はスキップされるようになります。

この修正は、リンカが命令のセマンティクスをより正確に理解し、適切なシンボル解決を行うための重要な改善です。

関連リンク

  • Go言語の初期開発に関する情報源(公式ドキュメントやブログ記事など)
  • Plan 9のツールチェインに関するドキュメント
  • Goアセンブリの構文に関する公式ドキュメント

参考にした情報源リンク

  • Go言語のソースコード(特にsrc/cmd/6l/ディレクトリ)
  • Go言語の初期のコミット履歴
  • Plan 9のドキュメント(特にアセンブラとリンカに関するもの)
  • Goアセンブリに関する公式ドキュメントやチュートリアル
  • リンカの動作原理に関する一般的な情報