肩の力を抜いてください。共用オブジェクトは「オブジェクト指向テクノロジー」とは何の関係もありません。ここでお話しするのは、Linuxプラットフォーム上で動的にリンクされたライブラリー
(WindowsでいうDLL)
です。コーディングのたびに、わたしたちはライブラリーを使用してきました。例えばCの
printf()
などの簡単な関数や、C++ 汎用関数ライブラリーの
sort(
)
などの複雑な関数などです。ライブラリーを使用すれば、毎日のプログラミングは簡単になり、開発者は目前の作業に集中することができます。作成するコードごとに、printf()
やファイル入出力関数を作成することなど考えられないことです!
ほとんどのソフトウェア作成は、ライブラリーに大きく依存しています。ライブラリーを使用して、画面への出力からネットワークへのログオンまで、さまざまなタスクを実行することができます。これらのライブラリーには、システムに同梱されてくるものと、サード・パーティーのベンダーやユーザー自身が作成するものがあります。コンパイルのプロセス時に、これらのライブラリーはアプリケーションにリンクされます。アプリケーションが使用するライブラリーが多く、しかも、すべてのコードがリンクされているとすると、アプリケーションのサイズは桁外れの大きさになってしまいます。「共用ライブラリー」は、アプリケーションのソースにはリンクされませんが、アプリケーションで必要とされるときに動的にロードされます。
共用オブジェクト作成のお話に入る前に、コンパイル・プロセスについて、そして共用オブジェクトとは何かについて整理しておきましょう。以下に有名な、今では歴史的ともいえる例になっているhello worldのコードを示します。
リスト1: Hello.c; 共用オブジェクトの定義
#include "stdio.h"
void main()
{
printf("Hello World!");
}
|
このプログラムをコンパイルするには、以下のコマンド行を使用します。
$ gcc -o hello hello.c |
これで、hello という実行可能ファイルが作成されます。これで、コンパイラーは以下のステップに進むことができます。
- 構文チェック: ファイルの構文をチェックする。
-
コンパイル: ファイルをコンパイルして、コードのオブジェクト・ファイルを生成する。未解決の関数名
(この場合は
printf()) は、生成されたオブジェクト・ファイルではマークされています (以下の ファイル・フォーマット を参照)。 -
リンク: リンカーという別個のプログラムを呼び出す (UNIXでは、このプログラムは
ldです)。リンカーは、コードの各種ライブラリー内を検索して、関数と変数の解決を試みます。たとえば、printf()のコードは、ファイルlibc.a(またはlibc.so) に常駐しています。標準セット以外のライブラリーが必要な場合は、指定しなければなりません。
コードをコンパイルしてリンクする前述のコマンド行は、以下の2つの部分に分けることができます。
>$ gcc -c hello.c $ ld -lc -o hello hello.c |
1つ目は、コンパイル・ステップです (
-c
オプションで指定します)。2つ目は、リンク・ステップで、
ld
プログラムを使用して実行可能プログラムhelloを生成します。
このタイプのリンクのことを静的リンク と言います。静的コンパイルを使用して、ライブラリー内のコードはアプリケーションと結び付きます (この方法は、実行可能ファイルを非常に大きくするという短所があります)。実際に使用されているアプリケーションは、数百もの関数をライブラリー (その多くが提供されています) として使用します。これらのライブラリーの中には、標準のもの、サード・パーティーのもの、そして社内で作成したものがあります。静的コンパイルを使用する場合、実行可能ファイルのサイズは最終的にかなりの大きさになりますが、そのすべてを実行時にメモリーにロードしなければなりません。そのため、ある関数が使用されるかどうかに関係なく、そのコードはメモリー内にあることになります。
必要になった時点で、ライブラリーを動的にロードするメカニズムがあれば、プログラムが必要とするメモリー・サイズが減少するだけでなく、アプリケーションを小さく分割することができるので、状況はかなり改善されるでしょう。また、配布、インストール、アップグレードも容易になります。これにぴったりのメカニズムが存在するのです。いわゆる動的にリンクするライブラリー (WindowsではDLL、Linuxでは共用オブジェクト) です。こうしたメカニズムを使用しているアプリケーションのことを、動的実行可能ファイルと言います。
共用オブジェクトに入る前に、ライブラリーの命名規則について簡単に説明しましょう。静的ライブラリーは一般的にlib という文字から始まり、拡張子は.a です。共用オブジェクトには、soname とreal name という2つの異なる名前が付いています。soname は、接頭部が "lib" で、その後にライブラリーの名前、".so"、もう1つドット、そしてメジャー・バージョン番号を示す数字が続きます。sonameは、パス情報を先頭に付けることで完全修飾にすることもできます。real nameは、ライブラリーのコンパイル・コードを含む実際のファイルの名前です。real nameは、sonameにドット、マイナー番号、もう1つドット、そしてリリース番号を追加したものです(リリース番号とその前のドットはオプションです)。
もう1つ、参考資料のProgram-Library How-To で定義されている、linker name という名前があります。この名前は、バージョン番号情報なしでsonameを参照する場合に使用することができます。このライブラリーを使用するクライアントは、linker nameを使用してライブラリーを参照します。一般に、これはsonameへのリンクです。そして、sonameはreal nameへのリンクです。
たとえば、sonameが
/usr/lib/libhello.so.1
としましょう。これは、完全修飾sonameで、
/usr/lib/libhello.so.1.5
にリンクします。対応するlinker nameは
/usr/lib/libhello.so
です。これだと、多くの名前を管理しなければならないようですが、名前の管理に役立つツールがあります
(後述の
ユーティリティー・プログラムとツール
の
ldconfig
を参照)。
さて、いよいよ本論に入り、サンプルの共用オブジェクトを作成しましょう。ライブラリーのsonameは
libprint.so.1
で、real nameは
libprint.so.1.0
です。このライブラリーには、"String: "
という単語、およびその後に引き数として渡された ストリングを出力する、
printstring(char*)
という関数が1つあります。
ライブラリーを使用可能にするために作成しなければならないファイルは基本的に2つあります。1つ目は、ヘッダー・ファイルです。ライブラリーから取り出せるすべての関数を宣言し、クライアントのコードに取り込まれます。2つ目は、関数の定義です。共用オブジェクトとしてコンパイルおよび配置すべき関数を定義します。この例では、ヘッダー・ファイルは以下のようになります。
リスト2: Libprint.hコード; ヘッダー・ファイル
/* file libprint.h - for example use! */
void printstring(char* str);
|
ライブラリーのコードはかなり基本的なもので、以下に示します。
リスト3: libprint.cコード
/* file libprint.c */
#include "stdio.h"
void printstring(char* str)
printf("String: %s\n", str);
}
|
_init(void)
と
_fini(void)
という2つの特殊な関数があります。これらは、ライブラリーがロードされる度に常に動的ローダーによって自動的に呼び出されます。これらの2つの関数については、デフォルトで提供されますが、これらを使わずに独自の関数を作成することもできます。libprint.cコードにこれらの2つの関数を追加して、呼び出される度に診断メッセージが出力されるようにしましょう。
リスト4: _init() と_fini() のコード
void _init()
{
printf("Inside _init()\n");
}
void _fini()
{
printf("Inside _fini()\n");
}
|
このコードをリスト3のコードと結合します。簡単でしょう? カスタム・ライブラリーを作成するには、
libprint.c
と
libprint.h
のテンプレートを使用して、必要な関数を作成するだけです。では、先に進み、ライブラリーをコンパイルしましょう。
ライブラリーをコンパイルする場合のコマンド・シーケンスは以下のとおりです。
$ gcc -fPIC -c libprint.c $ ld -shared -soname libprint.so.1 -o libprint.so.1.0 -lc libprint.o |
gccコマンド行の
-fPIC
オプションに注意してください。これは、位置独立コード(Position Independent Code)
を生成するのに欠かせません。簡単に言えば、このコマンドは「1つのプロセスのプロセス・スペース内のどこにでもロードできるコードを生成する」という意味です。また、共用オブジェクトにとっても非常に重要です。このオプションを使用することで、発生する再配置の数が非常に少なくなります。実行可能ファイルで使用する共用オブジェクトをロードする場合は、その共用オブジェクトにスペースを割り振らなければなりません。そして、テキスト・セクションとデータ・セクションにも位置を割り振らなければなりません。位置とは独立した方法でこれらを作成しておかないと、共用オブジェクトをロードするプログラムが実行する再配置はかなりの数になり、パフォーマンスに悪影響を及ぼします。
では、
ld
に渡すオプションを分析しましょう。
-shared
オプションは、出力ファイルを共用ライブラリーと見なすことを示します。
-soname name
オプションにより、sonameを指定できます。
-o name
では、共用オブジェクトのreal nameを指定します。sonameとreal
nameは、ライブラリーのインストール時に使用するので、これらの2つの名前を指定することは重要です。
ライブラリーを作成し終えたので、インストールして、そのライブラリーを使用する小さいクライアント・プログラムを作成しましょう。共用ライブラリーのインストールには、
ldconfig
という特殊なプログラムを使用します。一般に、共用ライブラリーは、
/usr/lib
、
lib
、または
/usr/local/lib
のいずれかにインストールします。ライブラリーを作成したら、これらのディレクトリーの1つにコピーします。そして、
ldconfig
プログラムを実行します。
ldconfig
$ldconfig -v -n .
...:
libprint.so.1 => ./libprint.so.1.0
|
libprint.so.1.0
への
libprint.so.1
という名前のシンボリック・リンクを作成しました。次のインストール・ステップは、以下のようにlinker
nameへの別のリンクを作成することです。
linker nameのリンク
$ ln -sf libprint.so libprint.so.1
|
/usr/lib
ディレクトリー、
lib
ディレクトリー、または
/usr/local/lib
ディレクトリーにコピーするには、スーパー・ユーザー権限が必要です。アプリケーションを実行すると、これらのディレクトリーが自動的に検索されて、ライブラリーが解決されます。スーパー・ユーザー権限がない場合は、共用ライブラリーはどのディレクトリーにもインストールできますが、別のセットを実行しなければ、この共用ライブラリーを使用する実行可能ファイルを実行することはできません。環境変数
LD_LIBRARY_PATH
を設定し、共用ライブラリーが常駐するパスを指定しなければなりません。たとえば、共用ライブラリーが実行可能ファイルと同じディレクトリーにある場合は、以下のように指定します。
LD_LIBRARY_PATH
$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
|
これで、作成した共用ライブラリーを使用するクライアントをコンパイルし、実行するための準備が整いました。共用ライブラリーの使用は本当に簡単ですね。:)
リスト5: Client.c; ライブラリーの関数
printstring()
を使用するサンプル・クライアント
#include "libstring.h"
void main()
}
printf("In Main!\n");
printstring("In Main!");
}
|
このプログラムを実行可能ファイルにコンパイルするには、以下のコマンド行を使用します。
$ gcc -o client client.c -L. -lprint |
これで、ライブラリーがコードと同じディレクトリー (これは
-L. -lprint
で示します) に常駐するものと見なす、client
という実行可能ファイルが生成されます。実行時に、以下の出力が生成されます。
$ client Inside _init() In Main! String: In Main! Inside _fini() $ |
これで、共用オブジェクトを作成し、インストールし、そして使用しました。では、client
を実行しようとするときに、どのような事が起きるのか見てみましょう。プログラムの開始時に、システムはこのプログラムが動的ライブラリーを参照していることを認識します。そのため、ローダー
/lib/ld-linux.so.X
(Xはバージョン番号です)
が呼び出され、必要なライブラリーがロードされます。実行可能ファイルがどのライブラリーを参照しているかの判断には、
ldd
を使用することができます。
ldd
$ ldd client
libprint.so.1 => ./libprint.so.1
libc.so.6 =>
/lib/libc.so.6
/lib/ld-linux.so.2=> /lib/ld-linux.so.2
|
これで、ファイルclientがどのライブラリーを参照しているかが通知されます。これらのライブラリーは、ローダーによってロードされて、対応する
_init()
セクションが呼び出されます。ローダーはまず
LD_LIBRARY_PATH
環境変数で指定されたパスでライブラリーを探し、その後で
/etc/ld.so.conf
で指定された共用パスに移ります。ライブラリーが見つからなかった場合は、エラー・ステータスが返されます。通常、ローダーはライブラリーをロードして、プログラムの通常の実行が再開されます。これはすべてユーザーに意識されることなく実行されます。
次に、fileツール、nmツール、objdumpツール という3つの非常に役立つバイナリー・ユーティリティーについて簡単に説明しましょう。
ファイルのタイプを判別する場合は、fileプログラムを使用します。テストするファイルには、テキスト・ファイル (たとえば、libprint.c)、実行可能ファイル (たとえば、client)、データ (たとえば、/dev/hda5) が挙げられます。fileは、特定のファイルをコンパイルしたときのプラットフォームを判別したり、ファイルが実行可能ファイルかどうかを判別したりする場合に非常に役立ちます。このツールを呼び出すには、以下のコマンド行を使用します。
File
$ file client
client: ELF 32-bit LSB executable, Intel 80386, version 1, dynamically linked
(uses shared libs), not stripped
|
nmは、オブジェクト内に存在するシンボルをすべてリストします
(オブジェクトとは、一般にオブジェクト・ファイルまたはライブラリーのことです)。nmを通してオブジェクトを渡すと、このオブジェクトが使用またはエクスポートした関数の名前、オブジェクトの各種セクション、シンボル、およびオブジェクト・タイプが表示されます。たとえば、シンボルは、未定義であったり、外部であったりする場合があります。また、グローバルや別の識別子かもしれません。
libprint.so
で
nm
を実行すると、以下の出力が生成されます。
nm
$ nm libprint.so |
objdumpは、オブジェクト・ファイルに関する情報を表示します。この情報は、コマンド行で指定することができます。objdumpに渡されたオプションにより、表示する情報が制御されます。オブジェクト・ファイルの内部を細かく調べる場合に便利なユーティリティーです。
共用オブジェクトの説明を、興味深く読んでいただけたとしたら、幸いです。これを機に、この分野の知識を深めてください。役に立つ筈です。
本稿で紹介されている内容は著者個人のものであり、Sapient Corporationが関与するものではありません。
-
fileコマンドのバージョン3.27が記載されている
fileのmanページ
を参照してください。
-
objdumpコマンドについては、
objdump のmanページ
を参照してください。
-
簡単なチュートリアル
Building Shared objects on Sun Solaris
を参照してください。
-
動的なリンクに関する一般的概念
は、"AIX Version 4.3 General Programming
Concepts" で入手することができます。
Ashish Bansal 氏は、インドのバラナシ (Varanasi) にある Banaras Hindu University の Institute of Technology で、電子工学と通信エンジニアリングの学士号を取得しました。現在は、Sapient Corporation のソフトウェア・エンジニアとして活躍中です。氏のメール・アドレスは、abansal@sapient.com です。