2014年6月18日水曜日

Cの静的ライブラリ、共有ライブラリの作成手順メモ


静的ライブラリと共有ライブラリの作り方の手順を残す。

まず、言葉の定義として、
静的ライブラリは、スタティックライブラリ(static library)と同義である。英語訳しただけだ。

共有ライブラリは、動的(ダイナミック)ライブラリ(dynamic library)とほぼ同じだが同義ではない。
共有ライブラリは動的にロードされるのだが、下の2つの場合がある。
1. ビルド時に読み込むライブラリ内オブジェクトをリンクし、実行時にそれを動的に読み込む場合
2. 実行時に初めて読み込むライブラリが決まり、実行時にリンクしてそれを動的に読み込む場合
前者のことを指す場合は、動的ライブラリ、
後者の場合は、動的リンク、
という言葉を用いることにする。

もう一点。動的にリンクされた実行ファイルと共有ライブラリをstatifierを使って
静的にまとめる場合などもあるかもしれないが、そういった場合はどう表現すべきだろうか。
その時は、そう書く。またここではその説明もしないので気にしない。


では本内容をまとめておく。
【静的ライブラリの作成】
【共有ライブラリの作成】
二つを比較すると違いが鮮明になり分かりやすいだろう。

共有ライブラリの作成中に一部理解しにくい部分があるだろう。
そこを次の章で補う。
【共有ライブラリの依存関係の確認】

さらにこれも試してみる。
【共有ライブラリの差し替え】

また、ビルド時には不明であった共有ライブラリを実行時に
リンクして動的に読み込ませる手段と、
この中での共有ライブラリの差し替え方法を記載しておく。
【動的リンクローダの利用】
【動的リンクローダ利用時の共有ライブラリの差し替え】


【静的ライブラリの作成】
ライブラリにする関数を作成する。
$ vi aaa.c
void aaa() {
  puts("aaa");
}
bbb.c も同様。

オブジェクトファイルを作るためにコンパイル(のみを行う)。
$ gcc -c -o aaa.o aaa.c
$ gcc -c -o bbb.o bbb.c

オブジェクトファイル群を一つのアーカイブにする。
$ ar rusv libccc.a aaa.o bbb.o
r : replace(既存なら置換、新規なら挿入)
u : update(タイムスタンプを見て新しいものを置換)
s : storeroom(書庫インデックスを作成しリンクが高速になる)
v : verbose(詳細モード)

できあがったアーカイブの中身を確認する。
$ ar tv libccc.a

静的ライブラリを利用する実行ファイルを作成する。
$ cat main.c
int main() {
 aaa();
 bbb();
 return 0;
}

コンパイル。
$ gcc -c -o main.o main.c

ライブラリをリンクする。
ライブラリの必要なオブジェクトファイルが実行コード中にコピーされる。
$ gcc -o main main.o -L. -lccc

下記でもよい。
$ gcc -o main main.o -L. libccc.a

実行できる。
$ ./main
aaa
bbb



【共有ライブラリの作成】
まずはオブジェクトファイルを作る。
共有ライブラリ用のオブジェクトファイルには、
位置独立コードである、PIC(Position Independent Code)を指定した方がよい。
(参考) Linux の共有ライブラリを作るとき PIC でコンパイルするのはなぜか

$ gcc -fPIC -c -o aaa.o aaa.c
$ gcc -fPIC -c -o bbb.o bbb.c

共有ライブラリはアーカイブファイルではなく、
一つのオブジェクトファイルとして作られる。
$ gcc -shared -o libccc.so aaa.o bbb.o

上記でいいのだが、sonameを考慮した共有ライブラリを今回は作る。
$ gcc -shared -Wl,-soname,libccc.so.0 -o libccc.so.0.0.0 aaa.o bbb.o
-Wlによってカンマで区切られたパラメータをリンカに渡せる。
soname(shared object name)が実行時にリンクする共有オブジェクト名になる。
つまり、libccc.so.0である。
ただし、実際に作成するオブジェクトのファイル名はlibccc.so.0.0.0とする。

そのため、シンボリックリンクを張っておく必要がある。
$ ln -s libccc.so.0.0.0 libccc.so.0

命名規則は一般的な規則でそろえるべできある。
互換性のあるマイナーバージョン、リリースバージョン時に管理がしやすい。
・soname名
lib + ライブラリ名 + .so + メジャーバージョン
・実際のファイル名
lib + ライブラリ名 + .so + メジャーバージョン + マイナーバージョン + リリースバージョン


共有ライブラリのリンクを実施する(静的ライブラリの場合と同じである)。
ただし、動作は大きく異なる。
共有オブジェクトのSONAMEを実行ファイルのNEEDEDに設定(あとで説明)するだけであり、
オブジェクト自体はコピーされない。
$ gcc -o main main.o -L. -lccc
または
$ gcc -o main main.o -L. libccc.so.0

動くだろうか。しかしこれでは動作しない。
$ ./main
./main: error while loading shared libraries: libccc.so.0: cannot open shared object file: No such file or directory
ダイナミックリンクローダがライブラリを見つけられないためである。

実行ファイルを実行するときに、動的リンカローダが必要な共有ライブラリを探し出し、
実行時に実行バイナリと同じプロセス空間で共有ライブラリを使えるように
メモリのマップを操作するためである。

一時的であれば環境変数を指定すればよい(上書きしないように念のため:で追記しておく)。
$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH

恒久的な対応をとる場合は、 /etc/ld.so.conf あたりにパスを書いて、
ldconfig で認識させればよい。

これで動くはずである。
$ ./main



【共有有ライブラリの依存関係の確認】
共有ライブラリの依存関係が意図通りになっているか確認しておこう。
※実行結果は一部略

実行ファイルやオブジェクトファイルのフォーマットを規定している
ELF(Executable and Linking Format)をのぞいてみる。
必要な共有ライブラリはELFの動的セクションのNEEDEDに記載がある。
このNEEDEDがSONAMEと一致している。
$ objdump -p main | grep NEEDED
NEEDED               libccc.so.0

$ readelf -d main | grep NEEDED
0x00000001 (NEEDED)   Shared library: [libccc.so.0]

ただし上の二つでは
ライブラリがその中で他のライブラリを必要としている場合、
また手動でたどっていかなければならず手間である。
また、SONAMEの実際のパスと割り当てられたメモリアドレスも確認できない。
そういった用途にはlddが便利である。
$ ldd main
libccc.so.0 => $HOME/lib/libccc.so.0 (0x00b4b000)



【共有ライブラリの差し替え】
共有ライブラリ内のオブジェクトよりも優先的に、
別の同名共有オブジェクトを使うようにすることもできる。

aaa()関数名は同じである。
内部では先に作ったものと区別がつくように処理が変わっている。
$ vi aaa_fake.c
void aaa() {
  puts("aaa_fake");
}

共有ライブラリを作り直す。
$ gcc -shared -fPIC -o aaa_fake.so aaa_fake.c

LD_RELOADを指定することで、このパスの共有オブジェクトが優先的に探索される。
$ export LD_PRELOAD=./aaa_fake.so

意図通り入れ替わっていることが分かる。
$ ./main
aaa_fake
bbb



【動的的リンクローダの利用】
ビルド時には不明であった共有ライブラリを実行時に動的に読み込ませることもできる。
共有ライブラリの作成の章で作ったライブラリをロードさせてみる。

dlopenで共有のライブラリのハンドルを得て、
dlsymにてハンドルからオブジェクトのシンボルを探索する。
dlopen内のRTLD_LAZYオプションはdlsymでシンボルを探索するまで
シンボルの解決をしないという意味である。
説明は最低限だが、あとはコードを見てもらった方が早いだろう。

(main.c)
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

int main() {
  void *handle;
  int (*aaa) ();
  int (*bbb) ();

  handle = dlopen("./libccc.so", RTLD_LAZY);

  if (handle == NULL) {
    fprintf (stderr, "%s", dlerror() );
    exit (EXIT_FAILURE);
  }

  aaa = dlsym(handle, "aaa");
  bbb = dlsym(handle, "bbb");

  if (aaa == NULL || bbb == NULL) {
    fprintf(stderr, "%s\n", dlerror() );
    exit(EXIT_FAILURE);
  }

  aaa();
  bbb();

  dlclose(handle);

  exit(EXIT_SUCCESS);

  return 0;
}

動的ライブラリをロードするためのライブラリが必要なため、
-lでdlを別途読み込ませること。
$ gcc -o main main.c -ldl 
$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
$ ./main
aaa
bbb



【動的的リンクローダ利用時の共有ライブラリの差し替え】
dlsym時にRTLD_DEFAULTを使うとディフォルトのライブラリ検索順番で、
RTLD_NEXTを使うとライブラリ検索順序の中で現在のライブラリ以降で
最初に関数が現れるところを探すよう指示てできる。
この機能を使うことで、別の共有ライブラリの 関数へのラッパーを提供することができる。

LD_PRELOADで指示する共有ライブラリ内から対象のオブジェクトを見つけられるか確認する。

// aaa = dlsym(handle, "aaa");
aaa = dlsym(RTLD_DEFAULT, "aaa");

またRTLD_DEFAULTやRTLD_NEXTはGNU拡張なので、
コードの最上段に以下を加えておく必要がある。

#define _GNU_SOURCE
#include <dlfcn.h>

これでビルドできるはずである。
$ gcc -o main main.c -ldl

LD_PRELOADはLD_LIBRARY_PATHよりも優先されるので、
RTLD_DEFAULTにすると、LD_PRELOADしたオブジェクトが、
RTLD_NEXTにすると、次の検索対象である、LD_LIBRARY_PATHからオブジェクトが探索される。
ここではdlsymにRTLD_DEFAULTを指定したのでどうなるかは想像できるだろう。
$ export LD_PRELOAD=./aaa_fake.so
$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
$ ./main
aaa_fake
bbb


最後に、LD_PRELOADをunsetして、
元のlibccc.so.0内のaaa()を読むことも確認しておく。
$ unset LD_PRELOAD
$ gcc -o main main.c -ldl
$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
$ ./main
しかし、aaa()が見えずに、エラーになる。
LD_LIBRARY_PATHへのパスは通しているにも関わらず、である。
少し悩んでしまったが、共有オブジェクトのSONAMEを
実行ファイルのNEEDEDに設定していないからである。

$ gcc -o main main.o -L. -lccc
または
$ gcc -o main main.o -L. libccc.so.0
$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
$ ./main
aaa
bbb



『参考』