リンカーとライブラリ

第 3 章 実行時リンカー

概要

「動的実行プログラム」の初期設定と実行の一部として、インタプリタは、アプリケーションのその依存関係への結合を完了させるために呼び出されます。Solaris では、このインタプリタを実行時リンカーと呼びます。

動的実行プログラムのリンク編集中に、特別な .interp セクションが、関連するプログラムのヘッダーとともに作成されます。このセクションには、プログラムのインタプリタを指定するパス名が組み込まれています。リンカーによって提供されたデフォルトの名前は、32 ビットの実行プログラムの場合は実行時リンカー /usr/lib/ld.so.1、64 ビット実行プログラムの場合は /usr/lib/64/ld.so.1 となります。

動的実行プログラムの実行プロセス中に (exec(2) を参照)、カーネルはファイル mmap(2) を参照) を対応付けして、プログラムのヘッダー情報 (「プログラムヘッダー」を参照) を使って、必要なインタプリタの名前を検出します。カーネルは、このインタプリタの対応付けと、インタプリタへの制御の転送を行い、同時に重要な情報を渡して、インタプリタがアプリケーションの結合を続行してからそのアプリケーションを実行できるようにします。

またアプリケーションの初期設定に加えて、実行時リンカーは、サービスを提供します。このサービスを使用すると、追加オブジェクトを対応付けして、シンボルをその中に結合することにより、アプリケーションはそのアドレススペースを拡張できます。

次に、この章で説明する、実行時リンカーの機能を簡単に紹介します。

共有オブジェクトの依存関係の配置

通常、動的実行プログラムのリンク編集中に、1 つまたは複数の共有オブジェクトが明示的に参照されます。これらのオブジェクトは、依存関係として動的実行プログラム内に記録されます (詳細については、「共有オブジェクトの処理」を参照)。

実行時リンカーは、まず最初にこの依存情報を配置し、これを使用して関連オブジェクトの配置および対応付けを行います。これらの依存関係は、実行プログラムのリンク編集中に参照された順番で処理されます。

動的実行プログラムの依存関係がすべて読み込まれると、これらの依存関係も読み込まれた順番に検査され、追加の依存関係が配置されます。この処理は、すべての依存関係の配置と読み込みが完了するまで続きます。この技術の結果、すべての依存関係が幅優先順になります。

実行時リンカーによって検索されるディレクトリ

デフォルトでは、実行時リンカーが、依存関係の検出場所として認識しているのは、32 ビットの依存関係の場合は /usr/lib、64 ビットの依存関係の場合は /usr/lib/64 という標準的なディレクトリだけです。単純なファイル名で指定された依存関係には、このディレクトリ名が接頭辞として付き、この接頭辞が付いたパス名は、実際のファイルを配置する場合に使用されます。

動的実行プログラムまたは共有オブジェクトの実際の依存関係は、ldd(1) を使用して表示できます。たとえば、ファイル /usr/bin/cat には次のような依存関係があります。


$ ldd /usr/bin/cat
        libc.so.1 =>     /usr/lib/libc.so.1
        libdl.so.1 =>    /usr/lib/libdl.so.1

ここでは、ファイル /usr/bin/cat は、依存関係を持つか、またはファイル libc.so.1libdl.so.1 が必要です。

ファイル内に実際に記録された依存関係は、ファイルの .dynamic セクションと NEEDED タグの付いたエントリの参照を表示する dump(1) コマンドを使用して検査できます。次に例を示します。


$ dump -Lvp /usr/bin/cat

/usr/bin/cat:
[INDEX] Tag      Value
[1]     NEEDED   libc.so.1
.........

上記の ldd(1) の例で表示された依存関係 libdl.so.1 は、ファイル /usr/bin/cat 内に記録されないことに注意してください。これは、つまり ldd(1) が、指定されたファイルのすべての依存関係を示していて、libdl.so.1 は、実際に /usr/lib/libc.so.1 の依存関係であるためです。

上記の dump(1) の例では、依存関係は、`/' が含まれていない単純なファイル名で表示されています。単純なファイル名を使用することは、実行時リンカーが、一連の規則に従って必要なパス名を生成する場合に必要です。`/' が組み込まれたファイル名は、そのまま使用されます。

単純なファイル名の記録は、標準的な、依存関係を記録する最も柔軟性の高いメカニズムで、依存関係内の単純な名前を記録する、リンカーの -h オプションを使用して実行されます (このトピックの詳細については、「命名規約」「共有オブジェクト名の記録」を参照)。

通常、依存関係は、/usr/lib あるいは /usr/lib/64 以外に配布されます。動的実行プログラムまたは共有オブジェクトが、他のディレクトリに依存関係を配置する必要がある場合、実行時リンカーは、明示的に、このディレクトリを検索するように指示されます。

追加の検索パスを実行時リンカーに指示する場合は、動的実行プログラムまたは共有オブジェクトのリンク編集中に、「実行パス」を記録する方法をお勧めします (この情報の記録方法の詳細については、「実行時リンカーが検索するディレクトリ」を参照)。

実行パスの記録は、dump(1) を使用して表示し、RPATH タグの付いたエントリを参照できます。次に例を示します。


$ dump -Lvp prog

prog:
[INDEX] Tag      Value
[1]     NEEDED   libfoo.so.1
[2]     NEEDED   libc.so.1
[3]     RPATH    /home/me/lib:/home/you/lib
.........

ここでは、proglibfoo.so.1 上に依存関係を持っていて、実行時リンカーのデフォルトロケーション /usr/lib を調べる前に、ディレクトリ /home/me/lib/home/you/lib を検索するように要求します。

実行時リンカーの検索パスに追加するもう 1 つの方法は、環境変数 LD_LIBRARY_PATH を設定する方法です。この環境変数は、プロセスの始動時に 1 度分析され、コロンで区切られたディレクトリのリストに設定できます。実行時リンカーは、このリストに設定したディレクトリを、指定された実行パスまたはデフォルトのディレクトリよりも前に検索します。

これらの環境変数は、アプリケーションを強制的にローカルな依存関係に結合するといったデバッグの目的に適しています。次に例を示します。


$ LD_LIBRARY_PATH=. prog

ここで、上記の例のファイル prog は、現在の作業ディレクトリ内で検出された libfoo.so.1 に結合されます。

この環境変数の使用は、実行時リンカーの検索パスに影響する一時的なメカニズムとしては有用ですが、作成ソフトウェアの場合は、環境変数を使用するには大きな支障があります。この環境変数を参照できる動的実行プログラムは、その検索パスを拡張させます。これにより、全体のパフォーマンスが低下する場合があります。また、「環境変数の使用」「実行時リンカーが検索するディレクトリ」で示したように、この環境変数はリンカーに影響を及ぼします。

64 ビットの実行プログラムが、検出する名前と一致する 32 ビットのライブラリを組み込んだ検索パスを持つように、またはその逆の環境を継承できます。このような場合、実行時リンカーは一致しない 32 ビットライブラリを拒否し、有効な 64 ビットと一致するライブラリを検出して、検索パスの処理を続けます。一致するものが見つからない場合には、エラーメッセージが表示されます。このエラーは、LD_DEBUG 環境変数 (「デバッギングエイド」を参照) を設定してファイルを組み込むことによって、詳細に監視できます。


$ LD_LIBRARY_PATH=/usr/bin/64 LD_DEBUG=files /usr/bin/ls
...
00283: file=libc.so.1;  needed by /usr/bin/ls
00283: 
00283: file=/usr/lib/64/libc.so.1  rejected: ELF class mismatch: ¥
00283:                                  32-bit/64-bit
00283: 
00283: file=/usr/lib/libc.so.1  [ ELF ]; generating link map
00283:     dynamic:  0xef631180  base:  0xef580000  size:      0xb8000
00283:     entry:    0xef5a1240  phdr:  0xef580034  phnum:           3
00283:      lmid:           0x0
00283: 
00283: file=/usr/lib/libc.so.1;  analyzing  [ RTLD_GLOBAL  RTLD_LAZY ]
...

依存関係が配置できない場合は、ldd(1) により、オブジェクトが検出できないことが表示され、また、アプリケーションを実行しようとすると、実行時リンカーから該当するエラーメッセージが表示されます。


$ ldd prog
        libfoo.so.1 =>   (file not found)
        libc.so.1 =>     /usr/lib/libc.so.1
        libdl.so.1 =>    /usr/lib/libdl.so.1
$ prog
ld.so.1: prog: fatal: libfoo.so.1: open failed: No such file or directory

デフォルトの検索パスの設定

実行時リンカー (/usr/lib または /usr/lib/64) によって使用されるデフォルトの検索パスは、実行時構成ファイルを使用して管理できます。このファイルは、crle(1) ユーティリティによって作成され、正しい実行パスで作成されなかったアプリケーションについて検索パスを設定する場合に便利です。

構成ファイルのデフォルトの場所は、/var/ld/ld.config (32 ビットアプリケーションの場合) または /var/ld/64/ld.config (64 ビットアプリケーションの場合) で、システム上の、それぞれのタイプのアプリケーションすべてに影響します。構成ファイルはこれ以外の場所にも作成でき、実行時リンカー LD_CONFIG 環境変数を使用してこれらのファイルを選択できます。後者の方法は、構成ファイルをデフォルトの場所にインストールする前にテストする場合に便利です。

動的ストリングトークン

実行時リンカーは、実行パス (DT_RUNPATH または DT_RPATH)、フィルタ (DT_FILTER)、または補助フィルタ (DT_AUXILIARY) 内で使用される場合には、次のストリングトークンが置換されます。

$ISALIST

このプラットフォームで実行可能なネイティブの命令セット (isalist(1) を参照) に展開する。このトークンを含むパス名は、使用可能な各命令セットに置き換えられる。「命令セット固有の共有オブジェクト」を参照。

上記で指定されたパス内あるいは依存関係 (DT_NEEDED) エントリ内で実行時リンカーが使用されると、その実行時リンカーは次のストリングトークンを置き換えます。

$ORIGIN

オブジェクトが読み込まれるディレクトリを指定する。通常は、単独のバンドルされていないパッケージ内に依存関係を配置する場合に使用される。「関連する依存関係の配置」を参照

$OSNAME

オペレーティングシステムの名前に展開 (uname(1) -s を参照) する。「プラットフォーム固有の共有オブジェクト」を参照

$OSREL

オペレーティングシステムのリリースに展開 (uname(1) -r を参照) する。「プラットフォーム固有の共有オブジェクト」を参照

$PLATFORM

現在のマシンの現在のプロセッサタイプに展開 (uname(1) -i を参照) する。「プラットフォーム固有の共有オブジェクト」を参照


注 -

$PLATFORM は「Solaris 2.5」に付加されましたが、「Solaris 2.6」より前のものでは、補助フィルタ (DT_AUXILIARY) を表現するためだけに使用できます。「補助フィルタの生成」を参照してください。


再配置処理

実行時リンカーは、アプリケーションが要求する依存関係がすべて配置され、読み込まれると、各オブジェクトを処理し、必要な再配置すべてを実行します。

オブジェクトのリンク編集中に、入力再配置の可能なオブジェクトとともに提供された再配置の情報が、出力ファイルに適用されます。ただし、動的実行プログラムまたは共有オブジェクトを作成している場合には、ほとんどの再配置は、リンク編集時には完了できません。それは、再配置には、オブジェクトがメモリー内に読み込まれる時だけ入手することができる、論理アドレスが必要だからです。このような場合、リンカーは新しい再配置を出力ファイルイメージの一部として記録し、これが、実行時リンカーが現在処理する必要がある情報になります。

再配置のさまざまなタイプの詳細については、「再配置型 (プロセッサ固有の) 」を参照してください。ここでは、再配置を 1 つか 2 つのタイプに分けて説明します。

オブジェクトの再配置記録は、dump(1) を使用して表示できます。次に例を示します。


$ dump -rvp libbar.so.1

libbar.so.1:

.rela.got:
Offset      Symndx                Type              Addend

0x10438     0                     R_SPARC_RELATIVE  0
0x1043c     foo                   R_SPARC_GLOB_DAT

ここでは、ファイル libbar.so.1 には、「大域オフセットテーブル」(.got セクション) が更新される必要があることを示す、2 つの再配置記録が組み込まれています。

最初の再配置は、単純な相対再配置です。このことは、再配置タイプとシンボルインデックス (Symndx) フィールドがゼロであることから分かります。この再配置では、オブジェクトがメモリーに読み込まれたベースアドレスを使用して、関連する .got オフセットを更新する必要があります。

2 番目の再配置では、シンボル foo のアドレスが必要です。この再配置を完了させるには、実行時リンカーが、これまでに読み込まれた動的実行プログラムと依存関係からこのシンボルを配置する必要があります。

シンボルの検索

オブジェクトがシンボルを必要とする場合、実行時リンカーはそのシンボルを、オブジェクトのシンボル要求の検索範囲と、プロセス内の各オブジェクトによって提供されるシンボルの可視性に基づいて検索します。これらの属性は、読み込まれる時に、オブジェクトのデフォルトとして使用され、dlopen(3DL) の特別なモードとしても使用されます。さらに、場合によっては、オブジェクトの構築時に、オブジェクト内に記録されます。

平均的なユーザーであれば、動的実行プログラムとその依存関係、および dlopen(3DL) を通じて入手したオブジェクトに適用されるデフォルトのシンボル検索モードが、理解できるようになります。前者の検索モードについては、次の項、「デフォルトの検索」で概要を説明します。種々のシンボル検索に活用できる後者については、「シンボル検索」で説明しています。

リンカーの -B direct オプションを使って動的オブジェクトを作成すると、別のシンボル検索モデルが使用されます (「外部結合」を参照)。この場合、実行時リンカーは、シンボルを検索する際に、リンク編集時にそのシンボルを指定したオブジェクトから直接探します。このモデルの詳細は、「直接結合」を参照してください。

デフォルトの検索

動的実行プログラムと、ともに読み込まれるすべての依存関係には、ワールド検索範囲と、大域シンボル可視性が割り当てられます (「シンボル検索」を参照)。これにより、実行時リンカーが、動的実行プログラムまたはこの実行プログラムとともに読み込まれた依存関係すべてを調べてシンボルを検出する場合、各オブジェクトの検索は、動的実行プログラムから開始され、次にそのオブジェクトが対応付けされた順番でそれぞれの依存関係を検索していきます。

前の項で説明したように、ldd(1) を使用すると、動的実行プログラムの依存関係は読み込まれた順番にリストされます。そのため、共有オブジェクト libbar.so.1 がシンボル foo の再配置を行うためにそのアドレスを必要とし、かつ共有オブジェクトが動的実行プログラム prog の依存関係である場合には、実行時リンカーは、foo の検索を、最初に動的実行プログラム prog 内で実行し、次に共有オブジェクト /home/me/lib/libfoo.so.1 内で、最後に共有オブジェクト /home/me/lib/libbar.so.1 内で実行します。


$ ldd prog
        libfoo.so.1 =>   /home/me/lib/libfoo.so.1
        libbar.so.1 =>   /home/me/lib/libbar.so.1

注 -

シンボル検索は、シンボル名のサイズが増大し依存関係の数が増加すると、特にコストのかかる処理になる可能性があります。このパフォーマンスについての詳細は、「性能に関する考慮事項」で説明しています。これに代わる検索モデルについては、「直接結合」を参照してください。


割り込み (interposition)

最初に動的実行プログラム内でシンボルの検索を行い、次に各依存関係内で検索を行うという実行時リンカーのデフォルトのメカニズムは、要求されたシンボルの最初の出現が、この検索を満足させることを意味しています。そのため、同じシンボルの複数のインスタンスが存在する場合は、最初のインスタンスが、他のすべてのインスタンスに割り込みされます (「共有オブジェクトの処理」も参照)。

直接結合

リンカーの -B direct オプションを使ってオブジェクトを作成すると、参照されるシンボルと、定義を提供する依存条件との関係は、オブジェクトに記録されます。実行時リンカーは、デフォルトのシンボル検索モデルを使用する代わりに、この情報を使って関連するオブジェクトから直接シンボルを検索します。


注 -

-B direct オプションを使用すると、レイジーローディングも有効になります。これは、リンク編集のコマンド行の先頭に -z lazyload オプションを指定するのと同じことです (「動的依存関係のレイジーローディング」を参照)。


この直接結合モデルでは、多数のシンボル再配置や依存関係を伴う動的プロセスでのシンボル検索オーバーヘッドを大幅に削減できます。さらに、このモデルでは、同じ名前の複数のシンボルを、それらが直接結合されている個々のオブジェクトから見つけることができます。

しかし、直接結合ではデフォルトの検索モデルがバイパスされるため、従来から使用されている割り込みシンボルが迂回される可能性があります。デフォルトのモデルでは必ず、1 つのシンボルへのすべてのリファレンスは同じ 1 つの定義に結合されます。

直接結合環境でも、オブジェクト単位で、割り込みを行うことができます。それには、オブジェクトが割り込み処理として識別される必要があります。環境変数 LD_PRELOAD を使ってオブジェクトを読み込むか (「追加オブジェクトの読み込み」を参照)、リンカーの -z interpose オプションを使ってオブジェクトを作成すると、オブジェクトは割り込み処理として識別されます。実行時リンカーは、直接結合されたシンボルを検索する際に、割り込み処理として識別されたオブジェクトを最初に探してから、シンボル定義を指定するオブジェクトを探します。


注 -

直接結合を実行時に無効にするには、環境変数 LD_NODIRECT にヌル以外の値を設定します。


再配置が実行されるとき

再配置処理を、非シンボルとシンボルの 2 つのタイプに分けて簡素化し、簡単に説明してきましたが、さらに再配置処理の実行時に、これを区別すると便利です。このような、再配置されたオフセットに対して行われる参照のタイプによって、区別が実行されます。参照のタイプは、次のいずれかになります。

データ参照とは、アプリケーションコードによってデータ項目として使用されるアドレスを参照することです。実行時リンカーは、アプリケーションコードに関する知識がないため、このデータ項目がいつ参照されるか認識できません。そのため、データ再配置はすべて、アプリケーションが制御を入手する前の、処理の初期設定中に実行されます。

関数参照とは、アプリケーションコードによって呼び出された関数のアドレスを参照するものです。動的モジュールのコンパイルおよびリンク編集中に、大域関数の呼び出しは再配置されて、プロシージャのリンクテーブルエントリの呼び出しになります (これらのエントリにより、.plt セクションが構成されます)。

プロシージャのリンクテーブルのエントリは、最初に呼び出された制御が実行時リンカーに渡されたときに構成されます (「プロシージャのリンクテーブル (プロセッサ固有)」を参照)。実行時リンカーは、要求されたシンボルを検索しアプリケーション内の情報を書き換えます。そのため、後に発生する .plt エントリへの呼び出しは、関数に直接送信されます。このメカニズムを使用すると、このタイプの再配置を、関数の最初のインスタンスが呼び出されるまで延期することができます。この処理をレイジー結合と呼びます。

レイジー結合を実行する、実行時リンカーのデフォルトモードは、空文字以外の値に環境変数 LD_BIND_NOW を設定することにより上書きされます。この環境変数の設定を行うと、実行時リンカーは、プロセスの初期設定中にデータ参照と関数参照の両方の再配置を実行してから、アプリケーションに制御を転送します。次に例を示します。


$ LD_BIND_NOW=yes prog

ここでは、ファイル prog 内とその依存関係の中のすべての再配置は制御がアプリケーションに移る前に処理されることになります。

個々のオブジェクトも、オブジェクトが読み込み時に再配置処理が完了している必要があることを指示するために、リンカーの -z now オプションを使用して構築されます。この再配置の必要条件は、実行時にマークされたオブジェクトの依存関係にも伝達されます。

再配置エラー

最も一般的な再配置エラーは、シンボルが検出されるときに発生します。この状態になると、適切な実行時リンカーのエラーメッセージが表示され、アプリケーションは終了します。次に例を示します。


$ ldd prog
        libfoo.so.1 =>   ./libfoo.so.1
        libc.so.1 =>     /usr/lib/libc.so.1
        libbar.so.1 =>   ./libbar.so.1
        libdl.so.1 =>    /usr/lib/libdl.so.1
$ prog
ld.so.1: prog: fatal: relocation error: file ./libfoo.so.1: ¥
symbol bar: referenced symbol not found

ここでは、ファイル libfoo.so.1 内で参照されたシンボル bar は配置できません。


注 -

動的実行プログラムのリンク編集中に、このソートの再配置エラーが起こる可能性は、定義されていない重大なシンボルとしてフラグが付けられます (この例については、「実行可能ファイルの作成」を参照)。この実行時の再配置エラーは、main のリンク編集が、bar のシンボル定義が組み込まれた共有オブジェクト libbar.so.1 の異なったバージョンを使用した場合、または -z nodefs オプションがリンク編集の一部として使用された場合に発生します。


データ参照として使用されたシンボルが配置できないために、このタイプの再配置エラーが発生した場合は、そのエラー状態は、プロセスの初期設定の直後にも発生します。ただし、レイジー結合のデフォルトモードが原因で、関数参照として使用されるシンボルが検出できない場合は、このエラー状態は、アプリケーションが制御を受け取ってから発生します。

後者の場合、コードを実行する実行パスによって、エラー状態が発生するまでに数分または数ヶ月かかる場合もあり、あるいは発生しない場合もあります。この種のエラーを防ぐためには、動的実行プログラムまたは共有オブジェクトの再配置の必要条件を、ldd(1) を使用して有効にしておきます。

ldd(1) とともに -d オプションを指定すると、すべての依存関係が出力され、すべてのデータ参照の再配置処理が実行されます。データ参照が解析できない場合には、診断メッセージが作成されます。上記の例から、次のように明示されます。


$ ldd -d prog
        libfoo.so.1 =>   ./libfoo.so.1
        libc.so.1 =>     /usr/lib/libc.so.1
        libbar.so.1 =>   ./libbar.so.1
        libdl.so.1 =>    /usr/lib/libdl.so.1
        symbol not found: bar           (./libfoo.so.1)

ldd(1) とともに -r オプションを指定すると、すべてのデータと関数参照の再配置が処理されます。また、この両方ともが解析されない場合には、診断メッセージが作成されます。

追加オブジェクトの読み込み

前の項では、実行時リンカーが、動的実行プログラムとその依存関係からのプロセスを、各モジュールのリンク編集中に定義されたように初期設定する方法について説明しました。また、実行時リンカーでは、プロセスの初期設定中に新しいオブジェクトを採用できるという、一歩進んだ柔軟性も提供しています。

環境変数 LD_PRELOAD は、共有オブジェクトまたは再配置可能なオブジェクトのフレーム名、あるいは複数のフレーム名を空白で区切ったストリングに初期設定できます。これらのオブジェクトは、動的実行プログラムの後で、依存関係よりも前に読み込まれ、ワールド検索範囲と大域シンボル可視性が割り当てられます (「シンボル検索」を参照)。次に例を示します。


$ LD_PRELOAD=./newstuff.so.1 prog

ここでは、動的実行プログラム prog が読み込まれ、次に共有オブジェクト newstuff.so.1 が続き、その次に prog 内に定義された依存関係が続きます。これらのオブジェクトが処理される順番は、ldd(1) を使用して表示できます。


$ LD_PRELOAD=./newstuff.so.1 ldd prog
        ./newstuff.so.1 => ./newstuff.so
        libc.so.1 =>     /usr/lib/libc.so.1

次に別の例を示します。


$ LD_PRELOAD="./foo.o ./bar.o" prog

この例の事前読み込みは、少し複雑で時間がかかります。実行時リンカーは、最初に再配置可能オブジェクト foo.obar.o をリンク編集し、メモリー内に保持されていた共有オブジェクトを生成します。次にこのメモリーイメージは、この前の例で示した共有オブジェクト newstuff.so.1 の事前読み込みとまったく同じ方法で、動的実行プログラムとその依存関係との間に挿入されます。ここでも、これらのオブジェクトが処理される順番は、ldd(1) を使用して表示できます。


$ LD_PRELOAD="./foo.o ./bar.o" ldd prog
        ./foo.o =>       ./foo.o
        ./bar.o =>       ./bar.o
        libc.so.1 =>     /usr/lib/libc.so.1

オブジェクトを動的実行プログラムの後に挿入するこれらのメカニズムは、「割り込み (interposition)」で説明した割り込みの概念を別のレベルに受け継いだものです。これらのメカニズムを使用すると、標準的な共有オブジェクト内に常駐する関数の、新しいインプリメンテーション (実現) を試すことができます。この関数が組み込まれたオブジェクトをあらかじめ読み込むことにより、この関数は元のオブジェクトにも割り込みされます。そのため、古い関数は、新しく読み込まれたバージョンによって完全に隠れてしまいます。

この他にも事前読み込みは、標準的な共有オブジェクト内に常駐する関数を補強するために使用できます。これが、新しいシンボルを元のシンボルに割り込みさせる目的です。これにより、新しい関数は、元の関数への呼び出し機能も保持しながら、この他の処理も追加実行できます。このメカニズムには、元の関数に関連したシンボルエイリアスか (「単純な解析」を参照)、または元のシンボルのアドレスを検索する機能が必要です (「割り込み (interposition) の使用」を参照)。

動的依存関係のレイジーローディング

動的依存関係を読み込むデフォルトのモデルは、動的依存関係をメモリーに対応付けして、追加の依存関係があるかどうか調べます。もし依存関係がある場合には、これらは順番に、すぐに読み込まれます。このサイクルは、依存関係のツリー全体を使い果たすまで、つまりすべての内部オブジェクトの参照 (再配置) を解決するまで続けられます。

このモデルでは、これらの依存関係内のコードが実行中にアプリケーションによって実際に参照されるかどうかに関係なく、アプリケーションの依存関係がすべてメモリー内に読み込まれ、すべてのデータの再配置が実行されます。

レイジーローディングモデルでは、レイジーローディングのラベルが付いた依存関係は、明示的に参照が行われるまで読み込まれません。関数呼び出しレイジー結合 (「プロシージャのリンクテーブル (プロセッサ固有)」を参照) を利用して、依存関係の読み込みを最初に参照されるまで延期することができます。実際に参照されないオブジェクトは読み込まれません。


注 -

オブジェクトへの参照は、データ項目または関数に対して行うことができます (「再配置が実行されるとき」を参照)。データへの参照はオブジェクトが初期設定された時に解決される必要があるため、1 つのデータへの参照を満たすすべての依存関係はすぐにロードされなければなりません。そのため、そういった依存関係をレイジーロード可能として示すことは、あまり効果がありません。動的オブジェクト間のデータへの参照は、概してあまり推奨されません。


レイジーローディングの例として、liblddbg.so.4 というデバッギングライブラリを参照するリンカーそのものがあります。デバッギングを呼び出すことはまれなので、リンカーを呼び出すたびにこのライブラリを読み込むことは不要で、コストがかさみます。このライブラリをレイジー読み込みできるように指定することにより、その処理コストをデバッギング出力を必要とする読み込みに使うことができます。

このレイジーローディングモデルを実行するための代替メソッドは、必要に応じて依存関係に dlopen() または dlsym() を実行することです。これは、dlsym() を通じた参照の数が少ない場合、あるいはリンク編集時に依存関係の名前あるいはロケーションがわからない場合に最適です。ただし、名前やロケーションがわかっている依存関係のより複雑な相互作用については、通常のシンボル参照のコードを使用し、依存関係をレイジー読み込みに指定する方が簡単です。

レイジー読み込みあるいは通常の読み込みが実行されるようにオブジェクトに指定するにはそれぞれ、リンカーオプション -z lazyload-z nolazyload を使用します。これらのオプションはリンク編集コマンド行の位置によって決まり、フラグを持つ依存関係が見つかると、そのフラグで指定された読み込み属性が採用されます。

ここでは、libdebug.so.1 に依存する単純なプログラムをレイジー読み込みにする例を示します。動的セクション (.dynamic) を検査すると libdebug.so.1 にレイジー読み込みのマークが付けられていることが示され、シンボル情報セクション (.SUNW_syminfo) を検査すると、その読み込みをトリガーするシンボルの参照が示されます。


$ cc -o prog prog.c -L. -zlazyload -ldebug -znolazyload -R'$ORIGIN'
$ elfdump -d prog

Dynamic Section:  .dynamic
     index  tag           value
       [1]  POSFLAG_1     0x1           [ LAZY ]
       [2]  NEEDED        0x123         libdebug.so.1
       [3]  NEEDED        0x131         libc.so.1
       [6]  RPATH         0x13b         $ORIGIN
...
$ elfdump -y prog

Syminfo section: .SUNW_syminfo
     index flgs boundto                symbol
			...
      [52] DL       [1] libdebug.so.1   debug

値に LAZY が指定された POSFLAG_1 は、次の NEEDED エントリ libdebug.so.1 がレイジー読み込みされるように指定することに注意してください。ただし、libc.so.1 は前に LAZY フラグが無いので、prog の初期始動時に読み込まれます。


注 -

実行時にレイジーローディングを無効にするには、環境変数 LD_NOLAZYLOAD をヌル以外の値に設定します。


初期設定および終了ルーチン

制御をアプリケーションに転送する前に、実行時リンカーは、アプリケーションおよびその依存関係内で検出された初期設定セクションを処理します。これらの .preinit_array.init_array.init セクションは、動的オブジェクトが構築される際にリンカーによって作成され、それぞれ .dynamic タグの DT_PREINIT_ARRAYDT_INIT_ARRAYDT_INIT でラベル付けされます (「初期設定および終了セクション」を参照)。

DT_PREINIT_ARRAY および DT_INIT_ARRAY で指定された配列内に含まれたアドレスを持つ関数は、配列内でアドレスが出現する順と同じ順序で実行時リンカーによって実行されます。1 つのオブジェクトに DT_INIT エントリと DT_INIT_ARRAY エントリの両方が含まれる場合は、そのオブジェクトについて DT_INIT エントリによって参照される関数は、DT_INIT_ARRAY エントリによって参照される関数より前に処理されます。

動的実行可能ファイルは、.preinit_array セクション内で初期設定前関数を提供することができます。これらの関数は、実行時リンカーがプロセスイメージを構築して再配置を実行し終わった後で、かつ他の初期設定関数の前に実行されます。初期設定前関数は、共有オブジェクト内では許可されません。


注 -

動的実行可能ファイル内のすべての DT_INIT セクションは、コンパイラドライバから供給されるプロセスの起動メカニズムによって、アプリケーション自体から呼び出されます。動的実行可能ファイルの DT_INIT セクションは、そのすべての依存関係の初期設定セクションが実行された後で、最後に呼び出されます。


Solaris 2.6 より前のリリースでは、依存関係からの初期設定ルーチンは、読み込まれた順序の降順で、つまり、ldd(1) を使用して表示された依存関係の順と逆の順序で呼び出されていました。

Solaris 2.6 リリースからは、実行時リンカーは、読み込まれた依存関係から、初期設定ルーチンの依存関係の配列リストを作成します。このリストは、各オブジェクトが表す依存関係の相関関係に加えて、表示された依存関係の外部で発生した結合から構成されます。

初期設定セクションは、依存関係が配列された順序の逆の順序で実行されます。循環性のある依存関係が検出された場合は、その周期を形成するオブジェクトは、トポロジカルソートは実行できません。そのため、この初期設定セクションは、読み込まれた順序の逆の順序で実行されることになります。

-i オプションを指定した ldd(1) を使用すると、オブジェクトの依存関係の初期設定の順番を表示できます。たとえば、次の動的実行プログラムとその依存関係は、循環性のある依存関係を示しています。


$ dump -Lv B.so.1 | grep NEEDED
[1]     NEEDED      C.so.1
$ dump -Lv C.so.1 | grep NEEDED
[1]     NEEDED      B.so.1
$ dump -Lv main | grep NEEDED
[1]     NEEDED      A.so.1
[2]     NEEDED      B.so.1
[3]     NEEDED      libc.so.1
$ ldd -i main
        A.so.1 =>        ./A.so.1
        B.so.1 =>        ./B.so.1
        libc.so.1 =>     /usr/lib/libc.so.1
        C.so.1 =>        ./C.so.1
        libdl.so.1 =>    /usr/lib/libdl.so.1

   cyclic dependencies detected, group[1]:
        ./libC.so.1
        ./libB.so.1

   init object=/usr/lib/libc.so.1
   init object=./A.so.1
   init object=./C.so.1 - cyclic group [1], referenced by:
        ./B.so.1
   init object=./B.so.1 - cyclic group [1], referenced by:
        ./C.so.1

注 -

Solaris 8 10/00 より前のリリースでは、環境変数 LD_BREADTH に空文字以外の値を設定すると、リンカーで初期設定セクションを Solaris 2.6 より前の順序で実行することができました。しかし、多数のアプリケーションを初期化すると、依存関係が複雑になり、位相的な並び替えが必要になるため、この機能は Solaris 8 10/00 から無効にされています。LD_BREADTH の設定は無視され、メッセージは表示されません。


初期設定処理は、dlopen(3DL) が指定された実行中のプロセスに追加されたオブジェクトごとに繰り返されます。

循環的な依存関係が存在することはよくあるため、実行時リンカーはさらに、初期設定を動的に呼び出して、そのオブジェクトのコードが呼び出される前に初期設定セクションを実行しようとします。実行時リンカーは、シンボルの結合の際に、結合する先のオブジェクトの初期設定セクションがすでに呼び出されているかどうかを判定し、呼び出されていなければ、シンボル結合手順を終る前に、それを呼び出します (「再配置が実行されるとき」を参照)。初期設定呼び出しの正確な順序は、環境変数 LD_DEBUGbasic を指定すれば見ることができます (「デバッギングエイド」を参照)。

動的オブジェクトは、終了セクションも提供できます。これらの .fini_array および .fini セクションは、動的オブジェクトが構築され、.dynamic タグの DT_FINI_ARRAY および DT_FINI でラベル付けされる際に、リンカーによって作成されます (「初期設定および終了セクション」 を参照)。

終了ルーチンは、atexit(3C) によって記録できるように構成されます。これらのルーチンは、プロセスが exit(2) を呼び出したとき、またはオブジェクトが、dlclose(3DL) が指定された実行プロセスから除去されたときに呼び出されます。

DT_FINI_ARRAY によって指定された配列内に含まれたアドレスを持つ関数は、その配列内でアドレスが出現する順序と逆の順序で、実行時リンカーに実行されます。1 つのオブジェクトに DT_FINI エントリと DT_FINI_ARRAY エントリの両方が含まれる場合は、そのオブジェクトについて DT_FINI_ARRAY エントリによって参照される関数は、DT_FINI エントリによって参照される関数よりも前に処理されます。


注 -

動的実行プログラム内の .fini セクションは、コンパイラドライバから提供されるプロセスの終了メカニズムによってアプリケーション自体から呼び出されます。動的実行プログラムの .fini セクションは、その依存関係の終了セクションが実行される前に、最初に呼び出されます。


Solaris 2.6 リリースからは、終了ルーチンが依存関係の配列順に呼び出されます。Solaris 2.6 より前のリリースの場合、終了ルーチンは読み込まれた順に呼び出されていました。

この初期設定および終了の呼び出し順序は、簡単明瞭に見えますが、この順序を強調しすぎないように注意が必要です。オブジェクトの順序は、共有オブジェクトとアプリケーションの開発によって左右される場合があるからです (詳細については、「依存関係の並べ変え」を参照)。

セキュリティ

セキュアプロセスには、その依存関係と実行パスを評価し、不当な依存関係の置換またはシンボルの割り込みを防ぐために使用されるいくつかの制約があります。

実行時リンカーは、そのユーザーがスーパーユーザーではなく、実際のユーザーと実効ユーザー ID が同じではない場合、または実際のグループと実効グループ ID が同じではない場合、プロセスをセキュアとして分類します (getuid(2)geteuid(2)getgid(2) および getegid(2) を参照)。

実行時リンカーが認識するデフォルトのトラストディレクトリは、32 ビットのオブジェクトの場合は /usr/lib/secure、64 ビットオブジェクトの場合は /usr/lib/secure/64 です。ユーティリティ crle(1) を使用すれば、セキュアアプリケーション向けに追加のトラストディレクトリを指定できます。この方法を使用する場合には、管理者は、ターゲットディレクトリを悪意のある侵入から適切に保護する必要があります。

LD_LIBRARY_PATH ファミリの環境変数がセキュアプロセス用に有効である場合は (「実行時リンカーによって検索されるディレクトリ」を参照)、この変数によって指定されたトラストディレクトリのみが、実行時リンカーの検索規則を補強するために使用されます。

セキュアプロセスでは、実行パスがフルパスである場合は、アプリケーションまたはその依存関係によって指定された実行パスの指定 (「実行時リンカーによって検索されるディレクトリ」を参照) が使用されます。つまり、パス名の冒頭部分に / が付く場合です。

セキュアプロセスでは、$ORIGIN 文字列の拡張は (「セキュリティ」を参照)、トラストディレクトリに拡張される場合のみ有効です。

追加オブジェクトは、LD_PRELOAD 環境変数 (「追加オブジェクトの読み込み」を参照)、または LD_AUDIT 環境変数 (「実行時リンカーの監査インタフェース」を参照) を使用したセキュアプロセスで読み込まれます。これらのオブジェクトはフルパス名または単純ファイル名で指定しなければなりません。フルパス名は、既知のトラストディレクトリに限定されます。単純ファイル名 (名前に / がついていない) は、前述した検索パスの制約に従って配置され、既知のトラストディレクトリにのみ解決されることになります。

セキュアプロセスでは、単純ファイル名を構成する依存関係は、前述のパス名の制約を使用して処理されます。フルパス名または相対パス名で表示された依存関係は、そのまま使用されます。そのため、セキュアプロセスの開発者は、フルパス名または相対パス名の依存関係として参照されるターゲットディレクトリを、不当な侵入から確実に保護する必要があります。

セキュアプロセスを作成する場合には、依存関係の表示や、dlopen(3DL)パス名の構築に、相対パス名は使用しないことをお勧めします。この制約は、アプリケーションと依存関係すべてに適用されます。

実行時リンクのプログラミングインタフェース

ここまでは、アプリケーションのリンク編集中に指定された依存関係が、プロセスの初期設定中に実行時リンカーによってどのように処理されるかについて説明してきました。このメカニズムに加えて、アプリケーションは、追加オブジェクトと結合することにより、その実行中にアドレススペースを拡張できます。この拡張は、アプリケーションのリンク編集中に指定された依存関係の処理と同じ実行時リンカーのサービスを、アプリケーションが要求できるようにすることで提供されます。

この遅延オブジェクトの結合処理には、いくつかの利点があります。

以下は、アプリケーションが追加の共有オブジェクトにアクセスするために実行する典型的な手順を示しています。これについては次の項で説明します。

実行時リンカーのサービスは、ヘッダーファイル dlfcn.h 内に定義され、共有オブジェクト libdl.so.1 によってアプリケーションで使用できるようになります。次に例を示します。


$ cc -o prog main.c -ldl

ここでは、ファイル main.c は、ルーチンの dlopen(3DL) ファミリのどれでも参照でき、アプリケーション prog は、実行時にこれらのルーチンと結合できます。

追加オブジェクトの読み込み

追加オブジェクトは、dlopen(3DL) を使用して、実行プロセスのアドレススペースに追加できます。この関数は、引数としてファイル名と結合モードを入手し、アプリケーションにハンドルを戻します。このハンドルを使用すると、アプリケーションは、dlsym(3DL)を使用することによってシンボルを配置できます。

ファイル名が、単純ファイル名で指定されている、つまり名前の中に `/' が組み込まれていない場合、実行時リンカーは一連の規則を使用して、適切なパス名を生成します。`/' が組み込まれたファイル名は、そのまま使用されます。

これらの検索パスの規則は、最初の依存関係の配置に使用された規則と全く同じものです (「実行時リンカーによって検索されるディレクトリ」を参照)。たとえば、ファイル main.c は、以下のようなコードフラグメントが組み込まれている場合、共有オブジェクト foo.so.1 を配置するために、実行時リンカーは、プロセスの初期設定時に表示された LD_LIBRARY_PATH 定義に、リンク編集 prog 中に指定された実行パスを続けて入力し、最後にデフォルトのロケーション /usr/lib を入力して使用します。


#include        <stdio.h>
#include        <dlfcn.h>

main(int argc, char ** argv)
{
    void *  handle;
    .....

    if ((handle = dlopen("foo.so.1", RTLD_LAZY)) == NULL) {
            (void) printf("dlopen: %s¥n", dlerror());
            exit (1);
    }
    .....

ファイル名が次のように指定されている場合、実行時リンカーは、プロセスの現在の作業ディレクトリ内でこのファイルだけを検索します。


if ((handle = dlopen("./foo.so.1", RTLD_LAZY)) == NULL) {

注 -

dlopen(3DL) を使用して指定された共有オブジェクトは、そのバージョンのファイル名で参照することをお勧めします (バージョンについての詳細は、「バージョンアップファイル名の管理」を参照)。


必要なオブジェクトが配置されていない場合は、dlopen(3DL) によって NULL ハンドルが戻されます。この場合、dlerror(3DL) を使用すると、失敗した真の理由を表示できます。次に例を示します。


$ cc -o prog main.c -ldl
$ prog
dlopen: ld.so.1: prog: fatal: foo.so.1: open failed: No such ¥
file or directory

dlopen(3DL) によって追加されたオブジェクトに、他のオブジェクトに依存する関係がある場合、その依存関係もプロセスのアドレススペースに配置されます。このプロセスは、指定されたオブジェクトの依存関係がすべて読み込まれるまで継続されます。この依存関係のツリーをグループと呼びます。

dlopen(3DL) によって指定されたオブジェクト、または依存関係がすでにプロセスイメージの一部である場合は、そのオブジェクトはこれ以上処理されません。しかし、この場合でも有効なハンドルは、アプリケーションに戻されます。このメカニズムにより、同じオブジェクトが複数回読み込まれることを防ぐことができます。また、このメカニズムを使用すると、アプリケーションは専用のハンドルを入手できます。たとえば、前述した main.c の例には、次のような dlopen() 呼び出しが組み込まれている場合、dlopen(3DL) から戻されたハンドルは、アプリケーションそのものの中、プロセスの初期設定の一部として読み込まれた依存関係の中、または RTLD_GLOBAL フラグが指定された dlopen(3DL) を使用してプロセスのアドレススペースに追加されたオブジェクトの中のシンボルを配置できます。


if ((handle = dlopen((const char *)0, RTLD_LAZY)) == NULL) {

再配置処理

「概要」で説明したように、オブジェクトの配置および対応付け後、実行時リンカーは各オブジェクトを処理し、必要な再配置を実行する必要があります。dlopen(3DL) を使用してプロセスのアドレススペースに配置されたオブジェクトは、同じ方法で再配置する必要もあります。

単純なアプリケーションの場合には、このプロセスはまったく関係ないこともありますが、多くのオブジェクトを含む多数の dlopen(3DL) 呼び出しと、共通の依存関係も伴う複雑なアプリケーションを所有するユーザーにとって、このことは非常に重要です。

再配置は、実行された時間によって分類されます。実行時リンカーのデフォルトの動作では、初期設定時にデータ参照の再配置がすべて処理され、通常レイジー結合と呼ばれるプロセスの実行時に関数参照がすべて処理されます。

この同じメカニズムは、モードが RTLD_LAZY として定義されているときに、dlopen(3DL) を使用して追加されたオブジェクトに適用されます。 この代わりとしては、オブジェクトが追加されたときに、オブジェクトの再配置すべてをすぐに実行する必要があります。これは、モード RTLD_NOW を使用することによって、 またはリンカーの -z now オプションを使用して作成されたときに、オブジェクト内のこの必要条件を記録することによって実現されます。この再配置の必要条件は、オープン状態のオブジェクトの依存関係に伝達されます。

また、再配置は、非シンボリックおよびシンボリックにも分類できます。このセクションの後半では、シンボル再配置がいつ発生するかに関係なく、この再配置に関連した問題について、シンボル検索の詳細に焦点をあてて説明します。

シンボル検索

dlopen(3DL) によって取得したオブジェクトが大域シンボルを参照する場合は、実行時リンカーは、プロセスを作成したオブジェクトのプールからこのシンボルを配置する必要があります。直接結合が無い場合は、dlopen(3DL) によって入手されたオブジェクトには、次の節で記述されているようにデフォルトのシンボル検索モデルが適用されます。ただし、プロセスを作成したオブジェクトの属性と結合される dlopen(3DL) のモードは、代わりのシンボル検索のモデルに提供されます。

直接結合を指定されたオブジェクトでは、それに対応する依存関係から直接、シンボルが検索されます (「直接結合」を参照)。ただし、この後で述べるすべての属性はそのまま有効です。

オブジェクトの 2 つの属性は、シンボル検索に影響を与えます。1 つ目は、オブジェクトシンボルの検索範囲の要求で、2 つ目は、プロセス内の各オブジェクトが提供するシンボルの可視性です。オブジェクトの検索範囲には次のものがあります。

ワールド (world)

オブジェクトは、プロセス内の他の大域オブジェクト内で検索されます。

グループ (group)

オブジェクトは、同じグループ内のオブジェクト内でのみ検索されます。dlopen(3DL) を使用して入手されたオブジェクトから作成された依存関係ツリー、またはリンカーの -B group オプションを使用して構築されたオブジェクトから作成された依存関係ツリーは、固有のグループを形成します。

オブジェクトからのシンボルの可視性には、次のものがあります。

大域 (global)

オブジェクトのシンボルは、ワールド検索範囲を持つオブジェクトから参照できます。

ローカル (local)

オブジェクトのシンボルは、同じグループを構成する他のオブジェクトからのみ参照されます。

デフォルトにより、dlopen(3DL) を使用して入手したオブジェクトには、ワールドシンボル検索範囲とローカルシンボル可視性が割り当てられます。「デフォルトのシンボル検索モデル」では、このデフォルトモデルを使用して、典型的なオブジェクトグループのインタラクションについて説明しています。「大域オブジェクトの定義」「グループの分離」、および 「オブジェクト階層」では、デフォルトのシンボル検索の展開に dlopen(3DL) モードとファイル属性を使用する例を示しています。

デフォルトのシンボル検索モデル

dlopen(3DL) によって追加された各オブジェクトでは、実行時リンカーは、最初に動的実行プログラム内でシンボルを検索し、次に、プロセスの初期設定中に提供されたそれぞれのオブジェクト内を検索します。ただし、シンボルが検出されない場合には、実行時リンカーは、dlopen(3DL) によって入手されたオブジェクト内と、その依存関係内の検索を続行します。

たとえば、動的実行プログラム prog と共有オブジェクト B.so.1 を入手してみます。この 2 つには、それぞれ以下の単純な依存関係が付いています。


$ ldd prog
        A.so.1 =>        ./A.so.1
$ ldd B.so.1
        C.so.1 =>        ./C.so.1

prog が、dlopen(3DL) を使用して共有オブジェクト B.so.1 を入手した場合、共有オブジェクト B.so.1C.so.1 の再配置に必要なシンボルが、最初に prog 内で検索され、A.so.1B.so.1C.so.1 の順番に検索されます。このような単純なケースでは、dlopen(3DL) によって入手された共有オブジェクトは、アプリケーションの元のリンク編集の末尾に追加されたと考える方が簡単な場合があります。たとえば、上記のオブジェクトの参照を図示すると、次のようになります。

図 3-1 単一の dlopen() 要求

Graphic

dlopen(3DL) から入手されたオブジェクトによって要求されたシンボル検索は、影付きのブロックで示しています。このシンボル検索は、動的実行プログラム prog から、最後の共有オブジェクト C.so.1 へと進みます。

このシンボル検索は、読み込まれたオブジェクトに割り当てられた属性によって確立されます。動的実行プログラムとそれと同時に読み込まれたすべての依存関係には、大域シンボル可視性が割り当てられ、新しいオブジェクトにはワールドシンボルの検索範囲が割り当てられることを思い出してください。これによって、新しいオブジェクトは元のオブジェクト内を調べてシンボルを検索できます。また、新しいオブジェクトは、固有のグループを形成し、このグループ内では、各オブジェクトはローカルシンボル可視性を持ちます。そのため、グループ内の各オブジェクトは、他のグループ構成要素内でシンボルを検索できます。

これらの新しいオブジェクトは、アプリケーションまたはその最初のオブジェクトの依存関係によって要求される、通常のシンボル検索には影響を与えません。たとえば、上記の dlopen(3DL) が実行された後で、A.so.1 に関数再配置が必要な場合、実行時リンカーの再配置シンボルの通常の検索は、progA.so.1 で実施されますが、その後で B.so.1 または C.so.1 は検索されません。

このシンボル検索は、読み込まれたときにオブジェクトに割り当てられた属性によって実行されます。動的実行プログラムとこれとともに読み込まれた依存関係に割り当てられたワールドシンボルの検索範囲では、ローカルシンボル可視性だけを提供する新しいオブジェクト内を検索できません。

これらのシンボル検索とシンボル可視性の属性は、そのプロセスのアドレススペースへの投入とオブジェクト間の依存の関係に基づいて、オブジェクト間の関係を保持します。指定された dlopen(3DL) に関連したオブジェクトを割り当てることにより、固有のグループでは、同じ dlopen(3DL) と関連したオブジェクトだけが、グループ内のオブジェクトと関連する依存関係の中の検索ができます。

このオブジェクト間の関係を定義するという概念は、複数の dlopen(3DL) を実行するアプリケーション内では、より明確になります。たとえば、共有オブジェクト D.so.1 に次の依存関係があるとします。


$ ldd D.so.1
        E.so.1 =>         ./E.so.1

このとき prog アプリケーションが、共有オブジェクト B.so.1 に加えて、この共有オブジェクトにも dlopen(3DL) を実行した場合は、オブジェクト間のシンボル検索の関係は、図 3-2 のように表すことができます。

図 3-2 複数の dlopen() 要求

Graphic

B.so.1D.so.1 の両方にシンボル foo の定義が組み込まれ、C.so.1E.so.1 にこのシンボルが必要とする再配置が組み込まれている場合、固有のグループに対するオブジェクトの関係によって、C.so.1B.so.1 の定義に結合され、E.so.1D.so.1 の定義に結合されます。このメカニズムは、dlopen(3DL) への複数の呼び出しにより入手されたオブジェクトの最も直感的な結合を提供するためのものです。

オブジェクトが、前述した処理の進行の中で使用される場合、それぞれの dlopen(3DL) が実施された順番は、結果として発生するシンボル結合には影響しません。ただし、複数のオブジェクトに共通の依存関係がある場合は、結果の結び付きは、dlopen(3DL) 呼び出しが実行された順番による影響を受けます。

次に、同じ共通依存関係を持つ共有オブジェクト O.so.1P.so.1 の例を示します。


$ ldd O.so.1 
        Z.so.1 =>        ./Z.so.1
$ ldd P.so.1 
        Z.so.1 =>        ./Z.so.1

この例では、prog アプリケーションは、各共有オブジェクトに dlopen(3DL) を使用しています。共有オブジェクト Z.so.1 が、O.so.1P.so.1 両方の共通依存関係であるため、図 3-3 のようにこの依存関係は 2 つの dlopen(3DL) 呼び出しに関連する両方のグループに割り当てられます。

図 3-3 共通依存関係を伴う複数の dlopen() 要求

Graphic

この結果、O.so.1P.so.1 の両方がシンボルの検索に Z.so.1 を使用できます。ここで重要なのは、dlopen(3DL) の順序に限って言えば、Z.so.1O.so.1P.so.1 の両方の中でシンボルを検索できることです。

そのため、O.so.1P.so.1 の両方に、Z.so.1 の再配置に必要なシンボル foo の定義が組み込まれている場合、実際に発生する結び付きを予期することはできません。それは、この結び付きが dlopen(3DL) 呼び出しの順序の影響を受けるからです。シンボル foo の機能が、シンボルが定義されている 2 つの共有オブジェクト間で異なる場合、Z.so.1 コードを実行したすべての結果は、アプリケーションの dlopen(3DL) の順序によって異なる可能性があります。

大域オブジェクトの定義

dlopen(3DL) によって入手されたオブジェクトへのデフォルトのローカルシンボルの可視性の割り当ては、モード引数に RTLD_GLOBAL フラグを指定することによって、大域に拡大ができます。このモードでは、dlopen(3DL) によって入手されたオブジェクトは、シンボルを配置するためのワールドシンボルの検索範囲の指定された他のオブジェクトによって使用することができます。

また、RTLD_GLOBAL フラグが指定された dlopen(3DL) によって入手されたオブジェクトは、dlopen(0) を使用したシンボル検索にも使用できます (「追加オブジェクトの読み込み」を参照)。


注 -

ローカルシンボルの可視性を持つグループの構成要素が、他の大域シンボルの可視性を必要とするグループによって参照される場合、オブジェクトの可視性はローカルと大域の両方を連結したものになります。この後大域グループの参照が削除されても、この格上げされた属性はそのまま残ります。


グループの分離

dlopen(3DL) によって入手されたオブジェクトへのデフォルトのワールドシンボルの検索範囲の割り当ては、モード引数に RTLD_GROUP フラグを指定することによって、グループに縮小することができます。このモードでは、dlopen(3DL) によって入手されたオブジェクトは、そのオブジェクト固有のグループ内でしかシンボルの検索ができません。

オブジェクトがリンカーの -B group オプションを使用して構築された場合、オブジェクトには、割り当てられたグループシンボルの検索範囲があります。


注 -

グループ検索機能を持つグループの構成要素が、ワールド検索機能を必要とする他のグループによって参照された場合、オブジェクトの検索機能はグループとワールドが結合したものになります。この後ワールドグループの参照が削除されても、この格上げされた属性はそのまま残ります。


オブジェクト階層

dlopen(3DL) によって入手された最初のオブジェクトが、2 番目のオブジェクトに dlopen(3DL) を使用した場合、両方のオブジェクトは 1 つのグループに割り当てられます。これにより、オブジェクトが互いにシンボルを配置し合うことを防ぐことができます。

実装の中には、最初のオブジェクトの場合、シンボルを 2 番目のオブジェクトの再配置用にエキスポートする必要がある場合もあります。この必要条件は、次の 2 つのメカニズムのいずれかによって満たすことができます。

最初のオブジェクトを 2 番目のオブジェクトの明示的な依存関係にした場合、これは 2 番目のオブジェクトのグループにも割り当てられます。そのため、最初のオブジェクトは、2 番目のオブジェクトの再配置に必要なシンボルも提供できます。

ただし、ほとんどのオブジェクトが 2 番目のオブジェクトに dlopen(3DL) を実行し、これらの最初のオブジェクトが、それぞれ 2 番目のオブジェクトの再配置を満足させる同じシンボルをエキスポートする必要がある場合、2 番目のオブジェクトには明示的な依存関係を割り当てることはできません。この場合、2 番目のオブジェクトの dlopen(3DL) モードは、RTLD_PARENT フラグを使用して補強できます。このフラグによって、2 番目のオブジェクトのグループが、明示的な依存関係が伝達されたのと同じ方法で、最初のオブジェクトに伝達されます。

上記の 2 つのテクニックには、1 つ異なる点があります。明示的な依存関係を指定する場合、その依存関係そのものは、2 番目のオブジェクトの dlopen(3DL) 依存関係ツリーの一部になるため、dlopen(3DL) を使用したシンボル検索が可能になります。RTLD_PARENT を使用して 2 番目のオブジェクトを入手する場合、最初のオブジェクトは、dlopen(3DL) を使用したシンボルの検索に使用できるようにはなりません。


注 -

dlopen(3DL) によって 2 番目のオブジェクトが、大域シンボル可視性が指定された最初のオブジェクトから入手された場合、RTLD_PARENT モードは冗長で他に影響を与えることはありません。このような状態は、dlopen(3DL) がアプリケーションから呼び出されたとき、またはアプリケーションの中の依存関係の 1 つから呼び出されたときに多く発生します。


新しいシンボルの入手

プロセスは、dlsym(3DL) を使用して特定のシンボルのアドレスを入手できます。この関数は、ハンドルとシンボルをとり、呼び出し元にそのシンボルのアドレスを戻します。ハンドルは、次の方法でシンボルの検索を指示します。

最初の例は、最も一般的なものです。ここでは、アプリケーションは、追加オブジェクトをそのアドレススペースに追加し、さらに dlsym(3DL) を使用して関数またはデータシンボルを配置します。次に、アプリケーションは、これらのシンボルを使用して、新しいオブジェクト内で提供されるサービスを呼び出します。たとえば、次のコードが組み込まれた main.c ファイルを取り上げてみます。


#include    <stdio.h>
#include    <dlfcn.h>

main()
{
    void *  handle;
    int *   dptr, (* fptr)();

    if ((handle = dlopen("foo.so.1", RTLD_LAZY)) == NULL) {
            (void) printf("dlopen: %s¥n", dlerror());
            exit (1);
    }

    if (((fptr = (int (*)())dlsym(handle, "foo")) == NULL) ||
        ((dptr = (int *)dlsym(handle, "bar")) == NULL)) {
            (void) printf("dlsym: %s¥n", dlerror());
            exit (1);
    }

    return ((*fptr)(*dptr));
}

ここで、シンボル foobar は、ファイル foo.so.1 内で検索された後で、このファイルに関連した依存関係が検索されます。次に、関数 foo は、単一の引数 bar によって return() ステートメントの一部として呼び出されます。

アプリケーション prog が、上記のファイル main.c を使用して構築された場合は、その最初の依存関係は次のものになります。


$ ldd prog
        libdl.so.1 =>    /usr/lib/libdl.so.1
        libc.so.1 =>     /usr/lib/libc.so.1

dlopen(3DL) 内に指定されたファイル名に値 0 がある場合、シンボル foobar は、prog/usr/lib/libdl.so.1/usr/lib/libc.so.1 の順番で検索されます。

ハンドルがシンボル検索を開始するルートを指示している場合は、この検索メカニズムは、「シンボルの検索」で説明したものと同じモデルに従います。

要求されたシンボルが配置されていない場合は、dlsym(3DL) は、NULL 値を戻します。この場合、dlerror(3DL) を使用すると、失敗の真の理由を示すことができます。次に例を示します。


$ prog
dlsym: ld.so.1: main: fatal: bar: can't find symbol

ここでは、アプリケーション prog は、シンボル bar を配置できませんでした。

機能のテスト

特別なハンドル RTLD_DEFAULT を使用すると、アプリケーションは他のシンボルの存在をテストできます。シンボル検索は、呼び出しオブジェクトを再配置する場合に使用されるものと同じモデルに従います (「セキュリティ」を参照)。たとえば、アプリケーション prog に次のようなコードフラグメントが組み込まれているとします。


if ((fptr = (int (*)())dlsym(RTLD_DEFAULT, "foo")) != NULL)
        (*fptr)();

この場合 foo は、prog/usr/lib/libdl.so.1/usr/lib/libc.so.1 の順番で検索されます。このコードフラグメントが、図 3-2 の例で示すようにファイル B.so.1 に組み込まれていた場合、foo の検索は B.so.1C.so.1 でも継続して行われます。

このメカニズムによって、「ウィークシンボル」で説明した定義されていないウィークリファレンスの代わりに使用できる、パワフルで柔軟性のある代替機能が提供されます。

割り込み (interposition) の使用

特別なハンドル RTLD_NEXT を使用すると、アプリケーションは、シンボルの範囲内で次のシンボルを配置できます。たとえば、アプリケーション prog に次のようなコードフラグメントが組み込まれているとします。


if ((fptr = (int (*)())dlsym(RTLD_NEXT, "foo")) == NULL) {
        (void) printf("dlsym: %s¥n", dlerror());
        exit (1);
}

return ((*fptr)());

この場合 foo は、prog に関連した共有オブジェクト内で、この場合は /usr/lib/libdl.so.1 の次に /usr/lib/libc.so.1 が検索されます。このコードフラグメントが、図 3-2 の例で示すように、ファイル B.so.1 に組み込まれている場合は、foo は関連する共有オブジェクト C.so.1 の中だけで検索されます。

RTLD_NEXT を使用することによって、シンボル割り込みを活用できます。たとえば、オブジェクト関数は、オブジェクトの前に付けて割り込みでき、これにより、元の関数の処理を補強できます。次のコードフラグメントが共有オブジェクト malloc.so.1 内にある場合、以下のようになります。


#include    <sys/types.h>
#include    <dlfcn.h>
#include    <stdio.h>

void *
malloc(size_t size)
{
    static void * (* fptr)() = 0;
    char             buffer[50];

    if (fptr == 0) {
        fptr = (void * (*)())dlsym(RTLD_NEXT, "malloc");
        if (fptr == NULL) {
                (void) printf("dlopen: %s¥n", dlerror());
                return (0);
        }
    }

    (void) sprintf(buffer, "malloc: %#x bytes¥n", size);
    (void) write(1, buffer, strlen(buffer));
    return ((*fptr)(size));
}

この共有オブジェクトを、malloc(3C) が常駐するシステムライブラリ /usr/lib/libc.so.1 の間に割り込むことにより、元の関数呼び出されて配置が完了する前に、この関数への呼び出しが次のように割り込まれます。


$ cc -o malloc.so.1 -G -K pic malloc.c
$ cc -o prog file1.o file2.o ..... -R. malloc.so.1
$ prog
malloc: 0x32 bytes
malloc: 0x14 bytes
..........

あるいは、次のように入力しても、上記のものと同じ割り込みを実行できます。


$ cc -o malloc.so.1 -G -K pic malloc.c
$ cc -o prog main.c
$ LD_PRELOAD=./malloc.so.1 prog
malloc: 0x32 bytes
malloc: 0x14 bytes
..........

注 -

割り込みテクニックを使用する場合、反復する可能性がある処理には注意が必要です。前の例では、printf(3C) を直接使用する代わりに sprintf(3C) を使用して診断メッセージを形成し、printf(3C)malloc(3C) を使用する可能性があることによる反復を防いでいます。


動的実行プログラムまたはあらかじめ読み込まれたオブジェクト内で RTLD_NEXT を使用することにより、予測可能で有用な割り込みテクニックが使用できます。ただし、このテクニックを汎用オブジェクトの依存関係内で使用する場合には、実際に読み込まれる順番が必ず予測できるとは限らないため、注意が必要です (「依存関係の並べ変え」を参照)。

機能チェッカ

リンカーによって構築された実行プログラムファイルまたは共有オブジェクトファイルは、新しい実行時リンカー機能を要求できます。実行時リンカーが実行に必要な実行時機能をサポートしているかどうかを検査するために、アプリケーション .init セクションによって関数 _check_rtld_feature() が呼び出されます。現在チェックされる機能については、表 7-47 を参照してください。

デバッギングエイド

Solaris リンカーには、デバッギングライブラリが付いていて、これを使用すると実行時のリンクプロセスをより詳細に監視できます。このライブラリは、アプリケーションと依存関係の実行を理解したり、デバッグする場合に役立ちます。これはビジュアルエイドで、このライブラリを使用して表示される情報のタイプは定数のままであると予期されますが、この情報の正確な形式は、リリースごとにわずかに変更される場合があります。

実行時リンカーをよく理解していないと、デバッギング出力のなかには理解できないものがある可能性がありまが、一般的には、興味深い面が多いといえます。

デバッギングは、環境変数 LD_DEBUG を使用して実行します。すべてのデバッギングの出力は、接頭辞としてプロセス識別子を持っていて、デフォルトごとに、標準的なエラーに対して送信されます。この環境変数は、1 つまたは複数のトークンを使用して、必要なデバッギングタイプを示す必要があります。

このデバッギングオプションとともに使用できるトークンは、LD_DEBUG=help を使って表示できます。どの動的実行プログラムを使用しても、この情報を要求することができます。この場合、プロセス自体が終了した後でこの情報が表示されます。次に例を示します。


$ LD_DEBUG=help prog
11693:
11693:           For debugging the runtime linking of an application:
11693:                  LD_DEBUG=token1,token2  prog
11693:           enables diagnostics to the stderr.  The additional
11693:           option:
11693:                  LD_DEBUG_OUTPUT=file
11693:           redirects the diagnostics to an output file created
11593:           using the specified name and the process id as a
11693:           suffix.  All diagnostics are prepended with the
11693:           process id.
11693:
11693:
11693: basic     provide basic trace information/warnings
11693: bindings  display symbol binding; detail flag shows
11693:           absolute:relative addresses
11693: detail    provide more information in conjunction with other
11693:           options
11693: files     display input file processing (files and libraries)
11693: help      display this help message
11693: libs      display library search paths
11693: move      display move section processing
11693: reloc     display relocation processing
11693: symbols   display symbol table processing;
11693:           detail flag shows resolution and linker table addition
11693: versions  display version processing
11693: audit     display runtime link-audit processing

注 -

これは一例で、実行時リンカーに有効なオプションを示しています。正確なオプションについては、リリースごとに異なる場合があります。


環境変数 LD_DEBUG_OUTPUT を使用すると、出力ファイルを標準エラーの代わりに使用するように指定できます。出力ファイルの名前には、接尾辞としてプロセス ID が付きます。

セキュアアプリケーションのデバッギングは実行できません。

実行時に発生するシンボル結合の表示機能は、最も有効なデバッギングオプションの 1 つです。たとえば、2 つのローカル共有オブジェクト上に依存関係を持つ、非常に単純な動的実行プログラムを取り上げてみます。


$ cat bar.c
int bar = 10;
$ cc -o bar.so.1 -Kpic -G bar.c

$ cat foo.c
foo(int data)
{
        return (data);
}
$ cc -o foo.so.1 -Kpic -G foo.c

$ cat main.c
extern  int     foo();
extern  int     bar;

main()
{
        return (foo(bar));
}
$ cc -o prog main.c -R/tmp:. foo.so.1 bar.so.1

実行時シンボルは、LD_DEBUG=bindings を設定することによって表示されます。


$ LD_DEBUG=bindings prog
11753: .......
11753: binding file=prog to file=./bar.so.1: symbol bar
11753: .......
11753: transferring control: prog
11753: .......
11753: binding file=prog to file=./foo.so.1: symbol foo
11753: .......

ここでは、データの再配置に必要なシンボル bar が、アプリケーションが制御を受け取る前に結合されます。これに対して、関数の再配置に必要なシンボル foo は、関数が最初に呼び出されたときに、アプリケーションが制御を受け取った後で結合されます。これは、レイジー結合のデフォルトモードを示しています。環境変数 LD_BIND_NOW が設定されている場合、シンボル結合はすべて、アプリケーションが制御を受け取る前に実行されます。

現在の結合ロケーションの実際の相対アドレスに関する追加情報は、LD_DEBUG=bindings,detail を設定すると入手できます。

実行時リンカーが、関数の再配置を実行すると、関数 .plt に関連したデータも書き換えられるため、この後に発生する呼び出しは、関数に直接送信されます。環境変数 LD_BIND_NOT は、あらゆる値に設定されて、このデータの更新を防ぐことができます。この変数を、詳細結合に対するデバッギング要求とともに使用すると、関数結合すべての完全な実行時アカウントを入手できます。この組み合わせによる出力は、膨大なものになるため、アプリケーションのパフォーマンスは低下します。

この他にも、実行時のさまざまな検索パスの使用状況が表示できます。たとえば、依存関係の配置に使用される検索パスのメカニズムは、次のように LD_DEBUG=libs を設定して表示できます。


$ LD_DEBUG=libs prog
11775:
11775: find object=foo.so.1; searching
11775:  search path=/tmp:.  (RPATH from file prog)
11775:  trying path=/tmp/foo.so.1
11775:  trying path=./foo.so.1
11775:
11775: find object=bar.so.1; searching
11775:  search path=/tmp:.  (RPATH from file prog)
11775:  trying path=/tmp/bar.so.1
11775:  trying path=./bar.so.1
11775: .......

ここでは、アプリケーション prog 内に記録された実行パスは、2 つの依存関係 foo.so.1bar.so.1 の検索に影響を与えます。

これと同様の方法で、各シンボルを検索する検索パスは、LD_DEBUG=symbols を設定して表示できます。これを bindings 要求と結合すると、次のように、シンボル再配置プロセスが完全に表示されます。


$ LD_DEBUG=bindings,symbols
11782: .......
11782: symbol=bar;  lookup in file=./foo.so.1  [ ELF ]
11782: symbol=bar;  lookup in file=./bar.so.1  [ ELF ]
11782: binding file=prog to file=./bar.so.1: symbol bar
11782: .......
11782: transferring control: prog
11782: .......
11782: symbol=foo;  lookup in file=prog  [ ELF ]
11782: symbol=foo;  lookup in file=./foo.so.1  [ ELF ]
11782: binding file=prog to file=./foo.so.1: symbol foo
11782: .......

注 -

上記の例では、シンボル bar は、アプリケーション prog 内では検索されません。これは、コピーの再配置を処理するときに行なわれる最適化が原因です (この再配置タイプの詳細については、「コピー再配置」を参照)。