Presentation is loading. Please wait.

Presentation is loading. Please wait.

1 組込みエンジニアのための Linux 入門 ダイナミックリンク編 (2) 2007.7.12 株式会社アプリックス 小林哲之.

Similar presentations


Presentation on theme: "1 組込みエンジニアのための Linux 入門 ダイナミックリンク編 (2) 2007.7.12 株式会社アプリックス 小林哲之."— Presentation transcript:

1 1 組込みエンジニアのための Linux 入門 ダイナミックリンク編 (2) 2007.7.12 株式会社アプリックス 小林哲之

2 2 このスライドの対象とする方 今までずっと組込み機器のプロジェ クトに携わってきて最近は OS に Linux を使っている方々

3 3 このスライドの目的 Linux で使用されているダイナミック リンクの仕組みを理解し現在のプロ ジェクトに役立てる。 – 仕組みを知らなくてもプログラムは動 くが、トラブルに対処したり性能を引 き出すためには仕組みの理解が重要。

4 4 今日のお題 ダイナミックリンクライブラリの関数呼 び出しの実際 – 裏でリンカ、ローダはどんなことをしてくれ ているのか? ダイナミックリンクライブラリを作って 動かしてみる –PIC, シンボルの visibility 残念ながら prelink は次回以降で。

5 5 ダイナミックリンクライブラ リの 関数呼び出しの実際

6 6 ダイナミックリンクライブラリ main() program libraries リンク時に配置されるアドレスが決まっている - 1 プロセスにひとつしかないので固定アドレスでよい。 リンク時に配置されるアドレスが決まっていない。 - 1 プロセスに複数のライブラリが使われる。 - ライブラリ同士が重ならないように配置しなければ ならない。 ロード時に配置されるアドレスが決まる。

7 7 実際のコードを見てみる main hello2 puts programlibc hello1 #include void hello2(char *msg) { puts(msg);} void hello1(char *msg) { hello2(msg);} int main() { hello1("Hello, world"); return 0;}

8 8 ダイナミックリンクライブラリの 関数呼び出し hello1hello2 puts programlibc ? リンク時にはアドレス が決まっていない。 どうやって呼び出す? main

9 9 ダイナミックリンクライブラリの 関数呼び出し hello1hello2 puts programlibc puts@plt GOT リンカが生成 ジャンプテーブル経由でライブラリの関数を呼び出す。 ローダが 値を入れる PLT: Procedure Linkage Table GOT: Global Offset Table main

10 10 hello1 から hello2 の呼び出し (i386) 08048354 : 8048354:55 push %ebp 8048355:89 e5 mov %esp,%ebp 8048357:83 ec 08 sub $0x8,%esp 804835a:8b 45 08 mov 0x8(%ebp),%eax 804835d:89 04 24 mov %eax,(%esp) 8048360:e8 2f ff ff ff call 8048294 8048365:c9 leave 8048366:c3 ret 08048367 : 8048367:55 push %ebp 8048368:89 e5 mov %esp,%ebp 804836a:83 ec 08 sub $0x8,%esp 804836d:8b 45 08 mov 0x8(%ebp),%eax 8048370:89 04 24 mov %eax,(%esp) 8048373:e8 dc ff ff ff call 8048354 8048378:c9 leave 8048379:c3 ret

11 11 08048294 : 8048294:ff 25 88 95 04 08 jmp *0x8049588 804829a:68 10 00 00 00 push $0x10 804829f:e9 c0 ff ff ff jmp 8048264 hello2 から puts の呼び出し (i386) 08048354 :... 8048360:e8 2f ff ff ff call 8048294 08049588: libc の puts.got 間接ジャンプ

12 12 hello1 から hello2 の呼び出し (arm) 0000837c : 837c:e52de004 strlr, [sp, #-4]! 8380:e24dd004 subsp, sp, #4; 0x4 8384:ebffffc4 bl829c 8388:e28dd004 addsp, sp, #4; 0x4 838c:e8bd8000 ldmiasp!, {pc} 00008390 : 8390:e52de004 strlr, [sp, #-4]! 8394:e24dd004 subsp, sp, #4; 0x4 8398:ebfffff7 bl837c 839c:e28dd004 addsp, sp, #4; 0x4 83a0:e8bd8000 ldmiasp!, {pc}

13 13 0000837c :... 8384:ebffffc4 bl829c 829c:e28fc600 addip, pc, #0; 0x0 82a0:e28cca08 addip, ip, #32768; 0x8000 82a4:e5bcf2bc ldrpc, [ip, #700]! hello2 から puts の呼び出し (arm) 10560: libc の puts.got ip = (0x829c + 8) + 0x8000 + 700 = 0x10560 pc = *(ip)

14 14 Lazy binding ( 遅延バインディング ) ローダが GOT の値の設定するが、その方法に2 つの選択肢 – 起動時に全て設定 ( 環境変数 LD_BIND_NOW=1 としたときの動作 ) – 実行時に最初に使用された時に設定 (= lazy binding) ( デフォルトの動作 ) 関数の参照の初期値にはローダ内にある参照を解決するため の 関数 ( = __dl_runtime_resolve) がセットされている。 最初に使用したときにその関数が呼ばれて、 GOT に 解決されたアドレスが書き込まれる。 変数の参照は起動時に全て解決される。

15 15 08048294 : 8048294:ff 25 88 95 04 08 jmp *0x8049588 804829a:68 10 00 00 00 push $0x10 804829f:e9 c0 ff ff ff jmp 8048264 Lazy binding の実際 (i386) 08048354 :... 8048360:e8 2f ff ff ff call 8048294 08049588:.got 初期値として 0x0804829a( = puts@plt + 6) が入っている plt を経由してローダの __dl_runtime_resolve へ puts の参照を解決して ここを書き換え puts にジャンプ puts@plt を表す識別子

16 16 00008288 : 8288:e52de004 strlr, [sp, #-4]! 828c:e59fe004 ldrlr, [pc, #4]; 8298 8290:e08fe00e addlr, pc, lr 8294:e5bef008 ldrpc, [lr, #8]! 8298:000082bc.word0x000082bc 0000837c :... 8384:ebffffc4 bl829c 829c:e28fc600 addip, pc, #0; 0x0 82a0:e28cca08 addip, ip, #32768; 0x8000 82a4:e5bcf2bc ldrpc, [ip, #700]! Lazy binding の実際 (arm) 1055c: 10560:.got ip = (0x829c + 8) + 0x8000 + 700 = 0x10560 pc = *(ip) lr = 0x000082bc + (0x8290 + 8) + 8 = 0x1055c pc = *(lr) ローダの __dl_runtime_resolve

17 17 ダイナミックリンクのためのサイ ズの増加 関数のエントリひとつごとに (arm) –plt スタブ 3 命令 12 バイト –GOT 1 エントリ 4 バイト – シンボルの文字列 平均 10 バイトくらい? – その他... ? ダイナミックリンクライブラリの利点に比べれば 細かいことだが、把握しておいたほうがいいかも

18 18 PLT, GOT をどうしてもどうしても 節約したい場合 ライブラリ関数へのポインタを取得して、 それ経由で呼び出せばよい。 #include int (*puts_addr)(const char*); void hello1(char* msg) { puts_addr(msg); } int main() { puts_addr = puts; hello1("hello, world"); return 0; } ただしこう書いても puts_addr に得られるアドレスは libc の puts でなくて puts@plt なので意味が無い。

19 19 dlsym を利用してアドレスを得 る $ cat p2.c #include int (*puts_addr)(const char*); void hello1(char* msg) { puts_addr(msg); } int main() { puts_addr = dlsym(RTLD_DEFAULT, "puts"); hello1("hello, world"); return 0; } $ cc -D_GNU_SOURCE p2.c -ldl $ libc のように必ずロードされているとわかっているライブラリでは dlopen しなくても、 RTLD_DEFAULT という定義済みのハンドラが 使用できる。 (dlclose を心配しなくても済む。 )

20 20 自作ライブラリの場合なら 関数ポインタ ( のストラクチャ ) へのポイン タを返す関数を用意する。 int func_a() int func_b() int func_c() static int func_a() static int func_b() static int func_c() FUNCS* get_funcs() typedef struct { int (*func_a)(); int (*func_b)(); int (*func_c)(); } FUNCS; APIs single API

21 21 まとめ ダイナミックリンクライブラリの関数呼 び出しと プログラム内の関数呼び出しでは、 C の ソース上ではほとんど変わらないが、実 際の動作ではダイナミックリンクライブ ラリの呼び出しは複雑。 複雑な仕組みはリンカとローダが隠蔽し てくれている。

22 22 ダイナミックリンクライブラ リの 作って動かしてみる

23 23 ダイナミックリンクライブラリの 作り方 リンク時に –shared をつける。 [ 必須 ] コンパイル時に –fpic をつけて、 Position Independent Code にする。 [ 推奨 ] ( 後述 ) –-fpic と -fPIC の違いはドキュメント参照 ライブラリの名前は lib ~.so とする。 gcc4.0 以降の場合は 適切な visibility を指定する。 ( 後述 )

24 24 簡単な実例 $ make cc -fpic -c -o hello1.o hello1.c cc -fpic -c -o hello2.o hello2.c cc -shared -o libhello.so hello1.o hello2.o -lc $ file libhello.so libhello.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), not stripped $

25 25 ダイナミックリンクライブラリの インストールの方法 暫定的に使う場合 – 環境変数 LD_LIBRARY_PATH に設定する。 – コロン ( : ) で区切って複数のライブラリを指定できる。 恒久的なインストール –/lib または /usr/lib に置くか、 /etc/ld.so.conf に 登録されているディレクトリに置くか、ライブラリ の あるディレクトリを /etc/ld.so.conf に登録する。 – その後、ルート権限で /sbin/ldconfig コマンドを実行 する。 – ライブラリの情報が /etc/ld.so.cache にキャッシュさ れる ので、 LD_LIBRARY_PATH を使うより起動時間が短 縮できる。

26 26 PIC (Position Independent Code) 絶対アドレスによる参照の代わりに、プログラ ムカウンタからの相対アドレスを使用する。 ロードするアドレスが変わってもコードを書き 換える必要が無い。 –i386 ではプログラムカウンタ相対のアドレッシング モードがないので汚いコードになる。 PC を取得するために call 命令を使ってスタックに PC を書き 出させるなど。 – 組込み系で使用するプロセッサでも数命令多く必要 な場合がある。 コードサイズが少し大きくなり、その分だけ実 行時間も増える。

27 27 PIC/ 非 PIC のコードの比較 extern int gvar; static int svar; void setGvar(int var) { gvar = var; } void setSvar(int var) { svar = var; } グローバル変数、スタティック変数への書き込み

28 28 グローバル変数への書き込み (arm) setGvar: ldr r3,.L3.LPIC0: add r3, pc, r3 ldr r2,.L3+4 ldr r3, [r3, r2] str r0, [r3, #0] bx lr.L4:.align 2.L3:.word _GLOBAL_OFFSET_TABLE_ -(.LPIC0+8).word gvar(GOT) PIC setGvar: ldr r3,.L3 str r0, [r3, #0] bx lr.L4:.align 2.L3:.word gvar Non PIC 3 命令 + 1 ワード増加 ( 計 16 バイト ) 変数 gvar の絶対アドレス GLOBAL_OFFSET_TABLE gvar(GOT) gvar

29 29 static 変数への書き込み (arm) setSvar: ldr r3,.L7.LPIC1: add r3, pc, r3 ldr r2,.L7+4 str r0, [r3, r2] bx lr.L8:.align 2.L7:.word _GLOBAL_OFFSET_TABLE_ -(.LPIC1+8).word.LANCHOR0(GOTOFF) setSvar: ldr r3,.L7 str r0, [r3, #0] bx lr.L8:.align 2.L7:.word.LANCHOR0 Non PICPIC 2 命令 + 1 ワード増加 ( 計 12 バイト ) グローバル変数のときより 1 命令少ないのは GOT にオフセットでなく変数そのものが格納されるため static 変数 svar の絶対アドレス GLOBAL_OFFSET_TABLE gvar(GOT) gvar

30 30 グローバル関数の呼び出し (arm) call_gfunc: str lr, [sp, #-4]! sub sp, sp, #4 bl gfunc add sp, sp, #4 ldmfd sp!, {pc} call_gfunc: str lr, [sp, #-4]! sub sp, sp, #4 bl gfunc(PLT) add sp, sp, #4 ldmfd sp!, {pc} extern int gfunc(); void call_gfunc() { gfunc(); } Non PIC PIC gfunc の直接呼出し ( ローダが gfunc のアドレスが 確定した後にこの bl 命令のオフセットを 書き換える。 ) PLT スタブ経由

31 31 Non PIC のコードの問題点 setGvar: ldr r3,.L3 str r0, [r3, #0] bx lr.L4:.align 2.L3:.word gvar Non PIC 変数 gvar の絶対アドレス ダイナミックリンクライブラリの場合は ロードした後に絶対アドレスが決まる。 ローダが解決した値をここに書き込む ( ローダの負荷が大きく起動時間増大 ) コード領域に書き込みが発生する コード領域のそのページは 共有できない (dirty で private なページになる。) 物理メモリ使用量増大 PIC ならば実行され ないコード領域は 物理メモリにロード されない。

32 32 シンボルの visibility デフォルトでは全ての static でない関数、変数は ライブラリの外部からの参照が可能になってい る。 → そのため、 PLT スタブ経由でのアクセスに なっている → オーバーヘッド多め デフォルトの設定を「ライブラリ外から参照不 可」にしてインタフェース関数のみを明示的に 外部から参照を許可するほうがよい。 シンボルの visibility の設定は gcc 4.0 以降で可能。

33 33 libhello hellohello1 puts libhellolibc hello2 $ cat hello1.c void hello1() { hello2();} void hello() { hello1();} $ cat hello2.c #include void hello2(){ puts("Hello, world\n");} $ make cc -fpic -c -o hello1.o hello1.c cc -fpic -c -o hello2.o hello2.c cc -shared -o libhello.so hello1.o hello2.o -lc $

34 34 実際には hellohello1 puts libhellolibc hello2 puts @plt hello2 @plt hello1 @plt hello1, hello2 の呼び出しはライブラリ内に閉じているのにも かかわらず PLT スタブ経由の呼び出しになってしまう。 (static 関数にすれば直接コールされるが同じソースファイルにある必要がある... )

35 35 余談 hellohello1 puts libhello1 libc hello2 puts@plt hello2 @plt hello1 @plt hello1 同一の関数名 hello1 を持つライブラリ libhello1 を作って 環境変数 LD_PRELOAD を使って検索パスの前に置くと この図のように、 libhello を変更せずに関数 hello1 を置き換える ことができる。 “BINARY HACKS” hack #60 LD_PRELOAD で共有ライブラリを差し替える libhello

36 36 visibility を設定すると hellohello1 puts libhellolibc hello2 puts@plt hello だけ公開し、それ以外を非公開にする $cat hello1.c #define EXPORT __attribute__((visibility ("default"))) void hello1() { hello2();} EXPORT void hello(){ hello1();} $ make cc -fpic -fvisibility=hidden -c -o hello1.o hello1.c cc -fpic -fvisibility=hidden -c -o hello2.o hello2.c cc -shared -o libhello.so hello1.o hello2.o -lc $ 余分な PLT が節約できた。

37 37 まとめ ダイナミックリンクのためには -fpic をつけ てコンパイルするのが定跡。 -fpic をつけないほうがコードはすっきりし ているが、起動時にローダに負担がかか り、コード領域が他のプロセスと共有で きなくなる。 gcc4.0 以降ならばシンボルの visibility を適 切にコントロールすることで実行時シン ボル解決のオーバーヘッドを低減できる。

38 38 参考文献 “How To Write Shared Libraries” http://people.redhat.com/drepper/dsohowto.pdf “Linkers & Loaders” オーム社 “BINARY HACKS” オライリージャパン “GNU Development Tools” Wataru Nishida GNU C ライブラリのソース http://www.gnu.org/software/libc/ http://www.gnu.org/software/libc/ その他たくさんの WEB 検索結果


Download ppt "1 組込みエンジニアのための Linux 入門 ダイナミックリンク編 (2) 2007.7.12 株式会社アプリックス 小林哲之."

Similar presentations


Ads by Google