Linux の共有ライブラリの挙動について
前フリが長くなったので基礎的な部分は独立した article にしました。でも基礎的な内容だけに間違いが多そうです。ご指摘お願いします。
下記に「Hack#??」と書いてあるものはすべて Binary Hacks の Hack です。書籍をお持ちの方はそちらをご参照ください。
基礎
まず用語定義を引用しておきます。
Binary Hacks - Hack#6
- シンボル(symbol)
- 一般的には記号を意味するが,Binary Hacks の文脈では,リンカが関数や変数を識別するときに用いる名前のことを指す。
(強調部は dayflower による)
さて,共有ライブラリを利用する際の挙動について「Hack#6 静的ライブラリと共有ライブラリ」から抜粋します。
ここでのポイントは,共有ライブラリ単位で処理が行われるということと,リンクする時には必要としている共有ライブラリの
SONAME
だけを実行可能ファイルにNEEDED
として登録してあるということです。共有ライブラリをリンクした実行ファイルを実行する時に,動的リンカローダ(
Binary Hacks - Hack#6ld.so
)がNEEDED
の情報を使って必要としている共有ライブラリを探し出し,実行時にそのプロセスのメモリマップを操作して共有ライブラリと実行バイナリを同じプロセス空間で使えるようにしています。
さらに付け加えると,
- 最終的なバイナリ(実行ファイルや共有ライブラリ)には,外部参照・動的リンクするシンボルと,必要となる共有ライブラリ(
NEEDED
)が別々に格納されている - 両者のマッピングはない。すなわち,あるシンボルがどの共有ライブラリ由来のものかという情報は格納されていない。
具体例をみてみます。シンボルの探索とバインドの様子をみるためには,環境変数 LD_DEBUG
に symbols,bindings
を指定して実行します。
$ LD_DEBUG=symbols,bindings ./main ...... snip snip snip ...... symbol=fwrite; lookup in file=./main [0] symbol=fwrite; lookup in file=/usr/lib/libz.so.1 [0] symbol=fwrite; lookup in file=/lib/tls/i686/cmov/libm.so.6 [0] symbol=fwrite; lookup in file=/lib/tls/i686/cmov/libc.so.6 [0] binding file ./main [0] to /lib/tls/i686/cmov/libc.so.6 [0]: normal symbol `fwrite' [GLIBC_2.0] ...... snip snip snip ......
LD_DEBUG=symbols
によってシンボルの lookup の様子が,LD_DEBUG=bindings
によってシンボルの binding の様子が,ログに出力されています。もちろんこれらは単独で指定することもできます。ほか LD_DEBUG
については「Hack#58 プログラムが main()
にたどりつくまで」を参照してください。オンラインでは元ネタが 「プログラムはどう動くのか? 〜 ELFの黒魔術をかいまみる」にあります*1。
さて,出力をみると,fwrite
というシンボルを,./main,libz.so,libm.so,libc.so の順に探していること,最終的に libc.so にバインドしたことがわかります。
これらの共有ライブラリは先の引用の説明のとおり,./main の dynamic section に NEEDED
として登録されているためです。これらはビルド時に,
$ gcc -o main main.o -lz -lm
のように指定したために登録されました*2。つまり,リンカ(ld)がバイナリの dynamic section に登録したわけです。このことからも,あるシンボルがどの共有ライブラリに所属するのか判断しようがないことがわかります。
共有ライブラリ内でのシンボルの解決
下記のようなライブラリがあるとします。
/* libplace.c: */ #include <stdio.h> const char * get_right(void) { return "右"; } void navigate(void) { fprintf(stdout, "コンビニは%sにあります\n", get_right()); }
身も蓋もない例ですみません。get_right()
も navigate()
も外部に公開している関数となっていることに注意してください。この例だと普通 get_right()
は static
宣言すると思いますが,実際のライブラリでは,別々のソースになっているなど外部公開関数とせざるをえないケースはよくあります。
これを普通に共有ライブラリとしてビルドします((一般的には -Wl,-soname,〜
でバージョンつき共有ライブラリ名を指定しますが今回は略します))。
$ gcc -shared -fPIC -o libplace.so libplace.c
共有ライブラリというのは実態はひとまとまりのオブジェクトなので,このように,静的ライブラリと異なりアーカイバ(ar)ではなくリンカ(ld)で作成します。
静的ライブラリの場合は,複数のオブジェクトファイルのアーカイブでしたが,共有ライブラリの場合は,複数のオブジェクトファイルを1つの巨大なオブジェクトファイルにしてそれらを共有できるようにしたものです。
Binary Hacks - Hack#6
この共有ライブラリを利用する main.c は下記の通りです。
/* main.c: */ void navigate(void); /* prototype for external */ int main(int argc, char *argv[]) { navigate(); return 0; }
これを libplace.so を使うようにビルドします(カレントディレクトリにライブラリがあるので -L.
を指定しています)。
$ gcc -o main main.c -L. -lplace
readelf -d
コマンドで main のダイナミックセグメントを表示すると NEEDED
がわかります。
$ readelf -d main| grep NEEDED 0x00000001 (NEEDED) Shared library: [libplace.so] 0x00000001 (NEEDED) Shared library: [libc.so.6]
いっぽう,ダイナミックセグメントのシンボルを表示してみます。
$ readelf -s -D main Symbol table for image: Num Buc: Value Size Type Bind Vis Ndx Name 9 0: 0804835c 0 FUNC GLOBAL DEFAULT 11 _init 6 0: 08049658 0 NOTYPE GLOBAL DEFAULT ABS _edata 2 0: 00000000 0 NOTYPE WEAK DEFAULT UND _Jv_RegisterClasses 1 0: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__ 5 1: 0804965c 0 NOTYPE GLOBAL DEFAULT ABS _end 4 1: 00000000 61 FUNC GLOBAL DEFAULT UND navigate 3 1: 00000000 434 FUNC GLOBAL DEFAULT UND __libc_start_main 10 1: 0804851c 0 FUNC GLOBAL DEFAULT 14 _fini 8 2: 08049658 0 NOTYPE GLOBAL DEFAULT ABS __bss_start 7 2: 0804853c 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used ...... snip snip snip ......
navigate
はモジュール(main)内で未定義(UND
)のシンボルであることがわかります。
さて,まあ,実行すると下記のようになります(使用する共有ライブラリがカレントディレクトリにあるので LD_LIBRARAY_PATH=.
を指定しています)。
$ LD_LIBRARY_PATH=. ./main コンビニは右にあります
さきほどと同じようにシンボル名の解決模様を眺めるため LD_DEBUG=symbols,bindings
を指定して実行してみます。
$ LD_DEBUG=symbols,bindings LD_LIBRARY_PATH=. ./main ...... snip snip snip ...... calling init: /lib/tls/i686/cmov/libc.so.6 calling init: ./libplace.so symbol=__libc_start_main; lookup in file=./main [0] symbol=__libc_start_main; lookup in file=./libplace.so [0] symbol=__libc_start_main; lookup in file=/lib/tls/i686/cmov/libc.so.6 [0] binding file ./main [0] to /lib/tls/i686/cmov/libc.so.6 [0]: normal symbol `__libc_start_main' [GLIBC_2.0] initialize program: ./main transferring control: ./main symbol=navigate; lookup in file=./main [0] symbol=navigate; lookup in file=./libplace.so [0] binding file ./main [0] to ./libplace.so [0]: normal symbol `navigate' symbol=get_right; lookup in file=./main [0] symbol=get_right; lookup in file=./libplace.so [0] binding file ./libplace.so [0] to ./libplace.so [0]: normal symbol `get_right' ...... snip snip snip ...... コンビニは右にあります ...... snip snip snip ......
先ほど未定義だった navigate
というシンボルが libplace.so で見つかったことがわかります。
というのは先ほどの例と同じなので新しい発見はありませんが,その後の部分でおもしろいことがわかります。
libplace.so 内の navigate()
という関数の中で get_right()
を呼び出しているのですが,これは libplace.so 内にあるシンボルです。よって直接呼び出しても構わないはずですが,get_right
シンボルを探索・バインドしています。すなわち,同一モジュール内の(グローバル)関数の呼び出しにおいてすら ld.so(動的リンカ)を経由しているのです。
これを図示すると下記のようになります。
別々の共有ライブラリでのシンボルの衝突
- 未解決シンボルに共有ライブラリ名があらかじめバインドされているわけではない
- 共有ライブラリ内のグローバルシンボルの解決においてすら動的リンカを経由する
これらの仕様は時として悲劇を招くことがあります((逆にポジティブに利用すると LD_PRELOAD
環境変数を使って既存の関数を置き換えることができます。が,この稿では説明しません。「Hack#60 LD_PRELOAD
で共有ライブラリを置き換える」を参照してください。))。それが「Hack#19 リンク時のシンボルの衝突に注意する」です。
さきほどの例の続きです。下記のようなあたらしいライブラリがあるとします。
/* libcond.c: */ const char * get_right(void) { return "正しい"; }
かなり恣意的ですが get_right()
という関数を定義してあります。
これをさきほどと同じように共有ライブラリとしてビルドします。
$ gcc -shared -fPIC -o libcond.so libcond.c
main.c をビルドしなおします。
$ gcc -o main main.c -L. -lcond -lplace
libcond が libplace より先にリンクされているのがミソです。
これを実行すると
$ LD_LIBRARY_PATH=. ./main コンビニは正しいにあります
このようにおかしな出力になってしまいます。
これは,もうおわかりのとおり,get_right
シンボルの解決の際,NEEDED
で先に格納されている libcond.so が利用されてしまったことによります。しつこいようですが LD_DEBUG
環境変数を利用した出力を示します。
$ LD_DEBUG=symbols,bindings LD_LIBRARY_PATH=. ./main ...... snip snip snip ...... initialize program: ./main transferring control: ./main symbol=navigate; lookup in file=./main [0] symbol=navigate; lookup in file=./libcond.so [0] symbol=navigate; lookup in file=./libplace.so [0] binding file ./main [0] to ./libplace.so [0]: normal symbol `navigate' symbol=get_right; lookup in file=./main [0] symbol=get_right; lookup in file=./libcond.so [0] binding file ./libplace.so [0] to ./libcond.so [0]: normal symbol `get_right' ...... snip snip snip ......
個人でプログラムを書いている場合,このようにシンボルが衝突してしまうケースはあまりないでしょう。しかし,他のライブラリをリンクすることは日常的であり,プログラムが複雑化している現代では,リンクするライブラリ数は膨大になっています。また各ライブラリにどのような関数があるのかすべて知って使うわけではありません。
さらに,今回の場合呼び出し規約が一致しているためそれほどひどい出力にはなりませんでしたが,場合によっては(というよりかなりありがちですが)引数や戻り値などの規約が違うことがありえます。また処理自体が大幅に違うものであれば破壊的動作を示すことになります。
解決策は
この問題の解決策の一つは,簡単には,関数や変数の名前にユニーク名称を付与する,というものです。
今回の例だと,
- libplace.so の
get_right()
はplace_get_right()
に - libcond.so の
get_right()
はcond_get_right()
に
する,などです。とはいえこれも実施するのはなかなか大変ですね。
C++ の場合,シンボル名にクラス名やネームスペースが含まれますし,呼び出し規約(引数の型や戻り値の型)も含まれるので,(クラス名などがかぶらないかぎり)半自動的にユニークなシンボルになります。
C の場合でも,関数本体は static
にしておき,関数ポインタを含む構造体経由で関数を実行するようにすれば(自力 vtbl?),内的な関数は(ソースを分割したとしても)ローカルにバインドできます。そのようなスタイルのコードが増えてきた背景は,オブジェクト指向ということはもちろんあるとは思いますが,案外このようなシンボルの衝突回避が理由としてあげられるのかもしれません。
あと,別の解決策として -fPIC
をつけずに共有ライブラリを作るというのがあります(たぶん……それでいけますよね?)。しかしこれはおすすめできません。
さて,こんなのやってらんねー,という方のための情報があるのですが,続きます→ld 2.18 の -Bsymbolic オプションを使うと共有ライブラリ内でシンボルをローカルバインドできる - daily dayflower
Windows の共有ライブラリ(DLL)について
Windows の場合,暗黙的リンク*3においてすら,未解決外部シンボル名(実際には序数経由のケースが多いですが)と共有ライブラリ名(DLL)の対応はとられています(Dependency Walker などのプログラムでその様子を見ることができます)。またライブラリ内で関数を呼び出した場合もリンク時に直接解決されます……と思いますがちょっとこのへんの知識はあやふやです。
さいごに
Binary Hacks ―ハッカー秘伝のテクニック100選 から結構引用してしまいましたが実際役に立ちました。最初は「結構知っていることも多いし,逆に知らない部分はマニアックだし,レベルがまちまちでいまいち使えないなー」と思っていたのですが,すいません,たかをくくってました。会社の備品を参照しながら書きましたが,暇ができたら自分の分も購入したいと思います。
Binary Hacks ―ハッカー秘伝のテクニック100選
- 作者: 高林哲,鵜飼文敏,佐藤祐介,浜地慎一郎,首藤一幸
- 出版社/メーカー: オライリー・ジャパン
- 発売日: 2006/11/14
- メディア: 単行本(ソフトカバー)
- 購入: 23人 クリック: 383回
- この商品を含むブログ (223件) を見る