「再配置処理」では、実行時リンカーが動的実行可能ファイルと共有オブジェクトを再配置して、実行可能プロセスを作成するためのメカニズムについて説明しました。「シンボルの検索」と 「再配置が実行されるとき」は、この再配置処理を 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) は、動的実行可能ファイルが、共有オブジェクトが提供しなければならないデータすべてをコピーするけれども、その割り当てスペースで許容できる量しか受け付けないということを知らせています。
位置に依存しないコードだけでアプリケーションを作成すれば、コピー再配置を完全に排除することができます。「位置に依存しないコード」を参照してください。