リンク編集プロセスにより、1 つまたは複数の入力ファイルから出力ファイルが作成されます。出力ファイルの作成は、リンカーのオプションによって指示され、入力部分は入力ファイルによって提供されます。
ファイルはすべて、「実行可能リンク形式」(ELF) で表現されます。ELF 書式の詳細については、第 7 章オブジェクトファイル形式を参照してください。この概要として、「セクション」と「セグメント」という 2 つの ELF 構造を紹介します。
セクションとは、ELF ファイル内で処理できる、分割できない最小単位のことです。セグメントとは、セクションの集合で、exec(2) または実行時リンカー ld.so.1(1) でメモリーイメージに対応付けできる最小単位 (これ以上分割できない単位) です。
ELF セクションには多くのタイプがありますが、リンク編集フェーズに関して次の 2 つのカテゴリに分類されます。
プログラム命令 .text およびその関連データ .data や .bss など、その解釈がアプリケーションに対してだけ意味のある「プログラムデータ」を含むセクション。
.symtab や .strtab に含まれるシンボルテーブル情報や .rela.text などの再配置情報など、「リンク編集情報」を含むセクション。
基本的には、リンカーにより、「プログラムデータセクション」が連結されて出力ファイルになります。「リンク編集情報」セクションは、その他のセクションを修正するためにリンカーによって解釈されます。情報セクションは、後で行われる出力ファイル処理で使用される新しい出力情報セクションの生成にも使用されます。
リンカーの、次のような単純な機能の内訳については、この章で説明します。
すべての提供オプションの検証と整合性チェック。
入力再配置可能オブジェクトの同じ特性を持つセクションを連結することによる出力ファイル内での新しいセクションの形成。これらの連結されたセクションは、次に、出力セグメントへと連結できます。
定義の参照を検証およびまとめるための再配置可能オブジェクトおよび共有オブジェクトのシンボルテーブル情報の処理。出力ファイル内での新しいシンボルテーブルまたはテーブルの生成。
入力再配置可能オブジェクトの再配置情報の処理、および出力ファイルを構成するセクションへのこの情報の適用。さらに、実行時リンカーが使用するために出力再配置セクションも生成されます。
作成されたすべてのセグメントを記述するプログラムヘッダーの生成。
必要に応じた、共有オブジェクトの依存関係やシンボルの結合などの情報を実行時リンカーに提供する、動的リンク情報セクションの生成。
「セクション」と関連する「セクション」を連結して「セグメント」にするといった連結プロセスは、リンカー内のデフォルト情報を使用して実行されます。通常、ほとんどのリンク編集では、リンカーによって提供されるデフォルトの「セクション」と「セグメント」の処理で十分です。ただしこれらのデフォルトは、対応する -mapfile を指定した M オプションを使用して操作できます。第 9 章mapfile のオプションを参照してください。
リンカーは、コマンド行から直接実行することもコンパイラドライバから呼び出すようにすることもできます。次の 2 つの節では、この両方の方法を詳しく説明します。ただし、通常は、コンパイラドライバを使用することをお勧めします。コンパイル環境は、多くの場合、コンパイラドライバだけが認識し、頻繁に変化する複雑な操作の連続によって構成されています。
リンカーを直接的に起動させる場合は、出力を作成するために必要なすべてのオブジェクトファイルとライブラリを提供する必要があります。リンカーは、出力の作成に使用するつもりのオブジェクトモジュールまたはライブラリに関して、仮説を立てることをしません。たとえば、次のコマンドは、入力ファイル test.o のみを使って a.out という名前の動的実行可能ファイルを作成するように、リンカーに命令します。
$ ld test.o |
通常、動的実行可能ファイルには、特殊な起動コードおよび終了処理コードが必要です。このコードは、言語またはオペレーティングシステム固有のもので、通常、コンパイラドライバによって提供されるファイルを通じて提供されます。
また、自分専用の初期設定コードおよび終了コードも指定できます。このコードは、実行時リンカーで正確に認識され、使用できるようにするために、正確にカプセル化およびラベル付けを行う必要があります。このカプセル化とラベル付けも、コンパイラドライバによって提供されたファイルを通じて提供されます。
実行可能ファイルや共有オブジェクトなどの実行時オブジェクトを作成するときは、コンパイラドライバを使ってリンカーを起動する必要があります。リンカーの直接起動をお勧めするのは、-r オプションを使用して、中間再配置可能オブジェクトを作成する場合だけです。
リンカーを利用する一般的な方法は、言語固有のコンパイラドライバを使用する方法です。アプリケーションを構成する入力ファイルとともに、cc(1)、CC(1) などのコンパイラドライバを指定します。すると、コンパイラドライバは、追加ファイルとデフォルトライブラリを追加して、リンク編集を完了させます。これらの追加ファイルは、次のようにコンパイルの呼び出しを拡張することによって参照できます。
$ cc -# -o prog main.o /usr/ccs/bin/ld -dy /opt/COMPILER/crti.o /opt/COMPILER/crt1.o \ /usr/ccs/lib/values-Xt.o -o prog main.o \ -YP,/opt/COMPILER/lib:/usr/ccs/lib:/usr/lib -Qy -lc \ /opt/COMPILER/crtn.o |
この例は、コンパイラドライバによって組み込まれた実際のファイルの例ですが、リンカー起動の表示に使用されるメカニズムによって異なる場合があります。
リンカーは 32 ビットアプリケーションおよび 64 ビットアプリケーションとして提供されています。各リンカーは 32 ビットオブジェクトおよび 64 ビットオブジェクトで動作可能です。ただし、リンク編集に 32 ビットオブジェクトと 64 ビットオブジェクトを混在させることはできません。32 ビットリンカーも 64 ビットオブジェクトを生成できますが、生成されるオブジェクトのサイズは、.bss を除いて、2G バイトに制限されます。
デフォルトでは、コンパイラドライバにより 32 ビットリンカーが実行されます。このリンカーは、コマンド行をチェックして、リンク編集を完了するために 64 ビットリンカーを実行すべきかどうかを判別します。
32 ビットのリンク編集と 64 ビットのリンク編集を区別する際、通常はコマンド行オプションは必要ありません。リンカーの操作モードは、コマンド行の最初の入力再配置可能オブジェクトの ELF クラスによって制御されます。mapfile またはアーカイブライブラリからだけ行われるリンクなどの特別なリンク編集は、コマンド行オブジェクトの影響を受けません。これらのリンク編集はデフォルトで 32 ビットモードなので、64 ビットリンク編集を行うときはコマンド行オプションを指定する必要があります。
64 ビットリンカーは、次のいずれかの条件の下で実行されます。
きわめて大規模な 32 ビットオブジェクトを作成すると、32 ビットリンカーで使用可能な仮想メモリーを使い果たしてしまうことがあります。-z altexec64 オプションを指定することで、対応する 64 ビットリンカーの使用を強制できます。64 ビットリンカーは、32 ビットオブジェクトの構築に、より大きな仮想アドレス空間を提供します。
LD_ALTEXEC 環境変数を使用して、代替リンカーを指定することもできます。
リンカーに対するオプションの大部分は、コンパイラドライバのコマンド行経由で渡すことができます。コンパイラオプションとリンカーオプションは、ほとんど重複する部分はありません。重複が発生した場合は、通常、特定のオプションをリンカーに渡すことを許可するコマンド行構文が、コンパイラドライバによって提供されます。また、LD_OPTIONS 環境変数を設定して、リンカーにオプションを渡すこともできます。
$ LD_OPTIONS="-R /home/me/libs -L /home/me/libs" cc -o prog main.c -lfoo |
-R オプションと -L オプションは、リンカーによって解釈されます。これらのオプションは、コンパイラドライバから受け取るコマンド行オプションより優先されます。
リンカーは、オプションリスト全体を構文解析し、無効なオプションまたは関連する引数が無効であるオプションを調べます。どちらかの無効なオプションが検索された場合は、該当するエラーメッセージが生成されます。致命的なエラーの場合は、リンカーは強制終了します。次の例では、リンカーの検査により、不当なオプション -X と -z オプションに不当な引数が検出されています。
$ ld -X -z sillydefs main.o ld: illegal option -- X ld: fatal: option -z has illegal argument `sillydefs' |
1 つの引数を必要とするオプションが 2 回指定された場合、リンカーは適切な警告を生成したあと、リンク編集を継続します。
$ ld -e foo ...... -e bar main.o ld: warning: option -e appears more than once, first setting taken |
また、リンカーはオプションリストの検査も行なって重大な不一致も検出します。
$ ld -dy -a main.o ld: fatal: option -dy and -a are incompatible |
すべてのオプションを処理しても重大なエラー状態が検出されなかった場合は、リンカーは次に入力ファイルの処理を行います。
通常使用されるリンカーオプションについては、付録 A リンカーのクイックリファレンスを参照してください。また、全リンカーオプションの詳細については、ld(1) のマニュアルページを参照してください。
リンカーは、入力ファイルをコマンド行上に表示された順番に読み取ります。各ファイルは、開かれ、そのファイルの ELF タイプを判別するために検査され、どのように処理する必要があるかが決定されます。リンク編集に必要な入力に適用するファイルタイプは、リンク編集の結合モード、「静的」または「動的」のいずれかによって決定されます。
「静的」モードでは、リンカーが入力ファイルとして受け入れるのは、再配置可能オプションまたはアーカイブライブラリだけです。「動的」モードでは、リンカーは、共有オブジェクトも受け入れます。
再配置可能オブジェクトは、リンク編集プロセスへのもっとも基本的な入力ファイルタイプを示しています。これらのファイル内の「プログラムデータ」のセクションは、生成される出力ファイルイメージに連結されます。「リンク編集情報」のセクションは、後で使用するために整理されます。新しい情報セクションが生成され、取って代わられるので、情報セクションは出力ファイルイメージの一部にはなりません。シンボルは、内部シンボルテーブルに集められ、検査および解決されます。このテーブルを使用して、出力イメージ内に 1 つ以上のシンボルテーブルが作成されます。
入力ファイルはリンク編集コマンド行に直接指定できますが、アーカイブライブラリと共有オブジェクトは通常、-l オプションを使って指定します。「追加ライブラリとのリンク」を参照してください。リンク編集時のアーカイブライブラリと共有オブジェクトの解釈は、かなり違います。次の 2 つの項で、この違いについて説明します。
アーカイブは、ar(1) を使用して構築されます。アーカイブは通常、アーカイブシンボルテーブルを持つ再配置可能オブジェクトの集合で構成されます。このシンボルテーブルにより、これらの定義の提供するオブジェクトとシンボル定義との関係がわかります。デフォルトでは、リンカーを使用すると、アーカイブメンバーを選択して抽出できます。リンカーは、未解決のシンボル参照を使用して、アーカイブから結合プロセスの完了に必要なオブジェクトを選択します。1 つのアーカイブのすべてのメンバーを明示的に抽出することもできます。
リンカーがアーカイブから再配置可能オブジェクトを抽出するのは、次のような場合です。
アーカイブに、現在リンカーの内部シンボルテーブル内に保持されている、シンボル参照を満たすシンボル定義が入っている場合。この参照は、「未定義」シンボルと呼ばれる場合もあります。
アーカイブに、現在リンカーの内部シンボルテーブル内に保持されている、未確認シンボル定義を満たすデータシンボル定義が入っている場合。この例としては、FORTRAN COMMON ブロック定義があります。この定義により、同じ DATA シンボルを定義する再配置可能オブジェクトが抽出されます。
アーカイブのメンバーに、隠された可視性または保護された可視性を必要とする参照に一致するシンボル定義が含まれる場合。表 7–20 を参照してください。
リンカーの -z allextract が実行された場合。このオプションにより、選択式のアーカイブ抽出は中止され、処理中のアーカイブからアーカイブメンバーがすべて抽出されます。
選択式アーカイブ抽出において、ウィークシンボル参照では、-z weakextract オプションが有効になっていないかぎり、アーカイブからのオブジェクト抽出は実行されません。詳細は、「単純な解決」を参照してください。
オプション -z weakextract、-z allextract、および -z defaultextract を使用すると、複数のアーカイブ間でアーカイブ抽出メカニズムを切り替えることができます。
選択的なアーカイブ抽出によって、リンカーは 1 つのアーカイブで複数のパスを作成します。必要に応じて、リンカー内部のシンボルテーブルに累積されているシンボル情報を満たすために、再配置可能オブジェクトが抽出されます。リンカーが、再配置可能オブジェクトを抽出せずに、アーカイブを通るフルパスを作成すると、次の入力ファイルが処理されます。
アーカイブが検出されたときに必要な再配置可能オブジェクトだけを抽出することから、コマンド行でのアーカイブの位置が重要であることがわかります。「コマンド行上のアーカイブの位置」を参照してください。
リンカーはアーカイブで複数のパスを作成してシンボルを解決しますが、このメカニズムはかなり負担が大きいものです。特に再配置可能オブジェクトのランダムな組織を含む大きなアーカイブでは、負担が大きくなります。この場合は、lorder(1) や tsort(1) などのツールを使用して、アーカイブ内の再配置可能オブジェクトを整理してください。オブジェクトを整理することで、リンカーが実行するパスの数を減らすことができます。
共有オブジェクトは、分割不可能な、1 つまたは複数の入力ファイルの以前の編集によって生成された総体単位です。リンカーが共有オブジェクトを処理すると、共有オブジェクトの全内容は、その結果作成された出力ファイルイメージの論理的な部分になります。この論理的な組み込みは、リンク編集プロセスにとって共有オブジェクト内に定義されたすべてのシンボルエントリが利用可能になることを意味しています。
共有オブジェクトのプログラムデータセクションとほとんどのリンク編集情報セクションは、リンカーでは使用されません。これらのセクションは、共有オブジェクトが結合されて実行可能プロセスが生成されるときに、実行時リンカーによって解釈されます。ただし、共有オブジェクトの生成は記憶されます。このオブジェクトが実行時に利用可能にしなければならない依存関係であることを示す情報が、出力ファイルイメージに保存されます。
デフォルトでは、リンク編集の一部として指定された共有オブジェクトはすべて、作成されるオブジェクト内に依存関係として記録されます。この記録は、そのオブジェクトが、共有オブジェクトによって提供された実際の参照シンボルを生成するかどうかに関係なく実行されます。実行時のリンクのオーバーヘッドを最小限に抑えるために、構築中のオブジェクトからのシンボル参照を解決する依存関係だけを指定してください。リンカーのデバッグ機能および -u オプションを指定した ldd(1) を使用して、使用されない依存関係を確認することができます。リンカーの -z ignore オプションは、使用されていない共有オブジェクトの依存関係の記録を抑制するために使用できます。
共有オブジェクトに、ほかの共有オブジェクトに対する依存関係がある場合、この依存関係も処理できます。この処理は、すべてのコマンド行入力ファイルが処理されたあと、シンボル解決プロセスを完了するために実行されます。ただし、生成される出力ファイルイメージ内に、共有オブジェクト名は依存関係として記録されません。
コマンド行での共有オブジェクトの位置は、アーカイブ処理の場合ほど重要ではありませんが、その位置は広範囲に影響を及ぼす可能性があります。同じ名前の複数のシンボルを、再配置可能オブジェクトと共有オブジェクト間や複数の共有オブジェクト間に出現させることができます。「シンボル解決」を参照してください。
リンカーによって処理される共有オブジェクトの順序は、出力ファイルイメージ内に格納された従属情報に保持されます。遅延読み込みがない場合、実行時リンカーは指定された共有オブジェクトを同じ順序で読み込みます。そのため、リンカーと実行時リンカーは、多重に定義された一連のシンボルのうち、1 つのシンボルの最初のエントリを選択します。
多重シンボル定義は、-m オプションを使用して生成されるロードマップ出力で報告されます。
通常、コンパイラドライバによって、適切なライブラリがリンカーに指定されているかどうかが確認されますが、ほとんどの場合、自分独自のライブラリを指定することが必要です。共有オブジェクトとアーカイブは、リンカーに対して必要な入力ファイルの名前を明示的につけることで指定できます。ただし、より一般的で柔軟性が高いのは、リンカーの -l オプションを使用する方法です。
ライブラリの命名規約規約によると、共有オブジェクトは通常、接頭辞 lib と接尾辞 .so によって指定されます。アーカイブは、接頭辞 lib と接尾辞 .a によって指定されます。たとえば、libfoo.so は、コンパイル環境に使用できる「foo」実装の共有オブジェクトバージョンです。libfoo.a は、ライブラリのアーカイブバージョンです。
これらの規則は、リンカーの -l オプションによって認識されます。このオプションは、通常、追加ライブラリをリンク編集に供給する場合に使用します。次の例では、リンカーに libfoo.so を検索するように指示します。リンカーが libfoo.so を検索できない場合は、libfoo.a を検索してから次の検索ディレクトリに移動します。
$ cc -o prog file1.c file2.c -lfoo |
命名規約には、共有オブジェクトのコンパイル環境での使用に関するものと、共有オブジェクトの実行時環境での使用に関するものがあります。コンパイル環境では、単に .so 接尾辞を使用するのに対し、実行時環境では、通常、追加のバージョン番号を指定した接尾辞を使用します。「命名規約」 および 「バージョン管理ファイル名の管理」を参照してください。
動的モードでリンク編集を行う場合、共有オブジェクトとアーカイブとを組み合わせたものへのリンクを選択できます。静的モードでリンク編集を行う場合、入力を受け入れるのはアーカイブライブラリだけです。
動的モードで -l オプションを使用する場合、リンカーはまず指定された名前と一致する共有オブジェクトの指定ディレクトリを検索します。一致するものが見つからない場合、リンカーは、次に同じディレクトリ内でアーカイブライブラリを検索します。静的モードで -l オプションを使用する場合は、アーカイブライブラリだけが検索されます。
ライブラリ検索メカニズムにより動的モードで共有オブジェクトを検索する場合、指定したディレクトリがまず検索され、次にアーカイブライブラリが検索されます。検索タイプをより詳細に制御するには、-B オプションを使用します。
コマンド行に -B dynamic オプション、-B static オプションを指定することによって、ライブラリの検索対象をそれぞれ共有オブジェクト、アーカイブに切り替えることができます。たとえば、アプリケーションをアーカイブ libfoo.a と共有オブジェクト libbar.so にリンクするには、次のコマンドを発行します。
$ cc -o prog main.o file1.c -Bstatic -lfoo -Bdynamic -lbar |
オプション -B static と -B dynamic は、正確には対称ではありません。-B static を指定すると、リンカーは、次の-B dynamic の発生まで入力として共有オブジェクトを受け入れません。しかし、-B dynamic を指定すると、リンカーは、指定されたディレクトリ内で、最初に共有オブジェクトを検索し、次にアーカイブを検索します。
前述の例の詳しい説明として、リンカーはまず libfoo.a を探します。次にリンカーは libbar.so を探し、見つからない場合に libbar.a を探します。
コマンド行上のアーカイブの位置は、作成される出力ファイルに影響を及ぼします。リンカーはアーカイブを検索して、以前に参照したことのある定義されていない仮の外部参照だけを解決します。この検索が完了し、必要な再配置可能オブジェクトが抽出された後で、リンカーはコマンド行上の次の入力ファイルに移動します。
このためデフォルトでは、コマンド行上で先行するアーカイブを、後続の入力ファイルからの新しい参照の解決に使用することはありません。たとえば、次のコマンドでは、file1.c で得たシンボル参照を解決するためだけに、libfoo.a を検索するように、リンカーに指示しています。libfoo.a アーカイブは、file2.c または file3.c のシンボル参照を解決するためには、使用されません。
$ cc -o prog file1.c -Bstatic -lfoo file2.c file3.c -Bdynamic |
あるアーカイブからのメンバーの抽出をほかのアーカイブからのメンバーの抽出で解決しなければならないというように、アーカイブ間に相互依存関係が存在する場合があります。依存関係が循環している場合は、前方の参照を解決するために、コマンド行上でアーカイブを繰り返し指定する必要があります。
$ cc -o prog .... -lA -lB -lC -lA -lB -lC -lA |
アーカイブの繰り返し指定の決定と管理はやっかいなものです。-z rescan オプションを指定すれば、この処理は簡単になります。すべての入力ファイル処理の後、このオプションはアーカイブリスト全体を再処理します。この処理は、シンボル参照を解決する追加のアーカイブメンバーの位置を特定しようとします。アーカイブ全体を走査しても新しい再配置可能オブジェクトが抽出されないと、アーカイブの再走査は終了します。前述の例は、次のように単純化できます。
$ cc -o prog -z rescan .... -lA -lB -lC |
原則として、コマンド行の最後にアーカイブを指定するのが最善の方法です。ただし、複数の定義が衝突するために必要となる場合は除きます。
これまでの例はすべて、コマンド行に指定されライブラリの検索場所をリンカーが認識していることを前提にしています。デフォルトでは、32 ビットオブジェクトをリンクする場合、リンカーがライブラリを検索するディレクトリとして認識しているのは、3 つの標準的なディレクトリ /usr/ccs/lib、/lib、および /usr/lib だけです。64 ビットオブジェクトをリンクする場合は、2 つの標準的なディレクトリ /lib/64 と /usr/lib/64 だけを使用します。これ以外のディレクトリを検索させたい場合には、リンカーの検索パスに明示的に付加する必要があります。
リンカー検索パスを変更するには、コマンド行オプションを使用する方法と、環境変数を使用する方法があります。
-L オプションを使用すると、ライブラリ検索パスに新しいパス名を追加できます。このオプションは、コマンド行上で遭遇したその地点で、検索パスを変更します。たとえば、次のコマンドは、path1、/usr/ccs/lib、/lib、/usr/lib の順に libfoo を検索します。このコマンドは、path1、path2、/usr/ccs/lib、/lib、/usr/lib の順に libbar を検索します。
$ cc -o prog main.o -Lpath1 file1.c -lfoo file2.c -Lpath2 -lbar |
-L オプションを使用して定義されたパス名は、リンカー専用です。これらのパス名は、作成される出力ファイルイメージには記録されません。したがって、実行時リンカーはこれらのパス名を使用できません。
カレントディレクトリ内のライブラリの検索にリンカーを使用する場合は、-L を指定する必要があります。ピリオド (.) で現在のディレクトリを表すことができます。
-Y オプションを使用すると、リンカーが検索するデフォルトのディレクトリを変更できます。このオプションに指定する引数は、ディレクトリのリストをコロンで区切った書式で示します。たとえば、次のコマンドは、ディレクトリ /opt/COMPILER/lib と /home/me/lib 内だけを調べて libfoo を検索します。
$ cc -o prog main.c -YP,/opt/COMPILER/lib:/home/me/lib -lfoo |
-Y オプションを使用して指定したディレクトリは、-L オプションを使用して補足できます。多くの場合、コンパイラドライバには、コンパイラ固有の検索パスを指定するための -Y オプションがあります。
環境変数 LD_LIBRARY_PATH を使用しても、リンカーのライブラリ検索パスに付加できます。一般に、LD_LIBRARY_PATH には、コロンで区切られたディレクトリリストをとります。LD_LIBRARY_PATH のもっとも一般的な書式は、セミコロンで区切られた 2 つのディレクトリリストです。これらのリストは、コマンド行で提供される -Y リストの前後に検索されます。
ここでは、LD_LIBRARY_PATH の設定と、いくつかの -L オプションを指定したリンカーの呼び出しを組み合わせています。
$ LD_LIBRARY_PATH=dir1:dir2;dir3 $ export LD_LIBRARY_PATH $ cc -o prog main.c -Lpath1 ... -Lpath2 ... -Lpathn -lfoo |
有効な検索パスは、dir1:dir2:path1:path2... pathn:dir3:/usr/ccs/lib:/lib:/usr/lib です。
LD_LIBRARY_PATH 定義の一部にセミコロンが指定されてない場合は、指定されたディレクトリリストは、-L オプションの後で解釈されます。次の例で有効な検索パスは、path1:path2... pathn:dir1:dir2:/usr/ccs/lib:/lib:/usr/lib です。
$ LD_LIBRARY_PATH=dir1:dir2 $ export LD_LIBRARY_PATH $ cc -o prog main.c -Lpath1 ... -Lpath2 ... -Lpathn -lfoo |
この環境変数は、実行時リンカーの検索パスを増強する場合にも使用できます。「実行時リンカーが検索するディレクトリ」を参照してください。この環境変数がリンカーに影響しないようにするには、-i オプションを使用します。
実行時リンカーは、デフォルトでは 2 つの場所で依存関係を検索します。32 ビットオブジェクトを処理する場合、デフォルトでは /lib と /usr/lib が検索されます。64 ビットオブジェクトを処理する場合、デフォルトでは /lib/64 と /usr/lib/64 が検索されます。このほかのディレクトリを検索する場合は、実行時リンカーの検索パスに明示的に追加する必要があります。
動的実行可能ファイルまたは共有オブジェクトが別の共有オブジェクトとリンクされるとき、これらの共有オブジェクトは依存関係として記録されます。このような依存関係は、プロセスの実行中に実行時リンカーによって再配置される必要があります。動的なオブジェクトをリンクする場合は、出力ファイルに 1 つ以上の検索パスを記録できます。この検索パスは、「実行パス」と呼ばれます。実行時リンカーは、オブジェクトの実行パスを使用して、オブジェクトの依存関係を特定します。
-z nodefaultlib オプションを使用すると、実行時にデフォルトの場所を検索しない特別なオブジェクトを作成できます。このオプションを使用すると、オブジェクトのすべての依存関係はその「実行パス」を使用して検索されます。このオプションがないと、実行時リンカーの検索パスをどのように拡張しても、最後に使用された検索パスが常にデフォルトの場所になります。
デフォルトの検索パスは、実行時構成ファイルを使って管理できます。「デフォルトの検索パスの設定」を参照してください。ただし、動的オブジェクトの作成者はこのファイルの存在に依存するべきではありません。常に、「実行パス」またはデフォルトの場所だけでオブジェクトの依存関係を検索できるようにしてください。
コロンで区切られたディレクトリリストを指定する -R オプションを使用すると、動的実行可能ファイルまたは共有オブジェクト内に「実行パス」を記録できます。次の例では、動的実行可能ファイル prog 内に「実行パス」/home/me/lib:/home/you/lib が記録されます。
$ cc -o prog main.c -R/home/me/lib:/home/you/lib -Lpath1 \ -Lpath2 file1.c file2.c -lfoo -lbar |
共有オブジェクトの依存関係を取得するとき、実行時リンカーはまず上記パスを検索してから、デフォルトの場所を検索します。この場合、この「実行パス」は、libfoo.so.1 と libbar.so.1 の検索に使用されます。
リンカーには複数の -R オプションを指定できます。複数指定された場合は、コロンで区切って連結されます。したがって、上記の例は次のように示すこともできます。
$ cc -o prog main.c -R/home/me/lib -Lpath1 -R/home/you/lib \ -Lpath2 file1.c file2.c -lfoo -lbar |
さまざまな場所にインストールされる可能性のあるオブジェクトについては、$ORIGIN 動的ストリングトークンを使用して、柔軟に「実行パス」を記録できます。「関連する依存関係の配置」を参照してください。
以前は、-R オプションの指定に代わる方法として、環境変数 LD_RUN_PATH を設定し、それをリンカーが使用できるようにする方法がありました。LD_RUN_PATH および -R の適用範囲と機能はまったく同じですが、この両方を指定した場合は、-R が LD_RUN_PATH より優先されます。
動的オブジェクトは、実行時の初期設定と終了処理のためのコードを提供することができます。動的オブジェクトの初期設定コードは、処理中に動的オブジェクトが読み込まれるたびに、1 回ずつ実行されます。動的オブジェクトの終了コードは、動的オブジェクトが処理から読み取り解除されるか、または処理の終了のたびに 1 回ずつ実行されます。このコードは、関数ポインタの配列、または単一コードブロックのうちいずれか 1 つのセクションタイプで組み込まれます。どちらのセクションタイプも、入力再配置可能オブジェクトの同類のセクションを連結して構築されます。
セクション .preinitarray、.initarray、および .finiarray はそれぞれ、実行時の「初期設定前」、初期設定、および終了関数の配列を提供します。動的オブジェクトを作成する際、リンカーはこれらの配列を .dynamic タグペアである DT_PREINIT_[ARRAY/ARRAYSZ]、DT_INIT_[ARRAY/ARRAYSZ]、および DT_FINI_[ARRAY/ARRAYSZ] でそれぞれ識別します。これらのタグは関連するセクションを識別して、セクションを実行時リンカーによって呼び出されるようにします。「初期設定前」の配列は、動的実行可能ファイルにのみ適用可能です。
これらの配列に割り当てられる関数は、作成されるオブジェクトから提供する必要があります。
.init と .fini セクションは、それぞれ実行時の初期設定と終了時のコードブロックを提供します。通常コンパイラドライバは、入力ファイルリストの冒頭部分と末尾に付加するファイルを使用して .init と .fini セクションを供給します。コンパイラが提供するこれらのファイルには、再配置可能オブジェクトの .init コードと .fini コードをそれぞれ独立した関数内にカプセル化する効果があります。これらの関数は、予約シンボル名 _init と _fini によりそれぞれ識別されます。動的オブジェクトを作成する際、リンカーはこれらのシンボルを .dynamic タグの DT_INIT と DT_FINI でそれぞれ識別します。これらのタグは関連するセクションを識別して、実行時リンカーによって呼び出されるようにします。
初期設定および終了コードの実行時の詳細は、「初期設定および終了ルーチン」を参照してください。
初期設定と終了関数の登録をリンカーから直接実行するには、-z initarray オプションと -z finiarray オプションを使用します。たとえば、次のコマンドの結果、関数 foo() のアドレスが .initarray 要素に配置され、関数 bar() のアドレスが .finiarray 要素に配置されます。
$ cat main.c #include <stdio.h> void foo() { (void) printf("initializing: foo()\n"); } void bar() { (void) printf("finalizing: bar()\n"); } void main() { (void) printf("main()\n"); } $ cc -o main -zinitarray=foo -zfiniarray=bar main.c $ main initializing: foo() main() finalizing: bar() |
初期設定および終了セクションの作成は、アセンブラを使用して直接実行できます。しかし、ほとんどのコンパイラは、その宣言を単純化するための特別なプリミティブを提供しています。たとえば、上記のコード例は、次に示す #pragma 定義を使用して書き直すことができます。これらの定義の結果、foo() に対する呼び出しが .init セクション内に配置され、bar() に対する呼び出しが .fini セクション内に配置されます。
$ cat main.c #include <stdio.h> #pragma init (foo) #pragma fini (bar) ....... $ cc -o main main.c $ main initializing: foo() main() finalizing: bar() |
初期設定コードと終了コードが複数の再配置可能オブジェクトに分散されると、アーカイブライブラリと共有オブジェクトに組み込まれた場合とで、異なる動作をする可能性があります。アーカイブを使用したアプリケーションのリンク編集は、アーカイブ内の一部オブジェクトしか抽出しない可能性があります。これらのオブジェクトは、アーカイブのメンバー全体に分散されている初期設定と終了コードの一部しか提供しない可能性があります。そして実行時に、コードのこの部分だけが実行されます。同じアプリケーションを共有オブジェクトを使用して構築した場合は、実行時に依存先が読み込まれると、累積された初期設定コードと終了コードのすべてが実行されます。
実行時にプロセス内で初期設定および終了コードをどのような順序で実行すべきかを判断することは、依存関係の分析を伴う複雑な問題を含んでいます。この分析を簡単にするには、初期設定および終了コードの内容を制限します。自己完結型の初期設定および終了コードを単純化すると、実行時の動作が予想可能になります。詳細については、「初期設定と終了の順序」を参照してください。
初期設定コードが、dldump(3C) を使ってメモリーをダンプできる動的オブジェクトとともに組み込まれている場合、データの初期設定だけを別個に行なってください。
入力ファイルの処理中に、入力再配置可能オブジェクトからすべての「ローカル」シンボルが出力ファイルイメージに渡されます。入力再配置可能オブジェクトから渡される大域シンボルはすべて、共有オブジェクト依存関係から渡される大域シンボルとともにリンカーの内部に蓄積されます。
入力ファイルから大域シンボルが供給されると、この内部シンボルテーブル内が検索されます。過去の入力ファイルで、同じ名前のシンボルに遭遇したことがある場合には、シンボル解決プロセスが呼び出されます。この解決プロセスは、再配置可能オブジェクトから渡される 2 つのエントリのうちどちらを保持するかを決定します。この解決プロセスは、共有オブジェクト依存関係への外部参照を確立する方法も決定します。
入力ファイルの処理が完了し、致命的なシンボル解決エラーが発生していない場合、リンカーは未解決のシンボル参照が残っていないかどうかを判断します。未解決のシンボル参照があると、リンク編集は強制終了します。
最後に、リンカーの内部シンボルテーブルが、作成されるイメージのシンボルテーブルに追加されます。
次の項では、シンボル解決と未定義シンボルの処理について詳しく説明します。
シンボル解決は、簡単で直感的に分かるものから、複雑で当惑するようなものまで、すべての範囲を実行します。ほとんどのシンボル解決は、リンカーによって自動的に実行されます。ただし、警告診断を伴う再配置や、致命的なエラー状態の原因となる再配置もあります。
もっとも一般的で単純な解決は、あるオブジェクトから別のオブジェクト内部のシンボル定義へのシンボル参照の結合です。この結合は、2 つの再配置可能オブジェクト間、および再配置可能オブジェクトと共有オブジェクト依存関係内で検出された最初の定義の間で発生する場合があります。通常、複雑な解決は、2 つ以上の再配置可能オブジェクトの間で発生します。
2 つのシンボルの解決は、シンボルの属性、シンボルを入手したファイルのタイプおよび生成されるファイルのタイプによって異なります。シンボルの属性についての詳細は、「シンボルテーブルセクション」を参照してください。ただし、次の説明では、次の 3 つのシンボルタイプが特定されます。
「一時的シンボル」– ファイル内で作成されたが、まだサイズが決められていないか、または記憶領域内に割り当てられていないシンボル。このようなシンボルは、初期化されていない C シンボル、または FORTRAN COMMON ブロックとしてファイル内に表示されます。
簡単な形式では、シンボル解決で優先関係が使用されます。この関係では、定義シンボルが一時的シンボルより優先され、一時的シンボルは未定義シンボルより優先されます。
次の C コードの例では、これらのシンボルタイプがどのようにして生成されるかを示しています。未定義シンボルの接頭辞は、u_ です。一時的シンボルの接頭辞は、t_ です。定義シンボルの接頭辞は、d_ です。
$ cat main.c extern int u_bar; extern int u_foo(); int t_bar; int d_bar = 1; int d_foo() { return (u_foo(u_bar, t_bar, d_bar)); } $ cc -o main.o -c main.c $ nm -x main.o [Index] Value Size Type Bind Other Shndx Name ............... [8] |0x00000000|0x00000000|NOTY |GLOB |0x0 |UNDEF |u_foo [9] |0x00000000|0x00000040|FUNC |GLOB |0x0 |2 |d_foo [10] |0x00000004|0x00000004|OBJT |GLOB |0x0 |COMMON |t_bar [11] |0x00000000|0x00000000|NOTY |GLOB |0x0 |UNDEF |u_bar [12] |0x00000000|0x00000004|OBJT |GLOB |0x0 |3 |d_bar |
単純なシンボル解決は、もっとも一般的です。この場合、類似する特徴を持ち、どちらかが優先される 2 つのシンボルが検出されます。このシンボル解決は、リンカーによって自動的に実行されます。たとえば、同じ結合を持つシンボルがあり、1 つのファイルからのシンボル参照が、別のファイルの定義または一時的シンボル定義に結合されているとします。あるいは、あるファイルからの一時的シンボル定義は、ほかのファイルからの定義シンボルの定義に結合されます。この解決は、2 つの再配置可能オブジェクト間、および再配置可能オブジェクトと共有オブジェクト依存関係内で検出された最初の定義の間で発生する場合があります。
解決されるシンボルは、大域結合またはウィーク結合されます。再配置可能オブジェクト内では、ウィーク結合の方が、大域結合よりも優先度が低くなります。異なる結合を伴う再配置可能オブジェクトシンボルは、わずかに変更された基本規則に従って解決されます。
ウィークシンボルは通常、個別にあるいは大域シンボルの別名として、コンパイラによって定義されます。このメカニズムでは、#pragma 定義を使用します。
$ cat main.c #pragma weak bar #pragma weak foo = _foo int bar = 1; int _foo() { return (bar); } $ cc -o main.o -c main.c $ nm -x main.o [Index] Value Size Type Bind Other Shndx Name ............... [7] |0x00000000|0x00000004|OBJT |WEAK |0x0 |3 |bar [8] |0x00000000|0x00000028|FUNC |WEAK |0x0 |2 |foo [9] |0x00000000|0x00000028|FUNC |GLOB |0x0 |2 |_foo |
ウィークの別名 foo に、大域シンボル _foo と同じ属性が割り当てられていることに注意してください。この関係は、リンカーによって保持され、その結果、シンボルには出力イメージ内の同じ値が割り当てられます。シンボル解決においては、ウィーク定義シンボルは、同じ名前の大域定義によって自動的に上書きされます。
単純なシンボル解決のもう 1 つの形式である「割り込み」は、再配置可能オブジェクトと共有オブジェクト間、または複数の共有オブジェクト間で発生します。この場合、シンボルが複数回定義されていれば、再配置可能オブジェクト、または複数の共有オブジェクト間の最初の定義がリンカーによって暗黙のうちに採用されます。再配置可能オブジェクトの定義、または最初の共有オブジェクトの定義は、ほかのすべての定義上に割り込みを行うといわれます。この割り込みを使用して、別の共有オブジェクトが提供する機能を無効にすることができます。再配置可能オブジェクトと共有オブジェクトの間、または複数の共有オブジェクト間で発生する複数回定義されたシンボルは、同一に扱われます。シンボルのウィーク結合や大域結合は、これとは無関係です。最初の定義を解決することにより、シンボルの結合に関係なく、リンカーと実行時リンカーの両方が一貫して動作します。
共有オブジェクト内部で定義されたウィークシンボルを、同じ共有オブジェクトに対するシンボル割り込みと組み合わせることにより、有用なプログラミングテクニックを使用できます。たとえば、標準 C ライブラリは、再定義可能ないくつかのサービスを提供していますが、ANSI C は、システムに必要な一連の標準サービスを定義します。厳格な適合プログラムでは、これらのサービスを置き換えることはできません。
たとえば、関数 fread(3C) は、ANSI C ライブラリの関数です。システム関数 read(2) は、ANSI C ライブラリの関数ではありません。適合する ANSI C プログラムは、read(2) を再定義でき、予測できる方法で fread(3C) を使用できなければなりません。
ここでの問題は、read(2) が、標準 C ライブラリ内に fread(3C) を実装する基盤になることです。このため、read(2) を再定義するプログラムは、fread(3C) の実装を混乱させる可能性があります。この混乱を避けるために、ANSI C では、実装に予約されていない名前を実装に使用できないように定めています。次の #pragma 指令で、このような予約名だけを定義します。この名前を使用して、関数 read(2) の別名を生成します。
#pragma weak read = _read |
こうすることにより、ユーザーは、_read() 関数を使用している fread(3C) の実装を危険にさらすことなく、自分専用の read() 関数を自由に定義できます。
このリンカーでは、標準 C ライブラリの共有オブジェクトまたはアーカイブバージョンのどちらかにリンクしている場合でも、read() を再定義できます。前者の場合には、割り込みによって方法が決められます。後者の場合には、read(2) の C ライブラリの定義をウィークにすることにより、自動的に上書き可能になります。
リンカーの -m オプションを使用して、割り込みされるすべてのシンボル参照のリストを、セクションの読み込みアドレス情報とともに標準出力に書き込んでください。
複雑な解決は、同じ名前を持つ 2 つのシンボルが、異なる属性とともに検出された場合に発生します。これらの場合、リンカーは警告メッセージを生成し、もっとも適切なシンボルを選択します。このメッセージは、シンボル、相反する属性、シンボル定義の元になるファイルの識別情報を示します。次の例では、データ項目の配列の定義が指定された 2 つのファイルで、サイズの必要条件が異なっています。
$ cat foo.c int array[1]; $ cat bar.c int array[2] = { 1, 2 }; $ cc -dn -r -o temp.o foo.c bar.c ld: warning: symbol `array' has differing sizes: (file foo.o value=0x4; file bar.o value=0x8); bar.o definition taken |
シンボルの整列要件が異なっている場合も、同様の診断が生成されます。この 2 つのケースの場合、リンカーの -t オプションを使用すると、診断を抑制できます。
異なる属性のもう 1 つの形式は、シンボルのタイプの違いです。次の例では、シンボル bar() は、データ項目と関数の両方として定義されています。
$ cat foo.c int bar() { return (0); } $ cc -o libfoo.so -G -K pic foo.c $ cat main.c int bar = 1; int main() { return (bar); } $ cc -o main main.c -L. -lfoo ld: warning: symbol `bar' has differing types: (file main.o type=OBJT; file ./libfoo.so type=FUNC); main.o definition taken |
この文脈では、シンボルのタイプは ELF で使用されるタイプです。このシンボルタイプは、単純な形式であることを除けば、プログラミング言語で使用されるデータ型には関連していません。
前の例のような場合、解決が再配置可能オブジェクトと共有オブジェクト間で行われる場合に再配置可能オブジェクトの定義が使用されます。または、2 つの共有オブジェクト間で解決が行われる場合は、最初の定義が使用されます。ウィーク結合または大域結合のシンボル間でこのような解決を行うと、警告も発せられます。
リンカーの -t オプションを使用しても、シンボルタイプ間の不一致は抑制できません。
解決できないシンボルの矛盾は、致命的なエラー状態や該当エラーメッセージの原因となります。このメッセージは、シンボルを提供したファイルの名前とともに、シンボル名を示します。出力ファイルは生成されません。この重大なエラー状態によってリンカーは停止しますが、すべての入力ファイルの処理が、まず最初に完了します。この要領で、重大な解決エラーをすべて識別できます。
もっとも一般的な致命的エラー状態は、2 つの再配置可能オブジェクト両方が、同じ名前のウィーク以外のシンボルを定義した場合に起こります。
$ cat foo.c int bar = 1; $ cat bar.c int bar() { return (0); } $ cc -dn -r -o temp.o foo.c bar.c ld: fatal: symbol `bar' is multiply-defined: (file foo.o and file bar.o); ld: fatal: File processing errors. No output written to int.o |
foo.c と bar.c に含まれるシンボル bar の定義が、互いに矛盾しています。リンカーは、どちらを優先すべきか判別できないため、通常はエラーメッセージを出力して終了します。リンカーの -z muldefs を使用すると、エラー状態を抑制できます。このオプションによって、最初のシンボル定義が使用されます。
すべての入力ファイルを読み取り、シンボル解決がすべて完了すると、リンカーは、シンボル定義に結合されていないシンボル参照の内部シンボルテーブルを検索します。これらのシンボル参照は、未定義シンボルと呼ばれます。未定義シンボルがリンク編集処理に及ぼす影響は、生成される出力ファイルのタイプや、シンボルのタイプによって異なります。
リンカーが実行可能出力ファイルを生成する際のデフォルト動作は、「未定義のままのシンボルが存在するかぎり、適切なエラーメッセージを出力して処理を終了する」というものです。次のように、再配置可能オブジェクト内のシンボル参照が、シンボル定義と絶対に一致しない場合に、シンボルは定義されないままの状態になります。
$ cat main.c extern int foo(); int main() { return (foo()); } $ cc -o prog main.c Undefined first referenced symbol in file foo main.o ld: fatal: Symbol referencing errors. No output written to prog |
同様に、共有オブジェクトを使って動的実行可能ファイルを作成する場合、未解決のままのシンボル定義が存在していると、未定義シンボルエラーが発生します。
$ cat foo.c extern int bar; int foo() { return (bar); } $ cc -o libfoo.so -G -K pic foo.c $ cc -o prog main.c -L. -lfoo Undefined first referenced symbol in file bar ./libfoo.so ld: fatal: Symbol referencing errors. No output written to prog |
前の例のように未定義シンボルを許可するには、リンカーの -z nodefs オプションを使用して、デフォルトエラー条件を抑制します。
-z nodefs オプションを使用する場合は、注意が必要です。処理の実行中に使用できないシンボル参照が要求されると、重大な実行時再配置エラーが発生します。このエラーは、アプリケーションをはじめて実行およびテストした際に検出される場合があります。しかし、実行パスがより複雑であるとエラー状態の検出に時間がかかり、時間とコストが浪費される場合があります。
シンボルは、再配置可能オブジェクト内のシンボル参照が、暗黙の内に定義された共有オブジェクト内のシンボル定義に結合されている場合にも、未定義シンボルのままになる場合があります。たとえば、上記の例で使用したファイル main.c および foo.c に次のように続く場合です。
$ cat bar.c int bar = 1; $ cc -o libbar.so -R. -G -K pic bar.c -L. -lfoo $ ldd libbar.so libfoo.so => ./libfoo.so $ cc -o prog main.c -L. -lbar Undefined first referenced symbol in file foo main.o (symbol belongs to implicit \ dependency ./libfoo.so) ld: fatal: Symbol referencing errors. No output written to prog |
prog は、libbar.so に対する「明示的」な参照に基づいて構築されます。libbar.so は、libfoo.so に依存しています。したがって、libfoo.so への暗黙的参照が prog から確立されます。
main.c は、libfoo.so によって作成されたインタフェースへの特定の参照を実行するため、prog は、実際に libfoo.so に依存性を持つことになります。ただし、生成される出力ファイル内に記録されるのは、明示的な共有オブジェクトの依存関係だけです。そのため、libbar.so の新しいバージョンが開発され、libfoo.so への依存性がなくなった場合、prog は実行に失敗します。
このため、このタイプの結合は致命的とみなされます。暗黙的参照は、prog のリンク編集中に直接ライブラリを参照することで明示的に行います。この例で示した重大なエラーメッセージ内に必要な参照のヒントがあります。
リンカーが共有オブジェクト出力ファイルを生成する場合、未定義シンボルをリンク編集の後も残すことができます。このデフォルト動作により、共有オブジェクトが、依存関係として共有オブジェクトを定義する動的実行可能ファイルからシンボルをインポートできます。
リンカーの -z defs オプションを使用すると、未定義シンボルが残っていた場合に、強制的に重大エラーにすることができます。共有オブジェクトを作成するときには、このオプションの使用をお勧めします。アプリケーションからシンボルを参照する共有オブジェクトは、extern mapfile 指令でシンボルを定義するとともに、-z defs オプションを使用できます。「mapfile を使用した追加シンボルの定義」を参照してください。
自己完結型の共有オブジェクトは、外部シンボルへのすべての参照は指定された依存関係によって満たされ、最大の柔軟性が提供されます。この共有オブジェクトは、共有オブジェクトの必要条件を満たす依存関係を判別し確立する手間をユーザーにかけることなく、多数のユーザーによって使用されます。
生成中の出力ファイルタイプがどのようなタイプであっても、未解決のウィークシンボル参照によって重大なエラー状態は発生しません。
静的実行可能プログラムを生成中の場合は、シンボルは絶対シンボルに変換され、ゼロの値が割り当てられます。
動的実行可能ファイルまたは共有オブジェクトの作成中の場合は、シンボルは定義されていないウィーク参照として残され、値には 0 が割り当てられます。プロセスの実行中に、実行時リンカーがこのシンボルを検索します。一致が検出されない場合、実行時リンカーは重大な実行時再配置エラーを生成する代わりに、その参照がゼロのアドレスに結合されます。
従来は、これらの定義されていないウィーク参照シンボルは、機能の存在をテストするためのメカニズムとして使用されていました。たとえば、次の C コードフラグは、共有オブジェクト libfoo.so.1 内で次のように使用されていました。
#pragma weak foo extern void foo(char *); void bar(char * path) { void (* fptr)(char *); if ((fptr = foo) != 0) (* fptr)(path); } |
libfoo.so.1 を参照するアプリケーションを構築すると、シンボル foo の定義が検出されたかどうかに関係なく、リンク編集は、正常に完了します。アプリケーションの実行中に、機能アドレスがゼロ以外をテストすると、その機能が呼び出されます。ただし、シンボル定義が検出されない場合には、機能アドレスはゼロをテストするため、その機能は呼び出されません。
コンパイルシステムは、定義されないセマンティクスを保持しながら、このアドレスの比較テクニックを参照します。その結果、テストステートメントは最適化処理によって削除されます。また、実行時シンボル結合メカニズムは、このテクニックの使用にほかの制限を課します。これらの制限によって、すべての動的オブジェクトでは一致モデルを利用できなくなります。
このような未定義ウィーク参照は推奨されていません。代わりに dlsym(3C) を RTLD_DEFAULT と使用するか、または RTLD_PROBE ハンドルを使用して、シンボルの有無をテストします。「機能のテスト」を参照してください。
入力ファイルの追加は、通常、その追加の順に出力ファイルに表示されます。ただし、一時的シンボルとそれに関連する記憶領域を処理するときに、例外が発生します。一時的シンボルは、その解決が完了するまで完全に定義されません。解決が再配置可能オブジェクトから定義シンボルに対して行われる場合、シンボルは定義順に表示されます。
シンボルグループの順序を制御する必要がある場合には、一時的定義は、ゼロで初期化されたデータ項目に再定義する必要があります。たとえば、次のような一時的定義をすると、出力ファイル内のデータ項目が、ソースファイル foo.c に記述された元の順序と比較されて再配列されます。
$ cat foo.c char A_array[0x10]; char B_array[0x20]; char C_array[0x30]; $ cc -o prog main.c foo.c $ nm -vx prog | grep array [32] |0x00020754|0x00000010|OBJT |GLOB |0x0 |15 |A_array [34] |0x00020764|0x00000030|OBJT |GLOB |0x0 |15 |C_array [42] |0x00020794|0x00000020|OBJT |GLOB |0x0 |15 |B_array |
これらのシンボルを、初期化されたデータ項目として定義することにより、入力ファイル内のこれらの相対順序が出力ファイル内に引き継がれます。
$ cat foo.c char A_array[0x10] = { 0 }; char B_array[0x20] = { 0 }; char C_array[0x30] = { 0 }; $ cc -o prog main.c foo.c $ nm -vx prog | grep array [32] |0x000206bc|0x00000010|OBJT |GLOB |0x0 |12 |A_array [42] |0x000206cc|0x00000020|OBJT |GLOB |0x0 |12 |B_array [34] |0x000206ec|0x00000030|OBJT |GLOB |0x0 |12 |C_array |
入力ファイルから提供されるシンボルのほかに、追加の大域シンボル参照や大域シンボル定義をリンク編集に対して提供することができます。もっとも簡単な形式で、シンボル参照は、リンカーの -u オプションを使用して作成できます。リンカーの -M オプションと関連 mapfile を使用すると柔軟性が高まります。この mapfile を使用すると、大域シンボル参照およびさまざまな大域シンボル定義を定義できます。
-u オプションを指定すると、リンク編集コマンド行から大域シンボル参照を作成するためのメカニズムが使用できます。このオプションを使用して、リンク編集を完全にアーカイブから実行することができます。このオプションは、複数のアーカイブから抽出するオブジェクトを選択する際の柔軟性も高めます。アーカイブの抽出については、「アーカイブ処理」を参照してください。
たとえば、動的実行可能プログラムを、シンボル foo と bar への参照を実行する再配置可能オブジェクト main.o から生成するとします。この場合、lib1.a 内に組み込まれた再配置可能オブジェクト foo.o からシンボル定義 foo を入手し、さらに lib2.a 内に組み込まれた再配置可能オブジェクト bar.o からシンボル定義 bar を入手します。
ただし、アーカイブ lib1.aにも、シンボル bar を定義する再配置可能オブジェクトが組み込まれています。この再配置可能オブジェクトは、lib2.a に提供されたものとは機能的に異なると想定します。必要なアーカイブ抽出を指定する場合は、次のようなリンク編集を使用できます。
$ cc -o prog -L. -u foo -l1 main.o -l2 |
-u オプションは、シンボル foo への参照を生成します。この参照によって、再配置可能オブジェクト foo.o がアーカイブ lib1.a から抽出されます。シンボル bar への最初の参照は lib1.a が処理されてから生じる main.o 内で実行されます。このため、再配置可能オブジェクト bar.o はアーカイブ lib2.a から入手されます。
この単純な例では、lib1.a からの 再配置可能オブジェクト foo.o は、シンボル bar の直接的または間接的な参照は行いません。lib1.a が bar を参照する場合、処理中に再配置可能オブジェクト bar.o も lib1.a から抽出されます。アーカイブを処理するリンカーのマルチパスについては、「アーカイブ処理」を参照してください。
広範囲な大域シンボル定義のセットは、リンカーの -M オプションと、関連する mapfile を使用して入手できます。シンボル定義 mapfile のエントリの構文は次のとおりです。
[ name ] { scope: symbol [ = [ type ] [ value ] [ size ] [ information ] ]; } [ dependency ]; |
このシンボル定義のセットのラベルが存在する場合は、それによってイメージ内の「バージョン定義」を識別できます。第 5 章アプリケーションバイナリインタフェースとバージョン管理を参照してください。
生成される出力ファイル内のシンボルのバインディングの可視性を示しています。mapfile で定義されたすべてのシンボルは、リンク編集プロセス中に、scope (スコープ) 内で global (大域) として処理されます。これらのシンボルは、入力ファイルのいずれかから入手された、同じ名前のほかの大域シンボルに対して解決されます。次の定義と別名は、作成されるオブジェクト内におけるシンボルの可視性を定義します。
このスコープの大域シンボルは、すべての外部オブジェクトに対して可視となります。このタイプのシンボルに対するオブジェクト内からの参照は実行時に結合されるため、介入が可能となります。この可視性スコープがデフォルトになりますが、これは、ほかのシンボル可視性テクニックを使って降格または削除することができます。このスコープ定義には、シンボルに STV_DEFAULT 可視性が指定された場合と同じ効果があります。表 7–20 を参照してください。
このスコープの大域シンボルは、すべての外部オブジェクトに対して可視となります。これらのシンボルに対するオブジェクト内からの参照はリンク編集時に結合されるため、実行時の介入は防止されます。この可視性スコープは、ほかのシンボル可視性テクニックを使って降格または削除することができます。このスコープ定義には、シンボルに STV_PROTECTED 可視性が指定された場合と同じ効果があります。表 7–20 を参照してください。
オブジェクトが単一のシンボルスコープを使って定義される場合、リンク編集時に、オブジェクト内のすべての再配置がそのオブジェクトに結合されます。この単一スコープでは、予約済みのシンボルさえもシンボルスコープに縮小されます。予約シンボル名のリストについては、「出力ファイルの生成」を参照してください。
このスコープの大域シンボルは、ローカル結合を持つシンボルに縮小されます。このスコープのシンボルは、ほかの外部オブジェクトから見えません。このスコープ定義には、シンボルに STV_HIDDEN 可視性が指定された場合と同じ効果があります。表 7–20 を参照してください。
このスコープの大域シンボルは hidden です。これらのシンボルテーブルのエントリは削除されます。リンカーの -z redlocsym オプションを使用して、ローカルシンボルを削除することもできます。
STV_ シンボル可視性属性は、コンパイラの処理するソースコードに埋め込まれたシンボル宣言に由来します。
シンボル名。この名前により、修飾属性に応じて、シンボル定義またはシンボル参照が生成されます。修飾属性のないもっとも簡潔な形式で、シンボル参照が作成されます。この参照は、「u オプションを使用した追加シンボルの定義」で説明した -u オプションを使用して生成する参照とまったく同じものです。 通常、このシンボル名に修飾属性が付いている場合には、シンボル定義は、関連する属性を使用して生成されます。
local スコープが定義される場合、このシンボル名を特別な「自動縮小 (auto-reduction)」指令「*」として定義できます。可視性が明示的に定義されていないシンボルは、生成される動的オブジェクト内のローカル結合に降格されます。明示的な可視性の定義は、mapfile 定義、再配置可能オブジェクト内にカプセル化された可視性定義のいずれかに起因します。
同様に、eliminate スコープが定義されている場合、シンボル名を特別な「自動削除 (auto-elimination)」指令「*」として定義できます。可視性が明示的に定義されていないシンボルは、生成される動的オブジェクトから削除されます。
シンボルのタイプ属性を示します。この属性は、COMMON、data、function のいずれかです。 COMMON 属性の結果は、一時的シンボル定義になります。data および function 属性の結果は、セクションシンボル定義または絶対的なシンボル定義になります。「シンボルテーブルセクション」を参照してください。
data 属性の結果として、OBJT シンボルが作成されます。size を伴うが value を伴わない data 属性では、このシンボルを ELF セクションに関連付けることでセクションシンボルが作成されます。このセクションは、ゼロで埋められます。
function 属性の結果として、FUNC シンボルが作成されます。size を伴うが value を伴わない function 属性では、このシンボルを ELF セクションに関連付けることにより、セクションシンボルが作成されます。このセクションには、空の関数戻り値 void (*)(void) が割り当てられます。
data または function 属性とともに value が指定されると、絶対値を表す ABS セクションインデックスを伴う適切なシンボルタイプが生成されます。
セクションデータシンボルの作成は、フィルタの作成時に役立ちます。実行可能ファイルからフィルタのセクションデータシンボルへの外部参照により、生成中のコピーが適切に再配置されます。「コピー再配置」を参照してください。
値の属性を示します。この属性は、V数字の形式をとります。この属性により、シンボル定義が作成されます。
サイズの属性を示します。この属性は、S数字の形式をとります。この属性により、シンボル定義が作成されます。
このキーワードは、シンボルに次の追加情報を提供します。
このシンボルが共有オブジェクト name に対する補助フィルタであることを示します。「補助フィルタの生成」を参照してください。
このシンボルを直接結合する必要があることを示します。このキーワードをシンボル定義で使用すると、参照が、構築中のオブジェクト内から定義に直接結合されます。このキーワードをシンボル参照で使用すると、定義を提供する依存関係に直接結合されます。「直接結合」を参照してください。このキーワードを PARENT キーワードとともに使用して、実行時に任意の親への直接結合を確立することもできます。
シンボルが、作成されるオブジェクトの外部で定義されていることを示します。通常、このキーワードは、コールバックルーチンへのラベル付けで定義されます。このキーワードを使用して、-z defs オプションで示される未定義シンボルを抑制できます。
このキーワードは、シンボル参照を生成する場合にのみ有効です。このシンボルの定義が、リンク編集時に結合されるオブジェクト内部で生成された場合には、暗黙的に無視されます。
このシンボルが共有オブジェクト name のフィルタであることを示します。「標準フィルタの生成」を参照してください。フィルタシンボルは、入力再配置可能オブジェクトから提供される補助実装を必要としません。したがって、シンボルの種類を定義してこの指令を使用し、絶対シンボルテーブルエントリを作成します。
このシンボルを直接結合してはならないことを示します。この状態は、作成されるオブジェクト内からの参照と外部参照に適用されます。「直接結合」を参照してください。このキーワードを PARENT キーワードとともに使用して、実行時に任意の親への直接結合を回避することもできます。
シンボルが作成中のオブジェクトの親で定義されることを示します。親とは、実行時にこのオブジェクトを明示的な依存関係として参照するオブジェクトです。親は、dlopen(3C) を使用して、このオブジェクトを実行時に参照することもできます。通常、このキーワードは、コールバックルーチンへのラベル付けで定義されます。このキーワードを DIRECT または NODIRECT キーワードとともに使用して、親への直接的または間接的な参照を個別に確立することもできます。このキーワードを使用して、-z defs オプションで示される未定義シンボルを抑制できます。
このキーワードは、シンボル参照を生成する場合にのみ有効です。このシンボルの定義が、リンク編集時に結合されるオブジェクト内部で生成された場合には、暗黙的に無視されます。
この定義が継承するバージョン定義を示します。第 5 章アプリケーションバイナリインタフェースとバージョン管理を参照してください。
バージョン定義または自動縮小のいずれかの指令が指定されている場合、バージョン情報が作成されるイメージ内に記録されます。このイメージが実行可能プログラムまたは共有オブジェクトである場合には、シンボル縮小も適用されます。
作成されるイメージが再配置可能オブジェクトである場合は、デフォルトにより、シンボル縮小は適用されません。この場合、シンボル縮小はバージョン情報の一部として記録されます。これらの縮小は、再配置可能オブジェクトが最終的に実行可能ファイルまたは共有オブジェクトの生成に使用されるときに適用されます。リンカーの -B reduce オプションを使用すると、再配置可能オブジェクトを生成するときに、強制的にシンボル縮小を実行できます。
バージョン情報の詳細については、第 5 章アプリケーションバイナリインタフェースとバージョン管理に記載されています。
インタフェース定義を確実に安定させるためには、シンボル名の定義に対しワイルドカードによる拡張を行わないようにします。
次の節では、mapfile 構文を使用した例をいくつか示します。
次の例では、3 つのシンボル参照を定義する方法を示します。これらの参照を使用して、アーカイブのメンバーを抽出します。このアーカイブ抽出は、複数の -u オプションをリンク編集に指定することにより実現できますが、この例では、最終的なシンボルの範囲を、ローカルに縮小する方法も示しています。
$ cat foo.c void foo() { (void) printf("foo: called from lib.a\n"); } $ cat bar.c void bar() { (void) printf("bar: called from lib.a\n"); } $ cat main.c extern void foo(), bar(); void main() { foo(); bar(); } $ ar -rc lib.a foo.o bar.o main.o $ cat mapfile { local: foo; bar; global: main; }; $ cc -o prog -M mapfile lib.a $ prog foo: called from lib.a bar: called from lib.a $ nm -x prog | egrep "main$|foo$|bar$" [28] |0x00010604|0x00000024|FUNC |LOCL |0x0 |7 |foo [30] |0x00010628|0x00000024|FUNC |LOCL |0x0 |7 |bar [49] |0x0001064c|0x00000024|FUNC |GLOB |0x0 |7 |main |
大域からローカルへのシンボル範囲の縮小の重要性については、「シンボル範囲の縮小」で説明しています。
次の例では、2 つの絶対シンボル定義を定義する方法を示します。そして、これらの定義を使用して、入力ファイル main.c からの参照を解決します。
$ cat main.c extern int foo(); extern int bar; void main() { (void) printf("&foo = %x\n", &foo); (void) printf("&bar = %x\n", &bar); } $ cat mapfile { global: foo = FUNCTION V0x400; bar = DATA V0x800; }; $ cc -o prog -M mapfile main.c $ prog &foo = 400 &bar = 800 $ nm -x prog | egrep "foo$|bar$" [37] |0x00000800|0x00000000|OBJT |GLOB |0x0 |ABS |bar [42] |0x00000400|0x00000000|FUNC |GLOB |0x0 |ABS |foo |
入力ファイルから入手される場合、関数のシンボル定義またはデータ項目は、通常、データ記憶域の要素に関連しています。mapfile 定義は、このデータ記憶域を構成するためには不十分であるため、これらのシンボルは、絶対値として残しておく必要があります。size が関連付けられるが、value は関連付けられない単純な mapfile 定義では、データ記憶域が作成されます。この場合、シンボル定義にはセクションインデックスが伴います。ただし、mapfile 定義に value を関連付けると、絶対シンボルが作成されます。シンボルが共有オブジェクト内で定義される場合、絶対定義は避けるようにしてください。「シンボル定義の増強」を参照してください。
mapfile は COMMON または一時的シンボルを定義する場合にも使用できます。ほかのタイプのシンボル定義とは違って、一時的シンボルは、ファイル内の記憶域を占有しませんが、実行時に割り当てる記憶域の定義は行います。そのため、このタイプのシンボル定義は、作成される出力ファイルの記憶域割り当ての一因となります。
一時的シンボルの特徴は、ほかのシンボルタイプとは異なり、その「値」の属性によって、その配列要件が示される点です。そのため、リンク編集の入力ファイルから入手される一時的定義の再配列に mapfile 定義を使用できます。
次の例では、2 つの一時的シンボルの定義を示しています。シンボル foo は、新しい記憶領域を定義しているのに対し、シンボル bar は、実際に、ファイル main.c 内の同じ一時的定義の配列を変更するために使用されます。
$ cat main.c extern int foo; int bar[0x10]; void main() { (void) printf("&foo = %x\n", &foo); (void) printf("&bar = %x\n", &bar); } $ cat mapfile { global: foo = COMMON V0x4 S0x200; bar = COMMON V0x100 S0x40; }; $ cc -o prog -M mapfile main.c ld: warning: symbol `bar' has differing alignments: (file mapfile value=0x100; file main.o value=0x4); largest value applied $ prog &foo = 20940 &bar = 20900 $ nm -x prog | egrep "foo$|bar$" [37] |0x00020900|0x00000040|OBJT |GLOB |0x0 |16 |bar [42] |0x00020940|0x00000200|OBJT |GLOB |0x0 |16 |foo |
このシンボル解決の診断は、リンカーの -t オプションを使用すると表示されません。
共有オブジェクト内での絶対データシンボルの作成は避けるべきです。通常、動的実行可能ファイルから、共有オブジェクト内のデータ項目への外部参照には、コピー再配置の作成が必要になります。「コピー再配置」を参照してください。このような再配置を行う場合は、データ項目をデータ記憶領域と関連付けるべきです。この関連付けは、再配置可能なオブジェクトファイル内にシンボルを定義することで行うことができます。この関連付けは、mapfile 内でシンボルを size 宣言あり、 value 宣言なしで定義しても行うことができます。「mapfile を使用した追加シンボルの定義」を参照してください。
データシンボルにはフィルタを適用できます。「フィルタとしての共有オブジェクト」を参照してください。このようなフィルタ適用を行うため、オブジェクトファイル定義は mapfile 定義で増強できます。次の例では、関数定義とデータ定義を含むフィルタを作成します。
$ cat mapfile { global: foo = FUNCTION FILTER filtee.so.1; bar = DATA S0x4 FILTER filtee.so.1; local: *; }; $ cc -o filter.so.1 -G -Kpic -h filter.so.1 -M mapfile -R. $ nm -x filter.so.1 | egrep "foo|bar" [39] |0x000102b0|0x00000004|OBJT |GLOB |0 |12 |bar [45] |0x00000000|0x00000000|FUNC |GLOB |0 |ABS |foo $ elfdump -y filter.so.1 | egrep "foo|bar" [1] F [0] filtee.so.1 bar [7] F [0] filtee.so.1 foo |
実行時に、外部オブジェクトからこれらのシンボルのいずれかへの参照は、「フィルティー」内の定義に解決されます。
mapfile 内のローカル範囲を持つようにシンボル定義を定義するとシンボルの最終的な結合を縮小できます。このメカニズムによって、入力の一部として生成ファイルを使用する将来のリンク編集でシンボルが表示されなくなります。実際、このメカニズムは、ファイルのインタフェースの厳密な定義をするために提供されているため、ほかのユーザーに対して、機能の使用を制限できます。
たとえば、簡単な共有オブジェクトを、ファイル foo.c と bar.c から生成するとします。ファイル foo.c には、ほかのユーザーも使用できるように設定するサービスを提供する大域シンボル foo が組み込まれています。ファイル bar.c には、共有オブジェクトの根底となるインプリメンテーションを提供するシンボル bar と str が組み込まれています。これらのファイルを使用して共有オブジェクトを作成すると、通常、次のように大域範囲が指定された 3 つのシンボルが作成されます。
$ cat foo.c extern const char * bar(); const char * foo() { return (bar()); } $ cat bar.c const char * str = "returned from bar.c"; const char * bar() { return (str); } $ cc -o libfoo.so.1 -G foo.c bar.c $ nm -x libfoo.so.1 | egrep "foo$|bar$|str$" [29] |0x000104d0|0x00000004|OBJT |GLOB |0x0 |12 |str [32] |0x00000418|0x00000028|FUNC |GLOB |0x0 |6 |bar [33] |0x000003f0|0x00000028|FUNC |GLOB |0x0 |6 |foo |
これで、libfoo.so.1 により提供された機能を、別のアプリケーションのリンク編集の一部として使用できます。シンボル foo への参照は、共有オブジェクトによって提供されたインプリメンテーションに結合されます。
大域結合により、シンボル bar と str への直接参照も可能です。ただし、この可視性は危険な結果を招く場合があります。関数 foo の基礎となるインプリメンテーションは、後から変更することがあるためです。それが原因で知らないうちに、bar または str に結合された既存のアプリケーションが失敗または誤作動を起こす可能性があります。
また、シンボル bar と str を大域結合すると、同じ名前のシンボルによって割り込まれる可能性があります。共有オブジェクト内へのシンボルの割り込みについては、「単純な解決」 で説明しています。この割り込みは、意図的に行うことができ、これを使用することにより、共有オブジェクトが提供する目的の機能を取り囲むことができます。また反対に、この割り込みは、同じ共通のシンボル名をアプリケーションと共有オブジェクトの両方に使用した結果として、知らないうちに実行される場合もあります。
共有オブジェクトを開発する場合は、シンボル bar と str の範囲をローカル結合に縮小して、このような事態から保護できます。次の例では、シンボル bar と str は、共有オブジェクトのインタフェースの一部としては利用できなくなっています。そのため、これらのシンボルは、外部のオブジェクトによって参照されることができないか、割り込みはできません。ユーザーは、インタフェースをこの共有オブジェクト用に効果的に定義できます。インプリメンテーションの基礎となる詳細を隠している間は、このインタフェースを管理できます。
$ cat mapfile { local: bar; str; }; $ cc -o libfoo.so.1 -M mapfile -G foo.c bar.c $ nm -x libfoo.so.1 | egrep "foo$|bar$|str$" [27] |0x000003dc|0x00000028|FUNC |LOCL |0x0 |6 |bar [28] |0x00010494|0x00000004|OBJT |LOCL |0x0 |12 |str [33] |0x000003b4|0x00000028|FUNC |GLOB |0x0 |6 |foo |
このようなシンボル範囲の縮小には、このほかにもパフォーマンスにおける利点があります。実行時に必要だったシンボル bar と str に対するシンボルの再配置は、現在は、関連する再配置に縮小されます。シンボル再配置のオーバーヘッドの詳細は、「再配置が実行されるとき」を参照してください。
リンク編集の間に処理されるシンボル数が多くなると、mapfile 内で各ローカル範囲の縮小を定義するのが困難になります。代わりとなる、より柔軟なメカニズムを使用すると、維持しなければならない大域シンボルの点で共有オブジェクトのインタフェースを定義できます。大域シンボルを定義すると、リンカーはその他のシンボルすべてをローカル結合にすることができます。このメカニズムは、特別な自動縮小指令の「*」を使用して実行します。たとえば、前の mapfile 定義を書き換えて、生成される出力ファイル内で必要な唯一の大域シンボルとして foo を定義します。
$ cat mapfile ISV_1.1 { global: foo; local: *; }; $ cc -o libfoo.so.1 -M mapfile -G foo.c bar.c $ nm -x libfoo.so.1 | egrep "foo$|bar$|str$" [30] |0x00000370|0x00000028|FUNC |LOCL |0x0 |6 |bar [31] |0x00010428|0x00000004|OBJT |LOCL |0x0 |12 |str [35] |0x00000348|0x00000028|FUNC |GLOB |0x0 |6 |foo |
この例では、バージョン名 libfoo.so.1.1 も mapfile 指令の一部として定義しています。このバージョン名により、ファイルのシンボルインタフェースを定義する、内部バージョン定義が確立されます。バージョン定義はできるだけ作成してください。バージョン定義によって、ファイルの展開全体を通して使用できる、内部バージョンメカニズムの基礎が形成されます。第 5 章アプリケーションバイナリインタフェースとバージョン管理を参照してください。
バージョン名が指定されていないと、出力ファイル名がバージョン定義のラベル付けに使用されます。出力ファイル内に作成されたバージョン情報は、リンカーの -z noversion オプションを使用して表示しないようにできます。
バージョン名を指定する場合は必ず、「すべて」の大域シンボルをバージョン定義に割り当てる必要があります。バージョン定義に割り当てられていない大域シンボルが残っていると、リンカーにより重大なエラー状態が発生します。
$ cat mapfile ISV_1.1 { global: foo; }; $ cc -o libfoo.so.1 -M mapfile -G foo.c bar.c Undefined first referenced symbol in file str bar.o (symbol has no version assigned) bar bar.o (symbol has no version assigned) ld: fatal: Symbol referencing errors. No output written to libfoo.so.1 |
-B local オプションを使用して、コマンド行から自動縮小指令「*」を表明することができます。前の例は、次のようにコンパイルすることもできます。
$ cc -o libfoo.so.1 -M mapfile -B local -G foo.c bar.c |
実行可能ファイルまたは共有オブジェクトを生成すると、シンボルの縮小によって、出力イメージ内にバージョン定義が記録されます。再配置可能オブジェクトの生成時にバージョン定義は作成されますが、シンボルの縮小処理は行われません。その結果、シンボル縮小のシンボルエントリは、大域のまま残されます。たとえば、自動縮小指令が指定された前の mapfile と、関連する再配置可能オブジェクトを使用して、シンボル縮小が表示されていない中間再配置可能オブジェクトが作成されます。
$ cat mapfile ISV_1.1 { global: foo; local: *; }; $ ld -o libfoo.o -M mapfile -r foo.o bar.o $ nm -x libfoo.o | egrep "foo$|bar$|str$" [17] |0x00000000|0x00000004|OBJT |GLOB |0x0 |3 |str [19] |0x00000028|0x00000028|FUNC |GLOB |0x0 |1 |bar [20] |0x00000000|0x00000028|FUNC |GLOB |0x0 |1 |foo |
このイメージ内に作成されたバージョン定義は、シンボル縮小が要求されたという事実を記録します。再配置可能オブジェクトが、最終的に、実行可能ファイルまたは共有オブジェクトの生成に使用されるときに、シンボル縮小が実行されます。すなわち、リンカーは、mapfile からバージョン管理データを処理するのと同じ方法で、再配置可能オブジェクト内に組み込まれたシンボル縮小を読み取り、解釈します。
そのため、上記の例で作成された中間再配置可能オブジェクトは、ここで、共有オブジェクトの生成に使用されます。
$ cc -o libfoo.so.1 -G libfoo.o $ nm -x libfoo.so.1 | egrep "foo$|bar$|str$" [22] |0x000104a4|0x00000004|OBJT |LOCL |0x0 |14 |str [24] |0x000003dc|0x00000028|FUNC |LOCL |0x0 |8 |bar [36] |0x000003b4|0x00000028|FUNC |GLOB |0x0 |8 |foo |
シンボル縮小は、通常、実行可能ファイルまたは共有オブジェクトが作成されたときに行う必要があります。ただし、再配置可能オブジェクトが作成されたときは、リンカーの -B reduce オプションを使用して強制的に実行されます。
$ ld -o libfoo.o -M mapfile -B reduce -r foo.o bar.o $ nm -x libfoo.o | egrep "foo$|bar$|str$" [15] |0x00000000|0x00000004|OBJT |LOCL |0x0 |3 |str [16] |0x00000028|0x00000028|FUNC |LOCL |0x0 |1 |bar [20] |0x00000000|0x00000028|FUNC |GLOB |0x0 |1 |foo |
シンボル縮小の拡張の 1 つは、オブジェクトのシンボルテーブルから特定のシンボルエントリを削除することです。局所シンボルは、オブジェクトの .symtab シンボルテーブルだけで管理されます。このテーブルは、リンカーの -s オプションまたは strip(1) を使用して、オブジェクトからすべて削除できます。しかし、.symtab シンボルテーブルは削除しないで、特定の局所シンボルだけを削除したいこともあります。
シンボル削除は、mapfile キーワード ELIMINATE を使用して実行できます。local 指令と同様に個別にシンボルを定義することも、特殊な自動削除指令「*」としてシンボル名を定義することもできます。次の例では、前述のシンボル縮小の例で使用したシンボル bar を削除しています。
$ cat mapfile ISV_1.1 { global: foo; local: str; eliminate: *; }; $ cc -o libfoo.so.1 -M mapfile -G foo.c bar.c $ nm -x libfoo.so.1 | egrep "foo$|bar$|str$" [31] |0x00010428|0x00000004|OBJT |LOCL |0x0 |12 |str [35] |0x00000348|0x00000028|FUNC |GLOB |0x0 |6 |foo |
-B eliminate オプションを使用して、コマンド行から「自動削除」指令「*」を表明することもできます。
作成するオブジェクトのシンボル参照が共有オブジェクト内の定義によって解決されると、そのシンボルは未定義のまま残ります。シンボルに対応する再配置情報が実行時の検索で使用されます。定義を提供する共有オブジェクトは、通常、1 つの依存条件になります。
実行時リンカーは、実行時にデフォルト検索モデルを使ってこの定義を見つけます。一般にオブジェクトは 1 つずつ検索されますが、その際、動的実行可能プログラムから、オブジェクトが読み込まれた順に各依存関係が処理されます 。
オブジェクトは、直接結合を使用するように作成することもできます。この方法では、シンボル参照と、シンボル定義を提供するオブジェクトとの関係は、作成されるオブジェクト内に維持されます。この情報を使えば、実行時リンカーは、参照先とシンボルを定義するオブジェクトを直接結合し、デフォルトのシンボル検索モデルをバイパスできます。「直接結合」を参照してください。
リンカーは、重複したエントリと末尾部分文字列を削除することによって、文字列テーブルを圧縮します。この圧縮により、どのような文字列テーブルでもサイズが相当小さくなります。たとえば、.dynstr テーブルを圧縮すると、テキストセグメントが小さくなるため、実行時のページング作業が減ります。このような利点があるため、文字列テーブルの圧縮はデフォルトで有効に設定されています。
非常に多くのシンボルを提供するオブジェクトによって、文字列テーブルの圧縮のためにリンク編集時間が延びる可能性があります。開発時にこの負担を避けるには、リンカーの -z nocompstrtab オプションを使用してください。リンク編集時に行われる文字列テーブルの圧縮は、リンカーのデバッグトークン -D strtab,detail を使用して表示できます。
入力ファイルの処理とシンボル解決がすべて重大なエラーが発生することもなく完了すると、リンカーは出力ファイルを生成します。リンカーは、出力ファイルの完成に必要な追加セクションをまず生成します。これらのセクションには、すべての入力ファイルから解決済みの大域およびウィークシンボル情報とともに局所シンボル定義を含むシンボルテーブルが含まれます。
また、実行時リンカーが必要とする、出力の再配置および動的情報セクションも組み込まれます。すべての出力セクション情報が設定された後、出力ファイルサイズの合計が計算されます。次に出力ファイルイメージが適宜作成されます。
動的実行可能プログラムまたは共有オブジェクトを作成するときに、通常、2 つのシンボルテーブルが生成されます。.dynsym とその関連文字列テーブル .dynstr には、レジスタ、大域シンボル、ウィークシンボル、およびセクションシンボルが組み込まれます。これらのセクションは、実行時処理イメージの一部としてマッピングされる text セグメントの一部となります。mmap(2) のマニュアルページを参照してください。このマッピングにより、実行時リンカーは、これらのセクションを読み取り、必要な再配置を実行できます。
.symtab とその関連文字列テーブル .strtab には、入力ファイル処理から収集されたすべてのシンボルが含まれています。これらのセクションは、プロセスイメージの一部として対応付けられません。これらのセクションは、リンカーの -s オプションを使用して、または、リンク編集後に strip(1) を使用して、イメージから取り除くことさえ可能です。
予約シンボルは、シンボルテーブルの生成中に作成されます。これらのシンボルは、リンクプロセスに対して特別な意味を持っています。コードでは、これらのシンボルを定義しないでください。
_end と同じ。このシンボルは、_START_ シンボルとともに、ローカル範囲を持ち、オブジェクトのアドレス範囲を確立する簡単な手段を提供します。
リンカーが提供するアドレステーブル ( .got セクション) への位置独立の参照。このテーブルは、-K pic オプションを指定してコンパイルしたオブジェクトで発生する、位置独立のデータ参照から構築されます。「位置独立のコード」を参照してください。
リンカーが提供するアドレステーブル (.plt セクション) への、位置独立の参照。このテーブルは、-K pic オプションを指定してコンパイルしたオブジェクトで発生する、位置独立の関数参照から構築されます。「位置独立のコード」を参照してください。
テキストセグメント内の最初の位置。このシンボルは、_END_ シンボルとともに、ローカル範囲を持ち、オブジェクトのアドレス範囲を確立する簡単な手段を提供します。
リンカーは、実行可能ファイルを生成する場合、追加シンボルを検出して実行可能ファイルのエントリポイントを定義します。シンボルがリンカーの -e オプションを使用して指定された場合、そのシンボルが使用されます。それ以外の場合は、リンカーは予約シンボル名 _start と main を検出します。
再配置可能オブジェクトのハードウェア機能とソフトウェア機能は、一般的にコンパイル時に記録されます。リンカーは入力再配置可能オブジェクトの機能を組み合わせて、出力ファイルの最終機能セクションを作成します。「ハードウェアおよびソフトウェア機能に関するセクション」を参照してください。
さらに、リンカーが出力ファイルを作成するときにも機能を定義できます。これらの機能は、mapfile とリンカーの -M オプションを使用して特定します。mapfile を使用して定義した機能は、入力再配置可能オブジェクトから提供される機能を増強したり、無効にしたりすることができます。
次の節では、mapfile を使用して機能を定義する方法を説明します。
オブジェクトのハードウェア機能は、オブジェクトを正しく実行するために必要なプラットフォームのハードウェア要件を特定します。この要件の例としては、一部の x86 アーキテクチャーで利用できる MMX または SSE を必要とするコードの特定があります。
ハードウェア機能要件は、次の mapfile 構文を使用して特定できます。
hwcap_1 = TOKEN | Vval [ OVERRIDE ]; |
hwcap_1 宣言は 1 つ以上のトークンで修飾されます。これはハードウェア機能のシンボル表現です。さらに、別の方法として、より多くの機能の 1 つを表す数値に V という接頭辞をつけて指定できます。SPARC プラットフォームでは、ハードウェア機能は sys/auxv_SPARC.h の AV_ の値として定義されます。x86 プラットフォームでは、ハードウェア機能は sys/auxv_386.h の AV_ の値として定義されます。
次の x86 の例では、オブジェクト foo.so.1 に必要なハードウェア機能として MMX と SSE が宣言されています。
$ egrep "MMX|SSE" /usr/include/sys/auxv_386.h #define AV_386_MMX 0x0040 #define AV_386_SSE 0x0800 $ cat mapfile hwcap_1 = SSE MMX; $ cc -o foo.so.1 -G -K pic -Mmapfile foo.c -lc $ elfdump -H foo.so.1 Hardware/Software Capabilities Section: .SUNW_cap index tag value [0] CA_SUNW_HW_1 0x840 [ SSE MMX ] |
再配置可能オブジェクトには、ハードウェア機能の値を含めることができます。リンカーは、複数の入力再配置可能オブジェクトからのハードウェア機能値を組み合わせます。この結果生じる CA_SUNW_HW_1 の値は、関連入力値のビット単位の OR となります。デフォルトでは、これらの値は、mapfile で指定されたハードウェア機能と組み合わせられます。
出力ファイルのハードウェア機能要件は、OVERRIDE キーワードを使用して mapfile から明示的に制御できます。OVERRIDE キーワードは、ハードウェア機能値 0 とともに、構築中のオブジェクトからハードウェア機能要件を事実上削除します。
$ elfdump -H foo.o Hardware/Software Capabilities Section: .SUNW_cap index tag value [0] CA_SUNW_HW_1 0x840 [ SSE MMX ] $ cat mapfile hwcap_1 = V0x0 OVERRIDE; $ cc -o bar.o -r -Mmapfile foo.o $ elfdump -H bar.o $ |
オブジェクトが定義したハードウェア機能要件は、実行時リンカーによってプロセスで利用できるハードウェア機能に対して検証されます。ハードウェア機能要件の一部を満足できない場合、そのオブジェクトは実行時に読み込みされません。たとえば、SSE 機能がプロセスで利用できない場合、ldd(1) は次のエラーを示します。
$ ldd prog foo.so.1 => ./foo.so.1 - hardware capability unsupported: \ 0x800 [ SSE ] .... |
異なるハードウェア機能を利用する動的オブジェクトは、フィルタを使用して柔軟な実行時環境を提供できます。「ハードウェア機能固有の共有オブジェクト」を参照してください。
オブジェクトのソフトウェア機能は、プロセスのデバッグまたは監視にとって重要なことがあるソフトウェアの特徴を特定します。現在、認識されているソフトウェア機能だけが、オブジェクトによるフレームポインタ使用に関係します。
オブジェクトは、フレームポインタ使用を認識することを示せます。この状態は、フレームポインタを使用中または未使用として宣言することで、修飾されます。
ソフトウェア機能フラグは、sys/elf.h で定義されています。
#define SF1_SUNW_FPKNWN 0x001 #define SF1_SUNW_FPUSED 0x002
これらのソフトウェア機能要件は、次の mapfile 構文を使用して特定できます。
sfcap_1 = TOKEN | Vval [ OVERRIDE ]; |
sfcap_1 の宣言は、トークン FPKNWN と FPUSED で修飾できます。または、これらの状態を表す数値を代わりに使用することもできます。
再配置可能オブジェクトには、ソフトウェア機能の値を含めることができます。リンカーは、複数の入力再配置可能オブジェクトからのソフトウェア機能値を組み合わせます。ソフトウェア機能は、mapfile も提供されます。デフォルトでは、mapfile のすべての値が、再配置可能オブジェクトで提供される値と組み合わせられます。
出力ファイルのソフトウェア機能要件は、OVERRIDE キーワードを使用して mapfile から明示的に制御できます。OVERRIDE キーワードは、ソフトウェア機能値 0 とともに、構築中のオブジェクトからソフトウェア機能要件を事実上削除します。
$ elfdump -H foo.o Hardware/Software Capabilities Section: .SUNW_cap index tag value [0] CA_SUNW_SF_1 0x3 [ SF1_SUNW_FPKNWN SF1_SUNW_FPUSED ] $ cat mapfile sfcap_1 = V0x0 OVERRIDE; $ cc -o bar.o -r -Mmapfile foo.o $ elfdump -H bar.o $ |
2 つのフレームポインタ入力値からの CA_SUNW_SF_1 値は、次のように計算されます。
表 2–1 CA_SUNW_SF_1 フレームポインタフラグ組み合わせ状態テーブル
入力ファイル 1 |
入力ファイル 2 |
||
---|---|---|---|
|
SF1_SUNW_FPKNWN SF1_SUNW_FPUSED |
SF1_SUNW_FPKNWN |
<unknown> |
SF1_SUNW_FPKNWN SF1_SUNW_FPUSED |
SF1_SUNW_FPKNWN SF1_SUNW_FPUSED |
SF1_SUNW_FPKNWN |
SF1_SUNW_FPKNWN SF1_SUNW_FPUSED |
SF1_SUNW_FPKNWN |
SF1_SUNW_FPKNWN |
SF1_SUNW_FPKNWN |
SF1_SUNW_FPKNWN |
<unknown> |
SF1_SUNW_FPKNWN SF1_SUNW_FPUSED |
SF1_SUNW_FPKNWN |
<unknown> |
この計算は、再配置可能オブジェクト値と mapfile 値にそれぞれ適用されます。.SUNW_cap セクションが存在しない場合や、このセクションに CA_SUNW_SF_1 の値が含まれない場合、SF1_SUNW_FPKNW フラグも SF1_SUNW_FPUSED フラグも設定されていない場合は、オブジェクトのフレームポインタソフトウェア機能は不明になります。
出力ファイルを作成すると、入力ファイルからのすべてのデータセクションは新しいイメージにコピーされます。入力ファイル内に指定された再配置は、出力イメージに適用されます。生成する必要がある追加の再配置情報も、新しいイメージに書き込まれます。
再配置処理には、通常、大きな問題はありませんが、特定のエラーメッセージを伴うエラー状態が発生することがあります。ここでは、2 つの状態について説明します。1 つは、位置に依存するコードによって発生するテキスト再配置です。この状態の詳細については、「位置独立のコード」を参照してください。もう 1 つは、ディスプレイスメント再配置に関連して発生します。ディスプレイスメント再配置については、次の項で詳しく説明します。
データ項目 (コピー再配置で使用可能) にディスプレイスメント再配置が適用されていると、エラー状態が発生することがあります。コピー再配置の詳細については、「コピー再配置」を参照してください。
ディスプレイスメント再配置は、再配置されるオフセットと再配置ターゲットが両方とも同じ位置だけ離れているかぎり有効です。コピー再配置では、共有オブジェクト内の大域データ項目が実行可能ファイルの .bss にコピーされます。このコピーは、実行可能ファイルの参照専用テキストセグメントを保持します。コピーされるデータにディスプレイスメント再配置が適用されていたり、外部再配置がコピーされるデータへのディスプレイスメントであったりすると、ディスプレイスメント再配置は無効になります。
ディスプレイスメント再配置の問題を検知するために、次の 2 つの領域で検証が試みられます。
最初は、共有オブジェクトの生成時に行われます。コピー再配置可能なデータ項目がディスプレイスメント再配置を伴うと問題が発生する可能性がある場合は、それらに対しフラグが立てられます。リンカーが共有オブジェクトを構築する際には、データ項目に対しどのような外部参照がされるかは不明です。したがって、フラグが立てられたデータ項目は、エラーを引き起こす可能性があります。
次の検証は、実行可能ファイルの生成時に行われます。コピー再配置のデータがディスプレイスメント再配置を伴う場合は、コピー再配置の作成に対しフラグが立てられます。
しかし、リンク編集で共有オブジェクトを作成するときに、共有オブジェクトに適用されたディスプレイスメント再配置が完了することがあります。これらのディスプレイスメント再配置には、フラグが立てられていない可能性があります。フラグの立てられていない共有オブジェクトを参照する実行可能ファイルのリンク編集では、コピー再配置のデータで有効になっているディスプレイスメントは不明となります。
このような問題の診断を助けるため、リンカーは、動的オブジェクトに対してディスプレイスメント再配置が使用されていると、1 つまたは複数の動的 DT_FLAGS_1 フラグを立てます (表 7–34 を参照)。さらに、その可能性のある再配置をリンカーの -z verbose オプションを使って表示することもできます。
たとえば、ディスプレイスメント再配置が適用される大域データ項目 bar[] を持つ共有オブジェクトを作成するとします。この項目は、動的実行可能ファイルから参照されると、コピー再配置される可能性があります。リンカーは、この状態に対する警告を出します。
$ cc -G -o libfoo.so.1 -z verbose -K pic foo.o ld: warning: relocation warning: R_SPARC_DISP32: file foo.o: symbol foo: \ displacement relocation to be applied to the symbol bar: at 0x194: \ displacement relocation will be visible in output image |
データ項目 bar[] を参照するアプリケーションを作成すると、コピー再配置が作成されます。このコピーは、無効なディスプレイスメント再配置の原因となります。リンカーはこの状況を明示的に検出できるため、-z verbose オプションが使用されていなくても、次のエラーメッセージを生成します。
$ cc -o prog prog.o -L. -lfoo ld: warning: relocation error: R_SPARC_DISP32: file foo.so: symbol foo: \ displacement relocation applied to the symbol bar at: 0x194: \ the symbol bar is a copy relocated symbol |
ldd(1) で -d、-r のいずれかのオプションを指定すると、ディスプレイスメント動的フラグによって同じような再配置警告が生成されます。
このようなエラー状態は、再配置するシンボル定義 (オフセット) と再配置のシンボルターゲットを両方ともローカルに置くことによって避けることができます。静的な定義を使用するか、リンカーの範囲指定を使用してください。「シンボル範囲の縮小」を参照してください。この種の再配置の問題は、機能インタフェースを使用して共有オブジェクト内のデータにアクセスすれば、回避することができます。
Solaris OS リンカーには、デバッギングライブラリが付いています。このライブラリを使用すると、リンク編集プロセスをより詳細に監視できます。このライブラリは、ユーザーのアプリケーションおよびライブラリのリンク編集を理解およびデバッグする場合に役立ちます。このライブラリを使用して表示される情報のタイプは、定数のままであると予期されます。ただし、この情報の正確な形式は、リリースごとに若干変更される場合があります。
ELF フォーマットを熟知していないと、デバッギング出力の中には見慣れないものがあるかもしれません。しかし、多くのものが一般的な関心を惹くものでしょう。
デバッグは、-D オプションを使用して実行できます。作成されるすべての出力は、標準エラーに送られます。このオプションは、1 つまたは複数のトークンで増強し、必要なデバッギングのタイプを指示する必要があります。使用できるトークンは、コマンド行で -D help を入力すれば表示できます。
$ ld -Dhelp ............ debug: files display input file processing (files and libraries) ............ |
ほとんどのコンパイラドライバは、前処理フェーズ中に -D オプションを解釈します。このため、リンカーにこのオプションを渡すためには、LD_OPTIONS 環境変数のメカニズムが適しています。
次の例では、入力ファイルの監視方法を示しています。この構文は、リンクを編集するときにどのライブラリが使用されているかを判別するときに利用できます。アーカイブから抽出されたオブジェクトもこの構文で表示されます。
$ LD_OPTIONS=-Dfiles cc -o prog main.o -L. -lfoo ............ debug: file=main.o [ ET_REL ] debug: file=./libfoo.a [ archive ] debug: file=./libfoo.a(foo.o) [ ET_REL ] debug: file=./libfoo.a [ archive ] (again) ............ |
ここでは、prog のリンク編集を満たすために、メンバー foo.o がアーカイブライブラリ libfoo.a から抽出されています。foo.o の抽出が、その他の再配置可能オブジェクトの抽出を認めていないことを検証するために、このアーカイブが 2 回検索されていることに注意してください。診断内に「(again)」が複数個含まれていることから、このアーカイブが lorder(1) や tsort(1) による並べ替えの候補であることがわかります。
symbols トークンを使用することにより、どのシンボルによってアーカイブメンバーが抽出されたか、また、最初のシンボル参照を実行したオブジェクトを判別できます。
$ LD_OPTIONS=-Dsymbols cc -o prog main.o -L. -lfoo ............ debug: symbol table processing; input file=main.o [ ET_REL ] ............ debug: symbol[7]=foo (global); adding debug: debug: symbol table processing; input file=./libfoo.a [ archive ] debug: archive[0]=bar debug: archive[1]=foo (foo.o) resolves undefined or tentative symbol debug: debug: symbol table processing; input file=./libfoo(foo.o) [ ET_REL ] ............. |
シンボル foo は、main.o によって参照されます。このシンボルは、リンカーの内部シンボルテーブルに追加されます。このシンボル参照によって、再配置可能オブジェクト foo.o が、アーカイブ libfoo.a から抽出されます。
この出力は、このマニュアル用に簡素化したものです。
detail トークンを、symbols トークンとともに使用すると、入力ファイル処理中のシンボル解決を監視できます。
$ LD_OPTIONS=-Dsymbols,detail cc -o prog main.o -L. -lfoo ............ debug: symbol table processing; input file=main.o [ ET_REL ] ............ debug: symbol[7]=foo (global); adding debug: entered 0x000000 0x000000 NOTY GLOB UNDEF REF_REL_NEED debug: debug: symbol table processing; input file=./libfoo.a [ archive ] debug: archive[0]=bar debug: archive[1]=foo (foo.o) resolves undefined or tentative symbol debug: debug: symbol table processing; input file=./libfoo.a(foo.o) [ ET_REL ] debug: symbol[1]=foo.c ............. debug: symbol[7]=bar (global); adding debug: entered 0x000000 0x000004 OBJT GLOB 3 REF_REL_NEED debug: symbol[8]=foo (global); resolving [7][0] debug: old 0x000000 0x000000 NOTY GLOB UNDEF main.o debug: new 0x000000 0x000024 FUNC GLOB 2 ./libfoo.a(foo.o) debug: resolved 0x000000 0x000024 FUNC GLOB 2 REF_REL_NEED ............ |
main.o からの、オリジナルの未定義シンボル foo が、アーカイブメンバー foo.o から抽出されたシンボル定義で上書きされます。このシンボルの詳細情報は、各シンボルの属性に反映されます。
上記の例からわかるように、デバッギングトークンのいくつかを使用すると、豊富な出力が作成されます。入力ファイルのサブセットに関するアクティビティーを監視するには、リンク編集コマンド行に直接 -D オプションを配置します。このオプションはオンとオフを切り替えることができます。次の例では、シンボル処理の表示がオンになるのは、ライブラリlibbar の処理中だけです。
$ ld .... -o prog main.o -L. -Dsymbols -lbar -D!symbols .... |
リンク編集コマンド行を入手するには、使用しているドライバからコンパイル行を拡張する必要があります。「コンパイラドライバを使用する」を参照してください。