ld 2.18 の -Bsymbolic オプションを使うと共有ライブラリ内でシンボルをローカルバインドできる
Linux の共有ライブラリの挙動について - daily dayflower の続きです。
下記は Ubuntu Hardy (8.04) i386 で動作確認しました。
ld 2.18 の -Bsymbolic
オプション
昨日書いた通り,Linux で共有ライブラリを利用したプログラムを作る場合,共有ライブラリ内部の関数呼び出し(や変数アクセス)は ld.so を経由して行われます。
このパフォーマンスゲインやシンボル解決の危険性を解決するために ld 2.18 から -Bsymbolic
オプションが新設されました。マニュアルから引用します。
-Bsymbolic
When creating a shared library, bind references to global symbols to the definition within the shared library, if any. Normally, it is possible for a program linked against a shared library to override the definition within the shared library. This option is only meaningful on ELF platforms which support shared libraries.
具体例
昨日のサンプルをそのまま使います。
libplace.so 内部のシンボル名解決を内部解決したいので,libplace.so だけを -Bsymbolic
オプションつきでビルドします。
$ gcc -Wl,-Bsymbolic -shared -fPIC -o libplace.so libplace.c
昨日のコマンドラインとの違いは「-Wl,-Bsymbolic
」が追加されただけです。このように,gcc からリンカ(ld)にオプションを渡すには gcc の「-Wl,
」オプションを使います。
libcond.so では内部のシンボル名解決を行っていないので(今回は)再ビルドの必要はありません。また,main も libcond と libplace を利用するという情報(NEEDED
)がすでに格納されているので再ビルドの必要はありません。
実行します。
$ LD_LIBRARY_PATH=. ./main コンビニは右にあります
おお,「正しい」ではなく「右」になりました。
LD_DEBUG=symbols,bindings
を利用した出力で追ってみましょう。
$ 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=fprintf; lookup in file=./libplace.so [0] ...... snip snip snip ......
シンボル navigate
が libplace.so にバインドされた部分は昨日と同じです。しかし,シンボル get_right
の lookup も binding も発生していません。
このように -Bsymbolic
オプションつきでリンクされた共有ライブラリでは,ライブラリ内部のシンボル解決は(ld.so を経由せず)直接行われます。
隠蔽には使えない
ただし,当たり前ですがライブラリ内部での関数呼び出し(や変数アクセス)が直接的になっただけであり,いままでと同様外部に export された状態になっています(上記の図をみればわかると思いますが)。先ほどの例だと navigate()
だけでなく get_right()
もシンボルテーブルに登録されています。リンカや外部プログラムやライブラリ利用者からは get_right()
が内部でのみ使いたいという意図がわからないので当たり前といえば当たり前ですが。
隠蔽には「Hack#29 ライブラリの外に公開するシンボルを制限する」を利用する必要があります。
適用範囲を関数のみに限定する -Bsymbolic-functions
同じようなオプションとして -Bsymbolic-functions
があります。
-Bsymbolic-functions
When creating a shared library, bind references to global function symbols to the definition within the shared library, if any. This option is only meaningful on ELF platforms which support shared libraries.
こちらは,ライブラリ内部の関数については -Bsymbolic
と同じくローカルバインドしますが,変数については通常と同じく ld.so 経由の動的リンクを行います。
実例として,libplace.c と libcond.c を下記のように書き換えました。
/* libplace.c: */ #include <stdio.h> const char *str_right = "右"; const char * get_right(void) { return str_right; } void navigate(void) { fprintf(stdout, "コンビニは%sにあります\n", get_right()); }
/* libcond.c: */ const char *str_right = "正しい"; const char * get_right(void) { return str_right; }
一度 str_right
という非 static
変数を経由しています。
これを先ほどと同じく -Bsymbolic
でビルド・実行すると
$ gcc -Wl,-Bsymbolic -shared -fPIC -o libplace.so libplace.c $ gcc -Wl,-Bsymbolic -shared -fPIC -o libcond.so libcond.c $ LD_LIBRARY_PATH=. ./main コンビニは右にあります
問題なく動きます。libplace::navigate()
→ libplace::get_right()
→ libplace::str_right
という順で参照がすすんでいるためです。
こんどは -Bsymbolic-functions
でビルド・実行してみます。
$ gcc -Wl,-Bsymbolic-functions -shared -fPIC -o libplace.so libplace.c $ gcc -Wl,-Bsymbolic-functions -shared -fPIC -o libcond.so libcond.c $ LD_LIBRARY_PATH=. ./main コンビニは正しいにあります
おかしくなりました。libplace::navigate()
→ libplace::get_right()
→ libcond::str_right
という参照になってしまったためです。
残念ながらこのようなオプションが用意された理由はわかりません。errno
みたいなケースを想定しているのかな?
ちなみに ld 2.18 では -Bsymbolic
と -Bsymbolic-functions
を併用した場合,-Bsymbolic-functions
のみ適用されますので注意が必要です。
ファイルの構造はどう違う?(上級編)
これらのオプションを変えてリンクしなおした共有ライブラリのサイズを見てみます。
$ ls -l libplace.*.so -rwxr-xr-x 5644 libplace.std.so -rwxr-xr-x 5620 libplace.symall.so -rwxr-xr-x 5612 libplace.symfunc.so
結構違いますね。では,readelf
コマンドで構造がどう違うのか見てみたいと思います。
バイナリ構造で一番大きく違うのは,リロケーション(再配置)情報です。
まずは通常の共有ライブラリから。
$ readelf -r libplace.std.so Relocation section '.rel.dyn' at offset 0x314 contains 8 entries: Offset Info Type Sym.Value Sym. Name 00001688 00000008 R_386_RELATIVE 0000168c 00000008 R_386_RELATIVE 00001690 00000008 R_386_RELATIVE 00001658 00000106 R_386_GLOB_DAT 00000000 __gmon_start__ 0000165c 00000206 R_386_GLOB_DAT 00000000 _Jv_RegisterClasses 00001660 00000b06 R_386_GLOB_DAT 00001690 str_right 00001664 00000406 R_386_GLOB_DAT 00000000 stdout 00001668 00000506 R_386_GLOB_DAT 00000000 __cxa_finalize Relocation section '.rel.plt' at offset 0x354 contains 4 entries: Offset Info Type Sym.Value Sym. Name 00001678 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__ 0000167c 00000307 R_386_JUMP_SLOT 00000000 fprintf 00001680 00000507 R_386_JUMP_SLOT 00000000 __cxa_finalize 00001684 00000807 R_386_JUMP_SLOT 0000049c get_right
いろいろ不審なシンボルがありますが,str_right
と get_right
というシンボルつきのエントリが存在することがわかります。str_right
の Sym.Value
の値(1690
)の Offset
に R_386_RELATIVE
Type なエントリーがあるのが一応チェックポイントかも。
これに対し,-Bsymbolic
つきでビルドしたものは下記のようになります。
$ readelf -r libplace.symall.so Relocation section '.rel.dyn' at offset 0x314 contains 8 entries: Offset Info Type Sym.Value Sym. Name 00001648 00000008 R_386_RELATIVE 0000166c 00000008 R_386_RELATIVE 00001670 00000008 R_386_RELATIVE 00001674 00000008 R_386_RELATIVE 00001640 00000106 R_386_GLOB_DAT 00000000 __gmon_start__ 00001644 00000206 R_386_GLOB_DAT 00000000 _Jv_RegisterClasses 0000164c 00000406 R_386_GLOB_DAT 00000000 stdout 00001650 00000506 R_386_GLOB_DAT 00000000 __cxa_finalize Relocation section '.rel.plt' at offset 0x354 contains 3 entries: Offset Info Type Sym.Value Sym. Name 00001660 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__ 00001664 00000307 R_386_JUMP_SLOT 00000000 fprintf 00001668 00000507 R_386_JUMP_SLOT 00000000 __cxa_finalize
R_386_GLOB_DAT
な str_right
と R_386_JUMP_SLOT
な get_right
が消えました。R_386_RELATIVE
なエントリの数は変わっていません。
-Bsymbolic-functions
つきの場合は下記のようになります。
$ readelf -r libplace.symfunc.so Relocation section '.rel.dyn' at offset 0x314 contains 8 entries: Offset Info Type Sym.Value Sym. Name 00001664 00000008 R_386_RELATIVE 00001668 00000008 R_386_RELATIVE 0000166c 00000008 R_386_RELATIVE 00001638 00000106 R_386_GLOB_DAT 00000000 __gmon_start__ 0000163c 00000206 R_386_GLOB_DAT 00000000 _Jv_RegisterClasses 00001640 00000b06 R_386_GLOB_DAT 0000166c str_right 00001644 00000406 R_386_GLOB_DAT 00000000 stdout 00001648 00000506 R_386_GLOB_DAT 00000000 __cxa_finalize Relocation section '.rel.plt' at offset 0x354 contains 3 entries: Offset Info Type Sym.Value Sym. Name 00001658 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__ 0000165c 00000307 R_386_JUMP_SLOT 00000000 fprintf 00001660 00000507 R_386_JUMP_SLOT 00000000 __cxa_finalize
変数用?セクションの .rel.dyn
は通常版とかわらず str_right
が生きていますが,関数用セクションから get_right
のエントリが消えています。
ほかにここでは詳しく紹介しませんが,-Bsymbolic
オプションの場合のみ(つまり変数シンボルも内部バインディングする場合のみ),ダイナミックセクションに SYMBOLIC
というエントリが出現するという違いがあります。
メカニズムの推測(上級編)
「Hack#58 プログラムが main()
にたどりつくまで」の「_GLOBAL_OFFSET_TABLE_
の再配置」ないし「Hack#72 オブジェクトファイルを自力でロードする」を参考に,objdump -Sr
を駆使して推測したメカニズムが以下になります。あくまで ld 2.18 時点での話です。
以下擬似コードで示します(厳密さに欠けますが,わかりやすくするため一部処理をはしょったりしています)。
libplace.c を -fPIC
でコンパイルした時点のオブジェクトは以下のようになっています。
section ".text" { function get_right() { ... } function navigate() { #pragma relocate(GOT) var pfunc = GOT['get_right']; #pragma relocate(get_right@PLT) call get_right@PLT(pfunc); } }
GOT だの PLT だのよくわからない用語がでてきてますが,GOT は関数ポインタのテーブル,PLT は動的リンクのために一度経由する関数を置く場所と考えてください。
ともかく,このオブジェクト単体では GOT や PLT の実体がないので,リンク時に値を埋め込むことになります。それを #pragma relocate
で擬似的に示しました。
このオブジェクトを通常の gcc -shared
で共有ライブラリとしてビルドすると,下記のようになります。
section ".text" { function get_right() { ... } function navigate() { var pfunc = GOT['get_right']; call get_right@PLT(pfunc); } } section ".plt" { function get_right@PLT(pfunc) { call *pfunc; } function resolve@PLT(label) { var func = LD.SO.resolve(label); GOT[label] = func; call *func; } } section ".got" { GOT['get_right'] = resolve@PLT('get_right'); }
うわ,いきなり複雑なコードになってしまいました。ノリで読解してほしいのですが,
GOT['get_right']
には,ld.so 経由でget_right
を resolve するための関数ポインタがあらかじめ入っている- はじめて
get_right@PLT(pfunc)
を実行したときに,get_right
を resolve して,GOT['get_right']
に代入する - 以降
get_right@PLT(pfunc)
を呼び出すと,resolve されてキャッシュされたget_right
を呼び出すことができる
みたいな構造になっています。結構複雑ですね(実際はもっと複雑ですが)。
これが,-Bsymbolic
つきでビルドすると,以下のようになります。
section ".text" { function get_right() { } function navigate() { var pfunc = GOT['get_right']; call get_right(); } }
おお,とてもシンプル。モジュール内の get_right()
を直接呼び出すコードに変わっています。
var pfunc
を取り出しているのは無駄ですが,これらの作業はリンカが行っているだけであり,リンカができるのはリロケーション(すなわち数値の書き換え)や結合くらいです。コードを削るということはできないので上記のようになります。
なお,関数については上記のように直接呼び出す形態にかわっていますが,変数については通常版と同じく一度 GOT を経由する形のようです。おそらく,GOT['str_right']
に元々 &str_right
が入っている(ようにリンクする)んじゃないかなぁと思いますが,不明です。
注意すること
(あとで書くかも)
まとめ
- ld の
-Bsymbolic
オプションを使うとライブラリ内部のシンボル解決が ld.so を経由しないローカルなものになります。- ライブラリの堅牢性が増します。
- パフォーマンスもやや向上します。
-Bsymbolic-functions
オプションを使うと,関数名に限ってシンボルのローカルバインディングが行われます。-Bsymbolic-functions
の利用には注意が必要です。- (現状)
-Bsymbolic
と-Bsymbolic-functions
を併用してはいけません。 -Bsymbolic
オプション等は,ライブラリ作成者が利用するべき(プライベートな)オプションです。ライブラリ利用者にオプションの使用を義務付けてはいけません。