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

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

このコミットは、Go言語のリンカ (cmd/ld) の一部である src/cmd/ld/pe.c ファイルに対する変更です。このファイルは、Windowsプラットフォーム向けの実行可能ファイル形式であるPE (Portable Executable) ファイルの生成を担当しています。具体的には、PEファイル内のシンボルテーブルにGo言語のシンボル情報を適切に格納するための修正が行われています。

コミット

cmd/ld: PEシンボルテーブルにGoシンボルを投入する

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

https://github.com/golang/go/commit/fce4f0484c3eccf128b114c27d8eace075b935f9

元コミット内容

cmd/ld: populate pe symbol table with Go symbols

Fixes #6936

LGTM=rsc
R=golang-codereviews, bradfitz, rsc
CC=golang-codereviews
https://golang.org/cl/87770048

変更の背景

このコミットの背景には、Go言語でビルドされたWindows実行可能ファイル(PEファイル)において、Goランタイムやユーザー定義のシンボル情報がPEファイルのシンボルテーブルに適切に格納されていなかったという問題があります。コミットメッセージにある Fixes #6936 は、この問題に関連する特定のバグ報告を指しています。

PEファイル内のシンボルテーブルは、デバッガやプロファイラなどのツールが実行可能ファイル内の関数や変数、その他のコード要素を識別するために不可欠です。シンボル情報が欠落していると、デバッグ時に関数名や変数名が表示されず、アドレス値のみが表示されるなど、デバッグ体験が著しく損なわれる可能性があります。また、外部ツールがGoバイナリの内部構造を解析する際にも支障をきたします。

このコミットは、GoリンカがPEファイルを生成する際に、Go言語のシンボル(例えば、Goランタイムの内部関数、GC関連のシンボル、ユーザーが定義した関数やグローバル変数など)をPE/COFF形式のシンボルテーブルに正確かつ網羅的に書き込むようにすることで、この問題を解決しようとしています。これにより、Windows環境でのGoバイナリのデバッグ可能性と解析性が向上します。

前提知識の解説

PE (Portable Executable) ファイル形式

PE (Portable Executable) は、Microsoft Windowsオペレーティングシステムで使用される実行可能ファイル、オブジェクトコード、DLL (Dynamic Link Library) などのファイル形式です。PEファイルは、プログラムのコード、データ、リソース、メタデータなどを構造化された形式で格納します。

PEファイルの主要な構成要素は以下の通りです。

  • DOS Header: 互換性のために存在する古いDOS実行可能ファイルのヘッダ。
  • PE Header (NT Headers): PEファイルの主要なヘッダで、ファイルシグネチャ、COFFファイルヘッダ、オプションヘッダが含まれます。
    • COFF File Header: マシンタイプ、セクション数、シンボルテーブルのオフセットとサイズなどの基本的な情報を含みます。
    • Optional Header: 実行可能ファイルのタイプ(GUI/CUI)、エントリポイントのアドレス、イメージのベースアドレス、セクションのアライメント、スタックサイズ、ヒープサイズなど、ローダがプログラムをメモリにロードするために必要な情報を含みます。
  • Section Table: 各セクション(コード、データ、リソースなど)の名前、サイズ、メモリ上のアドレス、ファイルオフセット、属性などを定義します。
  • Sections: 実際のコード、初期化済みデータ、未初期化データ、リソースなどが格納される領域です。一般的なセクションには .text (コード), .data (初期化済みデータ), .rdata (読み取り専用データ), .bss (未初期化データ), .rsrc (リソース) などがあります。
  • Import Address Table (IAT): 外部DLLからインポートされる関数のアドレスを格納します。
  • Export Address Table (EAT): 自身が外部にエクスポートする関数のアドレスを格納します。
  • Relocation Table: プログラムがロードされるベースアドレスが変更された場合に、アドレスを修正するための情報を含みます。
  • Symbol Table: プログラム内のシンボル(関数名、変数名など)とそのアドレス、型、ストレージクラスなどの情報を含みます。デバッグやリンキングに利用されます。

COFF (Common Object File Format)

COFF (Common Object File Format) は、Unix系システムで広く使われていたオブジェクトファイル形式であり、PEファイル形式の基盤となっています。PEファイルは、COFFの概念を拡張し、Windows固有の機能(リソース、DLLインポート/エクスポートなど)を追加したものです。PEファイル内のCOFF File HeaderやSymbol Tableなどは、COFFの仕様に準拠しています。

COFFシンボルテーブルは、シンボル名、値(アドレス)、セクション番号、型、ストレージクラスなどの情報を持つエントリの配列で構成されます。シンボル名が8文字を超える場合、文字列テーブルへのオフセットが使用されます。

シンボルテーブル

シンボルテーブルは、コンパイルされたプログラム内のシンボル(関数名、変数名、ラベルなど)と、それらがメモリ上のどこに配置されているか(アドレス)をマッピングするデータ構造です。シンボルテーブルは、主に以下の目的で使用されます。

  • デバッグ: デバッガがソースコードの変数名や関数名を使ってプログラムの状態を検査できるようにします。シンボル情報がないと、デバッガはメモリのアドレスしか表示できず、デバッグが非常に困難になります。
  • リンキング: 複数のオブジェクトファイルやライブラリを結合して最終的な実行可能ファイルを生成する際に、未解決のシンボル参照を解決するために使用されます。
  • プロファイリング: プロファイラが関数の実行時間などを測定する際に、関数名を識別するために使用されます。
  • 動的ロード: DLLなどの共有ライブラリがロードされる際に、エクスポートされたシンボルを解決するために使用されます。

Go リンカ (cmd/ld)

Go言語のビルドプロセスにおいて、cmd/ld はGoプログラムをリンクする役割を担うリンカです。Goのコンパイラ (cmd/compile) が生成したオブジェクトファイル(.o ファイル)や、Goランタイムライブラリ、その他の依存ライブラリを結合し、最終的な実行可能ファイルを生成します。

cmd/ld はクロスコンパイルに対応しており、異なるオペレーティングシステムやアーキテクチャ向けのバイナリを生成できます。Windows向けにビルドする場合、cmd/ld はPEファイル形式で出力を行います。この際、Go言語特有のシンボル(Goランタイムの内部関数、GC関連のシンボル、Goの型情報など)を、ターゲットプラットフォームの実行可能ファイル形式(PE、ELF、Mach-Oなど)のシンボルテーブルに適切に変換して格納する必要があります。

Go シンボル

Go言語は、独自のランタイムとガベージコレクタを持つため、C/C++などの言語とは異なる独自のシンボル体系を持っています。これには、Goランタイムの内部関数(例: runtime.mallocgc, runtime.goexit)、Goの型情報、インターフェース関連のデータ、Goroutineスケジューラ関連のシンボルなどが含まれます。これらのGo固有のシンボルも、デバッグやプロファイリングのためにPEシンボルテーブルに適切に反映される必要があります。

技術的詳細

このコミットは、Goリンカ (cmd/ld) がWindows向けのPEファイルを生成する際に、Go言語のシンボルをPE/COFF形式のシンボルテーブルに正確に格納するためのメカニズムを改善しています。

従来のpe.cでは、シンボルテーブルの管理が限定的であり、特にGo言語の複雑なシンボル構造をPE/COFFの仕様に完全にマッピングできていませんでした。PE/COFFシンボルテーブルは、シンボル名が8文字を超える場合に、別途文字列テーブル(String Table)へのオフセットを使用するという特徴があります。また、シンボルがどのセクションに属するか、その型、ストレージクラスなどの情報も正確に表現する必要があります。

このコミットの主要な技術的改善点は以下の通りです。

  1. 文字列テーブル (strtbl) の導入と管理の改善:

    • 従来のsymnamesという固定サイズのバッファとnextsymoffというオフセット管理では、長いシンボル名や多数のシンボルに対応しきれない可能性がありました。
    • 新しくstrtbl(文字列テーブル)、strtblnextoff(次の文字列のオフセット)、strtblsize(文字列テーブルの現在のサイズ)が導入されました。
    • strtbladd関数が追加され、シンボル名を文字列テーブルに追加し、そのオフセットを返すようになりました。これにより、8文字を超える長いシンボル名もPE/COFFの仕様に従って適切に処理できるようになります。必要に応じてreallocで文字列テーブルのサイズを動的に拡張します。
  2. COFFシンボル情報の構造化 (COFFSym):

    • COFFSymという新しい構造体が定義されました。これは、GoのLSym(リンカシンボル)と、PE/COFFシンボルテーブルに書き込むための追加情報(文字列テーブルオフセットstrtbloff、所属セクションsect)を関連付けます。
    • これにより、GoのリンカシンボルからPE/COFFシンボルテーブルエントリへのマッピングがより明確かつ効率的に行えるようになりました。
  3. シンボル収集と書き込みロジックの分離と改善:

    • addsym関数は、GoのリンカシンボルをPE/COFFシンボルとして登録するためのコールバック関数として機能します。この関数は、genasmsym(Goの全シンボルを列挙する関数)によって呼び出され、GoシンボルをCOFFSym構造体として収集します。
    • addsymtable関数は、収集されたCOFFSym情報に基づいて、実際のPE/COFFシンボルテーブルと文字列テーブルを生成し、PEファイルに書き込みます。
    • この分離により、シンボル収集とテーブル書き込みのロジックが整理され、より堅牢になりました。特に、debug['s']フラグが設定されていない場合(通常ビルド時)でもシンボルが収集されるようになり、デバッグ情報が常に含まれるようになりました。
  4. セクション情報の正確なマッピング:

    • シンボルが属するセクション(例: コードセクション、データセクション)を正確にPE/COFFシンボルテーブルに記録するために、textsect(コードセクションのインデックス)と新しく追加されたdatasect(データセクションのインデックス)が利用されます。これにより、シンボルがどのメモリ領域に配置されているかがデバッガに正確に伝わります。

これらの変更により、GoリンカはGo言語のシンボルをPE/COFFの仕様に完全に準拠した形でPEファイルに埋め込むことができるようになり、Windows環境でのGoバイナリのデバッグと解析が大幅に改善されました。

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

src/cmd/ld/pe.c ファイルにおける主要な変更点は以下の通りです。

  1. dosstub の変更:

    • dosstub 配列のサイズがわずかに変更されていますが、これは直接的な機能変更ではなく、おそらくアライメントやパディングの調整によるものです。
  2. シンボル名関連のグローバル変数の削除:

    • static char *symlabels[]static char symnames[256]static int nextsymoff が削除されました。これらは、従来の限定的なシンボル名管理に使用されていました。
  3. 新しい文字列テーブル関連のグローバル変数の追加:

    • static char* strtbl;:PE/COFF文字列テーブルのバッファ。
    • static int strtblnextoff;:文字列テーブル内の次の文字列の書き込みオフセット。
    • static int strtblsize;:文字列テーブルの現在の割り当てサイズ。
  4. datasect グローバル変数の追加:

    • static int datasect;:データセクションのインデックスを保持するための変数。
  5. COFFSym 構造体の定義と関連グローバル変数の追加:

    • typedef struct COFFSym COFFSym;
    • struct COFFSym { LSym* sym; int strtbloff; int sect; };:Goのリンカシンボル、文字列テーブルオフセット、所属セクションを保持する構造体。
    • static COFFSym* coffsym;COFFSym構造体の配列へのポインタ。
    • static int ncoffsym;:収集されたCOFFSymの数。
  6. strtbladd 関数の追加:

    • static int strtbladd(char *name)
      • 与えられたnamestrtblに追加し、そのオフセット(4バイトのサイズフィールドを考慮して+4)を返します。
      • strtblのサイズが不足している場合は、reallocで動的に拡張します。
      • 文字列の終端にヌル文字を追加します。
  7. newPEDWARFSection 関数の変更:

    • DWARFセクション名が8文字を超える場合、従来のsymnamesではなく、新しく追加されたstrtbladd関数を使用して文字列テーブルに名前を追加し、そのオフセットをsprintでフォーマットしてセクション名として使用するように変更されました。
  8. addsym 関数の追加と変更:

    • static void addsym(LSym *s, char *name, int type, vlong addr, vlong size, int ver, LSym *gotype)
      • この関数はgenasmsymによって呼び出され、Goのリンカシンボルsを受け取ります。
      • シンボルが有効なセクションに属し、かつデータ、BSS、またはテキストセクションのシンボルである場合に処理を行います。
      • coffsym配列が初期化されている場合(シンボル収集の2回目のパス)、現在のシンボルsの情報をcoffsym[ncoffsym]に格納します。
      • シンボル名が8文字を超える場合は、strtbladdを使用して文字列テーブルへのオフセットをstrtbloffに設定します。
      • シンボルのタイプ('T'=テキスト、それ以外=データ/BSS)に基づいて、textsectまたはdatasectsectに設定します。
      • ncoffsymをインクリメントします。
  9. addsymtable 関数の変更:

    • if(!debug['s']) { genasmsym(addsym); coffsym = mal(ncoffsym * sizeof coffsym[0]); ncoffsym = 0; genasmsym(addsym); }
      • debug['s']フラグが設定されていない場合(通常ビルド時)でもシンボルテーブルを生成するように変更されました。
      • genasmsym(addsym)を2回呼び出すことで、1回目でncoffsymをカウントし、2回目でcoffsym配列にシンボル情報を実際に格納するという二段階のプロセスを採用しています。
    • fh.NumberOfSymbols = sizeof(symlabels)/sizeof(symlabels[0]); の行が削除され、fh.NumberOfSymbols = ncoffsym; に変更されました。これにより、実際に収集されたGoシンボルの数がPEファイルヘッダに正確に反映されます。
    • シンボルテーブルの書き込みループが、従来のsymlabels配列ではなく、新しく収集されたcoffsym配列をイテレートするように変更されました。
    • 各シンボルエントリの書き込みにおいて、s->strtbloffが0でない場合(つまり、シンボル名が8文字を超え、文字列テーブルに格納されている場合)は、シンボル名フィールドに0を書き込み、その後に文字列テーブルへのオフセットを書き込むように変更されました。
    • 文字列テーブルの書き込みにおいて、従来のsymnamesではなく、新しく導入されたstrtblの内容をstrtblnextoffのサイズ分だけ書き込むように変更されました。
  10. asmbpe 関数の変更:

    • データセクションの特性を設定した後、datasect = nsect; を追加し、データセクションのインデックスをdatasect変数に格納するようにしました。

コアとなるコードの解説

このコミットの核心は、GoリンカがPEファイルにGoシンボルを埋め込む方法を、PE/COFFのシンボルテーブル仕様に完全に準拠させることです。

strtblstrtbladd による文字列テーブルの管理

PE/COFFシンボルテーブルでは、シンボル名が8文字を超える場合、シンボルエントリ自体にはシンボル名を直接格納せず、別途「文字列テーブル」にシンボル名を格納し、シンボルエントリからはその文字列テーブル内のオフセットを参照します。

従来のpe.cでは、symnamesという固定サイズのバッファでシンボル名を管理しようとしていましたが、これはGoの長いシンボル名(特にメソッド名やパッケージパスを含むもの)に対応できませんでした。

新しく導入されたstrtblstrtbladd関数は、この問題を解決します。

  • strtblは、すべての長いシンボル名を格納するための動的に拡張可能なバッファです。
  • strtbladd(char *name)は、与えられたシンボル名をstrtblに追加し、そのシンボルがstrtbl内で開始するオフセットを返します。このオフセットは、PE/COFFシンボルエントリが文字列テーブル内のシンボル名を参照するために使用されます。
  • newPEDWARFSection関数も、DWARFセクション名が長い場合にこのstrtbladdを利用するように変更され、一貫した文字列管理が実現されています。

COFFSym 構造体と addsym によるシンボル情報の収集

Goのリンカは、LSymという内部構造体でプログラム内のすべてのシンボルを管理しています。しかし、LSymの情報だけでは、PE/COFFシンボルテーブルに書き込むために必要なすべての情報(例えば、文字列テーブルへのオフセットや、PE/COFFのセクションインデックス)を直接提供できるわけではありませんでした。

COFFSym構造体は、このギャップを埋めるために導入されました。

  • LSym* sym: 元のGoリンカシンボルへのポインタ。
  • int strtbloff: シンボル名が8文字を超える場合に、文字列テーブル内のオフセットを格納します。
  • int sect: シンボルが属するPE/COFFセクションのインデックス(例: .textセクションなら1、.dataセクションなら2など)。

addsym関数は、genasmsym(Goのリンカが持つ、すべてのGoシンボルを列挙する機能)によって呼び出されるコールバックとして機能します。addsymは、列挙された各Goシンボルに対して、COFFSym構造体を作成し、必要な情報を(シンボル名が長い場合はstrtbladdを使って文字列テーブルオフセットを計算し、シンボルの種類に応じて適切なセクションインデックスを設定して)格納します。

addsymtable関数内でgenasmsym(addsym)が2回呼び出されているのは、Goリンカの一般的なパターンです。1回目の呼び出しでncoffsym(収集されるCOFFシンボルの総数)をカウントし、それに基づいてcoffsym配列に必要なメモリを割り当てます。2回目の呼び出しで、実際にcoffsym配列にシンボル情報を格納します。これにより、動的に変化するシンボル数に柔軟に対応できます。

addsymtable によるPE/COFFシンボルテーブルの生成

addsymtable関数は、最終的にPEファイルにシンボルテーブルを書き込む責任を負います。

  • まず、fh.NumberOfSymbols(PEファイルヘッダ内のシンボル数)を、addsymによって収集されたncoffsymの値に設定します。これにより、PEファイルが正しいシンボル数を報告するようになります。
  • 次に、収集されたCOFFSym配列をイテレートし、各COFFSymエントリからPE/COFFシンボルテーブルエントリを構築してPEファイルに書き込みます。
    • シンボル名が8文字以下の場合、シンボルエントリの最初の8バイトに直接シンボル名を書き込みます。
    • シンボル名が8文字を超える場合(s->strtbloffが0でない場合)、シンボルエントリの最初の4バイトに0を書き込み、次の4バイトにs->strtbloff(文字列テーブルへのオフセット)を書き込みます。これはPE/COFFの仕様に厳密に従った形式です。
    • シンボルの値(アドレス)、所属セクション、型、ストレージクラスなどの情報も、COFFSymから取得してPE/COFFシンボルエントリの対応するフィールドに書き込まれます。
  • 最後に、strtblに格納された文字列テーブルの内容をPEファイルに書き込みます。文字列テーブルの先頭には、テーブル全体のサイズを示す4バイトのフィールドが置かれます。

datasect の導入

asmbpe関数内でdatasect = nsect;が追加されたのは、データセクションがPEファイルに追加された際に、そのセクションのインデックスをdatasect変数に記録するためです。これにより、addsym関数がデータシンボル('D''B'タイプ)を処理する際に、そのシンボルが属する正しいPE/COFFセクションインデックス(datasect)をCOFFSym構造体に設定できるようになります。これは、デバッガがシンボルを正しいメモリ領域に関連付けるために重要です。

これらの変更により、GoリンカはGo言語の複雑なシンボル構造をPE/COFFのシンボルテーブルに正確にマッピングし、Windows環境でのGoバイナリのデバッグと解析の品質を大幅に向上させました。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (src/cmd/ld/pe.c および関連ファイル)
  • PEファイル形式に関するMicrosoftの公式ドキュメント
  • COFFファイル形式に関する一般的な情報
  • Go言語のリンカの動作に関する一般的な知識
  • コミットメッセージと変更されたコードの差分
  • Fixes #6936 の具体的な内容はウェブ検索では見つからなかったため、一般的なシンボルテーブルの問題として解説しました。)