Go to main content
Oracle® Solaris 11.3 リンカーとライブラリガイド

印刷ビューの終了

更新: 2015 年 10 月
 
 

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

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

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

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

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


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

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

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

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


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

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

初期設定および終了コードの制限と危険性

ELF 初期設定および終了セクションとルーチンは、オブジェクトのライフサイクルの重要なポイントで実行します。初期設定中、オブジェクトはメモリーにロードされていますが、完全には初期設定されていません。終了処理中、オブジェクトはまだメモリーにロードされていますが、もはや安全には使用できず、一部がプロセス状態から削除されている場合もあります。どちらのコンテキストでも、プロセス状態は完全には整合しておらず、安全に行えるコードについて大きな制限があります。危険性には一般に次のようなものがありますが、それだけにかぎられません。

  • デッドロックにつながる循環性のある依存関係。これは、あるオブジェクトの初期設定コードが別のオブジェクトのロードをトリガーし、今度はこのオブジェクトが最初のオブジェクトを呼び戻すという関係です。

  • スレッドシリアライズは、共有オブジェクトがマルチスレッドアプリケーションで使用されている場合に失敗します。2 つのスレッドが同時に、遅延ロードライブラリにアクセスしようとすることがあります。最初にそこにアクセスしたスレッドが、実行時リンカーにオブジェクトをロードさせ、初期設定コードの実行を開始させます。プログラマはしばしば、ELF 初期設定および終了コードの実行中に複数のスレッドが指定のオブジェクトにアクセスできないように、実行時リンカーが防止できるという間違った印象を持っていますが、これは事実ではありません。実行時リンカーは、初期設定コードが実行していると、ほかのスレッドがライブラリにアクセスしようとしてもこれを防止できません。したがって、2 番目のスレッドは不整合な状態でオブジェクトにアクセスする可能性があります。必要なロックを提供するか、呼び出し側に提供するように要求することによって、このようなアクセスをシリアライズすることはオブジェクトに任されています。

ELF 初期設定および終了セクションとルーチンでは任意のコードの実行が可能なため、通常のコンテキストで実行するコードで可能な処理を実行できるという錯覚が生じています。このように考えると、これらのコードは、明示的に関数を呼び出すことなく初期設定またはクリーンアップを行う便利な方法に過ぎないように思われます。この誤解により、診断が難しくなる可能性のある障害がもたらされます。

プログラマは、ELF 初期設定および終了コードを使用するときには注意し、操作のスコープと複雑さを抑える必要があります。リンカーと実行時リンカーは、このようなコードの内容または目的を認識せず、安全でないコードを診断することも防止することもできません。小さな自己完結型の操作が安全です。ほかのオブジェクトやプロセス状態へのアクセスを含む操作は安全でない可能性があります。ライブラリでは、初期設定および終了コードで複雑な操作を試みるのではなく、呼び出し側が実行する明示的な初期設定および終了関数を提供し、そのための要件を文書化する必要があります。

次のセクションでは、これらの問題について詳細に説明します。

初期設定と終了の順序

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

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

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

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

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

オブジェクトの依存関係の初期設定順序に関する静的解析を取得するには、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 を追加すると、広範な初期設定情報や終了情報を取得できます。この情報には、依存関係の一覧、位相的な処理、循環依存関係の特定などが含まれます。

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

  • 環境変数 LD_BIND_NOW の使用。

  • –z now オプションを使用して構築されたオブジェクト。

  • RTLD_NOW モードを使用して dlopen(3C) によって読み込まれたオブジェクト。

これまでに説明した初期設定手法だけでは、いくつかの動的な活動に対処できない可能性があります。初期設定セクションが、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) に対する呼び出しの結果としてプロセスから読み込み解除されるすべてのオブジェクトに対して、終了処理も行われます。

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

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