puts@plt

組込みエンジニアのためのLinux入門
ダイナミックリンク編(2)
2007.7.12
株式会社アプリックス
小林哲之
1
このスライドの対象とする方
• 今までずっと組込み機器のプロジェクト
に携わってきて最近はOSにLinuxを使っ
ている方々
2
このスライドの目的
• Linuxで使用されているダイナミックリン
クの仕組みを理解し現在のプロジェクト
に役立てる。
– 仕組みを知らなくてもプログラムは動くが、
トラブルに対処したり性能を引き出すため
には仕組みの理解が重要。
3
今日のお題
• ダイナミックリンクライブラリの関数呼び出し
の実際
– 裏でリンカ、ローダはどんなことをしてくれている
のか?
• ダイナミックリンクライブラリを作って動かして
みる
– PIC, シンボルのvisibility
残念ながらprelinkは次回以降で。
4
ダイナミックリンクライブラリの
関数呼び出しの実際
5
ダイナミックリンクライブラリ
program
libraries
main()
リンク時に配置されるアドレスが決まっている
- 1プロセスにひとつしかないので固定アドレスでよい。
リンク時に配置されるアドレスが決まっていない。
- 1プロセスに複数のライブラリが使われる。
- ライブラリ同士が重ならないように配置しなければ
ならない。
ロード時に配置されるアドレスが決まる。
6
実際のコードを見てみる
main
hello1
program
hello2
puts
libc
#include <stdio.h>
void hello2(char *msg) { puts(msg);}
void hello1(char *msg) { hello2(msg);}
int main() {
hello1("Hello, world"); return 0;}
7
ダイナミックリンクライブラリの関数呼
び出し
main
hello1
program
hello2
puts
libc
?
リンク時にはアドレス
が決まっていない。
どうやって呼び出す?
8
ダイナミックリンクライブラリの関数呼
び出し
リンカが生成
main
hello1
hello2
puts@plt
puts
program
GOT
libc
ローダが
値を入れる
ジャンプテーブル経由でライブラリの関数を呼び出す。
PLT: Procedure Linkage Table
GOT: Global Offset Table
9
hello1からhello2の呼び出し(i386)
08048354 <hello2>:
8048354:
55
8048355:
89 e5
8048357:
83 ec
804835a:
8b 45
804835d:
89 04
8048360:
e8 2f
8048365:
c9
8048366:
c3
08048367 <hello1>:
8048367:
55
8048368:
89 e5
804836a:
83 ec
804836d:
8b 45
8048370:
89 04
8048373:
e8 dc
8048378:
c9
8048379:
c3
08
08
24
ff ff ff
push
mov
sub
mov
mov
call
leave
ret
%ebp
%esp,%ebp
$0x8,%esp
0x8(%ebp),%eax
%eax,(%esp)
8048294 <puts@plt>
08
08
24
ff ff ff
push
mov
sub
mov
mov
call
leave
ret
%ebp
%esp,%ebp
$0x8,%esp
0x8(%ebp),%eax
%eax,(%esp)
8048354 <hello2>
10
hello2からputsの呼び出し(i386)
08048294 <puts@plt>:
8048294:
ff 25 88 95 04 08
804829a:
68 10 00 00 00
804829f:
e9 c0 ff ff ff
jmp
push
jmp
*0x8049588 間接ジャンプ
$0x10
8048264 <_init+0x18>
08048354 <hello2>:
...
8048360:
e8 2f ff ff ff
call
8048294 <puts@plt>
.got
08049588:
libcのputs
11
hello1からhello2の呼び出し(arm)
0000837c <hello2>:
837c:
e52de004
8380:
e24dd004
8384:
ebffffc4
8388:
e28dd004
838c:
e8bd8000
str
sub
bl
add
ldmia
lr, [sp, #-4]!
sp, sp, #4
; 0x4
829c <.text-0x30>
sp, sp, #4
; 0x4
sp!, {pc}
00008390 <hello1>:
8390:
e52de004
8394:
e24dd004
8398:
ebfffff7
839c:
e28dd004
83a0:
e8bd8000
str
sub
bl
add
ldmia
lr, [sp, #-4]!
sp, sp, #4
; 0x4
837c <hello2>
sp, sp, #4
; 0x4
sp!, {pc}
12
hello2からputsの呼び出し(arm)
829c:
82a0:
82a4:
e28fc600
e28cca08
e5bcf2bc
add
add
ldr
ip, pc, #0
; 0x0
ip, ip, #32768 ; 0x8000
pc, [ip, #700]!
ip = (0x829c + 8) + 0x8000 + 700 = 0x10560
pc = *(ip)
0000837c <hello2>:
...
8384:
ebffffc4
bl
829c <.text-0x30>
.got
10560:
libcのputs
13
Lazy binding (遅延バインディング)
• ローダがGOTの値の設定するが、その方法に2つ
の選択肢
– 起動時に全て設定
(環境変数LD_BIND_NOW=1としたときの動作)
– 実行時に最初に使用された時に設定(= lazy binding) (デ
フォルトの動作)
• 関数の参照の初期値にはローダ内にある参照を解決するための
関数( = __dl_runtime_resolve)がセットされている。
• 最初に使用したときにその関数が呼ばれて、GOTに
解決されたアドレスが書き込まれる。
• 変数の参照は起動時に全て解決される。
14
Lazy bindingの実際(i386)
puts@pltを表す識別子
08048294 <puts@plt>:
8048294:
ff 25 88 95 04 08
804829a:
68 10 00 00 00
804829f:
e9 c0 ff ff ff
jmp
push
jmp
*0x8049588
$0x10
8048264 <_init+0x18>
08048354 <hello2>:
...
8048360:
e8 2f ff ff ff
call
8048294 <puts@plt>
.got
08049588:
pltを経由してローダの
__dl_runtime_resolve へ
初期値として
0x0804829a( = puts@plt + 6)
が入っている
putsの参照を解決して
ここを書き換え
putsにジャンプ
15
Lazy bindingの実際(arm)
00008288 <.plt>:
8288:
e52de004
828c:
e59fe004
8290:
e08fe00e
8294:
e5bef008
8298:
000082bc
str
ldr
add
ldr
.word
lr, [sp, #-4]!
lr, [pc, #4]
; 8298 <.plt+0x10>
lr, pc, lr
pc, [lr, #8]!
0x000082bc
lr = 0x000082bc + (0x8290 + 8) + 8 = 0x1055c
pc = *(lr)
829c:
82a0:
82a4:
e28fc600
e28cca08
e5bcf2bc
0000837c <hello2>:
...
8384:
ebffffc4
.got
1055c:
10560:
add
add
ldr
ip, pc, #0
; 0x0
ip, ip, #32768 ; 0x8000
pc, [ip, #700]!
ip = (0x829c + 8) + 0x8000 + 700 = 0x10560
pc = *(ip)
bl
829c <.text-0x30>
ローダの__dl_runtime_resolve
16
ダイナミックリンクのためのサイズの
増加
• 関数のエントリひとつごとに (arm)
– pltスタブ 3命令 12バイト
– GOT 1エントリ 4バイト
– シンボルの文字列 平均10バイトくらい?
– その他 ... ?
ダイナミックリンクライブラリの利点に比べれば
細かいことだが、把握しておいたほうがいいかも
17
PLT, GOTをどうしてもどうしても節約
したい場合
• ライブラリ関数へのポインタを取得して、それ
経由で呼び出せばよい。
#include <stdio.h>
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なので意味が無い。
18
dlsymを利用してアドレスを得る
$ cat p2.c
#include <dlfcn.h>
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を心配しなくても済む。)
19
自作ライブラリの場合なら
• 関数ポインタ(のストラクチャ)へのポインタを
返す関数を用意する。
single API
FUNCS* get_funcs()
APIs
int func_a()
static int func_a()
int func_b()
static int func_b()
int func_c()
static int func_c()
typedef struct {
int (*func_a)();
int (*func_b)();
int (*func_c)();
} FUNCS;
20
まとめ
• ダイナミックリンクライブラリの関数呼び出しと
プログラム内の関数呼び出しでは、Cのソー
ス上ではほとんど変わらないが、実際の動作
ではダイナミックリンクライブラリの呼び出し
は複雑。
• 複雑な仕組みはリンカとローダが隠蔽してく
れている。
21
ダイナミックリンクライブラリの
作って動かしてみる
22
ダイナミックリンクライブラリの作り方
• リンク時に –shared をつける。[必須]
• コンパイル時に –fpic をつけて、Position
Independent Code にする。 [推奨] (後述)
– -fpicと-fPICの違いはドキュメント参照
• ライブラリの名前は lib~.so とする。
• gcc4.0以降の場合は
適切なvisibilityを指定する。(後述)
23
簡単な実例
$ 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
$
24
ダイナミックリンクライブラリのインス
トールの方法
• 暫定的に使う場合
– 環境変数 LD_LIBRARY_PATH に設定する。
– コロン(:)で区切って複数のライブラリを指定できる。
• 恒久的なインストール
– /lib または /usr/lib に置くか、 /etc/ld.so.conf に
登録されているディレクトリに置くか、ライブラリの
あるディレクトリを /etc/ld.so.confに登録する。
– その後、ルート権限で /sbin/ldconfig コマンドを実行する。
– ライブラリの情報が /etc/ld.so.cache にキャッシュされる
ので、LD_LIBRARY_PATHを使うより起動時間が短縮で
きる。
25
PIC (Position Independent Code)
• 絶対アドレスによる参照の代わりに、プログラムカウ
ンタからの相対アドレスを使用する。
ロードするアドレスが変わってもコードを書き換える
必要が無い。
– i386ではプログラムカウンタ相対のアドレッシングモード
がないので汚いコードになる。
• PCを取得するためにcall命令を使ってスタックにPCを書き出させ
るなど。
– 組込み系で使用するプロセッサでも数命令多く必要な場
合がある。
• コードサイズが少し大きくなり、その分だけ実行時間
も増える。
26
PIC/非PICのコードの比較
グローバル変数、スタティック変数への書き込み
extern int gvar;
static int svar;
void setGvar(int var) {
gvar = var;
}
void setSvar(int var) {
svar = var;
}
27
グローバル変数への書き込み(arm)
Non PIC
PIC
setGvar:
setGvar:
ldr
str
bx
r3, .L3
r0, [r3, #0]
lr
.align
2
ldr
r3, .L3
add
ldr
ldr
str
bx
r3,
r2,
r3,
r0,
lr
.align
2
.word
_GLOBAL_OFFSET_TABLE_
-(.LPIC0+8)
gvar(GOT)
.LPIC0:
.L4:
.L3:
pc, r3
.L3+4
[r3, r2]
[r3, #0]
.L4:
.word
gvar
.L3:
.word
変数gvarの絶対アドレス
3命令 + 1ワード増加 (計16バイト)
GLOBAL_OFFSET_TABLE
gvar
gvar(GOT)
28
static変数への書き込み(arm)
Non PIC
PIC
setSvar:
setSvar:
ldr
str
bx
r3, .L7
r0, [r3, #0]
lr
.align
2
.L8:
.LANCHOR0
.L7:
.L8:
.L7:
.word
ldr
r3, .L7
add
ldr
str
bx
r3, pc, r3
r2, .L7+4
r0, [r3, r2]
lr
.align
2
.word
_GLOBAL_OFFSET_TABLE_
-(.LPIC1+8)
.LANCHOR0(GOTOFF)
.LPIC1:
.word
2命令 + 1ワード増加 (計12バイト)
GLOBAL_OFFSET_TABLE
static変数svarの絶対アドレス
gvar(GOT)
gvar
グローバル変数のときより1命令少ないのは
29
GOTにオフセットでなく変数そのものが格納されるため
グローバル関数の呼び出し (arm)
extern int gfunc();
void call_gfunc() {
gfunc();
}
Non PIC
PIC
call_gfunc:
str
sub
bl
add
ldmfd
call_gfunc:
str
sub
bl
add
ldmfd
lr, [sp, #-4]!
sp, sp, #4
gfunc
sp, sp, #4
sp!, {pc}
gfuncの直接呼出し
(ローダがgfuncのアドレスが
確定した後にこのbl命令のオフセットを
書き換える。)
lr, [sp, #-4]!
sp, sp, #4
gfunc(PLT)
sp, sp, #4
sp!, {pc}
PLTスタブ経由
30
Non PICのコードの問題点
Non PIC
setGvar:
ldr
str
bx
r3, .L3
r0, [r3, #0]
lr
.align
2
.word
gvar
.L4:
.L3:
コード領域に書き込みが発生する
コード領域のそのページは
共有できない
(dirtyでprivateなページになる。)
物理メモリ使用量増大
変数gvarの絶対アドレス
ダイナミックリンクライブラリの場合は
ロードした後に絶対アドレスが決まる。
ローダが解決した値をここに書き込む
(ローダの負荷が大きく起動時間増大)
PICならば実行されな
いコード領域は
物理メモリにロードさ
れない。
31
シンボルのvisibility
• デフォルトでは全てのstaticでない関数、変数はライ
ブラリの外部からの参照が可能になっている。
→ そのため、PLTスタブ経由でのアクセスになって
いる
→ オーバーヘッド多め
• デフォルトの設定を「ライブラリ外から参照不可」にし
てインタフェース関数のみを明示的に外部から参照
を許可するほうがよい。
• シンボルのvisibilityの設定はgcc 4.0以降で可能。
32
libhello
libhello
hello
libc
hello1
hello2
puts
$ cat hello1.c
void hello1() { hello2();}
void hello() { hello1();}
$ cat hello2.c
#include <stdio.h>
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
$
33
実際には
libhello
hello
libc
hello1
hello1
@plt
hello2
hello2
@plt
puts
puts
@plt
hello1, hello2の呼び出しはライブラリ内に閉じているのにも
かかわらずPLTスタブ経由の呼び出しになってしまう。
(static関数にすれば直接コールされるが同じソースファイルにある必要がある...)
34
余談
libhello1
hello1
libhello
hello
libc
hello1
hello1
@plt
hello2
hello2
@plt
puts
puts@plt
同一の関数名hello1を持つライブラリlibhello1を作って
環境変数LD_PRELOADを使って検索パスの前に置くと
この図のように、libhelloを変更せずに関数hello1を置き換える
ことができる。
“BINARY HACKS” hack #60 LD_PRELOADで共有ライブラリを差し替える
35
visibilityを設定すると
helloだけ公開し、それ以外を非公開にする
libhello
hello
libc
hello1
hello2
余分なPLTが節約できた。
puts
puts@plt
$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
$
36
まとめ
• ダイナミックリンクのためには-fpicをつけてコ
ンパイルするのが定跡。
• -fpicをつけないほうがコードはすっきりしてい
るが、起動時にローダに負担がかかり、コー
ド領域が他のプロセスと共有できなくなる。
• gcc4.0以降ならばシンボルのvisibilityを適切
にコントロールすることで実行時シンボル解
決のオーバーヘッドを低減できる。
37
参考文献
• “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/
• その他たくさんのWEB検索結果
38