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_rightget_right というシンボルつきのエントリが存在することがわかります。str_rightSym.Value の値(1690)の OffsetR_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_DATstr_rightR_386_JUMP_SLOTget_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 オプション等は,ライブラリ作成者が利用するべき(プライベートな)オプションです。ライブラリ利用者にオプションの使用を義務付けてはいけません。