共有オブジェクトは、リンカーによって作成される出力形式の 1 つであり、-G オプションを指定して生成されます。次の例では、共有オブジェクト libfoo.so.1 は、入力ファイル foo.c から生成されます。
$ cc -o libfoo.so.1 -G -K pic foo.c |
共有オブジェクトとは、1 つまたは複数の再配置可能なオブジェクトから生成される表示できないユニットです。共有オブジェクトは、動的実行可能ファイルと結合して実行可能プロセスを形成することができます。共有オブジェクトは、その名前が示すように、複数のアプリケーションによって共有できます。このように共有オブジェクトの影響力は非常に大きくなる可能性があるため、この章では、リンカーのこの出力形式について前の章よりも詳しく説明します。
共有オブジェクトを動的実行可能ファイルや他の共有オブジェクトに結合するには、まず共有オブジェクトが必要な出力ファイルのリンク編集に使用可能でなければなりません。このリンク編集中、入力共有オブジェクトはすべて、作成中の出力ファイルの論理アドレス空間に追加された場合のように解釈されます。共有オブジェクトのすべての機能が、出力ファイルにとって使用可能になります。
これらの共有オブジェクトは、この出力ファイルの依存関係になります。出力ファイル内には、この依存関係を記述するための少量の登録情報が保持されます。実行時リンカーは、この情報を解釈し、実行可能プロセス作成の一部として、これらの共有オブジェクトの処理を完了します。
次の節では、コンパイル環境と実行時環境内での共有オブジェクトの使用法について詳しく説明します。これらの環境については、実行時リンクを参照してください。
リンカーも実行時リンカーも、ファイル名によってファイルを解釈しません。ファイルはすべて検査されて、その ELF タイプが判定されます (ELF ヘッダーを参照)。この情報から、リンカーはファイルの処理条件を推定します。ただし、共有オブジェクトは通常、コンパイル環境または実行時環境のどちらの一部として使用されるかによって、2 つの命名規約のうちどちらかに従います。
共有オブジェクトは、コンパイル環境の一部として使用される場合、リンカーによって読み取られて処理されます。これらの共有オブジェクトは、リンカーに渡されるコマンド行の一部で明示的なファイル名によって指定できますが、リンカーのライブラリ検索機能を利用するために -l オプションを使用する方が一般的です。共有オブジェクトの処理を参照してください。
このリンカー処理に適用する共有オブジェクトには、接頭辞 lib と接尾辞 .so を指定する必要があります。たとえば、/usr/lib/libc.so は、コンパイル環境に使用できる標準 C ライブラリの共有オブジェクト表現です。規則によって、64 ビットの共有オブジェクトは、64 と呼ばれる lib ディレクトリのサブディレクトリに置かれます。たとえば、/usr/lib/libc.so.1 の 64 ビット版は、/usr/lib/64/libc.so.1 です。
共有オブジェクトは、実行時環境の一部として使用される場合、実行時リンカーによって読み取られて処理されます。幾世代にも渡って公開される共有オブジェクトのインタフェースを変更できるようにするには、共有オブジェクトをバージョン番号の付いたファイル名にします。
バージョン付きファイル名は、通常、.so 接尾辞の後にバージョン番号が続くという形式をとります。たとえば、/usr/lib/libc.so.1 は、実行時環境で使用可能な標準 C ライブラリのバージョン 1 の共有オブジェクト表示です。
共有オブジェクトが、コンパイル環境内での使用をまったく目的としていない場合は、従来の lib 接頭辞がその名前から削除されることがあります。このカテゴリに属する共有オブジェクトの例には、dlopen(3DL) だけに使用されるオブジェクトがあります。実際のファイルタイプを示すために、接尾辞 .so は付けた方が望ましく、一連のソフトウェアリリースで共有オブジェクトの正しい結合を行うためにはバージョン番号も必要です。バージョン番号の付け方については、第 5 章「アプリケーションバイナリインタフェースとバージョン管理」を参照してください。
dlopen(3DL) で使用される共有オブジェクト名は通常、名前に「/」が付かない単純ファイル名として表されます。実行時リンカーは、この規則を使用して、実際のファイルを検索できます。詳細については、追加オブジェクトの読み込みを参照してください。
動的実行可能ファイルまたは共有オブジェクトでの依存関係の記録は、デフォルトでは、関連する共有オブジェクトがリンカーによって参照されるときのファイル名になります。たとえば、次の動的実行可能ファイルは、同じ共有オブジェクト libfoo.so に対して構築されますが、同じ依存関係の解釈は異なります。
$ cc -o ../tmp/libfoo.so -G foo.o $ cc -o prog main.o -L../tmp -lfoo $ dump -Lv prog | grep NEEDED [1] NEEDED libfoo.so $ cc -o prog main.o ../tmp/libfoo.so $ dump -Lv prog | grep NEEDED [1] NEEDED ../tmp/libfoo.so $ cc -o prog main.o /usr/tmp/libfoo.so $ dump -Lv prog | grep NEEDED [1] NEEDED /usr/tmp/libfoo.so |
上記の例が示すように、依存関係を記録するこのメカニズムでは、コンパイル手法の違いによって不一致が生じる可能性があります。また、リンク編集中に参照される共有オブジェクトの位置が、インストールされたシステムでの共有オブジェクトの最終的な位置と異なる場合があります。依存関係を指定するより一貫した手法として、共有オブジェクトは、それぞれの内部にファイル名を記録できます。共有オブジェクトは、このファイル名によって実行時に参照されます。
共有オブジェクトのリンク編集中、-h オプションを使用すると、その実行時名を共有オブジェクト自体に記録できます。次の例では、共有オブジェクトの実行時名 libfoo.so.1 は、ファイル自体に記録されます。この識別名は、soname と呼ばれます。
$ cc -o ../tmp/libfoo.so -G -K pic -h libfoo.so.1 foo.c |
次の例は、dump(1) を使用して SONAME タグを持つエントリを参照し、soname の記録を表示する方法を示しています。
$ dump -Lvp ../tmp/libfoo.so ../tmp/libfoo.so: [INDEX] Tag Value [1] SONAME libfoo.so.1 ......... |
リンカーが soname を含む共有オブジェクトを処理する場合、生成中の出力ファイル内に依存関係として記録されるのはこの名前です。
前の例から動的実行可能ファイル prog を作成しているときに、この新しいバージョンの libfoo.so が使用されると、実行可能ファイルを作成するための 3 つの方式すべてによって同じ依存関係が記録されます。
$ cc -o prog main.o -L../tmp -lfoo $ dump -Lv prog | grep NEEDED [1] NEEDED libfoo.so.1 $ cc -o prog main.o ../tmp/libfoo.so $ dump -Lv prog | grep NEEDED [1] NEEDED libfoo.so.1 $ cc -o prog main.o /usr/tmp/libfoo.so $ dump -Lv prog | grep NEEDED [1] NEEDED libfoo.so.1 |
上記の例では、-h オプションは、単純 (simple) ファイル名を指定するために使用されます。つまり、名前に「/」が付きません。この規約では、実行時リンカーが規則を使用して実際のファイルを検索できます。詳細については、共有オブジェクトの依存関係の検索を参照してください。
共有オブジェクトに soname を記録するメカニズムは、共有オブジェクトがアーカイブライブラリから処理される場合に重要です。
アーカイブは、1 つまたは複数の共有オブジェクトから構築し、動的実行可能ファイルまたは共有オブジェクトを生成するために使用できます。共有オブジェクトは、リンク編集の条件を満たすためにアーカイブから抽出できます。作成中の出力ファイルに連結される再配置可能オブジェクトの処理とは違って、アーカイブから抽出された共有オブジェクトは、すべて依存関係として記録されます。アーカイブ抽出の条件の詳細については、アーカイブ処理 を参照してください。
アーカイブ構成要素の名前はリンカーによって構築されて、アーカイブ名とアーカイブ内のオブジェクトの連結になります。次に例を示します。
$ cc -o libfoo.so.1 -G -K pic foo.c $ ar -r libfoo.a libfoo.so.1 $ cc -o main main.o libfoo.a $ dump -Lv main | grep NEEDED [1] NEEDED libfoo.a(libfoo.so.1) |
この連結名を持つファイルが実行時に存在することはほとんどないため、共有オブジェクト内に soname を与える方法が、依存関係の有意な実行時ファイル名を生成する唯一の手段です。
実行時リンカーは、アーカイブからオブジェクトを抽出しません。したがって、上記の例では、必要な共有オブジェクト依存関係をアーカイブから抽出して、実行時環境で使用できるようにする必要があります。
共有オブジェクトが実行可能ファイルまたは別の共有オブジェクトを作成するために使用される場合、リンカーは、いくつかの整合性検査を実行して、出力ファイル内に記録される依存関係名すべてが一意になるように保証します。
依存関係名の衝突は、リンク編集への入力ファイルとして使用される 2 つの共有オブジェクトがどちらも同じ soname を含む場合に発生する可能性があります。次に例を示します。
$ cc -o libfoo.so -G -K pic -h libsame.so.1 foo.c $ cc -o libbar.so -G -K pic -h libsame.so.1 bar.c $ cc -o prog main.o -L. -lfoo -lbar ld: fatal: recording name conflict: file `./libfoo.so' and \ file `./libbar.so' provide identical dependency names: libsame.so.1 ld: fatal: File processing errors. No output written to prog |
記録された soname を持たない共有オブジェクトのファイル名が、同じリンク編集中に使用された別の共有オブジェクトの soname に一致する場合にも同様のエラー状態が発生します。
生成中の共有オブジェクトの実行時名が、その依存関係の 1 つに一致する場合にも、リンカーは名前の衝突を報告します。次に例を示します。
$ cc -o libbar.so -G -K pic -h libsame.so.1 bar.c -L. -lfoo ld: fatal: recording name conflict: file `./libfoo.so' and \ -h option provide identical dependency names: libsame.so.1 ld: fatal: File processing errors. No output written to libbar.so |
共有オブジェクトは、独自の依存関係を持つことができます。実行時リンカーが検索するディレクトリでは、共有オブジェクトの依存関係を検索するために実行時リンカーが使用する検索規則について説明しています。共有オブジェクトがデフォルトディレクトリの /usr/lib (32 ビットオブジェクトの場合)、または /usr/lib/64 (64 ビットオブジェクトの場合) にないときは、実行時リンカーに対して検索場所を明示的に指示する必要があります。この種の条件を指示するために優先されるメカニズムは、リンカーの -R オプションを使用して、依存関係を持つオブジェクトに「実行パス」を記録するというものです。
次の例では、共有オブジェクト libfoo.so は、libbar.so に対する依存関係を持ちます。これは、実行時にディレクトリ/home/me/lib にあるものと予期されますが、ない場合はデフォルト位置にあるものと予期します。
$ cc -o libbar.so -G -K pic bar.c $ cc -o libfoo.so -G -K pic foo.c -R/home/me/lib -L. -lbar $ dump -Lv libfoo.so libfoo.so: **** DYNAMIC SECTION INFORMATION **** .dynamic: [INDEX] Tag Value [1] NEEDED libbar.so [2] RUNPATH /home/me/lib ......... |
共有オブジェクトでは、依存関係を検索するために必要な実行パスすべてを指定する必要があります。動的実行可能ファイルに指定された実行パスはすべて、動的実行可能ファイルの依存関係を検索するためにだけ使用されます。これらの実行パスは、共有オブジェクトの依存関係を検索するために使用されることはありません。
これに対して、環境変数 LD_LIBRARY_PATH
は、より大域的な適用範囲を持ちます。この変数を使用して指定されたパス名はすべて、実行時リンカーによって、すべての共有オブジェクト依存関係を検索するために使用されます。この環境変数は、実行時リンカーの検索パスに影響を与える一時的なメカニズムとして便利ですが、製品版ソフトウェアではできるだけ使用しないようにしてください。詳細は、実行時リンカーが検索するディレクトリを参照してください。
動的実行可能ファイルと共有オブジェクトが同じ共通の共有オブジェクトに対して依存関係を持つ場合は、オブジェクトが処理される順序が予測困難になる可能性があります。
たとえば、共有オブジェクトの開発者が、次の依存関係を持つ libfoo.so.1 を生成したものと想定します。
$ ldd libfoo.so.1 libA.so.1 => ./libA.so.1 libB.so.1 => ./libB.so.1 libC.so.1 => ./libC.so.1 |
この共有オブジェクトを使用して動的実行可能ファイル prog を作成し、libC.so.1 に対してさらに明示的な依存関係を定義すると、共有オブジェクトの順序は次のようになります。
$ cc -o prog main.c -R. -L. -lC -lfoo $ ldd prog libC.so.1 => ./libC.so.1 libfoo.so.1 => ./libfoo.so.1 libA.so.1 => ./libA.so.1 libB.so.1 => ./libB.so.1 |
共有オブジェクト libfoo.so.1 の依存関係に対して指定した処理順序の条件は、動的実行可能ファイル prog を構築した場合、保証されません。
シンボルの割り込みと .init セクションの処理を特に重要視する開発者は、共有オブジェクトの処理順序でのこのような変更の可能性に注意する必要があります。
「フィルタ」とは、代替共有オブジェクトへの間接参照を提供するために使用される特殊な形式の共有オブジェクトのことをいいます。共有オブジェクトフィルタには、標準フィルタと補助フィルタという 2 つの形式があります。
「標準フィルタ」は、基本的に単一のシンボルテーブルからなり、実行時環境からコンパイル環境を抽象化するメカニズムを提供します。このフィルタを使用するリンク編集は、フィルタ自体によって提供されるシンボルを参照しますが、シンボル参照の実装は、実行時に代替ソースから提供されます。
標準フィルタは、リンカーの -F フラグによって識別されます。このフラグは、実行時にシンボル参照を与える共有オブジェクトを示す関連ファイル名をとります。この共有オブジェクトは、「フィルティー (フィルタ対象)」と呼ばれます。-F フラグを複数回使用すると、複数のフィルティーを記録できます。
フィルティーを実行時に処理できないか、またはフィルタによって定義されたシンボルがフィルティー内に見つからない場合、致命的なエラー状態が発生します。
「補助フィルタ」も同様のメカニズムを備えていますが、フィルタ自体にそのシンボルに対応する実装が含まれます。フィルタを使用するリンク編集では、フィルタ自体によって提供されたシンボルを参照します。シンボル参照の実装は、実行時に代替ソースから提供できます。
補助フィルタは、リンカーの -f フラグを使用して識別されます。このフラグは、実行時にシンボルを与えるために使用できる共有オブジェクトを示す関連ファイル名をとります。この共有オブジェクトは、「フィルティー」と呼ばれます。-f フラグを複数回使用すると、複数のフィルティーを記録できます。
フィルティーを実行時に処理できないか、またはフィルティー内にフィルタが見つからないと、フィルタ内のシンボルの実装が使用されます。
標準フィルタを生成するには、まずフィルティー libbar.so.1 を定義し、それに対してこのフィルタ手法を適用します。このフィルティーは、いくつかの再配置可能オブジェクトから構築される場合があります。次の例では、これらのオブジェクトの 1 つは、ファイル bar.c から発生し、シンボル foo と bar を与えます。
$ cat bar.c char * bar = "bar"; char * foo() { return("defined in bar.c"); } $ cc -o libbar.so.1 -G -K pic .... bar.c .... |
次の例では、標準フィルタ libfoo.so.1 は、シンボル foo
と bar に対して生成されて、フィルティー libbar.so.1
への関連付けを示します。環境変数 LD_OPTIONS
は、このコンパイラドライバが
-f オプションをそれ自体のオプションの 1 つとして解釈しないようにするために使用されています。
$ cat foo.c char * bar = 0; char * foo(){} $ LD_OPTIONS='-F libbar.so.1' \ cc -o libfoo.so.1 -G -K pic -h libfoo.so.1 -R. foo.c $ ln -s libfoo.so.1 libfoo.so $ dump -Lv libfoo.so.1 | egrep "SONAME|FILTER" [1] SONAME libfoo.so.1 [2] FILTER libbar.so.1 |
リンカーは、標準フィルタ libfoo.so.1 を参照して動的実行可能ファイルまたは共有オブジェクトを作成する場合、シンボル解決中にフィルタのシンボルテーブルからの情報を使用します。詳細については、シンボル解決を参照してください。
実行時に、フィルタのシンボルを参照すると、必ずフィルティー libbar.so.1 がさらに読み込まれます。実行時リンカーは、このフィルティーを使用して、libfoo.so.1 によって定義されたシンボルを解決します。
たとえば、次の動的実行可能ファイル prog は、シンボル foo と bar を参照します。これらのシンボルは、フィルタ libfoo.so.1 からのリンク編集中に解決されます。
$ cat main.c extern char * bar, * foo(); main() { (void) printf("foo() is %s: bar=%s\n", foo(), bar); } $ cc -o prog main.c -R. -L. -lfoo $ prog foo() is defined in bar.c: bar=bar |
動的実行可能ファイル prog を実行すると、関数 foo() とデータ項目 bar が、フィルタ libfoo.so.1 からではなく、フィルティー libbar.so.1 から取得されます。
この例では、フィルティー libbar.so.1 がフィルタ libfoo.so.1 に一意に関連付けられています。このため、prog を実行した結果読み込まれる可能性がある他のオブジェクトからのシンボル参照を満たすために使用することができません。
標準フィルタは、既存の共有オブジェクトのサブセットインタフェース、または多数の既存の共有オブジェクトに及ぶインタフェースグループを定義するためのメカニズムとなります。Solaris オペレーティング環境では、いくつかのフィルタが使用されます。
/usr/lib/libsys.so.1 フィルタは、標準 C ライブラリ /usr/lib/libc.so.1 のサブセットを提供します。 このサブセットは、準拠するアプリケーションによってインポートしなければならない C ライブラリ内の ABI に準拠する関数とデータ項目を表わします。
/usr/lib/libdl.so.1 フィルタは、実行時リンカー自体へのユーザーインタフェースを定義します。このインタフェースは、コンパイル環境で (libdl.so.1 から) 参照されるシンボルと、実行時環境内で (ld.so.1 から) 作成される実際の実装結合間の抽象化を提供します。
/usr/lib/libxnet.so.1 フィルタは、複数のフィルティーを使用します。このライブラリは、/usr/lib/libsocket.so.1、 /usr/lib/libnsl.so.1、および /usr/lib/libc.so.1 から、ソケットと XTI インタフェースを提供します。
標準フィルタ内のコードは実行時に参照されないため、このフィルタ内に定義された関数に内容を加えても意味がありません。フィルタコードが再配置を必要とする場合がありますが、実行時にそのフィルタを処理すると不要なオーバーヘッドが生じます。関数は空のルーチンとして定義するか、直接 mapfile から定義してください。追加シンボルの定義を参照してください。
フィルタ内にデータシンボルを生成するときは、データ項目を必ず初期設定して、動的実行可能ファイルから参照されるように保証する必要があります。
リンカーによって実行される、より複雑なシンボル解決の中には、シンボルサイズを含むシンボルの属性に関する知識を必要とするものがあります。詳細については、シンボル解決を参照してください。このため、フィルタ内のシンボルの属性がフィルティー内のシンボルの属性と一致するようにシンボルを生成する必要があります。これにより、リンク編集処理では、実行時に使用されるシンボル定義と互換性のある方法でフィルタが解析されます。
リンカーは、最初に入力された再配置可能ファイルの ELF クラスを使用して、作成するオブジェクトのクラスを管理します。64 ビットフィルタを mapfile だけから作成するには、リンカーの -64 オプションを使用します。
補助フィルタの作成方法は、標準フィルタの場合と基本的に同じです (詳細については、標準フィルタの生成を参照)。まず、このフィルタ手法を適用するフィルティー libbar.so.1 を定義します。このフィルティーは、いくつかの再配置可能オブジェクトから構築される場合があります。これらのオブジェクトの 1 つは、ファイル bar.c から発生し、シンボル foo を提供します。
$ cat bar.c char * foo() { return("defined in bar.c"); } $ cc -o libbar.so.1 -G -K pic .... bar.c .... |
次の例では、補助フィルタ libfoo.so.1 が、シンボル foo
と bar に対して生成されて、フィルティー libbar.so.1
への関連付けを示します。環境変数 LD_OPTIONS
は、このコンパイラドライバが
-f オプションをそれ自体のオプションの 1 つとして解釈しないようにするために使用されています。
$ cat foo.c char * bar = "foo"; char * foo() { return ("defined in foo.c"); } $ LD_OPTIONS='-f libbar.so.1' \ cc -o libfoo.so.1 -G -K pic -h libfoo.so.1 -R. foo.c $ ln -s libfoo.so.1 libfoo.so $ dump -Lv libfoo.so.1 | egrep "SONAME|AUXILIARY" [1] SONAME libfoo.so.1 [2] AUXILIARY libbar.so.1 |
リンカーは、補助フィルタ libfoo.so.1 を参照して動的実行可能ファイルまたは共有オブジェクトを作成する場合、シンボル解決中にフィルタシンボルテーブルの情報を使用します。詳細については、シンボル解決を参照してください。
実行時にフィルタのシンボルを参照すると、フィルティー libbar.so.1 が検索されます。このフィルティーが見つかると、実行時リンカーは、このフィルティーを使用して、libfoo.so.1 によって定義されたすべてのシンボルを解決します。このフィルティーが見つからないか、またはフィルティーにフィルタからのシンボルがない場合は、フィルタ内のシンボルの元の値が使用されます。
たとえば、次の動的実行可能ファイル prog は、シンボル foo と bar を参照します。これらのシンボルは、フィルタ libfoo.so.1 からのリンク編集中に解決されます。
$ cat main.c extern char * bar, * foo(); main() { (void) printf("foo() is %s: bar=%s\n", foo(), bar); } $ cc -o prog main.c -R. -L. -lfoo $ prog foo() is defined in bar.c: bar=foo |
動的実行可能ファイル prog を実行すると、関数 foo() は、フィルタ libfoo.so.1 からではなく、フィルティー libbar.so.1 から取得されます。 ただし、データ項目 bar は、フィルタ libfoo.so.1 から取得されます。このシンボルは、フィルティー libbar.so.1 に代替定義を持たないためです。
補助フィルタは、既存の共有オブジェクトの代替インタフェースを定義するメカニズムとなります。このメカニズムは Solaris オペレーティング環境で使用されて、プラットフォーム固有の共有オブジェクト内に最適化された機能を提供します。例は、命令セット固有の共有オブジェクトおよび システム固有の共有オブジェクトを参照してください。
環境変数 LD_NOAUXFLTR
を設定して、実行時リンカーの補助フィルタ処理を無効にすることができます。補助フィルタはプラットフォーム固有の最適化に使用されることが多いので、フィルティーを使用する場合および性能インパクトを評価する場合にこのオプションが便利です。
実行時リンカーによるフィルタ処理は、フィルタ内のシンボルへの参照が生じるまで、フィルティーの読み込みを延期します。この実装は、各フィルティーに対して必要に応じて dlopen(3DL) を実行するフィルタに似ています。この実装は、ldd(1) などのツールによって生じる可能性がある、依存関係の報告における違いの原因となるものです。
フィルタを作成して、そのフィルティーを実行時に即時処理する場合は、リンカーの -z loadfltr
オプションを使用できます。また、プロセス内のフィルタすべての即時処理は、どの値にも環境変数 LD_LOADFLTR
を設定することによってトリガーすることもできます。
共有オブジェクトは、同じシステム内の複数のアプリケーションで使用できます。共有オブジェクトの性能は、それを使用するアプリケーションだけでなく、システム全体に影響します。
共有オブジェクト内の実際のコードは、実行中のプロセスの性能に直接影響しますが、ここでは共有オブジェクト自体の実行時処理に焦点を絞って性能の問題を説明します。次の節では、再配置によるオーバーヘッドとともに、テキストサイズや純度 (purity) などの面についても見ながら、この処理について詳しく説明します。
ELF ファイルの内容を解析するときに、さまざまなツールを利用できます。ファイルのサイズを表示するには、size(1) コマンドを使用します。次に例を示します。
$ size -x libfoo.so.1 59c + 10c + 20 = 0x6c8 $ size -xf libfoo.so.1 ..... + 1c(.init) + ac(.text) + c(.fini) + 4(.rodata) + \ ..... + 18(.data) + 20(.bss) ..... |
最初の例は、SunOS オペレーティングシステムの以前のリリースから使用されてきたカテゴリである、共有オブジェクトテキスト、データ、および bss のサイズを示します。
ELF 形式は、データをセクションに編成することによって、ファイル内のデータを表現するためのより精密な方法を提供します。2 番目の例は、ファイルの読み込み可能な各セクションのサイズを表示しています。
セクションは、セグメントと呼ばれる単位に割り当てられます。セグメントの一部は、ファイルの部分がメモリーにどのように割り当てられるかを記述します (mmap(2) のマニュアルページを参照)。これらの読み込み可能セグメントは、dump(1) コマンドを使用して、LOAD エントリを調べることによって表示できます。次に例を示します。
$ dump -ov libfoo.so.1 libfoo.so.1: ***** PROGRAM EXECUTION HEADER ***** Type Offset Vaddr Paddr Filesz Memsz Flags Align LOAD 0x94 0x94 0x0 0x59c 0x59c r-x 0x10000 LOAD 0x630 0x10630 0x0 0x10c 0x12c rwx 0x10000 |
共有オブジェクト libfoo.so.1 には、一般にテキストセグメントおよびデータセグメントと呼ばれる 2 つの読み込み可能なセグメントがあります。テキストセグメントは、その内容の読み取りと実行 (r-x) も可能になるように割り当てられます。これに対して、データセグメントは、その内容の変更 (rwx) も可能になるように割り当てられます。データセグメントのメモリーサイズ (Memsz) は、ファイルサイズ (Filesz) とは異なります。この違いは、データセグメントの一部であり、セグメントが読み込まれると動的に作成される .bss セクションを示すものです。
通常プログラマは、関数とデータ要素をそのコード内に定義するシンボルの点からファイルについて考えます。これらのシンボルは、nm(1) を使用して表示できます。次に例を示します。
$ nm -x libfoo.so.1 [Index] Value Size Type Bind Other Shndx Name ......... [39] |0x00000538|0x00000000|FUNC |GLOB |0x0 |7 |_init [40] |0x00000588|0x00000034|FUNC |GLOB |0x0 |8 |foo [41] |0x00000600|0x00000000|FUNC |GLOB |0x0 |9 |_fini [42] |0x00010688|0x00000010|OBJT |GLOB |0x0 |13 |data [43] |0x0001073c|0x00000020|OBJT |GLOB |0x0 |16 |bss ......... |
シンボルを含むセクションは、シンボルテーブルのセクションインデックス (Shndx) フィールドを参照し、dump(1) を使用してファイル内のセクションを表示することによって判定できます。次に例を示します。
$ dump -hv libfoo.so.1 libfoo.so.1: **** SECTION HEADER TABLE **** [No] Type Flags Addr Offset Size Name ......... [7] PBIT -AI 0x538 0x538 0x1c .init [8] PBIT -AI 0x554 0x554 0xac .text [9] PBIT -AI 0x600 0x600 0xc .fini ......... [13] PBIT WA- 0x10688 0x688 0x18 .data [16] NOBI WA- 0x1073c 0x73c 0x20 .bss ......... |
前出の nm(1) および dump(1) の例による出力は、セクション .init、.text、および .fini に対する関数 _init、foo、および _fini の関連付けを示しています。これらのセクションは読み取り専用であるため、テキストセグメントの一部です。
同様に、データ配列 data と bss は、それぞれセクション .data と .bss に関連付けられています。これらのセクションは書き込み可能であるため、データセグメントの一部です。
前出の dump(1) の表示は例のために簡素化されています。
アプリケーションがある共有オブジェクトを使用して構築される場合、そのオブジェクトの読み込み可能な内容全体が、実行時にそのプロセスの仮想アドレス空間に割り当てられます。共有オブジェクトを使用する各プロセスは、まずメモリー内にある共有オブジェクトの単一のコピーを参照します。
共有オブジェクト内の再配置は処理されて、シンボリック参照を該当する定義に結合します。これにより、共有オブジェクトがリンカーによって生成されたときには得られなかった真の仮想アドレスが計算されます。通常、これらの再配置によって、プロセスのデータセグメント内のエントリが更新されます。
メモリー管理スキーマは、プロセス間で共有オブジェクトの共有メモリーをページ細部のレベルで動的リンクするときの基本となります。メモリーページは、実行時に変更されていなければ共有できます。プロセスは、データ項目の書き込み時、または共有オブジェクトへの参照の再配置時に共有オブジェクトのページに書き込む場合、そのページの専用コピーを生成します。この専用コピーは、共有オブジェクトの他のユーザーに対して何も影響しません。ただし、このページは他のプロセス間での共有に伴う利点をすべて失います。この方法で変更されたテキストページは、「純粋でない」(impure) と呼ばれます。
メモリーに割り当てられた共有オブジェクトのセグメントは、2 つの基本的なカテゴリに分類されます。これは、読み取り専用の「テキスト」セグメントと、読み書き可能な「データ」セグメントです 。 ELF ファイルからこの情報を取得する方法については、ファイルの解析を参照してください。共有オブジェクトを開発するときの主要目的は、テキストセグメントを最大化して、データセグメントを最小化することにあります。これにより、共有オブジェクトの初期設定と使用に必要な処理の量を削減しながら、コード共有の量を最適化できます。次の節では、この目的を達成するために役立つメカニズムを示します。
オブジェクトを遅延読み込みするように設定すると、共有オブジェクトの依存関係の読み込みは、最初に参照されるまで延期できます。動的依存関係の遅延読み込みを参照してください。
小さいアプリケーションの場合、典型的な実行の流れでアプリケーションのすべての依存関係を参照する可能性があります。この場合、遅延読み込み可に設定されているかどうかに関係なく、アプリケーションはすべての依存関係を読み込みます。しかし、遅延読み込みでは依存関係の処理が処理の起動時から延期され、処理の実行期間全体にわたって広がります。
多くの依存関係を持つアプリケーションの場合、遅延読み込みを使用すると、一部の依存関係がまったく読み込まれないことがあります。特定の実行の流れで参照されない依存関係が、これにあたります。
コンパイラは、-K pic オプションによって、位置に依存しないコードを生成します。動的実行可能ファイル内のコードは、通常、メモリー内の固定アドレスに結合されていますが、位置に依存しないコードは、プロセスのアドレス空間内にある任意の場所に読み込みできます。このコードは、特定のアドレスに結合されていないため、それを使用する各プロセスの異なるアドレスでページ変更を行わなくても、正しく実行されます。このコードを使用してプログラムを作成すれば、実行時のページ変更が最も少なくて済みます。
位置に依存しないコードを使用すると、再配置可能な参照は、共有オブジェクトのデータセグメント内のデータを使用する間接参照として生成されます。テキストセグメントコードは読み取り専用のままになり、すべての再配置更新がデータセグメント内の対応するエントリに適用されます。これらの 2 つのセクションの使用方法については、大域オフセットテーブル (プロセッサ固有)と プロシージャのリンクテーブル (プロセッサ固有)を参照してください。
共有オブジェクトが位置に依存しないコードから構築される場合、テキストセグメントでは通常、実行時に大量の再配置を実行する必要があります。この再配置を処理するために実行時リンカーが用意されていますが、この処理によるシステムオーバーヘッドによって深刻な性能低下が生じるおそれがあります。
共有オブジェクトのうち、テキストセグメントに対して再配置を必要とするものを識別することができます。dump(1) を使用して、TEXTREL エントリの出力を調べます。次に例を示します。
$ cc -o libfoo.so.1 -G -R. foo.c $ dump -Lv libfoo.so.1 | grep TEXTREL [9] TEXTREL 0 |
TEXTREL エントリの値は不適切です。共有オブジェクトにこの値が存在する場合は、テキスト再配置があることを示しています。
テキスト再配置を含む共有オブジェクトの作成を防止するには、リンカーの-z text フラグを使用します。このフラグを使用すると、リンカーは、入力として使用された、位置に依存しないコード以外のコードのソースを示す診断を生成します。このようなコードは、意図した共有オブジェクトの生成に失敗します。次に例を示します。
$ cc -o libfoo.so.1 -z text -G -R. foo.c Text relocation remains referenced against symbol offset in file foo 0x0 foo.o bar 0x8 foo.o ld: fatal: relocations remain against allocatable but \ non-writable sections |
ファイル foo.o から位置に依存しないコード以外のコードが生成されたために、テキストセグメントに対して 2 つの再配置が生成されています。これらの診断は、可能な場合、再配置の実行に必要なシンボリック参照すべてを示します。この場合、再配置はシンボル foo と bar に対するものです。
共有オブジェクトの生成時にテキスト再配置が作成されるもう 1 つの一般的な原因は、位置に依存しない適切なプロトタイプによって符号化されていない手書きアセンブラコードを含めているというものです。
いくつかの単純なソースファイルをテストしながら、位置に依存しないコードを決定することもできます。中間アセンブラ出力を生成するコンパイラ機能を使用してください。
SPARC バイナリでは、-K pic オプションと -K PIC オプションの動作がわずかに違っており、大域オフセットテーブルエントリの参照方法が異なります。詳細は、大域オフセットテーブル (プロセッサ固有)を参照してください。
大域オフセットテーブルはポインタの配列で、エントリのサイズは、32 ビット (4 バイト) および 64 ビット (8 バイト) に固定です。-K pic を使用して生成される次のコード例は、エントリを参照します。
ld [%l7 + j], %o0 ! load &j into %o0 |
%l7 には、あらかじめ計算された参照元オブジェクトのシンボル _GLOBAL_OFFSET_TABLE_ の値が代入されます。
このコード例は、大域オフセットテーブルのエントリ用に 13 ビットの変位定数を提供します。つまり、この変位は、32 ビットのオブジェクトの場合は 2048 個の一意のエントリを提供し、64 ビットのオブジェクトの場合は 1024 個の一意のエントリを提供します。返されるエントリ数より多くのエントリを要求するオブジェクトの場合、リンカーは致命的なエラーを生成します。
$ cc -K pic -G -o lobfoo.so.1 a.o b.o ... z.o ld: fatal: too many symbols require `small' PIC references: have 2050, maximum 2048 -- recompile some modules -K PIC. |
このエラー状態を解決するには、入力再配置可能オブジェクトの一部をコンパイルするときに、-K PIC オプションを指定します。 このオプションは、32 ビットの定数を大域オフセットテーブルエントリに使用します。
sethi %hi(j), %g1 or %g1, %lo(j), %g1 ! get 32-bit constant GOT offset ld [%l7 + %g1], %o0 ! load &j into %o0 |
elfdump(1) を -G オプションと共に使用すれば、オブジェクトの大域オフセットテーブルの要件を調べることができます。リンカーのデバッグトークン -D got,detail を使用すれば、リンク編集中のこれらのエントリの処理を確認することもできます。
頻繁にアクセスするデータ項目に対しては、-K pic を使用する方法が有利です。どちらの方法でもエントリを参照することはできます。しかし、再配置可能オブジェクトをどちらの方法でコンパイルしたらいいのか決めるのには時間がかかる上、性能はわずかしか改善されません。すべての再配置型オブジェクトを -K PIC オプションを指定して再コンパイルする方が一般には簡単です。
構築中のオブジェクトによって使用されない関数やデータを保持することは無駄です。この対象物によってオブジェクトが肥大し、不必要な再配置のオーバーヘッドやそれに関連したページング動作が生じます。使用されない依存関係への参照も無駄です。これらの参照によって、他の共有オブジェクトの不必要な読み込みと処理が生じます。
リンカーのデバッギングトークン -D unused を使用すると、リンク編集中に使用されないセクションが表示されます。使用されていないと判断されたセクションは、リンク編集から削除するか、リンカーの -z ignore オプションを使用して排除すべきです。
次の場合に、リンカーは再配置可能オブジェクトのセクションを使用されていないと判断します。
セクションが割り当て可能
このセクションに結合 (再配置)する他のセクションがない
セクションが大域シンボルを提供しない
共有オブジェクトの外部インタフェースを定義することによって、リンカーのセクション排除能力を向上させることができます。インタフェースを定義することによって、インタフェースの一部として定義されなかった大域シンボルは局所シンボルになります。このような局所シンボルがほかのオブジェクトから参照されてもいない場合、排除の候補であると明確に識別されます。
関数やデータ変数が独自のセクションに割り当てられている場合、リンカーはこのような関数やデータ変数を個別に排除できます。このセクションの細分化は、-xF などのコンパイラオプションを使用して行います。以前のコンパイラには、関数を独自のセクションに割り当てる機能しかありませんでした。最近のコンパイラでは、-xF 構文が拡張されて、データ変数を独自のセクションに割り当てることができます。以前のコンパイラでは、-xF を使用するときには、C++ 例外処理を無効にする必要がありました。最近のコンパイラでは、この制限はなくなりました。
再配置可能オブジェクトの割り当て可能なセクションすべてが排除可能な場合、そのファイル全体がリンク編集から削除されます。
入力ファイルの排除に加えて、リンカーは使用されていない依存関係を判断できます。構築しているオブジェクトによって結合されていない場合、その依存関係は使用されていないと判断されます。-z ignore オプションを指定して構築したオブジェクトには、使用されていない依存関係は記録されません。
-z ignore オプションが適用されるのは、リンカーのコマンド行上でこのオプションの後に指定したファイルだけです。-z ignore オプションは -z record オプションで取り消すことができます。
基本システム で説明したように、共有オブジェクトのテキストセグメントだけが、それを使用するすべてのプロセスによって共有されます。オブジェクトのデータセグメントは、通常共有されません。共有オブジェクトを使用する各プロセスは、そのデータセグメント全体の専用メモリーコピーをそのセグメント内に書き込まれるデータ項目として生成します。データセグメントを削減するには、テキストセグメントに書き込まれることがないデータ要素を移動するか、またはデータ項目を完全に削除します。
次の節では、データセグメントのサイズを削減するために使用できるいくつかのメカニズムについて説明します。
読み取り専用のデータ要素はすべて、 const 宣言を使用して、テキストセグメントに移動する必要があります。たとえば、次の文字列は、書き込み可能なデータセグメントの一部である .data セクションにあります。
char * rdstr = "this is a read-only string"; |
これに対して、次の文字列は、テキストセグメント内にある読み取り専用データセクションである .rodata セクション内にあります。
const char * rdstr = "this is a read-only string"; |
読み取り専用要素をテキストセグメントに移動することによるデータセグメントの削減は目的に沿うものです。ただし、再配置を必要とするデータ要素を移動すると、逆効果になるおそれがあります。 たとえば、次の文字列配列があるとします。
char * rdstrs[] = { "this is a read-only string", "this is another read-only string" }; |
次の定義の方が良く思われるかもしれません。
const char * const rdstrs[] = { ..... }; |
この定義により、文字列とこれらの文字列へのポインタ配列は、確実に .rodata セクションに置かれます。ただし、ユーザーがアドレス配列を読み取り専用と認識しても、実行時にはこれらのアドレスを再配置しなければなりません。したがって、この定義では再配置が作成されます。この定義は次のように表わします。
const char * rdstrs[] = { ..... }; |
配列ポインタは、再配置できる書き込み可能なデータセグメント内に保持されます。配列文字列は、読み取り専用のテキストセグメント内に保持されます。
コンパイラによっては、位置に依存しないコードを生成するときに、実行時に再配置を行うことになる読み取り専用割り当てを検出できるものがあります。このようなコンパイラは、このような項目を書き込み可能なセグメントに配置します。たとえば、.picdata です。
多重定義されたデータを短縮すると、データを削減できます。同じエラーメッセージが複数回発生するプログラムの場合は、1 つの大域なデータを定義し、他のインスタンスすべてにこれを参照させると効率が良くなります。たとえば、次のように指定します。
const char * Errmsg = "prog: error encountered: %d"; foo() { ...... (void) fprintf(stderr, Errmsg, error); ...... |
この種のデータ削減に適した対象は文字列です。共有オブジェクトでの文字列の使用は、strings(1) を使用して調べることができます。次の例では、ファイル libfoo.so.1 内に、データ文字列のソートされたリストを生成します。このリスト内の各項目には、文字列の出現回数を示す接頭辞が付いています。
$ strings -10 libfoo.so.1 | sort | uniq -c | sort -rn |
データ項目用の常時記憶領域は、関連する機能が自動 (スタック) 変数を使用するように設計できる場合、完全に削除することができます。常時記憶領域を少しでも削除すると、通常これに対応して、必要な実行時再配置の数も減ります。
大きなデータバッファーは、通常、常時記憶領域を使用して定義するのではなく、動的に割り当てる必要があります。これにより、アプリケーションの現在の呼び出しで必要なバッファーだけが割り当てられるため、メモリー全体を節約できます。動的割り当てを行うと、互換性に影響を与えることなくバッファーのサイズを変更できるため、柔軟性も増します。
新しいページにアクセスするすべてのプロセスでページフォルトが発生します。これはコストのかかる操作です。共有オブジェクトは多数のプロセスで使用できるため、共有オブジェクトへのアクセスによって生成されるページフォルトの数を減らすと、プロセスおよびシステム全体の効率が改善されます。
使用頻度の高いルーチンとそのデータを隣接するページの集合として編成すると、参照の効率が良くなるため、性能は通常向上します。あるプロセスがこれらの関数の 1 つを呼び出すとき、この関数がすでにメモリー内にある場合があります。これは、この関数が、使用頻度の高い他の関数のすぐ近くに存在するためです。同様に、相互に関連する関数をグループ化すると、参照効率が向上します。たとえば、関数 foo() への呼び出しによって、常に関数 bar() が呼び出される場合は、これらの関数を同じページ上に置きます。cflow(1)、tcov(1)、prof(1)、および gprof(1) は、コードカバレージとプロファイリングを判定するために役立ちます。
関連する機能は、各自の共有オブジェクトに分離してください。標準 C ライブラリは従来、関連しない多数の関数を含んで構築されていました。たとえば、単一の実行可能ファイルがこのライブラリ内のすべてを使用することはほとんどありません。このライブラリは広範囲に使用されるため、実際に使用頻度の最も高い関数がどれかを判定することもかなり困難です。これに対して、共有オブジェクトを最初から設計する場合は、関連する関数だけを共有オブジェクト内に保持してください。これにより、参照ローカリティが改善するだけでなく、オブジェクト全体のサイズを減らすという効果も得られます。
再配置処理 では、実行時リンカーが動的実行可能ファイルと共有オブジェクトを再配置して、実行可能プロセスを作成するためのメカニズムについて説明しました。シンボルの検索と 再配置が実行されるときは、この再配置処理を 2 つの領域に分類して、関連のメカニズムを簡素化して説明しています。これらの 2 つのカテゴリは、再配置による性能への影響を考慮するためにも最適です。
実行時リンカーは、シンボルを検索する必要がある場合、デフォルトでは各オブジェクトを検索して検索を行います。実行時リンカーは、まず動的実行可能ファイルから始めて、オブジェクトが読み込まれるのと同じ順序で各共有オブジェクトへと進みます。ほとんどの場合、シンボル再配置を必要とする共有オブジェクトは、シンボル定義の提供者になります。
この状況では、この再配置に使用されるシンボルが共有オブジェクトのインタフェースの一部として必要ではない場合、このシンボルは静的変数または自動変数に変換される可能性が高くなります。シンボル削減は、共有オブジェクトのインタフェースから削除されたシンボルにも適用できます。詳細については、シンボル範囲の縮小を参照してください。これらの変換を行うことによって、リンカーは、共有オブジェクトの作成中にこれらのシンボルに対するシンボル再配置を処理しなければならなくなります。
共有オブジェクトから表示できなければならない唯一の大域データ項目は、そのユーザーインタフェースに関するものです。しかし、大域データは異なる複数のソースファイルにある複数の関数から参照できるように定義されていることが多いため、これは歴史的に達成が困難です。シンボルの縮小を適用することによって、不要な大域シンボルを削除できます。シンボル範囲の縮小を参照してください。共有オブジェクトからエクスポートされた大域シンボルの数を少しでも減らせば、再配置のコストを削減し、性能全体を向上させることができます。
直接結合を使用すると、多数のシンボル再配置や依存関係を伴う動的プロセスでのシンボル検索のオーバーヘッドを大幅に削減できます。直接結合を参照してください。
すべての即時参照再配置は、アプリケーションが制御を取得する前の、プロセスの初期設定中に実行する必要があります。これに対して、遅延参照は、関数の最初のインスタンスが呼び出されるまで延期できます。即時参照は通常、データ参照によって行われます。このため、データ参照の数を少なくすることによって、プロセスの実行時初期設定も削減されます。
初期設定再配置コストは、データ参照を関数参照に変換して延期することもできます。たとえば、機能インタフェースによってデータ項目を返すことができます。この変換を行うと、初期設定再配置コストがプロセスの実行期間中に効率的に分配されるため、性能は明らかに向上します。いくつかの機能インタフェースはプロセスの特定の呼び出しでは決して呼び出されない可能性もあるため、それらの再配置オーバーヘッドもすべてなくなります。
機能インタフェースを使用した場合の利点については、コピー再配置で説明します。この節では、動的実行可能ファイルと共有オブジェクトの間で使用される特殊でコストのかかる再配置メカニズムについて説明します。また、この再配置によるオーバーヘッドを回避する方法の例も示します。
再配置は、デフォルトでは、適用対象のセクションによってグループ化されます。ただし、オブジェクトを-z combreloc オプションによって構築すると、プロシージャのリンクテーブル再配置を除くすべてが、.SUNW_reloc という単一の共通セクションに置かれます。プロシージャのリンクテーブル (プロセッサ固有)を参照してください。
この方法で再配置レコードを結合すると、すべての RELATIVE 再配置を 1 つにグループ化できます。すべてのシンボルの再配置は、シンボル名によって並べ替えられます。RELATIVE 再配置をグループ化すると、DT_RELACOUNT/DT_RELCOUNT .dynamic エントリを使用した最適な実行時処理が行われます。シンボルのエントリを並べ替えると、実行時にシンボルを検索する時間を削減できます
共有オブジェクトは、通常、位置に依存しないコードによって構築されます。このタイプのコードから外部データ項目への参照は、1 組のテーブルによる間接アドレス指定を使用します。詳細については、位置に依存しないコード を参照してください。これらのテーブルは、データ項目の実アドレスによって実行時に更新されます。これらの更新されたテーブルによって、コード自体を変更することなくデータにアクセスすることができます。
ただし、動的実行可能ファイルは通常、位置に依存しないコードからは作成されません。これらのファイルが作成する外部データへの参照は、その参照を行うコードを変更することによって実行時にしか実行できないように見えます。読み取り専用のテキストセグメントの変更は、回避する必要があります。コピー再配置という再配置手法が、この参照を解決するために使用されます。
動的実行可能ファイルを作成するためにリンカーが使用され、データ項目への参照が依存共有オブジェクトのどれかに常駐するとします。動的実行可能ファイルの .bss で、共有オブジェクト内のデータ項目のサイズに等しいスペースが割り当てられます。このスペースには、共有オブジェクトに定義されているのと同じシンボリック名も割り当てられます。リンカーは、このデータ割り当てとともに特殊なコピー再配置レコードを生成して、実行時リンカーに対し、共有オブジェクトから動的実行可能ファイル内のこの割り当てスペースへデータをコピーするように指示します。
このスペースに割り当てられたシンボルは大域であるため、すべての共有オブジェクトからのすべての参照を満たすために使用されます。動的実行可能ファイルは、データ項目を継承します。この項目を参照するプロセス内の他のオブジェクトすべてが、このコピーに結合されます。コピーの元となるデータは使用されなくなります。
このメカニズムの次の例では、標準 C ライブラリ内で保持されるシステムエラーメッセージの配列を使用します。SunOS オペレーティングシステムの以前のリリースでは、この情報へのインタフェースが、2 つの大域変数 sys_errlist[] および sys_nerr によって提供されました。最初の変数はエラーメッセージ文字列を提供し、2 つ目の変数は配列自体のサイズを示しました。これらの変数はアプリケーション内で、通常次のように使用されていました。
$ cat foo.c extern int sys_nerr; extern char * sys_errlist[]; char * error(int errnumb) { if ((errnumb < 0) || (errnumb>= sys_nerr)) return (0); return (sys_errlist[errnumb]); } |
アプリケーションは、関数 error を使用して、番号 errnumb に対応するシステムエラーメッセージを取得します。
このコードを使用して作成された動的実行可能ファイルを調べると、コピー再配置の実装が更に詳細に示されます。
$ cc -o prog main.c foo.c $ nm -x prog | grep sys_ [36] |0x00020910|0x00000260|OBJT |WEAK |0x0 |16 |sys_errlist [37] |0x0002090c|0x00000004|OBJT |WEAK |0x0 |16 |sys_nerr $ dump -hv prog | grep bss [16] NOBI WA- 0x20908 0x908 0x268 .bss $ dump -rv prog **** RELOCATION INFORMATION **** .rela.bss: Offset Symndx Type Addend 0x2090c sys_nerr R_SPARC_COPY 0 0x20910 sys_errlist R_SPARC_COPY 0 .......... |
リンカーは、動的実行可能ファイルの .bss にスペースを割り当てて、sys_errlist および sys_nerr によって表わされるデータを受け取っています。これらのデータは、プロセス初期設定時に、実行時リンカーによって C ライブラリからコピーされます。このため、これらのデータを使用する各アプリケーションは、データの専用コピーを各自のデータセグメントで取得します。
この手法には、実際には 2 つの欠点があります。まず、各アプリケーションでは、実行時のデータコピーによるオーバーヘッドによって性能が低下します。もう 1 つは、データ配列 sys_errlist のサイズが、C ライブラリのインタフェースの一部になるという点です。新しいエラーメッセージが追加されるなど、この配列のサイズが変わったとします。この配列を参照する動的実行可能ファイルすべてで、新しいエラーメッセージにアクセスするための新しいリンク編集を行う必要があります。この新しいリンク編集が行われないと、動的実行可能ファイル内の割り当てスペースが不足して、新しいデータを保持できません。
このような欠点は、動的実行可能ファイルに必要なデータが機能インタフェースによって提供されればなくなります。ANSI C 関数 strerror(3C) は、提示されたエラー番号に基づいて該当するエラー文字列へのポインタを返します。この関数の実装状態は次のようになります。
$ cat strerror.c static const char * sys_errlist[] = { "Error 0", "Not owner", "No such file or directory", ...... }; static const int sys_nerr = sizeof (sys_errlist) / sizeof (char *); char * strerror(int errnum) { if ((errnum < 0) || (errnum>= sys_nerr)) return (0); return ((char *)sys_errlist[errnum]); } |
foo.c のエラールーチンは、ここではこの機能インタフェースを使用するように単純化できます。これによって、プロセス初期設定時に元のコピー再配置を実行する必要がなくなります。
また、データは共有オブジェクト限定のものであるため、そのインタフェースの一部ではなくなります。したがって、共有オブジェクトは、データを使用する動的実行可能ファイルに悪影響を与えることなく、自由にデータを変更できます。共有オブジェクトのインタフェースからデータ項目を削除すると、一般に共有オブジェクトのインタフェースとコードが維持しやすくなるとともに、性能も向上します。
ldd(1) に -d オプションまたは - r オプションのどちらかをつけて使用すると、動的実行可能ファイル内にコピー再配置があるかどうかを検査できます。
たとえば、動的実行可能ファイル prog が当初、次の 2 つのコピー再配置が記録されるように、共有オブジェクト libfoo.so.1 に対して構築されている場合を考えます。
$ nm -x prog | grep _size_ [36] |0x000207d8|0x40|OBJT |GLOB |15 |_size_gets_smaller [39] |0x00020818|0x40|OBJT |GLOB |15 |_size_gets_larger $ dump -rv size | grep _size_ 0x207d8 _size_gets_smaller R_SPARC_COPY 0 0x20818 _size_gets_larger R_SPARC_COPY 0 |
これらのシンボルについて異なるサイズを含む、この共有オブジェクトの新しいバージョンが提供されているとします。
$ nm -x libfoo.so.1 | grep _size_ [26] |0x00010378|0x10|OBJT |GLOB |8 |_size_gets_smaller [28] |0x00010388|0x80|OBJT |GLOB |8 |_size_gets_larger |
動的実行可能ファイルに対して ldd(1) を実行すると、次のように表示されます。
$ ldd -d prog libfoo.so.1 => ./libfoo.so.1 ........... copy relocation sizes differ: _size_gets_smaller (file prog size=40; file ./libfoo.so.1 size=10); ./libfoo.so.1 size used; possible insufficient data copied copy relocation sizes differ: _size_gets_larger (file prog size=40; file ./libfoo.so.1 size=80); ./prog size used; possible data truncation |
ldd(1) は、動的実行可能ファイルが、共有オブジェクトが提供しなければならないデータすべてをコピーするけれども、その割り当てスペースで許容できる量しか受け付けないということを知らせています。
位置に依存しないコードだけでアプリケーションを作成すれば、コピー再配置を完全に排除することができます。位置に依存しないコードを参照してください。
リンカーの-B symbolic オプションを使用すると、シンボルの参照を共有オブジェクト内の大域定義に結合できます。このオプションは、実行時リンカーそのものを作成するために設計されたという意味で、長い歴史があるといえます。
-B symbolic オプションを使用するときは、オブジェクトのインタフェースを定義し、非公開シンボルをローカルに縮小する必要があります。シンボル範囲の縮小を参照してください。-B symbolic を使用すると、直感的にはわからない副産物ができることがあります。
シンボリックに結合されたシンボルが割り込み (interposition) された場合、シンボリックに結合されたオブジェクトの外からのそのシンボルへの参照は、その割り込みに結合します。オブジェクトそのものはすでに内部的に結合されています。本質的に、同じ名前を持つ 2 つのシンボルは、プロセス内から参照されます。シンボリックに結合されたデータシンボルは、コピーを再配置し、同じ割り込み状態を作成します。コピー再配置を参照してください。
シンボリックに結合された共有オブジェクトは、.dynamic フラグ DF_SYMBOLIC で表されます。このタグは情報を提供するだけです。実行時リンカーは、これらのオブジェクトからのシンボルの検索を他のオブジェクトからの場合と同じ方法で処理します。シンボリック結合はリンカーフェーズで作成されたものと想定されます。
実行時リンカーは、アプリケーションの実行中に処理された共有オブジェクトすべてのプロファイリング情報を生成できます。実行時リンカーは、共有オブジェクトをアプリケーションに結合しなくてはならないため、すべての大域関数結合を横取りすることができます。これらの結合は、.plt エントリによって起こります。このメカニズムの詳細は、再配置が実行されるときを参照してください。
LD_PROFILE
環境変数には、プロファイルの対象とする共有オブジェクトの名前を指定します。この環境変数を使用すると、一度に
1 つの共有オブジェクトを解析できます。環境変数の設定は、1 つまたは複数のアプリケーションによる共有オブジェクトの使用を解析するために使用できます。次の例では、コマンド ls(1) の 1 回の呼び出しによる libc の使用が解析されます。
$ LD_PROFILE=libc.so.1 ls -l |
次の例では、環境変数の設定は構成ファイルに記録されます。この設定によって、アプリケーションが libc を使用するたびに、解析情報が蓄積されます。
# crle -e LD_PROFILE=libc.so.1 $ ls -l $ make $ ... |
プロファイリングが有効な場合、プロファイルデータファイルがなければ、ファイルが作成されます。このファイルは、実行時リンカーに割り当てられます。上記の例で、このデータファイルは /var/tmp/libc.so.1.profile です。64 ビットライブラリは、拡張プロファイル形式を必要とし、.profilex 接尾辞を使用して書かれます。代替ディレクトリを指定して、環境変数 LD_PROFILE_OUTPUT
によってプロファイルデータを格納することもできます。
このプロファイルデータファイルは、profil(2) データを保存して、指定の共有オブジェクトの使用に関連するカウント情報を呼び出すために使用されます。このプロファイルデータは、gprof(1) によって直接調べることができます。
gprof(1) は通常、cc(1) の -xpg オプションを使用してコンパイルされた実行可能ファイルにより作成された、gmon.out プロファイルデータを解析するために使用されます。実行時リンカーのプロファイル解析では、このオプションによってコードをコンパイルする必要はありません。依存共有オブジェクトがプロファイルされるアプリケーションは、profil(2) に対して呼び出しを行うことができません。これは、このシステム呼び出しでは、同じプロセス内で複数の呼び出しが行われないためです。同じ理由から、cc(1) の -xpg オプションによって、これらのアプリケーションをコンパイルすることもできません。このコンパイラによって生成されたプロファイリングのメカニズムが profil(2) の上にも構築されます。
このプロファイリングメカニズムの最も強力な機能の 1 つに、複数のアプリケーションに使用される共有オブジェクトの解析があります。通常、プロファイリング解析は、1 つまたは 2 つのアプリケーションを使用して実行されます。しかし共有オブジェクトは、その性質上、多数のアプリケーションで使用できます。これらのアプリケーションによる共有オブジェクトの使用方法を解析すると、共有オブジェクトの全体の性能を向上させるには、どこに注意すべきかを理解できます。
次の例は、ソース階層内でいくつかのアプリケーションを作成したときの libc の性能解析を示しています。
$ LD_PROFILE=libc.so.1 ; export LD_PROFILE $ make $ gprof -b /usr/lib/libc.so.1 /var/tmp/libc.so.1.profile ..... granularity: each sample hit covers 4 byte(s) .... called/total parents index %time self descendents called+self name index called/total children ..... ----------------------------------------------- 0.33 0.00 52/29381 _gettxt [96] 1.12 0.00 174/29381 _tzload [54] 10.50 0.00 1634/29381 <external> 16.14 0.00 2512/29381 _opendir [15] 160.65 0.00 25009/29381 _endopen [3] [2] 35.0 188.74 0.00 29381 _open [2] ----------------------------------------------- ..... granularity: each sample hit covers 4 byte(s) .... % cumulative self self total time seconds seconds calls ms/call ms/call name 35.0 188.74 188.74 29381 6.42 6.42 _open [2] 13.0 258.80 70.06 12094 5.79 5.79 _write [4] 9.9 312.32 53.52 34303 1.56 1.56 _read [6] 7.1 350.53 38.21 1177 32.46 32.46 _fork [9] .... |
特殊名 <external> は、プロファイル中の共有オブジェクトのアドレス範囲外からの参照を示しています。したがって、上記の例では、1634 は、動的実行可能ファイル、または プロファイル解析の進行中に libc によって結合された他の共有オブジェクトから発生した libc 内の関数 open(2) を呼び出しています。
共有オブジェクトのプロファイルは、マルチスレッド化に対し安全です。ただし、あるスレッドがプロファイルデータ情報を更新しているときに、もう 1 つのスレッドが fork(2) を呼び出す場合は例外です。fork1(2) を使用すると、この制限はなくなります。