リンカーとライブラリ

第 4 章 共有オブジェクト

共有オブジェクトは、リンカーによって作成される出力形式の 1 つであり、-G オプションを指定して生成されます。次の例では、共有オブジェクト libfoo.so.1 は、入力ファイル foo.c から生成されます。

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

共有オブジェクトとは、1 つまたは複数の再配置可能なオブジェクトから生成される分割できないユニットです。共有オブジェクトは、動的実行可能ファイルと結合して「実行可能」プロセスを形成することができます。共有オブジェクトは、その名前が示すように、複数のアプリケーションによって共有できます。このように共有オブジェクトの影響力は非常に大きくなる可能性があるため、この章では、リンカーのこの出力形式について前の章よりも詳しく説明します。

共有オブジェクトを動的実行可能ファイルやほかの共有オブジェクトに結合するには、まず共有オブジェクトが必要な出力ファイルのリンク編集に使用可能でなければなりません。このリンク編集中、入力共有オブジェクトはすべて、作成中の出力ファイルの論理アドレス空間に追加された場合のように解釈されます。共有オブジェクトのすべての機能が、出力ファイルにとって使用可能になります。

入力された共有オブジェクトはすべて、この出力ファイルの依存関係になります。出力ファイル内には、この依存関係を記述するための少量の登録情報が保持されます。実行時リンカーは、この情報を解釈し、「実行可能」プロセス作成の一部として、これらの共有オブジェクトの処理を完了します。

次の節では、コンパイル環境と実行時環境内での共有オブジェクトの使用法について詳しく説明します。これらの環境については、「実行時リンク」を参照してください。

命名規約

リンカーも実行時リンカーも、ファイル名によるファイルの解釈は行いません。ファイルはすべて検査されて、その ELF タイプが判定されます (「ELF ヘッダー」 を参照)。この情報から、リンカーはファイルの処理条件を推定します。ただし、共有オブジェクトは通常、コンパイル環境または実行時環境のどちらの一部として使用されるかによって、2 つの命名規約のうちどちらかに従います。

共有オブジェクトは、コンパイル環境の一部として使用される場合、リンカーによって読み取られて処理されます。これらの共有オブジェクトは、リンカーに渡されるコマンド行の一部で明示的なファイル名によって指定できますが、リンカーのライブラリ検索機能を利用するために -l オプションを使用する方が一般的です。「共有オブジェクトの処理」を参照。

このリンカー処理に適用する共有オブジェクトには、接頭辞 lib と接尾辞 .so を指定する必要があります。たとえば、/lib/libc.so は、コンパイル環境に使用できる標準 C ライブラリの共有オブジェクト表現です。規則によって、64 ビットの共有オブジェクトは、64 と呼ばれる lib ディレクトリのサブディレクトリに置かれます。たとえば、/lib/libc.so.1 の64 ビット版は、/lib/64/libc.so.1 です。

共有オブジェクトは、実行時環境の一部として使用される場合、実行時リンカーによって読み取られて処理されます。幾世代にも渡って公開される共有オブジェクトのインタフェースを変更できるようにするには、共有オブジェクトをバージョン番号の付いたファイル名にします。

バージョン付きファイル名は、通常、.so 接尾辞の後にバージョン番号が続くという形式をとります。たとえば、/lib/libc.so.1 は、実行時環境で使用可能な標準 C ライブラリのバージョン 1 の共有オブジェクト表示です。

共有オブジェクトが、コンパイル環境内での使用をまったく目的としていない場合は、慣習的な lib 接頭辞をその名前に付けないことがあります。このカテゴリに属する共有オブジェクトの例には、dlopen(3C) だけに使用されるオブジェクトがあります。実際のファイルタイプを示すために、接頭辞 .so は付けることを推奨します。また、一連のソフトウェアリリースで共有オブジェクトの正しい結合を行うためにはバージョン番号も必要です。バージョン番号の付け方については、第 5 章アプリケーションバイナリインタフェースとバージョン管理を参照してください。


注 –

dlopen(3C) で使用される共有オブジェクト名は通常、名前に「/」が付かない「単純」ファイル名として表されます。実行時リンカーは、この規則を使用して、実際のファイルを検索できます。詳細は、「追加オブジェクトの読み込み」を参照してください。


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

動的実行可能ファイルまたは共有オブジェクトでの依存関係の記録は、デフォルトでは、関連する共有オブジェクトがリンカーによって参照されるときのファイル名になります。たとえば、次の動的実行可能ファイルは、同じ共有オブジェクト libfoo.so に対して構築されますが、同じ依存関係の解釈は異なります。


$ cc -o ../tmp/libfoo.so -G foo.o
$ cc -o prog main.o -L../tmp -lfoo
$ elfdump -d prog | grep NEEDED
       [1]  NEEDED        0x123         libfoo.so.1

$ cc -o prog main.o ../tmp/libfoo.so
$ elfdump -d prog | grep NEEDED
       [1]  NEEDED        0x123         ../tmp/libfoo.so

$ cc -o prog main.o /usr/tmp/libfoo.so
$ elfdump -d prog | grep NEEDED
       [1]  NEEDED        0x123         /usr/tmp/libfoo.so

上記の例が示すように、依存関係を記録するこのメカニズムでは、コンパイル手法の違いによって不一致が生じる可能性があります。また、リンク編集中に参照される共有オブジェクトの位置が、インストールされたシステムでの共有オブジェクトの最終的な位置と異なる場合があります。依存関係を指定するより一貫した手法として、共有オブジェクトは、それぞれの内部にファイル名を記録できます。共有オブジェクトは、このファイル名によって実行時に参照されます。

共有オブジェクトのリンク編集中、-h オプションを使用すると、その実行時名を共有オブジェクト自体に記録できます。次の例では、共有オブジェクトの実行時名 libfoo.so.1 は、ファイル自体に記録されます。この識別名は、「soname」と呼ばれます。


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

次の例は、elfdump(1) を使用して SONAME タグを持つエントリを参照し、soname の記録を表示する方法を示しています。


$ elfdump -d ../tmp/libfoo.so | grep SONAME
       [1]  SONAME        0x123         libfoo.so.1

リンカーが「soname」を含む共有オブジェクトを処理する場合、生成中の出力ファイル内に依存関係として記録されるのはこの名前です。

前の例から動的実行可能ファイル prog を作成しているときに、この新しいバージョンの libfoo.so が使用されると、実行可能ファイルを作成するための 3 つの方式すべてによって同じ依存関係が記録されます。


$ cc -o prog main.o -L../tmp -lfoo
$ elfdump -d prog | grep NEEDED
       [1]  NEEDED        0x123         libfoo.so

$ cc -o prog main.o ../tmp/libfoo.so
$ elfdump -d prog | grep NEEDED
       [1]  NEEDED        0x123         libfoo.so

$ cc -o prog main.o /usr/tmp/libfoo.so
$ elfdump -d prog | grep NEEDED
       [1]  NEEDED        0x123         libfoo.so

上記の例では、-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
$ elfdump -d main | grep NEEDED
       [1]  NEEDED        0x123         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

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

共有オブジェクトは独自の依存関係を持つことができます。「実行時リンカーが検索するディレクトリ」では、共有オブジェクトの依存関係を検索するために実行時リンカーが使用する検索規則について説明しています。共有オブジェクトがデフォルト検索ディレクトリの中にない場合、実行時リンカーに検索場所を明示的に指示する必要があります。32 ビットオブジェクトの場合、デフォルト検索ディレクトリは /lib/usr/lib です。64 ビットオブジェクトの場合、デフォルト検索ディレクトリは /lib/64/usr/lib/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
$ elfdump -d libfoo.so | egrep "NEEDED|RUNPATH"
       [1]  NEEDED        0x123         libbar.so.1
       [2]  RUNPATH       0x456         /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 セクションの処理を特に重要視する開発者は、共有オブジェクトの処理順序でのこのような変更の可能性に注意する必要があります。

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

共有オブジェクトは、「フィルタ」として機能するように定義できます。この手法には、フィルタが提供するインタフェースと、代替共有オブジェクトとの関連付けが含まれます。代替共有オブジェクトは実行時に、「フィルタ」により提供される 1 つまたは複数のインタフェースを供給します。この代替共有オブジェクトは 「フィルティー」と呼ばれます。「フィルティー」は、共有オブジェクトと同じように構築されます。

フィルタ処理は、実行時環境からコンパイル環境を抽象化するメカニズムを提供します。リンク編集時には、フィルタインタフェースに結合するシンボル参照は、フィルタシンボル定義に解決されます。実行時には、フィルタインタフェースに結合するシンボル参照は代替共有オブジェクトにリダイレクトできます。

mapfile のキーワードである FILTER または AUXILIARY を使用することで、共有オブジェクト内に定義された個別インタフェースをフィルタとして定義できます。また、特定の共有オブジェクトが提供するすべてのインタフェースをフィルタとして定義することもできます。それには、リンカーの -F または -f フラグを使用します。これらの手法は、一般に個別に使用されますが、同じ共有オブジェクトの中で組み合わせることもできます。

フィルタ処理には、次に示す 2 つの形があります。

標準フィルタ処理

このフィルタ処理で必要となるのは、フィルタ処理対象のインタフェースのシンボルテーブルエントリだけです。実行時には、「フィルティー」からフィルタシンボル定義の実装を提供する必要があります。

リンカーの mapfile キーワード FILTER またはリンカーの -F フラグを使用すると、インタフェースは標準フィルタとして機能するように定義されます。この mapfile キーワードまたはフラグは、実行時にシンボル定義を提供する必要がある 1 つ以上の「フィルティー」の名前で修飾されます。

実行時に処理できない「フィルティー」はスキップされます。「フィルティー」内に標準フィルタシンボルが見つからない場合も、「フィルティー」はスキップされます。どちらの場合も、フィルタにより提供されるシンボル定義は、このシンボル検索を満たすためには使用されません。

補助フィルタ処理

このフィルタ処理は標準フィルタ処理と類似したメカニズムを提供しますが、補助フィルタインタフェースに対応するフォールバック実装がフィルタに含まれる点が異なります。実行時には、「フィルティー」からシンボル定義の実装を提供できます。

リンカーの mapfile キーワード AUXILIARY またはリンカーの -f フラグを使用すると、インタフェースは補助フィルタとして機能するように定義されます。この mapfile キーワードまたはフラグは、実行時にシンボル定義を提供できる 1 つ以上の「フィルティー」の名前で修飾されます。

実行時に処理できない「フィルティー」はスキップされます。「フィルティー」内に補助フィルタシンボルが見つからない場合も、「フィルティー」はスキップされます。どちらの場合も、フィルタにより提供されるシンボル定義は、このシンボル検索を満たすために使用されます。

標準フィルタの生成

標準フィルタを生成するには、まずフィルタ処理を適用する「フィルティー」を定義する必要があります。次の例では、シンボル foobar を提供する「フィルティー」filtee.so.1 を構築します。


$ cat filtee.c
char * bar = "defined in filtee";

char * foo()
{
        return("defined in filtee");
}
$ cc -o filtee.so.1 -G -K pic filtee.c

標準フィルタ処理は、2 つの方法のいずれかで実行できます。共有オブジェクトによって提供されるすべてのインタフェースをフィルタとして宣言するには、リンカーの -F フラグを使用します。フィルタとなる共有オブジェクトの個々のインタフェースを宣言するには、リンカーの mapfileFILTER キーワードを使用します。

次の例では、共有オブジェクト filter.so.1 がフィルタとして定義されています。filter.so.1 はシンボル foobar を提供し、それ自体が「フィルティー」filtee.so.1 のフィルタです。この例では、コンパイラドライバが -F オプションを解釈しないように、環境変数 LD_OPTIONS が使用されています。


$ cat filter.c
char * bar = NULL;

char * foo()
{
	return (NULL);
}
$ LD_OPTIONS='-F filtee.so.1' \
cc -o filter.so.1 -G -K pic -h filter.so.1 -R. filter.c
$ elfdump -d filter.so.1 | egrep "SONAME|FILTER"
    [2]  SONAME           0xee     filter.so.1
    [3]  FILTER           0xfb     filtee.so.1

動的実行可能ファイルまたは共有オブジェクトを作成する場合、リンカーは標準フィルタ filter.so.1 を依存関係として参照できます。リンカーは、フィルタのシンボルテーブルの情報を使用してシンボル解決を行います。しかし、実行時にフィルタのシンボルを参照すると、必ず「フィルティー」filtee.so.1 がさらに読み込まれます。実行時リンカーはこの「フィルティー」を使用して、filter.so.1 によって定義されたシンボルを解決します。この「フィルティー」が見つからないか、あるいは「フィルティー」内にフィルタシンボルが見つからない場合は、このシンボル検索でそのフィルタはスキップされます。

たとえば、次の動的実行可能ファイル prog は、シンボル foobar を参照します。これらのシンボルは、フィルタ filter.so.1 からのリンク編集中に解決されます。prog を実行すると、foobar が、フィルタ filter.so.1 からではなく、「フィルティー」 filtee.so.1 から取得されます。


$ cat main.c
extern char * bar, * foo();

void main()
{
        (void) printf("foo is %s: bar is %s\n", foo(), bar);
}
$ cc -o prog main.c -R. filter.so.1
$ prog
foo is defined in filtee: bar is defined in filtee

次の例では、共有オブジェクト filter.so.2 は、インタフェースの 1 つである foo を フィルティー filtee.so.1 上のフィルタとして定義します。


注 –

foo() にはソースコードが提供されていないので、mapfile のキーワードである FUNCTION を使用して、foo のシンボルテーブルエントリが作成されることを確認します。



$ cat filter.c
char * bar = "defined in filter";
$ cat mapfile
{
	global:
		foo = FUNCTION FILTER filtee.so.1;
};
$ cc -o filter.so.2 -G -K pic -h filter.so.2 -M mapfile -R. filter.c
$ elfdump -d filter.so.2 | egrep "SONAME|FILTER"
    [2]  SONAME           0xd8     filter.so.2
    [3]  SUNW_FILTER      0xfb     filtee.so.1
$ elfdump -y filter.so.2 | egrep "foo|bar"
    [1]  F    [3] filtee.so.1      foo
   [10]  D        <self>           bar

実行時にフィルタのシンボル foo を参照すると、必ず「フィルティー」filtee.so.1 がさらに読み込まれます。実行時リンカーは、「フィルティー」を使用して、filter.so.2 が定義したシンボル foo だけを解決します。シンボル bar への参照は、 filter.so.2 からのシンボルを常に使用し、このシンボルに対して「フィルティー」処理は定義されません。

たとえば、次の動的実行可能ファイル prog は、フィルタ filter.so.2 からのリンク編集中に解決されるシンボル foobar を参照します。prog の実行により、foo が「フィルティー」filtee.so.1 から取得され、bar がフィルタ filter.so.2 から取得されます。


$ cc -o prog main.c -R. filter.so.2
$ prog
foo is defined in filtee: bar is defined in filter

これらの例では、「フィルティー」filtee.so.1 がフィルタに一意に関連付けられています。このため、prog を実行した結果読み込まれる可能性があるほかのオブジェクトからのシンボル参照を満たすために、「フィルティー」を使用することができません。

標準フィルタは、既存の共有オブジェクトのサブセットインタフェースを定義するための便利なメカニズムを提供します。標準フィルタは、多数の既存の共有オブジェクトに及ぶインタフェースグループを作成します。標準フィルタはまた、インタフェースをその実装にリダイレクトする手段も提供します。一部の標準フィルタは、Solaris OS で使用されます。

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

/lib/libxnet.so.1 フィルタは、複数の「フィルティー」を使用します。このライブラリは、/lib/libsocket.so.1 /lib/libnsl.so.1、および /lib/libc.so.1 から、ソケットと XTI インタフェースを提供します。

libc.so.1 は、実行時リンカーへのインタフェースフィルタを定義します。これらのインタフェースは、libc.so.1 のコンパイル環境で参照されるシンボルと ld.so.1(1) の実行時環境内で作り出される実際の実装結合間の抽象化を提供します。

libnsl.so.1 は、標準フィルタ gethostname(3C)libc.so.1 に対して定義します。以前は、libnsl.so.1libc.so.1 もこのシンボルの同じ実装を提供していました。libnsl.so.1 をフィルタとして設定することで、gethostname() の実装は 1 つだけ必要となります。libnsl.so.1 は継続して gethostname() をエクスポートするため、このライブラリインタフェースも以前のリリースと互換性があります。

標準フィルタ内のコードは実行時に参照されることはないため、フィルタとして定義された関数に内容を追加しても意味がありません。どのようなフィルタコードでも再配置を必要とする場合があり、実行時にそのフィルタを処理すると不要なオーバーヘッドが生じます。関数は空のルーチンとして定義するか、直接 mapfile から定義してください。mapfile を使用した追加シンボルの定義」を参照してください。

フィルタ内にデータシンボルを生成するときは、常にデータをセクションに関連付けてください。この関連付けは、再配置可能なオブジェクトファイル内にシンボルを定義することで行うことができます。この関連付けは、mapfile 内でシンボルを size 宣言あり、 value 宣言なしで定義しても行うことができます。mapfile を使用した追加シンボルの定義」を参照してください。このようにデータを定義することで、動的実行可能ファイルからの参照が正しく確立されます。

リンカーによって実行される、より複雑なシンボル解決の中には、シンボルサイズを含むシンボルの属性に関する知識を必要とするものがあります。このため、フィルタ内のシンボルの属性が「フィルティー」内のシンボルの属性と一致するようにシンボルを生成する必要があります。属性の一貫性を維持することで、リンク編集処理では、実行時に使用されるシンボル定義と互換性のある方法でフィルタが解析されます。「シンボル解決」を参照してください。


注 –

リンカーは、処理される最初の再配置可能ファイルの ELF クラスを使用して、作成するオブジェクトのクラスを管理します。64 ビットフィルタを mapfile だけから作成するには、リンカーの -64 オプションを使用します。


補助フィルタの生成

補助フィルタを生成するには、まずフィルタ処理を適用する「フィルティー」を定義する必要があります。次の例では、シンボル foo を提供する「フィルティー」filtee.so.1 を構築します。


$ cat filtee.c
char * foo()
{
        return("defined in filtee");
}
$ cc -o filtee.so.1 -G -K pic filtee.c

補助フィルタ処理は、2 つの方法のいずれかで提供できます。共有オブジェクトによって提供されるすべてのインタフェースを補助フィルタとして宣言するには、リンカーの -f フラグを使用します。共有オブジェクトの個々のインタフェースを補助フィルタとして宣言するには、リンカーの mapfileAUXILIARY キーワードを使用します。

次の例では、共有オブジェクト filter.so.1 が補助フィルタとして定義されています。filter.so.1 はシンボル foobar を提供し、それ自体が「フィルティー」filtee.so.1 の補助フィルタです。この例では、コンパイラドライバが -f オプションを解釈しないように、環境変数 LD_OPTIONS が使用されています。


$ cat filter.c
char * bar = "defined in filter";

char * foo()
{
        return ("defined in filter");
}
$ LD_OPTIONS='-f filtee.so.1' \
cc -o filter.so.1 -G -K pic -h filter.so.1 -R. filter.c
$ elfdump -d filter.so.1 | egrep "SONAME|AUXILIARY"
    [2]  SONAME           0xee     filter.so.1
    [3]  AUXILIARY        0xfb     filtee.so.1

動的実行可能ファイルまたは共有オブジェクトを作成する場合、リンカーは補助フィルタ filter.so.1 を依存関係として参照できます。リンカーは、フィルタのシンボルテーブルの情報を使用してシンボル解決を行います。しかし、実行時にフィルタのシンボルを参照すると、「フィルティー」filtee.so.1 が検索されます。この「フィルティー」が見つかると、実行時リンカーは、この「フィルティー」を使用して、filter.so.1 によって定義されたすべてのシンボルを解決します。この「フィルティー」が見つからないか、あるいは「フィルティー」内にフィルタからのシンボルが見つからない場合は、フィルタ内の元のシンボルが使用されます。

たとえば、次の動的実行可能ファイル prog は、シンボル foobar を参照します。これらのシンボルは、フィルタ filter.so.1 からのリンク編集中に解決されます。prog を実行すると、foo が、フィルタ filter.so.1 からではなく、「フィルティー」 filtee.so.1 から取得されます。しかし、bar はフィルタ filter.so.1 から取得されます。これは、「フィルティー」filtee.so.1 内にこのシンボルの代替定義が存在しないためです。


$ cat main.c
extern char * bar, * foo();

void main()
{
        (void) printf("foo is %s: bar is %s\n", foo(), bar);
}
$ cc -o prog main.c -R. filter.so.1
$ prog
foo is defined in filtee: bar is defined in filter

次の例では、共有オブジェクト filter.so.2 は、インタフェース foo をフィルティーfiltee.so.1 上の補助フィルタとして定義します。


$ cat filter.c
char * bar = "defined in filter";

char * foo()
{
        return ("defined in filter");
}
$ cat mapfile
{
	global:
		foo = AUXILIARY filtee.so.1;
};
$ cc -o filter.so.2 -G -K pic -h filter.so.2 -M mapfile -R. filter.c
$ elfdump -d filter.so.2 | egrep "SONAME|AUXILIARY"
    [2]  SONAME           0xd8     filter.so.2
    [3]  SUNW_AUXILIARY   0xfb     filtee.so.1
$ elfdump -y filter.so.2 | egrep "foo|bar"
    [1]  A    [3] filtee.so.1      foo
   [10]  D        <self>           bar

実行時にフィルタのシンボル foo を参照すると、必ず「フィルティー」filtee.so.1 が検索されます。「フィルティー」が見つかると、「フィルティー」が読み込まれます。「フィルティー」は filter.so.2 によって定義されたシンボル foo の解決に使用されます。「フィルティー」が検索されなかった場合、filter.so.2 によって定義されたシンボル foo が使用されます。シンボル bar への参照は、 filter.so.2 からのシンボルを常に使用し、このシンボルに対して「フィルティー」処理は定義されません。

たとえば、次の動的実行可能ファイル prog は、フィルタ filter.so.2 からのリンク編集中に解決されるシンボル foobar を参照します。「フィルティー」 filtee.so.1 が存在する場合、prog の実行により foo が「フィルティー」 filtee.so.1 から、bar がフィルタ filter.so.2 から取得されます。


$ cc -o prog main.c -R. filter.so.2
$ prog
foo is defined in filtee: bar is defined in filter

「フィルティー」 filtee.so.1が存在しない場合、prog を実行すると、foobar がフィルタ filter.so.2 から取得されます。


$ prog
foo is defined in filter: bar is defined in filter

これらの例では、「フィルティー」filtee.so.1 がフィルタに一意に関連付けられています。このため、prog を実行した結果読み込まれる可能性があるほかのオブジェクトからのシンボル参照を満たすために、「フィルティー」を使用することができません。

補助フィルタは、既存の共有オブジェクトの代替インタフェースを定義するメカニズムとなります。このメカニズムは、ハードウェア機能での最適な機能およびプラットフォーム固有の共有オブジェクトを提供するために Solaris OS で使用されます。例は、「ハードウェア機能固有の共有オブジェクト」「命令セット固有の共有オブジェクト」、および 「システム固有の共有オブジェクト」を参照してください。


注 –

環境変数 LD_NOAUXFLTR を設定すれば、実行時リンカーの補助フィルタ処理を無効にすることができます。補助フィルタはプラットフォーム固有の最適化に使用されることが多いので、「フィルティー」の使用およびそれらの性能インパクトを評価する場合にこのオプションが便利です。


フィルタ処理の組み合わせ

標準フィルタを定義する個別インタフェースと補助インタフェースを定義する個別インタフェースを、同一の共有オブジェクト内に定義することができます。こうしたフィルタ定義の組み合わせを実現するには、mapfile のキーワードである FILTERAUXILIARY を使って、必要な「フィルティー」を割り当てます。

-F または -f オプションを使用して自身のインタフェースのすべてをフィルタとして定義する共有オブジェクトは、標準フィルタか補助フィルタのどちらかです。

共有オブジェクトでは、個々のインタフェースをフィルタとして機能するように定義するとともに、そのオブジェクトのすべてのインタフェースをフィルタとして機能するように定義することができます。その場合、特定のインタフェースに対して定義された個別フィルタ処理が、まず処理されます。個別インタフェースフィルタに対する「フィルティー」を確立できなかった場合は、フィルタのすべてのインタフェースに対して定義された「フィルティー」が必要に応じてフォールバックを提供します。

たとえば、フィルタ filter.so.1 があるとします。このフィルタでは、すべてのインタフェースが「フィルティー」filtee.so.1 に対する補助フィルタとして機能するように、リンカーの -f フラグを使って定義されています。さらに filter.so.1 では、個別インタフェース foo が「フィルティー」foo.so.1 に対する標準フィルタとなるように、mapfile のキーワード FILTER を使って定義されています。さらに filter.so.1 では、個別インタフェース bar が「フィルティー」bar.so.1 に対する補助フィルタとなるように、mapfile のキーワード AUXILIARY を使って定義されています。

foo への外部参照が発生すると、「フィルティー」foo.so.1 が処理されます。foofoo.so.1 で見つからなかった場合、このフィルタに対する処理はそれ以上実行されません。この場合にフォールバック処理が実行されない理由は、foo が標準フィルタとして定義されているからです。

bar への外部参照が発生すると、「フィルティー」bar.so.1 が処理されます。barbar.so.1 で見つからなかった場合、「フィルティー」filtee.so.1 によるフォールバック処理が実行されます。この場合にフォールバック処理が実行される理由は、bar が補助フィルタとして定義されているからです。barfiltee.so.1 で見つからなかった場合、最終的にはフィルタ filter.so.1 内の bar の定義に基づいて外部参照が解決されます。

「フィルティー」の処理

実行時リンカーによるフィルタ処理は、フィルタ内のシンボルが参照されるまで、フィルティーの読み込みを遅延します。この実装は、必要に応じてモード RTLD_LOCAL を使用して各「フィルティー」に対して dlopen(3C) を実行するフィルタに似ています。この実装は、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 エントリを調べることによって表示できます。


$ elfdump -p -NPT_LOAD libfoo.so.1

Program Header[0]:
    p_vaddr:      0           p_flags:    [ PF_X PF_R ]
    p_paddr:      0           p_type:     [ PT_LOAD ]
    p_filesz:     0x59c       p_memsz:    0x59c
    p_offset:     0           p_align:    0x10000

Program Header[1]:
    p_vaddr:      0x10630     p_flags:    [ PF_X PF_W PF_R ]
    p_paddr:      0           p_type:     [ PT_LOAD ]
    p_filesz:     0x10c       p_memsz:    0x12c
    p_offset:     0x630       p_align:    0x10000

共有オブジェクト libfoo.so.1 には、一般に「テキスト」セグメントおよび「データ」セグメントと呼ばれる 2 つの読み込み可能なセグメントがあります。テキストセグメントがマップされ、その内容 PF_X PF_R の読み込みと実行が可能になります。データセグメントもマップされ、その内容 PF_W が変更できるようになります。データセグメントのメモリーサイズ (p_memsz) は、ファイルサイズ (p_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) の表示は例のために簡素化されています。


基本システム

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

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

メモリー管理スキーマは、プロセス間で共有オブジェクトの共有メモリーをページ細部のレベルで動的リンクするときの基本となります。メモリーページは、実行時に変更されていなければ共有できます。プロセスは、データ項目の書き込み時、または共有オブジェクトへの参照の再配置時に共有オブジェクトのページに書き込む場合、そのページの専用コピーを生成します。この専用コピーは、共有オブジェクトのほかのユーザーに対して何も影響しません。ただし、このページはほかのプロセス間での共有に伴う利点をすべて失います。この方法で変更されたテキストページは、「純粋でない」(impure) と呼ばれます。

メモリーにマッピングされた共有オブジェクトのセグメントは、参照専用の「テキスト」セグメントと読み書きが可能な「データ」セグメントに分類されます。ELF ファイルからこの情報を取得する方法については、「ファイルの解析」を参照してください。共有オブジェクトを開発するときの主要目的は、テキストセグメントを最大化して、データセグメントを最小化することにあります。これにより、共有オブジェクトの初期設定と使用に必要な処理の量を削減しながら、コード共有の量を最適化できます。次の節では、この目的を達成するために役立つメカニズムを示します。

動的依存関係の遅延読み込み

オブジェクトを遅延読み込みするように設定すると、共有オブジェクトの依存関係の読み込みは、最初に参照されるまで延期できます。「動的依存関係の遅延読み込み」を参照してください。

小さいアプリケーションの場合、典型的な 1 つの実行スレッドでアプリケーションのすべての依存関係を参照することができます。この場合、依存関係が遅延読み込み可に設定されているかどうかに関係なく、アプリケーションはすべての依存関係を読み込みます。しかし、遅延読み込みでは依存関係の処理が処理の起動時から延期され、処理の実行期間全体にわたって広がります。

多くの依存関係を持つアプリケーションの場合、遅延読み込みを使用すると、一部の依存関係がまったく読み込まれないことがあります。特定の実行スレッドで参照されない依存関係は読み込まれません。

位置独立のコード

動的実行可能ファイル内のコードは通常「位置に依存」し、メモリー内の固定アドレスに結び付けられています。一方、共有オブジェクトは、異なるプロセス内の異なる位置に読み込むことができます。位置独立のコードは、特定のアドレスに結び付けられていません。このように独立しているため、コードは、そのコードを使用する各プロセス内の異なるアドレスで実際に実行できます。位置独立のコードは、共有オブジェクトを作成する場合に推奨します。

コンパイラは、-K pic オプションによって、位置独立のコードを生成できます。

共有オブジェクトが位置に依存するコードで構築されている場合、テキストセグメントには実行時に変更が必要となる場合があります。このような変更により、再配置可能な参照を、オブジェクトが読み込まれている位置に割り当てることができます。テキストセグメントの再配置を行うには、セグメントを書き込み可能として再度マッピングする必要があります。このような変更にはスワップ空間の予約が必要で、またプロセスのテキストセグメントの非公開コピーが行われます。テキストセグメントは複数のプロセス間では共有できなくなります。位置に依存するコードは、通常、対応する位置独立のコードよりも多くの実行時再配置を必要とします。概して、テキスト再配置を処理するオーバーヘッドは、重大な性能の低下の原因になる可能性があります。

位置独立のコードから構築された共有オブジェクトでは、そのデータセグメント内のデータを介した間接参照として、再配置可能な参照が生成されます。テキストセグメント内のコードは変更する必要はありません。すべての再配置更新がデータセグメント内の対応するエントリに適用されます。特定の間接参照のテクニックの詳細については、「大域オフセットテーブル (プロセッサ固有)」「プロシージャーのリンクテーブル (プロセッサ固有)」を参照してください。

このような再配置が存在する場合、実行時リンカーはテキスト再配置を処理しようとします。ただし、一部の再配置は実行時に処理できません。

x64 の位置に依存するコードのシーケンスは、下位 32 ビットのメモリーにのみ読み込み可能なコードを生成します。上位 32 ビットのアドレスはすべてゼロである必要があります。通常、共有オブジェクトはメモリーの最上位に読み込まれるため、上位 32 ビットのアドレスが必要になります。そのため、x64 共有オブジェクト内の、位置に依存するコードは、再配置の要件に対処するのに不十分です。共有オブジェクト内でそのようなコードを使用すると、実行時再配置エラーが発生する可能性があります。


$ prog
ld.so.1: prog: fatal: relocation error: R_AMD64_32: file \
    libfoo.so.1: symbol (unknown): value 0xfffffd7fff0cd457 does not fit

位置独立のコードはメモリー内の任意の場所に読み込めるため、x64 の共有オブジェクトの要件を満たします。

このような状況は、64 ビット SPARCV9 コードに使用されるデフォルトの ABS64 モードとは異なります。位置に依存するこのコードは通常、完全な 64 ビットアドレス範囲と互換性があります。したがって、位置に依存するコードのシーケンスは、SPARCV9 共有オブジェクト内に存在できます。64 ビット SPARCV9 コードに ABS32 モードまたは ABS44 モードのいずれかを使用しても、実行時に解決できない再配置が生じる可能性があります。ただし、これらの各モードでは、実行時リンカーがテキストセグメントを再配置する必要があります。

実行時リンカーの機能や、再配置要件の違いに関係なく、共有オブジェクトは位置独立のコードを使用して構築するべきです。

共有オブジェクトのうち、テキストセグメントに対して再配置を必要とするものを識別できます。次の例では、elfdump(1) を使用して、TEXTREL エントリという動的エントリが存在するかどうかを判別します。


$ cc -o libfoo.so.1 -G -R. foo.c
$ elfdump -d 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 に対するものです。

手書きのアセンブラコードが含まれ、その中に、位置独立の適切なプロトタイプが含まれていない場合、共有オブジェクト内ではテキスト再配置が発生する可能性もあります。


注 –

いくつかの単純なソースファイルをテストしながら、位置に依存しないコードを決定することもできます。中間アセンブラ出力を生成するコンパイラ機能を使用してください。


SPARC: -K pic-K PIC オプション

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 つのカテゴリは、再配置による性能への影響を考慮するためにも最適です。

シンボルの検索

実行時リンカーは、特定のシンボルを検索する必要が生じると、デフォルトでは各オブジェクト内でそのシンボルを検索します。まず動的実行可能プログラムから検索してから、 オブジェクトが読み込まれた順番に共有オブジェクトを検索します。ほとんどの場合、シンボル再配置を必要とする共有オブジェクトは、シンボル定義の提供者になります。

この状況では、この再配置に使用されるシンボルが共有オブジェクトのインタフェースの一部として必要ではない場合、このシンボルは「静的」変数または「自動」変数に変換される可能性が高くなります。シンボル削減は、共有オブジェクトのインタフェースから削除されたシンボルにも適用できます。詳細は、「シンボル範囲の縮小」を参照してください。これらの変換を行うことによって、リンカーは、共有オブジェクトの作成中にこれらのシンボルに対するシンボル再配置を処理しなければならなくなります。

共有オブジェクトから表示できなければならない唯一の大域データ項目は、そのユーザーインタフェースに関するものです。しかし、大域データは異なる複数のソースファイルにある複数の関数から参照できるように定義されていることが多いため、これは歴史的に達成が困難です。シンボルの縮小を適用することによって、不要な大域シンボルを削除できます。「シンボル範囲の縮小」を参照してください。共有オブジェクトからエクスポートされた大域シンボルの数を少しでも減らせば、再配置のコストを削減し、性能全体を向上させることができます。

直接結合を使用すると、多数のシンボル再配置や依存関係を伴う動的プロセスでのシンボル検索オーバーヘッドも大幅に削減できます。「直接結合」を参照してください。

再配置が実行されるとき

すべての即時参照再配置は、アプリケーションが制御を取得する前の、プロセスの初期設定中に実行する必要があります。これに対して、遅延参照は、関数の最初のインスタンスが呼び出されるまで延期できます。即時参照は通常、データ参照によって行われます。このため、データ参照の数を少なくすることによって、プロセスの実行時初期設定も削減されます。

初期設定再配置コストは、データ参照を関数参照に変換して延期することもできます。たとえば、機能インタフェースによってデータ項目を返すことができます。この変換を行うと、初期設定再配置コストがプロセスの実行期間中に効率的に分配されるため、性能は明らかに向上します。いくつかの機能インタフェースはプロセスの特定の呼び出しでは決して呼び出されない可能性もあるため、それらの再配置オーバーヘッドもすべてなくなります。

機能インタフェースを使用した場合の利点については、「コピー再配置」で説明します。この節では、動的実行可能ファイルと共有オブジェクトの間で使用される特殊でコストのかかる再配置メカニズムについて説明します。また、この再配置によるオーバーヘッドを回避する方法の例も示します。

再配置セクションの結合

再配置可能オブジェクト内の再配置セクションは通常、再配置の適用対象となるセクションとの 1 対 1 の関係が維持されます。ただし、実行可能ファイルまたは共有オブジェクトを -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 オプションを使用するときは、オブジェクトのインタフェースを定義し、非公開シンボルをローカルに縮小する必要があります。「シンボル範囲の縮小」を参照してください。-B symbolic を使用すると直感的にはわからない副産物ができることがあります。

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


注 –

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


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

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

LD_PROFILE 環境変数には、プロファイル対象となる共有オブジェクトの名前を指定します。この環境変数を使用すると、単一の共有オブジェクトを解析できます。環境変数の設定は、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 /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) を呼び出す場合は例外です。fork(2) を使用すると、この制限はなくなります。