オブジェクト不指向の共用オブジェクト

動的にロード可能なライブラリーを作成する方法

Ashish Bansal氏が、動的にロード可能なライブラリーを作成する方法を解説し、このプロセスで役立つツールを紹介します。コンパイル・プロセスと命名規則を見直し、共用ライブラリーの作成、コンパイル、およびインストールについて説明します。

Ashish Bansal (abansal@sapient.com), Software Engineer, Sapient Corporation

Ashish Bansal 氏は、インドのバラナシ (Varanasi) にある Banaras Hindu University の Institute of Technology で、電子工学と通信エンジニアリングの学士号を取得しました。現在は、Sapient Corporation のソフトウェア・エンジニアとして活躍中です。氏のメール・アドレスは、abansal@sapient.com です。



2001年 4月 01日

肩の力を抜いてください。共用オブジェクトは「オブジェクト指向テクノロジー」とは何の関係もありません。ここでお話しするのは、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 という実行可能ファイルが作成されます。これで、コンパイラーは以下のステップに進むことができます。

  1. 構文チェック: ファイルの構文をチェックする。
  2. コンパイル: ファイルをコンパイルして、コードのオブジェクト・ファイルを生成する。未解決の関数名 (この場合は printf() ) は、生成されたオブジェクト・ファイルではマークされています (以下の ファイル・フォーマット を参照)。
  3. リンク: リンカーという別個のプログラムを呼び出す (UNIXでは、このプログラムは ld です)。リンカーは、コードの各種ライブラリー内を検索して、関数と変数の解決を試みます。たとえば、 printf() のコードは、ファイル libc.a (または libc.so ) に常駐しています。標準セット以外のライブラリーが必要な場合は、指定しなければなりません。

コードをコンパイルしてリンクする前述のコマンド行は、以下の2つの部分に分けることができます。

 >$ gcc -c hello.c
 $ ld -lc -o hello hello.c

1つ目は、コンパイル・ステップです ( -c オプションで指定します)。2つ目は、リンク・ステップで、 ld プログラムを使用して実行可能プログラムhelloを生成します。

ファイル・フォーマット

ELFはExecutable and Linking Format の略です。このフォーマットは、Linuxを含め、ほとんどのUNIXプラットフォーム上のオブジェクト・ファイルに使用されます。オブジェクト・ファイルには基本的に3つのタイプがあります。

  1. 再配置可能ファイルには、実行可能ファイルや共用オブジェクト・ファイル、その他の再配置可能オブジェクトを作成するために他のオブジェクト・フィールドとリンク可能なコードやデータを格納します。
  2. 実行可能ファイルには、実行できるプログラムを格納します。
  3. 共用オブジェクト・ファイルには、他の共用オブジェクトや再配置可能ファイルへのリンク可能なコードやデータを格納します (以下の図に、オブジェクト・ファイルのフォーマットを示します)。

各ファイルには、ELFヘッダーファイルが存在し、ファイルの他の部分へのロードマップのような働きをします。セクションは、ファイル内で処理できる最小単位を表し、リンクに必要な情報が格納されます。セクション・ヘッダー・テーブルには、ファイル内のセクションに関する情報が入ります。プログラム・ヘッダー・テーブルは (存在する場合)、プロセス・イメージを作成する方法を記述します。これは、オブジェクト・ファイルが実行可能ファイルである場合に使用します。 exec プログラムは、プログラム・ヘッダー・テーブルを使用して、プロセスをforkします。ELFヘッダーの位置はファイル内で一定ですが、他の部分は、図に示す位置とは異なる場合があるので注意してください。

ELFヘッダーは、 file プログラム (後述の ユーティリティー・プログラムとツール を参照) がファイルに関する情報を出力する場合に使用するものです (libelf ライブラリー・パッケージには、ELFヘッダー内の情報にアクセスするためのプログラミング・インターフェースが用意されています)。

このタイプのリンクのことを静的リンク と言います。静的コンパイルを使用して、ライブラリー内のコードはアプリケーションと結び付きます (この方法は、実行可能ファイルを非常に大きくするという短所があります)。実際に使用されているアプリケーションは、数百もの関数をライブラリー (その多くが提供されています) として使用します。これらのライブラリーの中には、標準のもの、サード・パーティーのもの、そして社内で作成したものがあります。静的コンパイルを使用する場合、実行可能ファイルのサイズは最終的にかなりの大きさになりますが、そのすべてを実行時にメモリーにロードしなければなりません。そのため、ある関数が使用されるかどうかに関係なく、そのコードはメモリー内にあることになります。

必要になった時点で、ライブラリーを動的にロードするメカニズムがあれば、プログラムが必要とするメモリー・サイズが減少するだけでなく、アプリケーションを小さく分割することができるので、状況はかなり改善されるでしょう。また、配布、インストール、アップグレードも容易になります。これにぴったりのメカニズムが存在するのです。いわゆる動的にリンクするライブラリー (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.clibprint.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/liblib 、または /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.sonm を実行すると、以下の出力が生成されます。

nm
$ nm libprint.so
00001490 A _DYNAMIC 00001480 A _GLOBAL_OFFSET_TABLE 00001510 A __bss_start 00001510 A _edata 00001510 A _end 00000452 A _etext 00000400 T _fini 000003d8 T _init 000003d8 t gcc2_compiled U printf@@GLIBC_2.0 00000428 T printstring

objdumpは、オブジェクト・ファイルに関する情報を表示します。この情報は、コマンド行で指定することができます。objdumpに渡されたオプションにより、表示する情報が制御されます。オブジェクト・ファイルの内部を細かく調べる場合に便利なユーティリティーです。

共用オブジェクトの説明を、興味深く読んでいただけたとしたら、幸いです。これを機に、この分野の知識を深めてください。役に立つ筈です。

本稿で紹介されている内容は著者個人のものであり、Sapient Corporationが関与するものではありません。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=231754
ArticleTitle=オブジェクト不指向の共用オブジェクト
publish-date=04012001