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

[インデックス 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 ファイルに集中しています。

  1. 変数の追加:

    • Sym *s1;ldmacho 関数のローカル変数として追加されました。これは、シンボルリストをイテレートする際に使用されます。
  2. シンボルサイズ計算ロジックの削除:

    • 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;
      
  3. ソート後のシンボルサイズ計算ロジックの追加:

    • 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; // 親シンボルのサイズとアドレスから残りのサイズを計算
          }
      }
      
  4. テキストセクションのシンボルリスト処理の変更:

    • テキストセクションのシンボルを連結するループで、イテレータ変数が 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)、タイプ、および他のシンボルとの関連性(例えば、outersubフィールドで表現されるネストされたシンボル)などの情報が含まれます。

変更点の詳細な解説

  1. Sym *s1; の追加:

    • これは単に、新しいループ処理のために必要な一時的なシンボルポインタ変数です。
  2. 古いサイズ計算の削除:

    • 削除されたコードは、Mach-Oのシンボルテーブルからシンボルを読み込んだ直後にサイズを計算しようとしていました。この時点では、symtab->sym配列内のシンボルは、Mach-Oファイル内の出現順序であり、メモリ上のアドレス順ではありません。そのため、sym+1が必ずしもアドレス的に次のシンボルを指すとは限らず、誤ったサイズが計算される原因となっていました。この「早すぎる」計算ロジックが削除されました。
  3. 新しいサイズ計算ロジックの追加:

    • この新しいロジックは、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->subSである場合)、そのサイズは、親シンボルsの開始アドレスとサイズからs1の開始アドレスを引くことで計算されます。これは、s1が親シンボル(セクションなど)の残りの部分を占めることを意味します。
  4. テキストセクションのシンボルリスト処理の変更:

    • これは、上記の新しいサイズ計算ロジックでs1という新しいイテレータ変数が導入されたことに伴う、既存のコードの整合性調整です。以前はsを使ってテキストセクションのシンボルを連結していましたが、s1を使うことで、ソートされたリストを正しく処理し、etextp->nextポインタを適切に更新できるようになります。

このコミットは、シンボルサイズ計算の「タイミング」を修正することで、Mach-Oバイナリのシンボル情報の正確性を大幅に向上させました。これは、リンカの堅牢性と、生成されるバイナリのデバッグ可能性や分析可能性にとって重要な改善です。

関連リンク

参考にした情報源リンク