リンカーとライブラリ

性能に関する考慮事項

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

共有オブジェクト内の実際のコードは、実行中のプロセスの性能に直接影響しますが、ここでは共有オブジェクト自体の実行時処理に焦点を絞って性能の問題を説明します。次の節では、再配置によるオーバーヘッドとともに、テキストサイズや純度 (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 に対する関数 _initfoo、および _fini の関連付けを示しています。これらのセクションは読み取り専用であるため、テキストセグメントの一部です。

同様に、データ配列 databss は、それぞれセクション .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 つの再配置が生成されています。これらの診断は、可能な場合、再配置の実行に必要なシンボリック参照すべてを示します。この場合、再配置はシンボル foobar に対するものです。

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


注 –

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


SPARC: -Kpic-KPIC オプション

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 -Kpic -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 basic を使用してリンク編集を行うと、使用されない対象物が表示されます。使用されていないと判断された対象物は、リンク編集から削除するか、リンカーの -z ignore オプションを使用して排除してください。

次の場合に、リンカーは再配置可能オブジェクトのセクションを使用されていないと判断します。

-z ignore オプションを使用して構築された共有オブジェクトからは、上の条件に一致するセクションは排除されます。共有オブジェクトの外部インタフェースを定義することによって、また -xF などのコンパイラオプションを使用してセクションの内容を改善することによって、リンカーのセクション排除能力を向上させられます。再配置可能オブジェクトの割り当て可能なセクションすべてが排除可能な場合、そのファイル全体がリンク編集から削除されます。

共有オブジェクトが生成される際に結合されない依存関係があった場合、リンカーはその依存関係が使用されていないと判断します。-z ignore オプションを使用して構築された共有オブジェクトには、使用されていないこれらの依存関係は記録されません。

共有可能性の最大化

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

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

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

読み取り専用のデータ要素はすべて、 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 オプションを使用するときは、オブジェクトのインタフェースを定義し、非公開シンボルをローカルに縮小する必要があります。シンボル範囲の縮小を参照してください。-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) を使用すると、この制限はなくなります。