リンカーとライブラリ

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

動的オブジェクトは、実行時の初期設定と終了処理のためのコードを提供することができます。動的オブジェクトの初期設定コードは、処理中に動的オブジェクトが読み込まれるたびに、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) に対する呼び出しの結果としてプロセスから読み込み解除されるすべてのオブジェクトに対して、終了処理も行われます。

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

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