5. 雑録

5.1. nm コマンド

nm(1) コマンドは、指定されたライブラリ内に存在するシンボルのリストを表示します。 静的ライブラリ、共有ライブラリのどちらに対しても機能します。 指定されたライブラリについて、nm(1) は、定義されているシンボルの名前、それぞれのシンボルの値、シンボルのタイプ、を表示できます。 また、そのライブラリ内に情報が存在するならば (-l オプションを参照してください)、シンボルがソースコード内のどこで (ファイル名と行番号) 定義されているかということも特定できます。

シンボルタイプについては、もう少し説明が必要です。 小文字はそのシンボルがローカルであることを意味し、大文字はそのシンボルがグローバル (外部定義) であることを意味します。 典型的なシンボルタイプは次のとおりです。

関数の名前は覚えているけれども、それがどのライブラリで定義されているか正確には思い出せない場合、nm の「-o」オプションを使い (コマンドラインではファイル名より前に置きます)、出力を grep することによって、ライブラリ名を見つけ出すことができます。 Bourne シェルであれば、/lib, /usr/lib, /usr/lib 直下のサブディレクトリ、および /usr/local/lib 内の全ライブラリを対象にして「cos」を検索するには、次のようにします。

  nm -o /lib/* /usr/lib/* /usr/lib/*/* \
        /usr/local/lib/* 2> /dev/null | grep 'cos$' 

nm に関する詳細な情報は、お手元のマシンにインストールされている nm の「info」文書 (info:binutils#nm) 内にあります。

5.2. ライブラリのコンストラクタ関数およびデストラクタ関数

ライブラリの初期化ルーチンと終了処理ルーチンは、gcc の __attribute__((constructor)) 関数属性と __attribute__((destructor)) 関数属性を使用してエクスポートします。 これらの関数属性に関する情報は gcc の info ページを参照してください。 コンストラクタルーチンは、dlopen() から戻る前に実行されます (または、ライブラリがプログラム開始時にロードされるならば main() 開始前)。 デストラクタルーチンは dlclose() から戻る前に実行されます (または、ライブラリがプログラム開始時にロードされたならば、exit() の後、もしくは main() 完了後)。 これらの関数の C 言語プロトタイプは次のようになります。

  void __attribute__ ((constructor)) my_init(void);
  void __attribute__ ((destructor)) my_fini(void);

共有ライブラリをコンパイルするときは、gcc オプションの「-nostartfiles」や「-nostdlib」を付けてはいけません。 これらのオプションが使われると、(特殊な方法を用いない限り) コンストラクタルーチンおよびデストラクタルーチンが実行されなくなってしまいます。

5.2.1. 特別な関数 _init と _fini (古い仕組み/危険)

歴史的な経緯により、コンストラクタとデストラクタを制御することができる二つの特別な関数、_init と _fini が存在しています。しかしながら、これらは古くなっており、使用すると、予測できない結果を招くことがあります。 あなたのライブラリでは、これらの関数を使用しないようにしてください。 かわりに、上で説明した関数属性の constructor と destructor を使用してください。

古いシステムや、_init または _fini を使用しているコードを扱わなければならないときのため、_init と _fini の動作をここで説明します。 二つの特別な関数 _init と _fini は、モジュールの初期化と終了処理のために定義されました。 「_init」関数がライブラリ内でエクスポートされている場合、そのライブラリが初めて (dlopen() により、または単に共有ライブラリとして) オープンされたときに呼び出されます。C プログラムでは、_init という名前の何らかの関数を定義しているということを意味します。_init に対応する関数として _fini が存在します。_fini は、(参照カウントをゼロにする dlclose() の呼出しか、またはプログラムの通常の終了により) クライアントがライブラリの使用を終了したときに呼び出されます。 これらの関数の C 言語プロトタイプは次のようになります。

  void _init(void);
  void _fini(void);

この場合は、gcc でファイルを「.o」ファイルにコンパイルするとき、忘れずに gcc オプション「-nostartfiles」を付けてください。 このオプションは、.so ファイルに対してシステムスタートアップライブラリをリンクしないよう、C コンパイラに伝えるものです。 このオプションを付けないと、「multiple-definition (重複定義)」エラーが発生してしまいます。 お勧めしている関数属性を用いてモジュールをコンパイルする方法と、この方法とは、全く異なるものなので注意してください。 _init と _fini に関する議論を加えることを提案してくれたこと、およびその記述を手伝ってくれたことに対して、Jim Mischel と Tim Gentry に感謝します。

5.3. 共有ライブラリはスクリプト化できる

通常のライブラリフォーマットとは異なる、特別なスクリプト言語で記述されたテキストファイルを、GNU ローダが共有ライブラリとして扱えることは、注目に値します。 この機能は、他のライブラリを間接的に結合させる際に便利です。 例えば、私のシステムの一つでは、/usr/lib/libc.so の中身は次のようになっています。

  /* GNU ld スクリプト
     共有ライブラリを使うが、幾つかの関数は静的ライブラリ内にしか
     存在しない。そのため、共有ライブラリ内に関数が見つからなければ、
     静的ライブラリを検索しにいく。*/
  GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a )

これに関するより詳しい情報は、ld リンカスクリプト (ld コマンド言語) に関する texinfo 文書を参照してください。 一般的な情報は info:ld#Options と info:ld#Commands にあり、よく使うコマンドは info:ld#Option Commands で説明されています。

5.4. シンボルのバージョン付けとバージョンスクリプト

通常、外部関数への参照は必要に応じてバインドされます。 アプリケーションの起動時に全てがバインドされるわけではありません。 共有ライブラリが古いものである場合、必要なインターフェースが存在しないかもしれません。 アプリケーションがそのようなインターフェースを使おうとしたときになって初めて、アプリケーションは突然予期せぬエラーを起こします。

この問題に対する一つの解決方法は、バージョンスクリプトによるシンボルのバージョン付けです。 シンボルのバージョン付けにより、アプリケーションにより使用されるライブラリが古すぎる場合に、そのアプリケーションを起動したときに警告が出るようになります。 これについては、バージョンスクリプトに関する ld マニュアルの説明 (http://www.gnu.org/software/binutils/manual/ld-2.9.1/html_node/ld_25.html) を参照してください。

5.5. GNU libtool

多くのシステムに移植する必要のあるアプリケーションを作成しているならば、ライブラリの構築とインストールについて、GNU libtool を使用することを検討したほうがよいかもしれません。 GNU libtool は、汎用的なライブラリサポートスクリプトです。 libtool は、共有ライブラリ使用時の複雑さを、一貫性のある移植性の高いインターフェースで隠蔽します。 libtool は、オブジェクト作成、ライブラリのリンク (静的および共有)、実行可能ファイルのリンク、実行可能ファイルのデバッグ、ライブラリのインストール、実行可能ファイルのインストール、に対して、移植性の高いインターフェースを提供します。 また、libltdl という、プログラムを動的にロードするための移植性のあるラッパーも、libtool には含まれています。詳細については、libtool の文書 http://www.gnu.org/software/libtool/manual.html を参照してください。

5.6. スペースを節約するためにシンボルを削除する

生成されたファイル内に含まれるシンボルは全て、デバッグの際に役に立ちますが、スペースを取ります。 スペースを節約する必要があるならば、シンボルの一部を削除することができます。

一番良い方法は、最初にいつも通りにオブジェクトファイルを生成し、デバッグとテストを全て実行してしまうことです (シンボルを含むオブジェクトファイルでは、デバッグとテストは非常に簡単です)。 その後、プログラムを完全にテストし終えたら、strip(1) を用いてシンボルを削除します。 strip(1) コマンドでは、どのシンボルを削除するかについて、いろいろと制御することができます。 詳細については strip(1) の文書を参照してください。

もう一つの方法は、GNU ld オプションの「-S」と「-s」を使用することです。 「-S」は、出力ファイルから、(全てではないですが) デバッグシンボル情報を除外します。 一方、「-s」は、出力ファイルから全てのシンボル情報を除外します。 「-Wl,-S」や「-Wl,-s」とすることにより、これらのオプションを gcc 経由で ld に渡せます。 常にシンボルを取り除くことにしていて、これらのオプションで十分というのならば、それでよいでしょう。 ただし、このやり方は、strip(1) に比べ柔軟性が下がります。

5.7. 極端に小さな実行可能ファイル

Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux (本当に小さな Linux 用 ELF 実行可能ファイル作成についてのすぐ役立つチュートリアル)」という文書が、あなたの役に立つかもしれません。 この文書は、非常に小さな実行可能プログラムの作り方について説明しています。 率直に言えば、一般的な状況下では、この文書で紹介されているトリックのほとんどは、使わないほうがよいです。 しかしこの文書は、ELF が実際にどのように機能するかを示しているという点において、教育上極めて有益です。

5.8. C++ vs. C

C++ プログラムを書いていて、C ライブラリ関数を呼び出すなら、C++ コード内でその C 関数を extern "C" として宣言する必要があるので注意してください。 そうしないと、リンカがその C 関数を見つけることができなくなってしまいます。 内部的に、C++ コンパイラは C++ 関数の名前を「変形 (mangle)」します (例えば、型情報を付加するために)。そのため、C++ コンパイラには、指定された関数を C 関数として呼び出すことを教えてやる必要があります (これにより、その C 関数の名前は変形されなくなります)。

C または C++ から呼び出されるプログラムライブラリを作成しているのなら、ライブラリユーザのため、この処理が自動的におこなわれるよう、ヘッダファイルに「extern "C"」を含めることをお勧めします。 ヘッダファイルの再読込みをスキップするためにヘッダファイルの先頭に置く、いつもの #ifndef と組み合わせて示すとすれば、C および C++ のどちらでも使用可能なヘッダファイルの典型例は、例えば foobar.h だとすると、次のようになります。

  /* foobar が何をするものなのか、ここで説明する。*/

  #ifndef FOOBAR_H
  #define FOOBAR_H

  #ifdef __cplusplus
  extern "C" {
  #endif

   ... foobar 用ヘッダコードをここに書く ...

  #ifdef  __cplusplus
  }
  #endif
  #endif

5.9. C++ 初期処理の高速化

KDE 開発者は、大きな GUI C++ アプリケーションの起動に長い時間がかかり、その理由の一つが、数多くのリロケーションをおこなう必要があることだと気付いています。 この問題に対して、幾つか解決方法があります。詳細については、「Making C++ ready for the desktop (by Waldo Bastian) (C++ コードをデスクトップ向けにする (Waldo Bastian 著))」を参照してください。

5.10. Linux Standard Base (LSB)

Linux Standard Base (LSB) プロジェクトの目的は、Linux ディストリビューション間の互換性を高め、LSB に準拠する全ての Linux システム上でソフトウェアアプリケーションの実行を可能とするための標準を開発し、普及させることです。 このプロジェクトのホームページは http://www.linuxbase.org/ です。

LSB 準拠アプリケーションの開発方法について簡単にまとめた、George Kraft IV (IBM Linux テクノロジーセンター上級ソフトウェアエンジニア) による素晴らしい文書、Developing LSB-certified applications: Five steps to binary-compatible Linux applications (訳注:日本語訳はこちら→ LSB 認定アプリケーション開発) が 2002 年 10 月に公開されました。 当然ながら、自分のコードの移植性を高めたいならば、標準化された移植層のみを使用してコードを書く必要があります。 さらに、LSB では、C/C++ プログラムのアプリケーション作成者が LSB に準拠しているかどうかをチェックするために使えるツールを、幾つか提供しています。 これらのツールは、リンカの幾つかの機能と、チェックをおこなうための特別なライブラリを使用しています。 もちろん、チェックをおこなうためには、ツール群をインストールする必要があります。 ツール群は LSB のウェブサイトから取得できます。 取得後は、C/C++ コンパイラとして単に "lsbcc" コンパイラを使ってください (lsbcc は、何かしらの LSB 規則が守られていないときにその旨を出力するためのリンク用環境変数を、内部的に作成します)。

  $ CC=lsbcc make myapplication
   (または)
  $ CC=lsbcc ./configure; make myapplication 

続けて、当該プログラムが LSB で標準化されている関数だけを使用していることを確認するため、 lsbappchk プログラムを使用します。

  $ lsbappchk myapplication

LSB のパッケージングガイドラインに従う必要もあります (例えば、RPM バージョン 3 を使う、LSB 準拠のパッケージ名を使う、アドオンソフトウェアの場合はデフォルトで /opt にインストールしなければならない、など)。 詳細については、先に言及した文書と LSB ウェブサイトを参照してください。

5.11. ライブラリ群を統合して大きな共有ライブラリへ

最初に小さいライブラリ群を作成しておき、後からそれらを大きなライブラリへとマージしたいという場合、どうしたらよいでしょうか? このようなときは、ld の "--whole-archive" オプションが便利です。 このオプションは、.a ファイル群を強制的に一つの .so ファイルへリンクする際に使用できます。

--whole-archive の使い方の一例を挙げます。

  gcc -shared -Wl,-soname,libmylib.$(VER) -o libmylib.so $(OBJECTS) \
      -Wl,--whole-archive $(LIBS_TO_LINK) -Wl,--no-whole-archive \
      $(REGULAR_LIBS)

ld 文書でも示されているとおり、最後に --no-whole-archive オプションを使うようにしてください。 そうしないと、gcc は標準ライブラリ群もマージしようとしてしまいます。 --whole-archive に関する記述の提案と作成の両方に対して、Kendall Bennett に感謝します。