[インデックス 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.type
がD_BRANCH
であるかどうかをチェックすることで、そのジャンプがグローバルシンボルへのジャンプなのか、それともローカルラベルへのジャンプなのかを区別します。
技術的詳細
このコミットは、Goリンカのsrc/cmd/6l/pass.c
ファイル内のpatch
関数を変更しています。patch
関数は、リンカのパッチングフェーズの一部であり、命令のアドレス解決や再配置を行う重要な部分です。
変更前のコードでは、ACALL
命令とAJMP
命令の両方に対して、一律にp->to.sym
(命令の宛先シンボル)を取得し、そのシンボルが存在すれば何らかの処理(デバッグ情報の出力など)を行っていました。これは、すべてのAJMP
命令がシンボル解決の対象となることを意味します。
変更後のコードでは、AJMP
命令に対する条件がp->as == AJMP && p->to.type != D_BRANCH
に修正されています。
この変更のポイントは以下の通りです。
ACALL
命令の扱い:ACALL
命令は常にシンボル解決の対象となります。関数呼び出しは通常、別の関数(シンボル)へのジャンプを伴うため、これは正しい挙動です。AJMP
命令のフィルタリング:AJMP
命令の場合、p->to.type != D_BRANCH
という条件が追加されました。p->to.type == D_BRANCH
の場合:これは、ジャンプの宛先がローカルな分岐先(ラベル)であることを示します。このようなジャンプは、リンカによるグローバルなシンボル解決を必要としません。例えば、JMP L1
のような命令で、L1
が現在の関数内で定義されたラベルである場合がこれに該当します。この場合、p->to.sym
はNULL
であるか、あるいは誤ったシンボルを指す可能性があります。この条件により、リンカはこのようなローカルジャンプに対してシンボル解決を試みなくなります。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)
これは、「もし現在の命令p
がACALL
命令であるか、またはAJMP
命令であるならば」という条件です。この条件では、すべてのAJMP
命令がシンボル解決の対象となっていました。 -
変更後:
if(p->as == ACALL || (p->as == AJMP && p->to.type != D_BRANCH))
これは、「もし現在の命令p
がACALL
命令であるならば、または(AJMP
命令であり、かつその宛先タイプがD_BRANCH
ではないならば)」という条件です。 この新しい条件により、AJMP
命令の中でも、その宛先がローカルな分岐(D_BRANCH
)ではない場合にのみ、シンボル解決のロジックに進むようになります。これにより、JMP main(SB)
のようなシンボル参照は処理され、JMP L1
のようなローカルラベル参照はスキップされるようになります。
この修正は、リンカが命令のセマンティクスをより正確に理解し、適切なシンボル解決を行うための重要な改善です。
関連リンク
- Go言語の初期開発に関する情報源(公式ドキュメントやブログ記事など)
- Plan 9のツールチェインに関するドキュメント
- Goアセンブリの構文に関する公式ドキュメント
参考にした情報源リンク
- Go言語のソースコード(特に
src/cmd/6l/
ディレクトリ) - Go言語の初期のコミット履歴
- Plan 9のドキュメント(特にアセンブラとリンカに関するもの)
- Goアセンブリに関する公式ドキュメントやチュートリアル
- リンカの動作原理に関する一般的な情報