リンカーとライブラリ

第 3 章 実行時リンカー

「動的実行可能プログラム」を初期化および実行するとき、アプリケーションとその依存関係を結合させるために、「インタプリタ」が呼び出されます。Solaris OS では、このインタプリタを実行時リンカーと呼びます。

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


注 –

ld.so.1 は、共有オブジェクトの特殊なケースです。ここではバージョン番号 1 が使われています。しかし、Solaris OS の今後のリリースによってバージョンアップされる可能性があります。


動的オブジェクトの実行プロセス中に、カーネルはファイルを読み込んで、プログラムのヘッダー情報を読み取ります。「プログラムヘッダー」を参照してください。この情報を使って、カーネルは必要なインタプリタの名前を検出します。カーネルは、このインタプリンタを読み込んで制御を移し、インタプリタがアプリケーションの実行を続行するために十分な量の情報を転送します。

アプリケーションの初期化に加え、実行時リンカーは、アプリケーションが自分のアドレス空間を拡張できるようにするサービスも提供します。この処理には、追加のオブジェクトの読み込みとこれらのオブジェクトが提供するシンボルへの結合が含まれます。

実行時リンカーは次の処理を実行します。

共有オブジェクトの依存性

実行時リンカーがプログラムのメモリーセグメントを作成するとき、依存性は、プログラムのサービスを提供するためにどの共有オブジェクトが必要であるかを示します。参照された共有オブジェクトとそれが依存するものを繰り返し結合することによって、実行時リンカーは完全なプロセスイメージを生成します。


注 –

共有オブジェクトが依存性リストにおいて複数回参照されるときでも、実行時リンカーはこの共有オブジェクトをプロセスに 1 回だけ結合します。


共有オブジェクトの依存関係の検索

動的実行可能プログラムのリンク中に、1 つまたは複数の共有オブジェクトが明示的に参照されます。これらのオブジェクトは、依存関係として動的実行可能プログラム内に記録されます。

実行時リンカーはこの依存情報を使用して、関連オブジェクトを検索して読み込みます。これらの依存関係は、実行プログラムのリンク編集中に参照された順番で処理されます。

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

実行時リンカーが検索するディレクトリ

実行時リンカーは、デフォルトでは 2 つの場所で依存関係を検索します。32 ビットオブジェクトを処理する場合、デフォルトでは /lib/usr/lib が検索されます。64 ビットオブジェクトを処理する場合、デフォルトでは /lib/64/usr/lib/64 が検索されます。単純なファイル名で指定された依存関係の前には、このデフォルトのディレクトリ名が付きます。このパス名を使用して、実際のファイルを見つけます。

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


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

ファイル /usr/bin/cat には依存関係があり、ファイル libc.so.1libm.so.2必要としています

オブジェクトに記録されている依存関係は、elfdump(1) を使用して調べることができます。このコマンドを使用すると、ファイルの .dynamic セクションを表示して、NEEDED タグがついているエントリを探すことができます。次の例では、前の ldd(1) の例に示されていた依存関係 libm.so.2 は、ファイル /usr/bin/cat に記録されません。ldd(1) が、指定されたファイルの依存関係の全体を示し、libm.so.2 は実際には /lib/libc.so.1 の依存関係となります。


$ elfdump -d /usr/bin/cat
 
Dynamic Section:  .dynamic:
     index  tag                value
       [0]  NEEDED            0x211               libc.so.1
       ...

上記の elfdump(1) の例では、依存関係は単純なファイル名として表示されています。つまり、ファイル名に「/」が含まれていません。実行時リンカーが一連のデフォルト検索規則に従ってパス名を生成するためには、単純なファイル名を使用する必要があります。「/」が組み込まれたファイル名は、そのまま使用されます。

単純なファイル名の記録は、標準的でもっとも柔軟性の高い、依存関係を記録するメカニズムです。リンカーに -h オプションを指定すると、依存関係内の単純な名前が記録されます。「命名規約」および 「共有オブジェクト名の記録」を参照してください。

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

追加の検索パスは、オブジェクトごとに、オブジェクトのリンク編集中に「実行パス」を記録して指定できます。この情報の記録方法の詳細については、「実行時リンカーが検索するディレクトリ」を参照してください。

実行パスの記録は、elfdump(1) を使用して表示できます。RUNPATH タグの付いた .dynamic エントリを例示します。次の例では、 proglibfoo.so.1 上に依存関係を持っています。実行時リンカーは、デフォルトの場所を調べる前に、ディレクトリ/home/me/lib/home/you/lib を検索しなければなりません。


$ elfdump -d prog | egrep "NEEDED|RUNPATH"
       [1]  NEEDED            0x4ce               libfoo.so.1
       [3]  NEEDED            0x4f6               libc.so.1
      [21]  RUNPATH           0x210e              /home/me/lib:/home/you/lib

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

これらの環境変数は、アプリケーションを強制的にローカルな依存関係に結合するといったデバッグの目的に適しています。次の例では、上記の例のファイル prog は、現在のカレントディレクトリ内で検出された libfoo.so.1 に結合されます。


$ LD_LIBRARY_PATH=. prog

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

環境変数で検索パスを指定した場合、64 ビットの実行プログラムが、目的の名前と一致する 32 ビットのライブラリが格納されているパスを検索することもありえます。あるいはその逆もありえます。このような場合、実行時リンカーは一致しない 32 ビットのライブラリを拒否し、検索を続行して目的の名前と一致する有効な 64 ビットのライブラリを探します。一致するものが見つからない場合には、エラーメッセージが表示されます。この拒否を詳細に監視するには、LD_DEBUG 環境変数を設定して、files のトークンを取り込みます。「デバッギングライブラリ」を参照してください。


$ LD_LIBRARY_PATH=/lib/64 LD_DEBUG=files /usr/bin/ls
...
00283: file=libc.so.1;  needed by /usr/bin/ls
00283: 
00283: file=/lib/64/libc.so.1  rejected: ELF class mismatch: 32–bit/64–bit
00283: 
00283: file=/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=/lib/libc.so.1;  analyzing  [ RTLD_GLOBAL  RTLD_LAZY ]
...

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


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

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

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

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

動的ストリングトークン

実行時リンカーは、さまざまな動的ストリングトークンを展開できます。このようなトークンは、フィルタ、「実行パス」、および依存関係の定義に利用できます。

再配置処理

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

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

再配置のさまざまなタイプの詳細については、「再配置型 (プロセッサ固有)」を参照してください。再配置には基本的に 2 つの種類があります。

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


$ elfdump -r libbar.so.1

Relocation Section:  .rel.got:
    type                               offset             section       symbol
  R_SPARC_RELATIVE                    0x10438             .rel.got  
  R_SPARC_GLOB_DAT                    0x1043c             .rel.got      foo

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

2 番目の再配置では、シンボル foo のアドレスが必要です。この再配置を完了させるには、実行時リンカーが、これまでに読み込まれた動的実行可能ファイルと依存関係のいずれかを使用して、このシンボルを検出する必要があります。

再配置シンボルの検索

実行時リンカーには、オブジェクトが必要とするシンボルを実行時に検索する責任があります。一般にユーザーは、動的実行可能ファイルやその依存関係および dlopen(3C) によって取得されたオブジェクトに適用される、デフォルトの検索モデルを理解するようになります。しかし、オブジェクトのシンボル属性や特定の結合要件が原因で、より複雑なシンボル検索が行われることもあります。

オブジェクトの 2 つの属性は、シンボル検索に影響を与えます。最初の属性は、要求元オブジェクトのシンボルの検索範囲です。2 つ目の属性は、プロセス内の各オブジェクトによって提供されるシンボルの可視性です。

これらの属性は、オブジェクトを読み込む際、デフォルトとして適用できます。これらの属性は、dlopen(3C) の特定のモードとしても提供できます。場合によっては、これらの属性をオブジェクトの構築時にオブジェクト内に記録することができます。

オブジェクトは、world 検索範囲または group 検索範囲、あるいはその両方を定義できます。

ワールド (world)

オブジェクトは、プロセス内のほかの任意の大域オブジェクト内でシンボルを検索できます。

group

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

オブジェクトは、オブジェクトのエクスポートされたシンボルがグローバルに参照可能か、ローカルに参照可能かを定義できます。

大域 (global)

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

ローカル (local)

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

もっとも単純な形のシンボル検索については、次の節「デフォルトのシンボル検索」で説明します。一般に、シンボル属性はさまざまな形の dlopen(3C) によって利用されます。これらのシナリオについては、「シンボルの検索」に記載されています。

動的なオブジェクトで直接結合を行うと、別のシンボル検索モデルが提供されます。このモデルでは、実行時リンカーは、リンク編集時に結合されたオブジェクトからシンボルを直接検索します。「直接結合」を参照してください。

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

動的実行可能プログラムと、ともに読み込まれるすべての依存関係には、「ワールド」検索範囲と、「大域」シンボル可視性が割り当てられます 。動的実行可能ファイルや、それとともに読み込まれた依存関係を対象としたデフォルトのシンボル検索では、各オブジェクトが検索されます。まず動的実行可能プログラムから検索してから、 オブジェクトが読み込まれた順番に依存関係を検索します。

ldd(1) を使用すると、動的実行可能ファイルの依存関係は読み込まれた順にリストされます。たとえば、動的実行可能ファイル prog で、依存関係として libfoo.so.1libbar.so.1 が指定されているとします。


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

再配置を実行するためにシンボル bar が必要な場合、実行時リンカーはまず bar を動的実行可能ファイル prog の中で検索します。シンボルが見つからない場合、実行時リンカーは共有オブジェクト /home/me/lib/libfoo.so.1 の中を検索し、最後に共有オブジェクト /home/me/lib/libbar.so.1 の中を検索します。


注 –

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


デフォルトの再配置処理モデルでは、遅延読み込み環境の遷移も提供します。現在読み込まれているオブジェクト内でシンボルが見つからない場合は、そのシンボルを特定するために、保留となっている遅延読み込みオブジェクトが処理されます。この読み込みによって、依存関係を完全には定義していないオブジェクトを補います。ただし、これにより遅延読み込みの利点が失われることがあります。

実行時割り込み

デフォルトで、実行時リンカーはまず動的実行可能プログラム内でシンボルを検索したあと、それぞれの依存関係を検索します。このモデルでは、必要なシンボルが最初に現れた時点で検索条件が満たされます。そのため、同じシンボルの複数のインスタンスが存在する場合は、最初のインスタンスが、ほかのすべてのインスタンスに割り込みます。

シンボル解決がどのように割り込みの影響を受けるかの概要については、「単純な解決」で説明しています。シンボルの可視性を変更し、偶発的な割り込みの可能性を低くするメカニズムは、「シンボル範囲の縮小」で説明しています。

オブジェクトが割り込み処理として明示的に識別されている場合、割り込みをオブジェクト単位で行えます。環境変数 LD_PRELOAD を使ってオブジェクトを読み込むか、リンカーの -z interpose オプションを使ってオブジェクトを作成すると、オブジェクトは割り込み処理として識別されます。実行時リンカーがシンボルを検索する場合、割り込むものとして識別されたオブジェクトはアプリケーションよりもあとで検索されますが、その他の依存関係よりは前に検索されます。

割り込み処理により提供されるすべてのインタフェースの使用が保証されるのは、プロセス再配置が行われる前に割り込み処理が読み込まれる場合のみです。環境変数 LD_PRELOAD を使用して提供される割り込み処理、またはアプリケーションの非遅延読み込み依存関係として確立される割り込み処理は、再配置処理が始まる前に読み込まれます。再配置が始まったあとでプロセスに挿入される割り込み処理は、通常の依存関係に降格されます。割り込み処理を降格できるのは、割り込み処理が遅延読み込みされた場合、または dlopen(3C) を使用した結果として読み込まれた場合です。前者のカテゴリは ldd(1) を使用して検出できます。


$ ldd -Lr prog
        libc.so.1 =>     /lib/libc.so.1
        foo.so.2 =>      ./foo.so.2
        libmapmalloc.so.1 =>     /usr/lib/libmapmalloc.so.1
        loading after relocation has started: interposition request \
                (DF_1_INTERPOSE) ignored: /usr/lib/libmapmalloc.so.1

注 –

遅延読み込みを行うために依存関係を処理している間に、明示的に定義された割り込み処理をリンカーが検出した場合、その割り込み処理は非遅延読み込み可能依存関係として記録されます。


直接結合

直接結合を使用するオブジェクトは、特定のシンボル参照とその定義を提供する依存関係との間の関係を保持します。実行時リンカーは、デフォルトのシンボル検索モデルを使用する代わりに、この情報を使って関連するオブジェクトから直接シンボルを検索します。直接結合情報は、リンク編集で指定される依存関係に対してのみ確立されます。したがって、-z defs オプションを推奨します。

シンボル参照からシンボル定義への直接結合を確立するには、次のいずれかのメカニズムを使用します。

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


注 –

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


デフォルトのシンボル検索モデルでは、1 つのシンボルへのすべての参照を、1 つの定義に結合することができます。直接結合はデフォルトの検索モデルをバイパスするため、暗黙的な割り込みシンボルを迂回します。ただし、割り込み処理として明示的に識別されているオブジェクトは、シンボル定義を供給するオブジェクトの前に検索されます。明示的な割り込み処理には、環境変数 LD_PRELOAD を使用して読み込まれたオブジェクトや、リンカーの -z interpose オプションを使用して作成されたオブジェクトが含まれます。「実行時割り込み」を参照してください。

デフォルト手法の代替実装を提供するインタフェースがいくつか存在します。これらのインタフェースは、その実装が、プロセス内におけるその手法の唯一のインスタンスであることを前提とします。この例の 1 つに malloc(3C) ファミリがあります。さまざまな malloc() ファミリの実装が存在しますが、各ファミリは、プロセス内で使用される唯一の実装であると想定されています。このようなファミリ内の特定のインタフェースへの直接結合は避けるべきであり、これは、同じプロセスによってその手法のインスタンスが複数参照される可能性があるためです。たとえば、プロセス内の 1 つの依存関係が libc.so.1 に直接結合し、一方で別の依存関係が libmapmalloc.so.1 に直接結合する可能性があります。malloc()free() の異なる 2 つの実装を一貫性のない方法で使用すると、エラーが発生しやすくなります。

プロセス内の唯一のインスタンスであると想定されているインタフェースを提供するオブジェクトは、インタフェースへの直接結合を避けるべきです。特定のインタフェースを、どの呼び出し元からも直接結合できないようにラベル付けするには、次のメカニズムのいずれかを使用します。

非直接ラベル付けを行うと、どのシンボル参照も特定の実装に直接結合できなくなります。参照を解決するためのシンボル検索では、デフォルトのシンボル検索モデルが使用されます。非直接ラベル付けは、Solaris OS に付属するさまざまな malloc() ファミリ実装を構築する目的で採用されました。


注 –

NODIRECT mapfile キーワードは、コマンド行オプション -B direct-z direct と組み合わせることができます。シンボルに NODIRECT を明示的に定義しない場合は、そのシンボルはコマンド行の指令に従います。同様に、DIRECT mapfile キーワードは、コマンド行オプション -B nodirect と組み合わせることができます。シンボルに DIRECT を明示的に定義しない場合は、そのシンボルはコマンド行の指令に従います。


再配置が実行されるとき

再配置は、再配置が実行されるタイミングで 2 つのタイプに区別できます。このような、再配置されたオフセットに対して行われる「参照」のタイプによって、次のように区別されます。

「即時参照」とは、オブジェクトが読み込まれたときにただちに決定しなければならない再配置のことです。この参照は、一般にオブジェクトコードで使用されるデータ項目、関数ポインタ、および位置依存共有オブジェクトからの関数呼び出しに対するものです。即時参照では、再配置された項目が参照されたことを実行時リンカーは認識できません。このため、すべての即時参照は、オブジェクトが読み込まれたら、アプリケーションが制御を獲得または再獲得する前に、再配置が完了する必要があります。

「遅延参照」とは、オブジェクトの実行時に決定できる再配置のことです。通常は、位置独立共有オブジェクトから大域関数への呼び出しか、動的実行可能ファイルから外部関数への呼び出しです。遅延参照を行う動的モジュールをコンパイルおよびリンク編集しているときに、関連付けられた関数呼び出しは、プロシージャーリンクテーブルのエントリへの呼び出しに変換されます。これらのエントリは、.plt セクションを構成します。プロシージャーリンクテーブルの各エントリは、関連付けられた再配置を伴う遅延参照になります。

プロシージャーリンクテーブルの特定のエントリに対する最初の呼び出しの実行中に、制御が実行時リンカーに渡されます。実行時リンカーは、関連付けられたオブジェクト内で必要なシンボルを検索し、エントリ情報を書き換えます。その後のプロシージャーリンクテーブルのエントリへの呼び出しは、直接関数に対して行われます。遅延参照では、関数が最初に呼び出されるまで、再配置を遅延させることができます。この処理は、「遅延」結合と呼ばれることがあります。

実行時リンカーのデフォルトモードは、プロシージャーリンクテーブルの再配置が行われるたびに遅延結合を実行する、というものです。デフォルトモードを無効にするには、環境変数 LD_BIND_NOW にヌル以外の任意の値を設定します。この環境変数の設定により、実行時リンカーは、オブジェクトが読み込まれた時点で、即時参照と遅延参照を両方とも再配置します。これらの再配置は、アプリケーションが制御を獲得または再獲得するまでの間に行われます。たとえば、環境変数を次のように設定して、ファイル prog とその依存関係内のすべての再配置が行われるとします。これらの再配置は、制御がアプリケーションに移る前に行われます。


$ LD_BIND_NOW=1 prog

オブジェクトへのアクセスは、RTLD_NOW として定義されたモードを指定して dlopen(3C) を使用することによっても行えます。リンカーの -z now オプションを使用してオブジェクトを構築すれば、オブジェクトが読み込まれたときに再配置処理を完了させる必要があることを示すことができます。この再配置要件は、実行時に指定したオブジェクトの依存先すべてに波及します。


注 –

前述の即時参照と遅延参照の例は、標準的なものです。ただし、プロシージャーリンクテーブルのエントリの作成は、リンク編集の入力として使用する再配置可能オブジェクトファイルが提供する再配置情報によって、最終的に制御されます。R_SPARC_WPLT30R_386_PLT32 などの再配置レコードには、プロシージャーリンクテーブルのエントリの作成が指定されています。こうした再配置は、位置独立のコードで共通です。

ただし、通常、動的実行可能ファイルは位置に依存するコードから作成されるため、プロシージャーリンクテーブルのエントリが必要であることを示さない場合があります。動的実行可能ファイルの位置は固定されているため、参照が外部関数定義に結合された時点で、リンカーはプロシージャーのリンクテーブルを作成できます。元の再配置レコードに関係なく、このプロシージャーリンクテーブルのエントリを作成できます。


再配置エラー

もっとも一般的な再配置エラーは、シンボルを検出できないときに発生します。この状態になると、適切な実行時リンカーのエラーメッセージが表示され、アプリケーションは終了します。次の例では、ファイル libfoo.so.1 内で参照されたシンボル bar は配置できません。


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

動的実行可能プログラムのリンク編集中に、この種の潜在的な再配置エラーは、定義されていない重大なシンボルとしてフラグが付けられます。例については、「実行可能ファイルの作成」を参照してください。ただし、実行時再配置エラーが発生するのは、実行時に配置される依存関係が、リンク編集の一部として参照される元の依存関係と互換性がない場合です。上記の例では、bar のシンボル定義を含む共有オブジェクト libbar.so.1 のバージョンに対して prog が構築されています。

リンク編集時に -z nodefs オプションを使用すると、オブジェクトの実行時再配置要件の検査が抑制されます。この抑制は、実行時再配置エラーになる可能性があります。

即時参照として使用されたシンボルが検出できないために再配置エラーが発生した場合、そのエラー状態は、プロセスの初期設定中、ただちに発生します。遅延結合のデフォルトモードにより、遅延参照として使用されるシンボルを検出できない場合は、このエラー状態は、アプリケーションが制御を受け取ってから発生します。後者の場合、コードを実行する実行パスによって、エラー状態が発生するまでに数分または数ヶ月かかる場合もあり、あるいは発生しない場合もあります。

この種のエラーを防ぐためには、動的実行プログラムまたは共有オブジェクトの再配置の必要条件を、ldd(1) を使用して有効にします。

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


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

ldd(1)-r オプションを指定すると、すべての即時参照と遅延参照の再配置が処理されます。また、このどちらかの再配置が解決できない場合には、診断メッセージが作成されます。

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

実行時リンカーでは、環境変数 LD_PRELOAD を使用することにより、プロセスの初期設定中に新しいオブジェクトを取り込めるという、一歩進んだ柔軟性も提供しています。この環境変数は、特定の共有オブジェクトまたは再配置可能オブジェクトのファイル名に初期設定することも、複数のファイル名を空白で区切った文字列に初期設定することもできます。これらのオブジェクトは、動的実行可能プログラムの後で、依存関係よりも前に読み込まれます。これらのオブジェクトには、「ワールド」検索範囲と「大域」シンボル可視性が割り当てられます。

次の例では、動的実行可能プログラム prog が読み込まれ、そのあとに共有オブジェクト newstuff.so.1 が続きます。続いて、prog 内で定義された依存関係が読み込まれます。


$ LD_PRELOAD=./newstuff.so.1 prog

これらのオブジェクトが処理される順序は、ldd(1) を使用して表示できます。


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

次の例では、事前読み込みは少し複雑で時間がかかります。


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

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


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

動的実行可能ファイルのあとにオブジェクトを挿入するこれらのメカニズムにより、割り込み機能が提供されます。これらのメカニズムを使用すると、標準的な共有オブジェクト内に存在する関数の、新しい実装を試すことができます。この関数が組み込まれたオブジェクトをあらかじめ読み込むことにより、このオブジェクトは元のオブジェクトに割り込みます。そして、元の機能は、事前読み込みされた新しいバージョンによって完全に隠されてしまいます。

このほかにも事前読み込みは、標準的な共有オブジェクト内に常駐する関数を補強するために使用できます。新しいシンボルが元のシンボルに割り込むことで、新しい関数はいくつかの追加処理を実行できます。新しい関数は元の関数を呼び出すこともできます。このメカニズムでは通常、dlsym(3C) と特別なハンドル RTLD_NEXT を使って元のシンボルのアドレスを取得します。

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

メモリーに動的オブジェクトが読み込まれる際、その動的オブジェクトに追加の依存関係がないか検査されます。デフォルトでは、存在する依存関係がただちに読み込まれます。このサイクルは、依存関係のツリー全体を使い果たすまで続けられます。最終的に、再配置で指定されたオブジェクト間のデータ参照すべてが解決されます。この処理は、これらの依存関係内のコードが実行中にアプリケーションによって実際に参照されるかどうかに関係なく、行われます。

遅延読み込みモデルでは、遅延読み込みのラベルが付いた依存関係は、明示的に参照が行われるまで読み込まれません。関数呼び出しの遅延結合を利用して、関数が最初に参照されるまで、依存関係の読み込みを延期することができます。結果として、参照されないオブジェクトは読み込まれません。

再配置参照は、即時か遅延です。即時参照はオブジェクトが初期化された時に解決される必要があるため、この参照を満たすすべての依存関係はすぐに読み込まれる必要があります。そのため、そういった依存関係を遅延読み込み可能として示すことは、あまり効果がありません。詳細は、「再配置が実行されるとき」を参照してください。動的オブジェクト間の即時参照は、概してあまり推奨されません。

遅延読み込みは、デバッグライブラリ liblddbg への参照のためにリンカーが使用します。デバッギングを呼び出すことはまれなので、リンカーを呼び出すたびにこのライブラリを読み込むことは不要で、コストがかさみます。このライブラリを遅延読み込みできるように指定することにより、ライブラリの処理コストをデバッギング出力を必要とする読み込みに使うことができます。

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

特定のオブジェクトを遅延読み込み、通常読み込みとして指定するには、リンカーオプション -z lazyload-z nolazyload をそれぞれ使用します。これらのオプションは、リンク編集コマンド行の位置に依存します。このオプションよりあとに指定される依存関係には、このオプションで指定されている読み込み属性が適用されます。デフォルトでは、-z nolazyload オプションが有効です。

次の単純なプログラムでは、libdebug.so.1 に対する依存関係が指定されています。動的セクション (.dynamic) では、libdebug.so.1 に対して遅延読み込みが指定されています。シンボル情報セクション (.SUNW_syminfo) では、libdebug.so.1 の読み込みをトリガーするシンボル参照が指定されています。


$ cc -o prog prog.c -L. -zlazyload -ldebug -znolazyload -lelf -R'$ORIGIN'
$ elfdump -d prog
 
Dynamic Section:  .dynamic
     index  tag           value
       [0]  POSFLAG_1     0x1           [ LAZY ]
       [1]  NEEDED        0x123         libdebug.so.1
       [2]  NEEDED        0x131         libelf.so.1
       [3]  NEEDED        0x13d         libc.so.1
       [4]  RUNPATH       0x147         $ORIGIN
       ...
$ elfdump -y prog
 
Syminfo section: .SUNW_syminfo
     index  flgs        bound to        symbol
      ....
      [52]  DL      [1] libdebug.so.1   debug

値に LAZY が指定された POSFLAG_1 は、次の NEEDED エントリ libdebug.so.1 が遅延読み込みされることを示しています。libelf.so.1 は前に LAZY フラグがないため、このライブラリはプログラムの初期始動時に読み込まれます。


注 –

libc.so.1 には、ファイルが遅延読み込みされてはならないという特別なシステム要件があります。libc.so.1 が処理される時点で -z lazyload が有効である場合、フラグは実際には無視されます。


遅延読み込みを使用するには、アプリケーションで使用されるオブジェクト全体に渡り依存関係と「実行パス」を正確に宣言しなければならない場合があります。たとえば、libX.so 内のシンボルを参照する 2 つのオブジェクト libA.solibB.so があるとします。libA.solibX.so を依存関係として宣言しますが、libB.so は宣言しません。通常、libA.solibB.so が併用される場合、libB.solibX.so を参照できます。これは、libA.so によってこの依存関係が利用可能になっているためです。しかし、libX.so が遅延読み込みされるように libA.so で宣言した場合、libB.so がこの依存関係を参照するときに libX.so を読み込めない可能性があります。libB.solibX.so を依存関係として宣言していても、その依存関係の特定に必要な「実行パス」を指定しなかった場合には、同様のエラーが発生する可能性があります。

遅延読み込みに関わらず、動的オブジェクトは、すべての依存関係と依存関係の特定方法を宣言しなければなりません。遅延読み込みでは、この依存情報がより重要な意味合いを持ちます。


注 –

環境変数 LD_NOLAZYLOAD をヌル以外の値に設定すれば、実行時に遅延読み込みを無効にできます。


dlopen() の代替手段の提供

遅延読み込みは、dlopen(3C)dlsym(3C) を使用する代わりになります。「実行時リンクのプログラミングインタフェース」を参照してください。たとえば、libfoo.so.1 の次のコードは、オブジェクトが読み込まれることを確認し、そのオブジェクトのインタフェースを呼び出します。


void foo()
{
    void * handle;

    if ((handle = dlopen("libbar.so.1", RTLD_LAZY)) != NULL) {
        int (* fptr)();

        if ((fptr = (int (*)())dlsym(handle, "bar1")) != NULL)
            (*fptr)(arg1);
        if ((fptr = (int (*)())dlsym(handle, "bar2")) != NULL)
            (*fptr)(arg2);
        ....
}

このコードは、必要なインタフェースを提供するオブジェクトが次の条件を満たす場合に、単純化できます。

遅延読み込みを活用することで、libbar.so.1 の同じ据え置き読み込みをアーカイブできます。この場合、関数 bar1() を参照すると、関連依存関係が遅延読み込みされます。さらに、標準関数呼び出しを使用すると、コンパイラまたは lint(1) 確認が提供されます。


void foo()
{
    bar1(arg1);
    bar2(arg2);
    ....
}
$ cc -G -o libfoo.so.1 foo.c -L. -zlazyload -zdefs -lbar -R'$ORIGIN'

ただし、必要なインタフェースを提供するオブジェクトが常に利用できるとは限らない場合、このモデルは失敗します。この場合、依存関係の名前なしで依存関係の有無をテストする機能が必要です。関数参照を満足する依存関係を使用できるかどうかをテストする手段が必要になります。

RTLD_PROBE ハンドルのある dlsym(3C) は、依存関係の有無および読み込みの確認に使用できます。たとえば、bar1() への参照は、リンク編集時に確立された遅延依存関係を利用できるかどうかを確認します。このテストは、dlopen(3C) が使用されたのと同じ方法で、依存関係が提供する関数への参照を制御するために使用できます。


void foo()
{
    if (dlsym(RTLD_PROBE, "bar1")) {
        bar1(arg1);
        bar2(arg2);
        ....
}

このテクニックは、標準関数呼び出しを使用するとともに、記録された依存関係の安全な据え置き読み込みを実現します。


注 –

この特別なハンドル RTLD_DEFAULT は、RTLD_PROBE の使用時と同じようなメカニズムを提供します。ただし、RTLD_DEFAULT を使用すると、存在しないシンボルを特定しようとして、処理中の遅延読み込みオブジェクトが保留されることがあります。この読み込みによって、依存関係を完全には定義していないオブジェクトを補います。ただし、これにより遅延読み込みの利点が失われることがあります。

遅延読み込みを使用するオブジェクトの作成には、-z defs オプションの使用をお勧めします。


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

動的オブジェクトは、実行時の初期設定と終了処理のためのコードを提供することができます。動的オブジェクトの初期設定コードは、処理中に動的オブジェクトが読み込まれるたびに、1 回ずつ実行されます。動的オブジェクトの終了コードは、動的オブジェクトが処理から読み取り解除されるか、または処理の終了のたびに 1 回ずつ実行されます。

実行時リンカーは、制御をアプリケーションに移す前に、アプリケーション内および読み込まれたすべての依存関係内で見つかったすべての初期設定セクションを処理します。プロセス実行中に新しい動的オブジェクトが読み込まれた場合、その初期設定セクションはオブジェクトの読み込みの一部として処理されます。初期設定セクションである .preinitarray.initarray、および .init は、動的オブジェクトの構築時にリンカーによって作成されます。

実行時リンカーは、.preinitarray セクションと .initarray セクションにアドレスが指定されている関数を実行します。これらの関数は、配列内でアドレスが出現する順序で実行されます。実行時リンカーは、.init セクションを独立した関数として実行します。1 つのオブジェクトに .init セクションと .initarray セクションの両方が含まれている場合は、そのオブジェクトに関しては、.initarray セクションによって定義されている関数よりも前に .init セクションが処理されます。

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


注 –

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


動的オブジェクトは、終了セクションも提供できます。終了セクションである .finiarray および .fini は、動的オブジェクトが構築される際にリンカーによって作成されます。

終了セクションはすべて、atexit(3C) に転送されます。これらの終了ルーチンは、プロセスが exit(2) を呼び出したときに呼び出されます。また終了セクションは、dlclose(3C) を持つ実行プロセスからオブジェクトが除去されたときにも呼び出されます。

実行時リンカーは、.finiarray セクションにアドレスが指定されている関数を実行します。これらの関数は、配列内でアドレスが出現する順序とは逆に実行されます。実行時リンカーは、.fini セクションを独立した関数として実行します。オブジェクトに .fini セクションと .finiarray セクションの両方が含まれている場合は、そのオブジェクトに関しては、.fini セクションによって定義されている関数よりも前に .finiarray セクションが処理されます。


注 –

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


リンカーによる初期設定セクションと終了セクションの作成についての詳細は、「初期設定および終了セクション」を参照してください。

初期設定と終了の順序

実行時にプロセス内で初期設定および終了コードをどのような順序で実行すべきかを判断することは、依存関係の分析を伴う複雑な問題を含んでいます。この処理は、初期設定セクションと終了セクションの導入以来、大きく発展してきました。この処理は、最新の言語と現在のプログラミング手法の期待を実現しようとするものです。しかし、ユーザーの期待にこたえるのが難しい状況もあります。これらの状況を理解し、初期設定および終了コードの内容を制限することで、柔軟で予測可能な実行時動作が得られます。

初期設定セクションの目的は、同一オブジェクト内のほかのコードが参照される前に小さなコードを実行することです。終了セクションの目的は、オブジェクトの実行完了後に小さなコードを実行することです。自己完結型の初期設定セクションや終了セクションは、これらの要件を容易に満たすことができます。

しかしながら、初期設定セクションは通常それよりも複雑であり、ほかのオブジェクトが提供する外部のインタフェースを参照します。したがって、ほかのオブジェクトからの参照が発生する前にオブジェクトの初期設定セクションを実行する必要がある場合には、依存関係が発生することになります。アプリケーションが大規模な依存関係の階層を確立する可能性があります。さらに、依存関係がその階層内で循環を形成する可能性もあります。こうした状況が、追加のオブジェクトを読み込んだりすでに読み込まれたオブジェクトの再配置モードを変更したりする初期設定セクションによって、さらに複雑になる可能性があります。これらの問題を解決するためにさまざまなソート手法や実行手法が開発されてきましたが、それらもすべて、これらのセクションの本来の目的を達成するためでした。

Solaris 2.6 より前のリリースでは、依存関係の初期設定ルーチンが呼び出される順序は、読み込まれた順序の「逆」、つまり ldd(1) を使用して表示される依存関係の順序とは逆でした。同様に、依存関係の終了ルーチンが呼び出される順序は、読み込まれた順序と同じでした。しかし、依存関係の階層が複雑化するにつれ、この単純な順序付け手法は適切とは言えなくなりました。

Solaris 2.6 リリースでは、実行時リンカーは、読み込まれたオブジェクトを位相的にソートしてリストを作成するようになりました。このリストは、各オブジェクトが表す依存関係の相関関係に加えて、示された依存関係の外部で発生したシンボル結合から構成されます。


注意 – 注意 –

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


初期設定セクションは、依存関係の位相的な順序とは逆に実行されます。循環性のある依存関係が検出された場合、循環の原因であるオブジェクトは、位相的にソートされません。循環性のある依存関係の初期設定セクションは、読み込まれた順序の逆に実行されます。同様に、終了セクションは、依存関係の位相的な順序で呼び出されます。循環性のある依存関係の終了セクションは、読み込まれた順序で実行されます。

オブジェクトの依存関係の初期設定順序に関する静的解析を取得するには、ldd(1)-i オプションを指定します。たとえば、次の動的実行可能プログラムとその依存関係は、循環性のある依存関係を示しています。


$ elfdump -d B.so.1 | grep NEEDED
    [1]     NEEDED      0xa9    C.so.1
$ elfdump -d C.so.1 | grep NEEDED
    [1]     NEEDED      0xc4    B.so.1
$ elfdump -d main | grep NEEDED
    [1]     NEEDED      0xd6    A.so.1
    [2]     NEEDED      0xc8    B.so.1
    [3]     NEEDED      0xe4    libc.so.1
$ ldd -i main
        A.so.1 =>        ./A.so.1
        B.so.1 =>        ./B.so.1
        libc.so.1 =>     /lib/libc.so.1
        C.so.1 =>        ./C.so.1
        libm.so.2 =>     /lib/libm.so.2

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

   init object=/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

この解析結果は単純に、明示的な依存関係を位相的にソートすることによって得られたものです。ただし、自身が必要とする依存関係を定義していないオブジェクトも頻繁に作成されます。このため、シンボル結合も依存関係解析の一部として組み込まれます。明示的な依存関係を持つシンボル結合を組み込むと、より正確な依存関係の構築に役立ちます。初期設定順序のより正確な静的解析を取得するには、ldd(1)-i オプションと -d オプションを指定してください。

オブジェクトの読み込みに使用されるもっとも一般的なモデルは遅延結合です。このモデルの場合、初期設定処理の前に処理されるのは「即時参照」シンボル結合だけです。「遅延参照」からのシンボル結合は保留されている場合があります。これらの結合は、それまでに確立された依存関係を拡張できます。すべてのシンボル結合が組み込まれた初期設定順序の静的解析を取得するには、ldd(1)-i オプションと -r オプションを指定してください。

実際には、ほとんどのアプリケーションが遅延結合を使用します。したがって、初期設定順序を計算する前に実行された依存関係解析は、ldd -id を使用した静的解析に従います。ただし、この依存関係解析は不完全である可能性があり、また循環依存関係が存在する可能性もあるため、実行時リンカーでは動的初期設定も使用できるようになっています。

動的初期設定は、あるオブジェクトの初期設定セクションを、その同じオブジェクト内の関数が呼び出される前に実行しようとします。実行時リンカーは、遅延シンボル結合の際に、結合する先のオブジェクトの初期設定セクションがすでに呼び出されているかどうかを判定します。呼び出されていなければ、実行時リンカーは、シンボル結合手順から戻る前にその初期設定セクションを実行します。

動的な初期設定は、ldd(1) では確認できません。しかし、LD_DEBUG 環境変数を設定してトークン init を含めることにより、実行時に初期設定呼び出しの正確な手順を確認できます。「デバッギングライブラリ」を参照してください。デバッグ用のトークン detail を追加すると、広範な初期設定情報や終了情報を取得できます。この情報には、依存関係の一覧、位相的な処理、循環依存関係の特定などが含まれます。

動的初期設定を使用できるのは、遅延参照を処理する場合だけです。この動的な初期設定を迂回するには、次の手段があります。

これまでに説明した初期設定手法だけでは、いくつかの動的な活動に対処できない可能性があります。初期設定セクションが、dlopen(3C) を使って明示的に、あるいは遅延読み込みやフィルタ使用によって暗黙的に、追加のオブジェクトを読み込む可能性があります。また、初期設定セクションが既存オブジェクトの再配置を促進する可能性もあります。遅延結合を採用するために読み込まれたオブジェクトがモード RTLD_NOW を指定した dlopen(3C) を使って参照された場合、その同じオブジェクトの結合が解決されます。この再配置促進によって結果的に、関数呼び出しの動的解決に使用可能な動的初期設定機能が抑制されます。

新しいオブジェクトが読み込まれるたびに、あるいは既存オブジェクトの再配置が促進されるたびに、それらのオブジェクトの位相的なソートが起動されます。その結果、元の初期設定の実行が中断されるとともに、新しい初期設定要件が確立され、関連する初期設定セクションが実行されます。このモデルの意図は、新しく参照されたオブジェクトを適切に初期設定し、それを元の初期設定セクションが確実に使用できるようにすることです。ところが、この並行処理が不要な再帰の原因となる可能性があります。

実行時リンカーは、遅延結合を採用したオブジェクトを処理する際に、特定レベルの再帰を検出できます。この再帰を表示するには、LD_DEBUG=init と設定します。たとえば、foo.so.1 の初期設定セクションを実行すると、別のオブジェクトが呼び出される可能性があります。そして、そのオブジェクトが foo.so.1 内のいずれかのインタフェースを参照していた場合、循環が形成されます。実行時リンカーは、遅延関数参照を foo.so.1 に結合する過程で、この再帰を検出できます。


$ LD_DEBUG=init prog
00905: .......
00905: warning: calling foo.so.1 whose init has not completed
00905: .......

実行時リンカーは、すでに再配置された参照を通じて発生した再帰を検出することはできません。

再帰は、多くのコストと問題を発生させる可能性があります。再帰が発生しないよう、初期設定セクションによって起動される可能性のある外部参照や動的読み込み活動の数を減らしてください。

初期設定処理は、dlopen(3C) を指定して実行しているプロセスに追加された、すべてのオブジェクトに対して繰り返されます。また、dlclose(3C) に対する呼び出しの結果としてプロセスから読み込み解除されるすべてのオブジェクトに対して、終了処理も行われます。

これまでの節では、ユーザーの期待に応えようとする方法で、初期設定セクションと終了セクションを実行するために使用されるさまざまな手法を説明してきました。しかし、依存関係同士の初期設定と終了の関係を単純化するためには、コーディングスタイルとリンク編集の助けも必要です。この単純化は、初期設定と予測可能な終了処理を助け、予期しない依存関係順序付けによる副作用の影響を受けにくくします。

初期設定セクションと終了セクションの内容は最小限に抑えてください。実行時にオブジェクトを初期化することによって、大域的なコンストラクタを避けてください。ほかの依存関係に対する初期設定および終了コードの依存を減らしてください。すべての動的オブジェクトについて依存関係の要件を定義してください。「共有オブジェクト出力ファイルの生成」を参照不要な依存関係を定義しないでください。「共有オブジェクトの処理」を参照。依存関係の循環を避けてください。初期設定または終了の順序に頼らないでください。オブジェクトの順序は、共有オブジェクトとアプリケーションの開発によって変更される場合があるからです。「依存関係の順序」を参照してください。

セキュリティー

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

実行時リンカーは、issetugid(2) システム呼び出しがプロセスに対して true を返した場合、そのプロセスを安全と分類します。

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

あるセキュリティー保護されたプロセスに対して LD_LIBRARY_PATH ファミリ環境変数が有効になっている場合、実行時リンカーの検索規則を拡張するために使用されるのは、この変数に指定されたトラストディレクトリだけです。「実行時リンカーが検索するディレクトリ」を参照してください。

セキュリティー保護されたプロセスでは、アプリケーションまたはその依存関係によって指定された「実行パス」の指定が使用されます。ただし、「実行パス」はフルパス名である、つまりパス名は「/」から始まる必要があります。

セキュリティー保護されたプロセスでは、$ORIGIN 文字列の拡張は、その文字列がトラストディレクトリに拡張されるときにかぎり許可されます。「セキュリティー」を参照してください。ただし、$ORIGIN を展開することですでに依存関係を提供したディレクトリに一致する場合、そのディレクトリは暗黙にセキュリティー保護されます。このディレクトリは、追加の依存関係を提供するために使用できます。

セキュリティー保護されたプロセスでは、LD_CONFIG は無視されます。セキュリティー保護されたプロセスは、デフォルト構成ファイルが存在する場合、この構成ファイルを使用します。crle(1) のマニュアルページを参照してください。

セキュリティー保護されたプロセスでは、LD_SIGNAL は無視されます。

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

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

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

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

アプリケーションのリンク編集中に指定された依存関係は、プロセスの初期設定中に実行時リンカーによって処理されます。このメカニズムに加えて、アプリケーションは、追加オブジェクトと結合することにより、その実行中にアドレススペースを拡張できます。アプリケーションは、アプリケーションの標準的な依存関係の処理に使用される、実行時リンカーの同じサービスを実際に使用します。

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

アプリケーションは、次の典型的な手順を使用して、追加の共有オブジェクトにアクセスできます。

実行時リンカーのサービスは、ヘッダーファイル dlfcn.h 内に定義されており、共有オブジェクト libc.so.1 経由でアプリケーションから使用できます。次の例では、ファイル main.c は、ルーチンのいずれの dlopen(3C) ファミリでも参照でき、アプリケーション prog は、実行時にこれらのルーチンと結合できます。


$ cc -o prog main.c

注 –

Solaris OS の以前のリリースでは、動的リンクインタフェースは、共有オブジェクト libdl.so.1 によって使用可能にされていました。libdl.so.1 は既存の依存関係をサポートするために今も使用できます。ただし、libdl.so.1 から提供されていた動的リンクインタフェースは、現在は libc.so.1 から使用できるようになりました。このため、-ldl によるリンクは必要なくなりました。


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

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

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

これらの検索パスの規則は、最初の依存関係の配置に使用された規則と全く同じものです。「実行時リンカーが検索するディレクトリ」を参照してください。たとえば、ファイル main.c には、次のようなコードフラグメントが組み込まれているとします。


#include        <stdio.h>
#include        <dlfcn.h>
 
int main(int argc, char ** argv)
{
        void *  handle;
        .....
 
        if ((handle = dlopen("foo.so.1", RTLD_LAZY)) == NULL) {
                (void) printf("dlopen: %s\n", dlerror());
                return (1);
        }
        .....

実行時リンカーは、共有オブジェクト foo.so.1 を検索するために、プロセスの初期設定時に存在する任意の LD_LIBRARY_PATH 定義を使用します。続いて、実行時リンカーは prog のリンク編集時に指定された「実行パス」を使用します。最後に、実行時リンカーは 32 ビットオブジェクトの場合は /lib/usr/lib、64 ビットオブジェクトの場合は /lib/64/usr/lib/64 のデフォルト位置を使用します。

パス名が次のように指定されているとします。


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

実行時リンカーは、プロセスの現在のカレントディレクトリ内でこのファイルだけを検索します。


注 –

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


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


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

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

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


        if ((handle = dlopen(0, RTLD_LAZY)) == NULL) {

この dlopen(3C) から返されたハンドルを使用すれば、アプリケーション自身の内部、プロセスの初期設定中に読み込まれたすべての依存関係内、またはプロセスのアドレススペースに追加されたすべてのオブジェクト内で、シンボル検索を行えます。それには、dlopen(3C)RTLD_GLOBAL フラグを指定します。

再配置処理

実行時リンカーは、オブジェクトを検出して読み込んだあと、各オブジェクトを処理し、必要な再配置を実行します。また、dlopen(3C) を使用してプロセスのアドレススペースに配置されたオブジェクトは同じ方法で再配置する必要があります。

単純なアプリケーションの場合には、このプロセスはそれほど重要な意味を持ちません。しかし、多くのオブジェクトを含む多数の dlopen(3C) 呼び出しと、共通の依存関係も伴う複雑なアプリケーションを所有するユーザーにとって、このプロセスは非常に重要です。

再配置は、その実行タイミングに基づいて分類できます。実行時リンカーのデフォルトの動作では、初期設定時に即時参照の再配置がすべて処理され、プロセスの実行時に遅延参照がすべて処理されます。後者の処理は通常、遅延結合と呼ばれます。

モードが RTLD_LAZY として定義されている時に dlopen(3C) を使って追加されたすべてのオブジェクトに対して、これと同じメカニズムが適用されます。代替手段は、オブジェクトが追加されるときにただちに実行されるオブジェクトのすべての再配置を要求することです。RTLD_NOW のモードを使用するか、またはリンカーの -z now オプションを使用して構築した際のオブジェクトにこの要件を記録できます。この再配置の必要条件は、オープン状態のオブジェクトの依存関係に伝達されます。

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

シンボルの検索

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

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

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

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

基本的な dlopen(3C) によって追加された各オブジェクトでは、実行時リンカーは、最初に動的実行可能ファイル内でシンボルを検索します。次に実行時リンカーは、プロセスの初期設定中に提供されたそれぞれのオブジェクト内を検索します。シンボルが見つからない場合、実行時リンカーは検索を続けます。実行時リンカーは次に dlopen(3C) によって取得されたオブジェクトおよびそのオブジェクトと依存関係にあるオブジェクトを検索します。

デフォルトのシンボル検索モデルは、遅延読み込み環境の遷移も行います。現在読み込まれているオブジェクト内でシンボルが見つからない場合は、そのシンボルを特定するために、保留となっている遅延読み込みオブジェクトが処理されます。この読み込みによって、依存関係を完全には定義していないオブジェクトを補います。ただし、これにより遅延読み込みの利点が失われることがあります。

次の例の動的実行可能プログラム prog と共有オブジェクト B.so.1 には、依存関係が付いています。


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

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

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

単一の dlopen() 要求。

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

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

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

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

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

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


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

このとき、prog アプリケーションが、共有オブジェクト B.so.1 に加えてこの共有オブジェクトも dlopen(3C) を使って読み込んだとします。次の図は、オブジェクト間のシンボル検索の関係を示しています。

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

複数の dlopen() 要求。

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

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

次に、同じ共通依存関係を持つ共有オブジェクト 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(3C) を使用しています。共有オブジェクト Z.so.1 が、O.so.1P.so.1 両方の共通依存関係であるため、Z.so.1 は 2 つの dlopen(3C) 呼び出しに関連する両方のグループに割り当てられます。この依存関係を次の図に示します。

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

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

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

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

大域オブジェクトの定義

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

また、RTLD_GLOBAL フラグが指定された dlopen(3C) によって入手されたオブジェクトは、dlopen() (値 0 のパス名を指定) を使用したシンボル検索にも使用できます。


注 –

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


グループの分離

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

リンカーの -B group オプションを使用して構築したオブジェクトには、グループのシンボル検索範囲を割り当てることができます。


注 –

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


オブジェクト階層

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

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

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

多くのオブジェクトが dlopen(3C) を使って 2 番目のオブジェクトを開くことができ、かつそれらの初期オブジェクトが 2 番目のオブジェクトの再配置を満たすために同じシンボルをエクスポートしなければならない場合、その 2 番目のオブジェクトに明示的な依存関係を割り当てることはできません。この場合、2 番目のオブジェクトの dlopen(3C) モードは、RTLD_PARENT フラグを使用して補強できます。このフラグによって、2 番目のオブジェクトのグループが、明示的な依存関係が伝達されたのと同じ方法で、最初のオブジェクトに伝達されます。

これら 2 つの手法の間には、小さな相違点が 1 つ存在しています。明示的な依存関係を指定する場合、その依存関係そのものは、2 番目のオブジェクトの dlopen(3C) 依存関係ツリーの一部になるため、dlsym(3C) を使用したシンボル検索が可能になります。RTLD_PARENT を使用して 2 番目のオブジェクトを入手する場合、最初のオブジェクトは、dlsym(3C) を使用したシンボルの検索に使用できるようにはなりません。

「大域」シンボル可視性を持つ初期オブジェクトが dlopen(3C) を使って 2 番目のオブジェクトを取得する場合、RTLD_PARENT モードは冗長かつ無害になります。このような状態は、dlopen(3C) がアプリケーションから呼び出されたとき、またはアプリケーションの中の依存関係の 1 つから呼び出されたときに多く発生します。

新しいシンボルの入手

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

次に、一般的なケースを示します。この例では、アプリケーションはそのアドレス空間に追加オブジェクトを追加します。続いてアプリケーションは、dlsym(3C) を使用して関数シンボルまたはデータシンボルを見つけます。次に、アプリケーションは、これらのシンボルを使用して、これらの新しいオブジェクト内で提供されるサービスを呼び出します。ファイル main.c には、次のコードが含まれます。


#include    <stdio.h>
#include    <dlfcn.h>
 
int main()
{
        void *  handle;
        int *   dptr, (* fptr)();
 
        if ((handle = dlopen("foo.so.1", RTLD_LAZY)) == NULL) {
                (void) printf("dlopen: %s\n", dlerror());
                return (1);
        }
 
        if (((fptr = (int (*)())dlsym(handle, "foo")) == NULL) ||
            ((dptr = (int *)dlsym(handle, "bar")) == NULL)) {
                (void) printf("dlsym: %s\n", dlerror());
                return (1);
        }
 
        return ((*fptr)(*dptr));
}

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

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


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

dlopen(3C) で指定されたファイル名の値が 0 の場合、シンボル foobar が、まず prog で検索され、次に /lib/libc.so.1 で検索されます。

ハンドルは、シンボル検索を開始するルートを示します。このルートから、検索メカニズムは 「再配置シンボルの検索」で説明されているモデルと同じモデルに従います。

要求されたシンボルが配置されていない場合は、dlsym(3C) は、NULL 値を戻します。この場合、dlerror(3C) を使用すると、失敗した真の理由を表示できます。次の例では、アプリケーション prog はシンボル bar を配置できません。


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

機能のテスト

特別なハンドル RTLD_DEFAULTRTLD_PROBE を使用すると、ほかのシンボルの有無を確認するためにアプリケーションをテストできます。シンボル検索は、呼び出しオブジェクトを再配置する場合に使用されるものと同じモデルに従います。「デフォルトのシンボル検索モデル」を参照してください。たとえば、アプリケーション prog に次のようなコードフラグメントが組み込まれているとします。


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

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

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

割り込みの使用

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


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

この場合、foo は、prog に関連する共有オブジェクト (この例では /lib/libc.so.1) 内で検索されます。このコード部分が図 3–1 に示されている例からファイル B.so.1 に含まれている場合、fooC.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 (NULL);
                }
        }
 
        (void) sprintf(buffer, "malloc: %#x bytes\n", size);
        (void) write(1, buffer, strlen(buffer));
        return ((*fptr)(size));
}

malloc.so.1 は、malloc(3C) が通常存在するシステムライブラリ /lib/libc.so.1 の前に割り込ませることができます。こうすれば、malloc() に対するすべての呼び出しは、本来の関数が呼ばれて割り当てを行う前に、割り込まれます。


$ 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 を使用することにより、予測可能な割り込みテクニックが使用できます。ただし、このテクニックを汎用オブジェクトの依存関係内で使用する場合には、実際に読み込まれる順番が必ず予測できるとは限らないため、注意が必要です。

デバッグ支援

Solaris OS リンカーには、デバッギングライブラリとデバッギング mdb(1) モジュールが組み込まれています。デバッギングライブラリを使用すると、実行時のリンクプロセスをより詳細に監視できます。mdb(1) モジュールを使用すると、プロセスのデバッグを対話形式で行うことができます。

デバッギングライブラリ

デバッギングライブラリは、アプリケーションの実行と依存関係を理解したり、デバッグする場合に役立ちます。このライブラリを使用して表示される情報のタイプは、定数のままであると予期されます。ただし、この情報の正確な形式は、リリースごとに若干変更される場合があります。

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

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

LD_DEBUG で使用可能なトークンを表示するには、LD_DEBUG=help を使用します。どのような動的実行可能プログラムを使用しても、この情報を要求することができます。これは、情報が表示されたあとでプロセスが終了するためです。


$ LD_DEBUG=help prog
......
11693: files     display input file processing (files and libraries)
......

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

セキュリティー保護されたアプリケーションのデバッギングは実行できません。

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


$ cat bar.c
int bar = 10;
$ cc -o bar.so.1 -K pic -G bar.c
 
$ cat foo.c
int foo(int data)
{
        return (data);
}
$ cc -o foo.so.1 -K pic -G foo.c
 
$ cat main.c
extern  int     foo();
extern  int     bar;
 
int 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 と設定すると、実際の結合位置の実アドレスと相対アドレスに関する追加情報が表示されます。

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


$ LD_DEBUG=libs prog
11775:
11775: find object=foo.so.1; searching
11775:  search path=/tmp:.  (RUNPATH/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:.  (RUNPATH/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 を設定して表示できます。symbolsbindings を組み合わせれば、シンボル再配置処理の全容を把握できます。


$ LD_DEBUG=bindings,symbols prog
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 内では検索されません。このようにデータ参照検索が省略される原因は、コピーの再配置時に使用される最適化にあります。この再配置タイプの詳細については、「コピー再配置」を参照してください。

デバッガモジュール

デバッガモジュールは、mdb(1) に読み込むことができる一群の dcmds および walkers を提供します。デバッガモジュールを使用すると、実行時リンカーのさまざまな内部データ構造を検査できます。このデバッグ情報の多くを理解するには、実行時リンカー内部に関する知識が必要です。こうした内部の情報は、リリースごとに異なる可能性があります。しかし、これらの情報は、動的にリンクされたプロセスの基本的な構成要素を明らかにし、さまざまなデバッグを助けます。

次の例に、mdb(1) とデバッガモジュールを使用したいくつかのシナリオを示します。


$ cat main.c
#include  <dlfnc.h>

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

        if ((handle = dlopen("foo.so.1", RTLD_LAZY)) == NULL)
                return (1);

        if ((fptr = (void (*)())dlsym(handle, "foo")) == NULL)
                return (1);

        (*fptr)();
        return (0);
}
$ cc -o main main.c -R.

mdb(1) がデバッガモジュール ld.so を自動的に読み込まなかった場合は、明示的に読み込んでください。それにより、デバッガモジュールの機能を確認できます。


$ mdb main
> ::load ld.so
> ::dmods -l ld.so

ld.so
-----------------------------------------------------------------
  dcmd Bind                 - Display a Binding descriptor
  dcmd Callers              - Display Rt_map CALLERS binding descriptors
  dcmd Depends              - Display Rt_map DEPENDS binding descriptors
  dcmd ElfDyn               - Display Elf_Dyn entry
  dcmd ElfEhdr              - Display Elf_Ehdr entry
  dcmd ElfPhdr              - Display Elf_Phdr entry
  dcmd Groups               - Display Rt_map GROUPS group handles
  dcmd GrpDesc              - Display a Group Descriptor
  dcmd GrpHdl               - Display a Group Handle
  dcmd Handles              - Display Rt_map HANDLES group descriptors
  ....
> ::bp main
> :r

プロセス内の動的オブジェクトは、リンクマップ Rt_map として表現され、このリンクマップは、リンクマップリスト上で管理されています。プロセスのすべてのリンクマップは、Rt_maps を使用して表示できます。


> ::Rt_maps
Link-map lists (dynlm_list): 0xffbfe0d0
----------------------------------------------
  Lm_list: 0xff3f6f60  (LM_ID_BASE)
  ----------------------------------------------
    lmco        rtmap       ADDR()     NAME()
    ----------------------------------------------
    [0xc]       0xff3f0fdc 0x00010000 main
    [0xc]       0xff3f1394 0xff280000 /lib/libc.so.1
  ----------------------------------------------
  Lm_list: 0xff3f6f88  (LM_ID_LDSO)
  ----------------------------------------------
    [0xc]       0xff3f0c78 0xff3b0000 /lib/ld.so.1

個々のリンクマップは、Rt_map を使用して表示できます。


> 0xff3f9040::Rt_map
Rt_map located at: 0xff3f9040
     NAME: main
 PATHNAME: /export/home/user/main
     ADDR: 0x00010000         DYN: 0x000207bc
     NEXT: 0xff3f9460        PREV: 0x00000000
      FCT: 0xff3f6f18    TLSMODID:          0
     INIT: 0x00010710        FINI: 0x0001071c
   GROUPS: 0x00000000     HANDLES: 0x00000000
  DEPENDS: 0xff3f96e8     CALLERS: 0x00000000
    .....

オブジェクトの .dynamic セクションは、ElfDyn dcmd を使用して表示できます。次の例は、最初の 4 つのエントリを表示しています。


> 0x000207bc,4::ElfDyn
Elf_Dyn located at: 0x207bc
    0x207bc  NEEDED       0x0000010f
Elf_Dyn located at: 0x207c4
    0x207c4  NEEDED       0x00000124
Elf_Dyn located at: 0x207cc
    0x207cc  INIT         0x00010710
Elf_Dyn located at: 0x207d4
    0x207d4  FINI         0x0001071c

mdb(1) は、遅延ブレークポイントを設定するときにとても有用です。この例では、関数 foo() に対するブレークポイントが有用です。foo.so.1 に対して dlopen(3C) が実行されるまでは、このシンボルはデバッガにとって未知であるにもかかわらず、遅延ブレークポイントを設定すると、動的オブジェクトが読み込まれたときに、実ブレークポイントが設定されます。


> ::bp foo.so.1`foo
> :c
> mdb: You've got symbols!
> mdb: stop at foo.so.1`foo
mdb: target stopped at:
foo.so.1`foo:   save      %sp, -0x68, %sp

この時点で、新しいオブジェクトが読み込まれました。


> *ld.so`lml_main::Rt_maps
lmco    rtmap       ADDR()     NAME()
----------------------------------------------
[0xc]   0xff3f0fdc 0x00010000 main
[0xc]   0xff3f1394 0xff280000 /lib/libc.so.1
[0xc]   0xff3f9ca4 0xff380000 ./foo.so.1
[0xc]   0xff37006c 0xff260000 ./bar.so.1

foo.so.1 のリンクマップは、dlopen(3C) から返されたハンドルを示しています。Handles を使用すると、ハンドルの構造体を展開できます。


> 0xff3f9ca4::Handles -v
HANDLES for ./foo.so.1
----------------------------------------------
  HANDLE: 0xff3f9f60 Alist[used 1: total 1]
    ----------------------------------------------
    Group Handle located at: 0xff3f9f28
    ----------------------------------------------
        owner:               ./foo.so.1
        flags: 0x00000000    [ 0 ]
       refcnt:          1    depends: 0xff3f9fa0 Alist[used 2: total 4]
        ----------------------------------------------
        Group Descriptor located at: 0xff3f9fac
           depend: 0xff3f9ca4    ./foo.so.1
            flags: 0x00000003    [ AVAIL-TO-DLSYM,ADD-DEPENDENCIES ]
        ----------------------------------------------
        Group Descriptor located at: 0xff3f9fd8
           depend: 0xff37006c    ./bar.so.1
            flags: 0x00000003    [ AVAIL-TO-DLSYM,ADD-DEPENDENCIES ]

ハンドルの依存関係は、dlsym(3C) 要求を満たすハンドルのオブジェクトを表現するリンクマップのリストです。この例では、依存関係は foo.so.1bar.so.1 です。


注 –

上の例は、デバッガモジュールの機能の基礎的な紹介になっていますが、正確なコマンド、使用方法、および出力は、リリースごとに異なる可能性があります。お使いのシステムで利用できる正確な機能については、mdb(1) の使用方法およびヘルプを参照してください。