[インデックス 16728] ファイルの概要
このコミットは、Go言語のリンカ (cmd/ld
) におけるMach-O形式のシンボルサイズ割り当てに関するバグ修正です。具体的には、シンボルのサイズを次のシンボルとのアドレス差分で計算する際に、シンボルがアドレス順にソートされる前に計算が行われていた問題を修正し、正確なサイズが割り当てられるように変更されました。
コミット
cmd/ld: correct assignment of sizes to mach-o symbols
If you compute the size by subtraction from the address
of the next symbol, it helps to wait until the symbols have
been sorted by address.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/11143043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a2c30fe6481fa5b18331bb54a660928f6816cc44
元コミット内容
commit a2c30fe6481fa5b18331bb54a660928f6816cc44
Author: Russ Cox <rsc@golang.org>
Date: Wed Jul 10 22:06:52 2013 -0400
cmd/ld: correct assignment of sizes to mach-o symbols
If you compute the size by subtraction from the address
of the next symbol, it helps to wait until the symbols have
been sorted by address.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/11143043
変更の背景
このコミットの背景には、Go言語のリンカがMach-O形式の実行可能ファイル(主にmacOSやiOSで使用されるバイナリ形式)を生成する際に、シンボルのサイズを誤って計算してしまうという問題がありました。
シンボルのサイズは通常、そのシンボルの開始アドレスから次のシンボルの開始アドレスを引くことで決定されます。しかし、この計算がシンボルがアドレス順にソートされる前に行われると、シンボルがメモリ上で連続していないか、あるいは順序が入れ替わっている場合に、誤ったサイズが割り当てられてしまいます。
この問題は、特にデバッグ情報やプロファイリングツールがシンボルサイズに依存する場合に、不正確な情報を提供する原因となります。リンカはバイナリの構造を正確に記述する責任があるため、この誤りを修正することは、生成されるバイナリの正確性と信頼性を確保するために不可欠でした。
コミットメッセージにある「If you compute the size by subtraction from the address of the next symbol, it helps to wait until the symbols have been sorted by address.」という記述は、この問題の核心を明確に示しています。つまり、シンボルサイズ計算のタイミングが不適切であったことが原因です。
前提知識の解説
1. Mach-O (Mach Object File Format)
Mach-Oは、AppleのmacOS、iOS、watchOS、tvOSなどのオペレーティングシステムで使用される実行可能ファイル、オブジェクトコード、共有ライブラリ、ダイナミックライブラリ、コアダンプなどのファイル形式です。ELF (Executable and Linkable Format) やPE (Portable Executable) と同様に、プログラムの実行に必要なコード、データ、メタデータを構造化して格納します。
Mach-Oファイルの主要な構成要素には以下が含まれます。
- ヘッダ (Header): ファイルの基本的な情報(CPUタイプ、ファイルタイプなど)を記述します。
- ロードコマンド (Load Commands): カーネルがプログラムをメモリにロードする方法を記述します。これには、セグメントの定義、シンボルテーブルの場所、ダイナミックリンカの情報などが含まれます。
- セグメント (Segments): 実行可能ファイルの論理的なメモリ領域を定義します。各セグメントは1つ以上のセクションを含みます。例えば、
__TEXT
セグメントはコードや読み取り専用データを含み、__DATA
セグメントは書き込み可能なデータを含みます。 - セクション (Sections): セグメント内のより細かい論理的なブロックです。例えば、
__TEXT
セグメントには__text
セクション(実行可能コード)や__cstring
セクション(C文字列)が含まれます。 - シンボルテーブル (Symbol Table): プログラム内のシンボル(関数名、変数名など)とそのアドレス、タイプなどの情報を格納します。リンカやデバッガがシンボルを解決するために使用します。
- リロケーションエントリ (Relocation Entries): コード内のアドレス参照を、ロード時に実際のメモリ位置に調整するための情報です。
2. シンボルとシンボルテーブル
シンボル (Symbol): プログラム内の特定のメモリ位置(関数、変数、ラベルなど)を識別するための名前です。例えば、C言語のmain
関数やグローバル変数my_global_var
はシンボルとして扱われます。シンボルは、ソースコードレベルの識別子と、バイナリ内の特定のアドレスを結びつけます。
シンボルテーブル (Symbol Table): バイナリファイル内に含まれるデータ構造で、シンボル名とそれに対応するアドレス、サイズ、タイプ(関数、データ、ファイルなど)、バインディング(グローバル、ローカルなど)などの情報がリストされています。リンカは、複数のオブジェクトファイルからのシンボルを解決し、最終的な実行可能ファイルを作成するためにシンボルテーブルを使用します。デバッガは、実行中のプログラムのメモリをシンボル名で参照するためにシンボルテーブルを利用します。
3. シンボルサイズの計算
多くのバイナリ形式では、シンボルのサイズは明示的に格納されることもありますが、特にコードセクション内の関数などのシンボルについては、そのシンボルの開始アドレスから次のシンボルの開始アドレスを引くことで計算されることが一般的です。
シンボルAのサイズ = (シンボルBの開始アドレス) - (シンボルAの開始アドレス)
この計算方法が機能するためには、シンボルがアドレス順にソートされていることが前提となります。もしソートされていない状態でこの計算を行うと、シンボルBがシンボルAよりも前のアドレスにある場合や、全く関係のないシンボルである場合に、負のサイズや不正確なサイズが計算されてしまいます。
4. Go言語のリンカ (cmd/ld
)
cmd/ld
は、Go言語のツールチェインの一部であるリンカです。Goのコンパイラ (cmd/compile
) が生成したオブジェクトファイル(.o
ファイル)を結合し、最終的な実行可能ファイルやライブラリを生成する役割を担います。cmd/ld
は、Goプログラムのクロスコンパイル能力を支える重要なコンポーネントであり、様々なターゲットアーキテクチャやOS(Linux, Windows, macOSなど)に対応したバイナリ形式(ELF, PE, Mach-Oなど)を生成できます。
このコミットは、cmd/ld
がMach-O形式のバイナリを生成する際の内部ロジック、特にシンボル情報の処理に関するものです。
技術的詳細
このコミットの技術的な詳細は、Mach-Oバイナリのシンボルサイズ計算のロジックが、シンボルがアドレス順にソートされる前に実行されていたという問題点に集約されます。
元のコードでは、ldmacho
関数内でMach-Oのシンボルテーブルを読み込み、Goの内部シンボル構造 (Sym
) に変換する際に、シンボルのサイズを即座に計算しようとしていました。
// 削除されたコードの一部
if(i+1 < symtab->nsym)
s->size = (sym+1)->value - sym->value;
else
s->size = sect->addr + sect->size - sym->value;
このコードは、現在のシンボルsym
の次のシンボル(sym+1)
のアドレスvalue
との差分を計算することで、s->size
を設定していました。しかし、この時点ではsymtab->sym
配列内のシンボルは、Mach-Oファイルから読み込まれた順序であり、必ずしもアドレス順にソートされているわけではありませんでした。そのため、sym+1
が必ずしもメモリ上で隣接する次のシンボルであるとは限らず、結果として誤ったサイズが計算される可能性がありました。
修正後のコードでは、この即時的なサイズ計算を削除し、シンボルがセクションごとにグループ化され、さらにlistsort
関数によってアドレス順にソートされた後に、改めてシンボルサイズを計算するロジックが追加されました。
新しいロジックは、ldmacho
関数の後半、セクションを処理するループ内で実行されます。
// 追加されたコードの一部
if(s->sub) {
s->sub = listsort(s->sub, valuecmp, offsetof(Sym, sub));
// assign sizes, now that we know symbols in sorted order.
for(s1 = s->sub; s1 != S; s1 = s1->sub) {
if(s1->sub)
s1->size = s1->sub->value - s1->value;
else
s1->size = s->value + s->size - s1->value;
}
}
ここで、s->sub
は、ある「外部」シンボル(例えば、セクションの開始シンボル)に属する「内部」シンボル(例えば、そのセクション内の関数)のリストを指します。このリストがlistsort(s->sub, valuecmp, offsetof(Sym, sub))
によってvalue
(アドレス)に基づいてソートされた後、for
ループで各シンボルs1
を順に処理し、その次のシンボルs1->sub
とのアドレス差分を計算してs1->size
に割り当てています。リストの最後のシンボルについては、そのシンボルが属するセクションの終わりまでの残りのサイズを割り当てています。
この変更により、シンボルサイズが計算される時点では、対象となるシンボルリストが必ずアドレス順にソートされていることが保証され、正確なサイズが割り当てられるようになりました。これは、リンカが生成するバイナリの正確性を高める上で重要な修正です。
コアとなるコードの変更箇所
変更は src/cmd/ld/ldmacho.c
ファイルに集中しています。
-
変数の追加:
Sym *s1;
がldmacho
関数のローカル変数として追加されました。これは、シンボルリストをイテレートする際に使用されます。
-
シンボルサイズ計算ロジックの削除:
ldmacho
関数内のシンボル読み込みループ (for(i=0; i<symtab->nsym; i++)
) の中で、シンボルのサイズを即座に計算していた以下のコードが削除されました。- if(i+1 < symtab->nsym) - s->size = (sym+1)->value - sym->value; - else - s->size = sect->addr + sect->size - sym->value;
-
ソート後のシンボルサイズ計算ロジックの追加:
ldmacho
関数内のセクション処理ループ (for(i=0; i<c->seg.nsect; i++)
) の中で、シンボルがソートされた後にサイズを計算する新しいロジックが追加されました。if(s->sub) { // s->sub は、あるシンボルに属するサブシンボルのリスト s->sub = listsort(s->sub, valuecmp, offsetof(Sym, sub)); // サブシンボルをアドレス順にソート // assign sizes, now that we know symbols in sorted order. for(s1 = s->sub; s1 != S; s1 = s1->sub) { // ソートされたサブシンボルリストをイテレート if(s1->sub) // 次のシンボルが存在する場合 s1->size = s1->sub->value - s1->value; // 次のシンボルとのアドレス差分でサイズを計算 else // リストの最後のシンボルの場合 s1->size = s->value + s->size - s1->value; // 親シンボルのサイズとアドレスから残りのサイズを計算 } }
-
テキストセクションのシンボルリスト処理の変更:
- テキストセクションのシンボルを連結するループで、イテレータ変数が
s
からs1
に変更されました。これは、上記のサイズ計算ループで導入されたs1
との整合性を保つためです。- for(s = s->sub; s != S; s = s->sub) { - etextp->next = s; - etextp = s; + for(s1 = s->sub; s1 != S; s1 = s1->sub) { + etextp->next = s1; + etextp = s1; }
- テキストセクションのシンボルを連結するループで、イテレータ変数が
コアとなるコードの解説
src/cmd/ld/ldmacho.c
は、GoリンカがMach-O形式のオブジェクトファイルやライブラリを読み込み、処理するためのC言語ソースファイルです。
ldmacho
関数
この関数は、Mach-Oファイルを解析し、その中のシンボル、セクション、リロケーション情報などをGoリンカの内部データ構造に変換する主要な役割を担います。
Sym
構造体
Goリンカが内部的にシンボル情報を管理するために使用する構造体です。この構造体には、シンボル名、アドレス (value
)、サイズ (size
)、タイプ、および他のシンボルとの関連性(例えば、outer
やsub
フィールドで表現されるネストされたシンボル)などの情報が含まれます。
変更点の詳細な解説
-
Sym *s1;
の追加:- これは単に、新しいループ処理のために必要な一時的なシンボルポインタ変数です。
-
古いサイズ計算の削除:
- 削除されたコードは、Mach-Oのシンボルテーブルからシンボルを読み込んだ直後にサイズを計算しようとしていました。この時点では、
symtab->sym
配列内のシンボルは、Mach-Oファイル内の出現順序であり、メモリ上のアドレス順ではありません。そのため、sym+1
が必ずしもアドレス的に次のシンボルを指すとは限らず、誤ったサイズが計算される原因となっていました。この「早すぎる」計算ロジックが削除されました。
- 削除されたコードは、Mach-Oのシンボルテーブルからシンボルを読み込んだ直後にサイズを計算しようとしていました。この時点では、
-
新しいサイズ計算ロジックの追加:
- この新しいロジックは、Mach-Oファイルのセクションを処理するループの中に配置されています。
if(s->sub)
: ここでのs
は、Mach-Oのセクションに対応するGoリンカのSym
構造体、またはMach-Oの外部シンボルに対応するSym
構造体を指します。s->sub
は、そのシンボルに「属する」サブシンボル(例えば、セクション内の関数や、外部シンボル内のネストされたシンボル)のリンクリストです。s->sub = listsort(s->sub, valuecmp, offsetof(Sym, sub));
: ここが最も重要な変更点です。listsort
関数は、リンクリストをソートするためのユーティリティ関数です。valuecmp
は、Sym
構造体のvalue
フィールド(シンボルのアドレス)に基づいて比較を行う関数ポインタです。offsetof(Sym, sub)
は、Sym
構造体内のsub
フィールドへのオフセットを渡し、リンクリストの次の要素へのポインタがどこにあるかをlistsort
に伝えます。この行により、s->sub
が指すサブシンボルのリストが、そのアドレス (value
) に基づいて昇順にソートされます。for(s1 = s->sub; s1 != S; s1 = s1->sub)
: ソートされたサブシンボルのリストをs1
を使ってイテレートします。S
はリンクリストの終端を示す特別なシンボルポインタです。if(s1->sub) s1->size = s1->sub->value - s1->value;
: 現在のシンボルs1
の次にソートされたシンボルs1->sub
が存在する場合、そのアドレス差分をs1
のサイズとして割り当てます。これにより、正確なシンボルサイズが計算されます。else s1->size = s->value + s->size - s1->value;
:s1
がリストの最後のシンボルである場合(つまりs1->sub
がS
である場合)、そのサイズは、親シンボルs
の開始アドレスとサイズからs1
の開始アドレスを引くことで計算されます。これは、s1
が親シンボル(セクションなど)の残りの部分を占めることを意味します。
-
テキストセクションのシンボルリスト処理の変更:
- これは、上記の新しいサイズ計算ロジックで
s1
という新しいイテレータ変数が導入されたことに伴う、既存のコードの整合性調整です。以前はs
を使ってテキストセクションのシンボルを連結していましたが、s1
を使うことで、ソートされたリストを正しく処理し、etextp->next
ポインタを適切に更新できるようになります。
- これは、上記の新しいサイズ計算ロジックで
このコミットは、シンボルサイズ計算の「タイミング」を修正することで、Mach-Oバイナリのシンボル情報の正確性を大幅に向上させました。これは、リンカの堅牢性と、生成されるバイナリのデバッグ可能性や分析可能性にとって重要な改善です。
関連リンク
- Go CL 11143043: https://golang.org/cl/11143043
参考にした情報源リンク
- Mach-O File Format Reference (Apple Developer Documentation): https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/MachORuntime/Reference/reference.html
- Go Source Code:
src/cmd/ld/ldmacho.c
(Go project on GitHub) - Go Linker Documentation (Go Wiki): https://go.dev/doc/articles/go_command (General Go command documentation, linker specifics are often found within the source or related design documents)
- Understanding the Go linker: https://go.dev/blog/go1.2 (Go 1.2 release notes, which might touch upon linker improvements around that time)
- Symbol Table (Wikipedia): https://en.wikipedia.org/wiki/Symbol_table
- Executable and Linkable Format (ELF) (for comparison): https://en.wikipedia.org/wiki/Executable_and_Linkable_Format