リンカーとライブラリ

第 4 章 共有オブジェクト

概要

共有オブジェクトは、リンカーによって作成される出力形式の 1 つであり、-G オプションを指定して生成されます。次に例を示します。


$ cc -o libfoo.so.1 -G -K pic foo.c

ここで、共有オブジェクト libfoo.so.1 は、入力ファイル 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 は付けた方が望ましく、一連のソフトウェアリリースで共有オブジェクトの正しい結合を行うためにはバージョン番号も必要です。


注 -

dlopen(3DL) で使用される共有オブジェクト名は通常、単純ファイル名として表わされます。つまり、名前に `/' が付きません。この規則によって、実行時リンカーは、実際のファイルを検索するための規則を自由に使用できます (詳細は、「追加オブジェクトの読み込み」を参照)。


第 5 章「バージョンアップ」では、一連のソフトウェアリリースで共有オブジェクトインタフェースのバージョンアップという概念についてさらに詳しく説明します。また、コンパイル環境と実行時環境の両方で使用される共有オブジェクト間の命名規約を調整するためのメカニズムについても説明します。ただし最初に、共有オブジェクトが各自の実行時名を記録するためのメカニズムを説明しておきます。

共有オブジェクト名の記録

動的実行可能ファイルまたは共有オブジェクトでの「依存関係」の記録は、デフォルトでは、関連する共有オブジェクトがリンカーによって参照されるときのファイル名になります。たとえば、次の動的実行可能ファイルは、同じ共有オブジェクト 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 オプションを使用すると、その実行時名を共有オブジェクト自体に記録できます。次に例を示します。


$ cc -o ../tmp/libfoo.so -G -K pic -h libfoo.so.1 foo.c 

ここで、共有オブジェクトの実行時名 libfoo.so.1 は、ファイル自体に記録されます。この識別名は soname と呼ばれ、その記録は dump(1) を使用し、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: file ./libbar.so: recording name `libsame.so.1' ¥
           matches that provided by file ./libfoo.so
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: file ./libfoo.so: recording name `libsame.so.1'  ¥
           matches that supplied with -h option
ld: fatal: File processing errors. No output written to libbar.so

依存関係を持つ共有オブジェクト

これまでこの章で示した例のほとんどは、共有オブジェクトの依存関係が動的実行可能ファイル内でどのように維持されるかを示していますが、共有オブジェクトが独自の依存関係を持つことは非常に一般的です (詳細は、「共有オブジェクトの処理」を参照)。

「実行時リンカーによって検索されるディレクトリ」では、共有オブジェクトの依存関係を検索するために実行時リンカーが使用する検索規則について説明しています。共有オブジェクトがデフォルトディレクトリの /usr/lib (32 ビットオブジェクトの場合)、または /usr/lib/64 (64 ビットオブジェクトの場合) にないときは、実行時リンカーに対して検索場所を明示的に指示する必要があります。この種の条件を指示するために優先されるメカニズムは、リンカーの -R オプションを使用して、依存関係を持つオブジェクトに「実行パス」を記録するというものです。次に例を示します。


$ 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]     RPATH    /home/me/lib
.........

ここで、共有オブジェクト libfoo.so は、libbar.so に対する依存関係を持ちます。これは、実行時にディレクトリ /home/me/lib にあるものと予期されますが、ない場合はデフォルト位置の /usr/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 を構築した場合には、設定した条件は処理順序に影響を与えません。

シンボルの割り込み (「シンボルの検索」「シンボル検索」、および 「割り込み (interposition) の使用」を参照) と .init セクションの処理 (「デバッギングエイド」を参照) を特に重要視する開発者は、共有オブジェクトの処理順序でのこのような変更の可能性に注意する必要があります。

フィルタとしての共有オブジェクト

フィルタとは、代替共有オブジェクトへの間接参照を提供するために使用される特殊な形式の共有オブジェクトのことをいいます。2 つの形式の共有オブジェクトフィルタがあります。

標準フィルタは、基本的に単一のシンボルテーブルからなり、実行時環境からコンパイル環境を抽象化するメカニズムを提供します。このフィルタを使用するリンク編集は、フィルタ自体によって提供されるシンボルを参照しますが、シンボル参照の解釈は、実行時に代替ソースから提供されます。

標準フィルタは、リンカーの -F フラグによって識別されます。このフラグは、実行時にシンボル参照を与える共有オブジェクトを示す関連ファイル名をとります。この共有オブジェクトは、フィルティー (フィルタ対象) と呼ばれます。-F フラグを複数回使用すると、複数のフィルティーを記録できます。

フィルティーを実行時理できないか、またはフィルタによって定義されたシンボルがフィルティー内に見つからない場合、そのフィルタは無視されて、シンボル解決は次に関連する依存関係に続けられます。

補助フィルタも同様のメカニズムを備えていますが、フィルタ自体にそのシンボルに対応する実装が含まれます。フィルタを使用するリンク編集ではフィルタ自体によって提供されたシンボルを参照しますが、シンボル参照の実装は、実行時に代替ソースから提供できます。

補助フィルタは、リンカーの -f フラグを使用して識別されます。このフラグは、実行時にシンボルを与えるために使用できる共有オブジェクトを示す関連ファイル名をとります。この共有オブジェクトは、フィルティーと呼ばれます。-f フラグを複数回使用すると、複数のフィルティーを記録できます。

フィルティーを実行時に処理できないか、またはフィルティー内にフィルタが見つからないと、フィルタ内のシンボルの実装が使用されます。

標準フィルタの生成

標準フィルタを生成するには、まずフィルティー libbar.so.1 を定義し、それに対してこのフィルタ手法を適用します。このフィルティーは、いくつかの再配置可能オブジェクトから構築される場合があります。これらのオブジェクトの 1 つは、ファイル bar.c から発生し、シンボル foobar を与えます。


$ 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 は、シンボル foobar に対して生成されて、フィルティー libbar.so.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

注 -

ここで、環境変数 LD_OPTIONS は、このコンパイラドライバが -F オプションをそれ自体のオプションの 1 つとして解釈しないようにするために使用されています。


リンカーは、標準フィルタ libfoo.so.1 を参照して動的実行可能ファイルまたは共有オブジェクトを作成する場合、シンボル解決中にフィルタシンボルテーブルからの情報を使用します (詳細は、「シンボル解析」を参照)。

実行時に、フィルタのシンボルを参照すると、必ずフィルティー libbar.so.1 がさらに読み込まれます。実行時リンカーは、このフィルティーを使用して、libfoo.so.1 によって定義されたシンボルを解釈処理します。

たとえば、次の動的実行可能ファイル prog は、シンボル foobar を参照します。これらは、リンク編集中にフィルタ 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/usr/lib/libdl.so.1 の 2 つがあります。

最初のフィルタは、標準 C ライブラリ /usr/lib/libc.so.1 のサブセットになります。このサブセットは、準拠するアプリケーションによってインポートしなければならない C ライブラリ内の ABI に準拠する関数とデータ項目を表わします。

2 つめのフィルタは、実行時リンカー自体へのユーザーインタフェースを定義します。このインタフェースは、コンパイル環境で (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 クラスを使って、作成するオブジェクトのクラスを管理します (「32 ビットおよび 64 ビット環境」を参照)。64 ビットフィルタをマップファイルだけから作成するには、リンカーに -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 が、シンボル foobar に対して生成されて、フィルティー libbar.so.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

注 -

ここで、環境変数 LD_OPTIONS は、このコンパイラドライバが -f オプションをそれ自体のオプションの 1 つとして解釈しないようにするために使用されています。


リンカーは、補助フィルタ libfoo.so.1 を参照して動的実行可能ファイルまたは共有オブジェクトを作成する場合、シンボル解決中、フィルタシンボルテーブルの情報を使用します (詳細については、「シンボル解析」を参照)。

実行時にフィルタのシンボルを参照すると、フィルティー libbar.so.1 が検索されます。このフィルティーが見つかると、実行時リンカーは、このフィルティーを使用して、libfoo.so.1 によって定義されたすべてのシンボルを解釈処理します。このフィルティーが見つからないか、またはフィルティーにフィルタからのシンボルがない場合は、フィルタ内のシンボルの元の値が使用されます。

たとえば、次の動的実行可能ファイル prog は、シンボル foobar を参照します。これらのシンボルは、フィルタ 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 で使用されて、プラットフォーム固有の共有オブジェクト内に最適化された機能を提供します。例は、「命令セット固有の共有オブジェクト」および 「プラットフォーム固有の共有オブジェクト」を参照してください。

フィルティーの処理

実行時リンカーによるフィルタ処理は、フィルタ内のシンボルへの参照が生じるまで、フィルティーの読み込みを延期します。この実装は、各フィルティーに対して必要に応じて dlopen(3DL) を実行するフィルタに似ています。この実装は、ldd(1) などのツールによって生じる可能性がある、依存関係の報告における違いの原因となるものです。

フィルタを作成して、そのフィルティーを実行時に即時処理する場合は、リンカーの -z loadfltr オプションを使用できます。また、プロセス内のフィルタすべての即時処理は、どの値にも環境変数 LD_LOADFLTR を設定することによってトリガーすることもできます。

性能に関する考慮事項

共有オブジェクトは、同じシステム内の複数のアプリケーションで使用できます。したがって、共有オブジェクトの性能の影響は、それを使用するアプリケーションだけでなく、システム全体に至るほどに大きくなる可能性があります。

共有オブジェクト内の実際のコードは、実行中のプロセスの性能に直接影響しますが、ここでは共有オブジェクト自体の実行時処理に焦点を絞って性能の問題を説明します。次の節では、再配置によるオーバーヘッドとともに、テキストサイズや純度 (purity) などの面についても見ながら、この処理について詳しく説明します。

役に立つツール

性能について述べる前に、使用可能なツールと ELF ファイルの内容の解析に対するその使用法を理解しておくと役立ちます。

ELF ファイルに定義されているセクションまたはセグメントのいずれかのサイズに対する参照は、頻繁に行われます (ELF 形式の詳細については、第 7 章「オブジェクトファイル」を参照)。ファイルのサイズは、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) .....

最初の例は、SunOSTM オペレーティングシステムの以前のリリースから引き続き使用されてきたカテゴリである、共有オブジェクトテキスト、データ、および 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 に対する関数 _initfoo、および _fini の関連付けが見られます。これらのセクションは読み取り専用であるため、テキストセグメントの一部です。

同様に、データ配列 databss は、それぞれセクション .data.bss に関連付けられています。これらのセクションは書き込み可能であるため、データセグメントの一部です。


注 -

前出の dump(1) の表示は例のために簡素化されています。


ここで説明したツール情報を利用すると、生成するすべての ELF ファイル内でのコードおよびデータの位置を解析できます。この知識は、次の節での説明を理解する上で役立ちます。

基本システム

アプリケーションがある共有オブジェクトを使用して構築される場合、そのオブジェクトの読み込み可能な内容全体が、実行時にそのプロセスの仮想アドレス空間に割り当てられます。共有オブジェクトを使用する各プロセスは、まずメモリー内にある共有オブジェクトの単一のコピーを参照します。

共有オブジェクト内の再配置は処理されて、シンボリック参照を該当する定義に結合します。これにより、共有オブジェクトがリンカーによって生成されたときには得られなかった真の仮想アドレスが計算されます。通常、これらの再配置によって、プロセスのデータセグメント内のエントリが更新されます。

メモリー管理スキーマは、プロセス間で共有オブジェクトの共有メモリーをページ細部のレベルで動的リンクするときの基本となります。メモリーページは、実行時に変更されていなければ共有できます。プロセスは、データ項目の書き込み時、または共有オブジェクトへの参照の再配置時に共有オブジェクトのページに書き込む場合、そのページの専用コピーを生成します。この専用コピーは共有オブジェクトの他のユーザーに対して何も影響しませんが、このページは、他のプロセス間での共有に伴う利点をすべて失います。この方法で変更されたテキストページは、「純粋でない」(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 つの再配置が生成されています。これらの診断は、可能な場合、再配置の実行に必要なシンボリック参照すべてを示します。この場合、再配置はシンボル foobar に対するものです。

-K pic オプションを使用しないという原因以外で、共有オブジェクトの生成時にテキスト再配置が作成される最も一般的な原因は、位置に依存しない適切なプロトタイプによって符号化されていない手書きアセンブラコードを含めているというものです。


注 -

中間アセンブラファイルを生成するコンパイラ機能を使用すると、いくつかの単純なテストケースソースファイルを試すことによって、位置からの独立性を有効にするために使用されるコーディング手法がわかります。


プロセッサによっては、位置に依存しないフラグの 2 つ目の形式として -K PIC も使用できます。このフラグを使用すると、追加コードによるオーバーヘッドと引き換えに、より多数の再配置を処理できます (詳細については、cc(1) を参照)。

共有可能性の最大化

「基本システム」で説明したように、共有オブジェクトのテキストセグメントだけがそれを使用するすべてのプロセスによって共有され、データセグメントは通常共有されません。共有オブジェクトを使用する各プロセスは、通常、そのデータセグメント全体の専用メモリーコピーをそのセグメント内に書き込まれるデータ項目として生成します。データセグメントを削減するにはテキストセグメントに書き込まれることがないデータ要素を移動するか、またはデータ項目を完全に削除します。

次の節では、データセグメントのサイズを削減するために使用できるいくつかのメカニズムについて説明します。

テキストへの読み取り専用データの移動

読み取り専用のデータ要素はすべて、テキストセグメントに移動する必要があります。これは、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) を使用して調べることができます。次に例を示します。


$ strings -10 libfoo.so.1 | sort | uniq -c | sort -rn  

上記のコードは、ファイル libfoo.so.1 内に、データ文字列のソートされたリストを生成します。このリスト内の各項目には、文字列の出現回数を示す接頭辞が付いています。

自動変数の使用

データ項目用の常時記憶領域は、関連する機能が自動 (スタック) 変数を使用するように設計できる場合、完全に削除することができます。常時記憶領域を少しでも削除すると、通常これに対応して、必要な実行時再配置の数も減ります。

バッファーの動的割り当て

大きなデータバッファーは、通常、常時記憶領域を使用して定義するのではなく、動的に割り当てる必要があります。これにより、アプリケーションの現在の呼び出しで必要なバッファーだけが割り当てられるため、メモリー全体を節約できます。動的割り当てを行うと、互換性に影響を与えることなくバッファーのサイズを変更できるため、柔軟性も増します。

ページング回数の削減

「共有可能性の最大化」で説明したメカニズムの多くは、共有オブジェクトを使用するときに生じるページングを削減するために役立ちます。ここでは、一般的なソフトウェア性能に関する考慮事項のいくつかについてさらに説明します。

新しいページにアクセスするすべてのプロセスでページフォルトが発生します。これはコストのかかる操作であり、また共有オブジェクトは多数のプロセスで使用できるため、共有オブジェクトへのアクセスによって生成されるページフォルトの数を減らすと、プロセスおよびシステム全体の効率が改善されます。

使用頻度の高いルーチンとそのデータを隣接するページの集合として編成すると、参照の効率が良くなるため、性能は通常向上します。あるプロセスがこれらの関数の 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) は、動的実行可能ファイルが、共有オブジェクトが提供しなければならないデータすべてをコピーするけれども、その割り当てスペースで許容できる量しか受け付けないということを知らせています。


注 -

位置に依存しないコードだけでアプリケーションを作成すれば、コピー再配置を完全に排除することができます (「位置に依存しないコード」を参照)。


-Bsymbolic の使用

リンカーの -Bsymbolic オプションを使用すると、シンボルの参照を共有オブジェクト内の大域定義に結合できます。このオプションは本来、実行時リンカーそのものを作成するために設計されたもので、長い歴史があるといえます。

オブジェクトのインタフェースを定義し、非公開シンボルをローカルに縮小することは、-Bsymbolic オプションを使用した実行時再配置のコストを削減する望ましい仕組みです (「シンボル範囲の縮小」を参照)。実際に、-Bsymbolic を使用すると直感的にはわからない副産物のできることがあります。

シンボリックに結合されたシンボルが割り込み (interposition) された場合、シンボリックに結合されたオブジェクトの外からのそのシンボルへの参照は、その割り込みに結合します。オブジェクトそのものはすでに内部的に結合されています。本質的に、同じ名前を持つ 2 つのシンボルは、プロセス内から参照されます。シンボリックに結合されたデータシンボルは、コピーを再配置し (「コピー再配置」を参照)、同じ割り込み状態を作成します。


注 -

シンボリックに結合された共有オブジェクトは、.dynamic エントリ DT_SYMBOLIC で表されます。このタグは情報を提供するだけで、実行時リンカーは、これらのオブジェクトからのシンボルの検索を他のオブジェクトからの場合と同じ方法で処理します。シンボリック結合はリンカーフェーズで作成されたものと想定されます。


共有オブジェクトのプロファイリング

実行時リンカーは、アプリケーションの実行中に処理された共有オブジェクトすべてのプロファイリング情報を生成できます。これが可能となるのは、実行時リンカーが、共有オブジェクトをアプリケーションに結合しなければなないためであり、したがって、実行時リンカーはすべての大域関数割り当てを傍受できます (これらの割り当ては、.plt エントリによって起こります。このメカニズムの詳細は、「再配置が実行されるとき」を参照)。

共有オブジェクトのプロファイルは、環境変数 LD_PROFILE でその名前を指定することによって有効になります。この環境変数を使用すると、一度に 1 つの共有オブジェクトを解析できます。ただし、環境変数の設定は、1 つまたは複数のアプリケーションによる共有オブジェクトの使用を解析するために使用できます。次の例では、コマンド ls(1) の 1 回の呼び出しによる libc の使用が解析されています。


$ LD_PROFILE=libc.so.1 ls -l

次の例では、環境変数の設定によって、アプリケーションが libc を使用するたびに、環境変数の設定期間に関する解析情報が蓄積されます。


$ LD_PROFILE=libc.so.1; export LD_PROFILE
$ 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) を使用すると、この制限はなくなります。