ELF/i386 の再配置情報
ELFでi386なときに使われる再配置情報は以下のとおりです(elf.hより).
/* i386 relocs. */ #define R_386_NONE 0 /* No reloc */ #define R_386_32 1 /* Direct 32 bit */ S+A #define R_386_PC32 2 /* PC relative 32 bit */ S+A-P #define R_386_GOT32 3 /* 32 bit GOT entry */ G+A-P #define R_386_PLT32 4 /* 32 bit PLT address */ L+A-P #define R_386_COPY 5 /* Copy symbol at runtime */ #define R_386_GLOB_DAT 6 /* Create GOT entry */ S #define R_386_JMP_SLOT 7 /* Create PLT entry */ S #define R_386_RELATIVE 8 /* Adjust by program base */ B+A #define R_386_GOTOFF 9 /* 32 bit offset to GOT */ S+A-GOT #define R_386_GOTPC 10 /* 32 bit PC relative offset to GOT */ GOT+A-P
S: 再配置するシンボルのアドレス
P: 再配置する領域のアドレス
GOT: GOTのアドレス
B: オブジェクトファイルがロードされた位置
L: PLTのエントリのアドレス
A: addend
ほんとはelf.hには8bitに対する再配置のR_386_8とかTLS用の再配置情報とかもっとたくさんあるんですがとりあえず基本はこれのようなので上記のそれぞれついて実際どのような場合に生じるのか調べてみます(SystemVのx86のABI(www.sco.com/developers/devspecs/abi386-4.pdf)では上の11個の再配置情報が定義されています.bfd/elf32-i386を見るとGNU elf extensionsと書いてあるのでどうやら拡張らしいですけどよく詳しいことわかってません.sunで使われてる?).
この辺りの話は環境依存ですが実行環境はUbuntu10.4LTS(linux 2.6.32),gcc 4.4.3です.
参考:
・http://www.amazon.co.jp/%E3%83%AA%E3%83%B3%E3%82%AB%E3%83%BB%E3%83%AD%E3%83%BC%E3%83%80%E5%AE%9F%E8%B7%B5%E9%96%8B%E7%99%BA%E3%83%86%E3%82%AF%E3%83%8B%E3%83%83%E3%82%AF%E2%80%95%E5%AE%9F%E8%A1%8C%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AB%E5%BF%85%E9%A0%88%E3%81%AE%E6%8A%80%E8%A1%93-COMPUTER-TECHNOLOGY-%E5%9D%82%E4%BA%95-%E5%BC%98%E4%BA%AE/dp/4789838072
・http://netwinder.osuosl.org/users/p/patb/public_html/elf_relocs.html
・http://www.acsu.buffalo.edu/~charngda/elf.html
・http://wiki.osdev.info/?ELF%2F%BC%C2%B9%D4%BB%FE%A4%CE%CF%C3%2F%A5%D7%A5%ED%A5%BB%A5%C3%A5%B5%A4%CB%B0%CD%C2%B8%A4%B9%A4%EB%CF%C3%2Fi386
・const char* const p = "ABC"; と const char q[] = "ABC"; はどちらがよいか、みたいな与太 - memologue
R_386_32 / R_386_PC
通常の再配置です.R_386_32は32bitの絶対値,R_386_PCは32bitのPC相対値です.これらは通常 -c フラグ付きでコンパイルしたオブジェクトファイルの断片内に存在します.具体的にはR_386_32は大域変数や文字列定数を参照する場面,R_386_PCは関数を呼び出す場面等で利用されます.
例:a.c
int a; int f(){ return; } int main(){ a = 1; f(); }
$ gcc -c -o a.o a.c $ objdump -r -d a.o a.o: file format elf32-i386 Disassembly of section .text: 00000000 <f>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 5d pop %ebp 4: c3 ret 00000005 <main>: 5: 55 push %ebp 6: 89 e5 mov %esp,%ebp 8: c7 05 00 00 00 00 01 movl $0x1,0x0 f: 00 00 00 a: R_386_32 a 12: e8 fc ff ff ff call 13 <main+0xe> 13: R_386_PC32 f 17: 5d pop %ebp 18: c3 ret
0xfの位置で大域変数aに1を代入していますが大域変数aは(ローカル変数のようにスタックではなく).bssセクションに配置されるためまだアドレスが決まっていません.そのため再配置が必要になります.また,0x12の位置で関数fを呼んでいますがcall命令はcall命令の次の命令からの相対アドレスを要求するためのPC相対な再配置が必要になります.ちなみに,call命令の引数とし既に0xfffffffc(-4)が代入されていいますが,これは次の命令からの相対アドレスにするためのオフセットです(リンカは0x13の位置から関数fの相対アドレスを求め,それに-4を加えた値が格納されます).
これらの再配置情報はリンクをおこない実行ファイルを生成するときに解決されます.
$ gcc -o a a.o $ objdump -r a a: file format elf32-i386 # 再配置情報がないので何も表示されない $ objdump -d -r a ... 080483b4 <f>: 80483b4: 55 push %ebp 80483b5: 89 e5 mov %esp,%ebp 80483b7: 5d pop %ebp 80483b8: c3 ret 080483b9 <main>: 80483b9: 55 push %ebp 80483ba: 89 e5 mov %esp,%ebp 80483bc: c7 05 18 a0 04 08 01 movl $0x1,0x804a018 80483c3: 00 00 00 80483c6: e8 e9 ff ff ff call 80483b4 <f> # 80483cb+ffffffe9(-23) = 80383b4 80483cb: 5d pop %ebp 80483cc: c3 ret ...
i386では共有ライブラリを使用しない場合使用する再配置情報はこの2つになります.
だいぶシンプルですね.
ところでこの場合f()はmain()と同じ場所で定義されてるのでリンク前に相対アドレスなら既に判明してるのでは..と思うけどどうなんでしょう.
実際関数f()をstaticで定義するとR_386_PC382は生成されません.
$ cat a.c int a; static int f(){ return; } int main(){ a = 1; f(); } $ gcc -c -o a.o a.c $ objdump -d -r a.o a.o: file format elf32-i386 Disassembly of section .text: 00000000 <f>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 5d pop %ebp 4: c3 ret 00000005 <main>: 5: 55 push %ebp 6: 89 e5 mov %esp,%ebp 8: c7 05 00 00 00 00 01 movl $0x1,0x0 f: 00 00 00 a: R_386_32 a 12: e8 e9 ff ff ff call 0 <f> 17: 5d pop %ebp 18: c3 ret
staticをつけるとリンク時に関数の相対位置が変更されないのが保証されるということ?
以下の話は共有ライブラリを使用する場合の話になります.
R_386_GOT32 / R_386_GOTPC / R_386_GOTOFF / R_386_PLT32
これら4つの再配置情報は共有ライブラリになるもののオブジェクトファイルの断片内に存在します.もう少し分かりやすく言えば gcc -c -fPIC でコンパイルしたときのオブジェクトファイル内に存在します(まぁ実際にはPICにしなくても共有ライブラリは作成できるわけですけどそうすると共有ライブラリロード時にテキスト領域の再配置が必要となって共有ライブラリという名前なのに共有できないみたいな自体が発生するので普通はしないはずです).これらの再配置情報を理解するためにはGOTとPLTについて理解する必要があります.
共有ライブラリは仮想記憶を利用してライブラリのテキスト領域(コード領域)を共有する仕組みですが,共有ライブラリはそれを利用するプログラムのどのアドレスに割り当てられるのかがプログラムを実行時でないと決まりません.そのため,共有ライブラリ内のテキスト領域では絶対アドレスを使用することができません.そこで使われるのがGOTとPLTです.GOTとPLTはそれぞれ共有ライブラリ内の変数および関数を参照する場合に使われます.変数にアクセスする場合にはまずGOTのその変数に対応する箇所にアクセスします.その場所に目的の変数のアドレスが入っています.それを間接参照することで変数cにアクセスします.このGOTが格納する変数のアドレスはライブラリがロードされるときに決定します(後述のR_GLOB_DAT参照).
例を見た方が分かりやすいと思うのでまずR_386_GOT32およびR_386_GOTPCを使用するプログラムを見てみます.
b.c
int c; int g(){ return c; }
$ gcc -o b.o -fPIC b.c $ objdump -d -r b.o b.o: file format elf32-i386 Disassembly of section .text: 00000000 <g>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: e8 fc ff ff ff call 4 <g+0x4> 4: R_386_PC32 __i686.get_pc_thunk.cx // call __i686.get_pc_thunk.cx 8: 81 c1 02 00 00 00 add $0x2,%ecx a: R_386_GOTPC _GLOBAL_OFFSET_TABLE_ // get GOT address e: 8b 81 00 00 00 00 mov 0x0(%ecx),%eax 10: R_386_GOT32 c // access c via GOT 14: 8b 00 mov (%eax),%eax 16: 5d pop %ebp 17: c3 ret Disassembly of section .text.__i686.get_pc_thunk.cx: 00000000 <__i686.get_pc_thunk.cx>: // get pc 0: 8b 0c 24 mov (%esp),%ecx // equavalant to movl %eip,%ecx 3: c3 ret
-fPICオプションをつけてコンパイルすることでgccに位置独立コードを生成するようにさせます.これによりリンクして共有ライブラリを生成したときにテキスト領域に関しては再配置がいらないオブジェクトファイルが生成されます.
さて,共有ライブラリ内で大域変数にアクセスするためにはGOTを経由してアクセスします.まず0x3行目で__i686.get_pc_thunk.cxを呼び出しますがこれはスタックの先頭(関数の戻り先アドレス)を%ecxにいれるだけです.これにより%ecxにcallの次の命令のアドレスが%ecxに入ります.この部分は通常の関数呼び出しなので再配置情報はR_386_PC32です.そして次に%ecxに値を足しますが,ここで足すのは_GLOBAL_OFFSET_TABLE_への相対アドレスです.これにより_GLOBAL_OFFSET_TABLE_の絶対アドレスが%ecxに入ることになります.このようにR_386_GOTPCは_GLOBAL_OFFSET_TABLE_の相対アドレスを求めるために利用されます.そしてさらにそのアドレス(_GLOBAL_OFFSET_TABLE_の先頭)に大域変数cのエントリへのオフセットを加えます.このために使われるのがR_386_GOT32です.これにより%ecxには大域変数cに対応する_GLOBAL_OFFSET_TABLE_の絶対アドレスが入ったことになります.そして,このアドレスの位置に実際の変数cのアドレスが入っているのでそれを間接参照することでcの値を取り出します(14行目).
_GLOBAL_OFFSET_TABLE_はリンク時に作成されるので,このときにこれらの再配置情報を解決します.逆にいえばR_386_GOT32はリンカに対してGOTを作れと指示するようなものなんだと思います.このR_386_GOT32からリンク時に後述のR_386_GLOB_DATが生成されるようです.
▼ここだと_GLOBAL_OFFSET_TABLE_の値は.gotセクションの中央と書いてありますが自分の環境だと_GLOBAL_OFFSET_TABLEの値は.got.pltセクションの先頭でした.
.gotセクションの先頭ではないです(変数に対応するエントリへのアクセスは負のオフセット,PLTに対応するエントリへのアクセスは正のオフセットで指定しているということ.gotセクション先頭を指すよりこっちのが分かりやすいから?)
▼R_386_GOT32は資料だとG+A-PとなっていますがこれってG+Aなんじゃ…?
▼もしもGOTの対応するエントリからの相対アドレスを計算する再配置(GOT+G+A-P)があれば一つ命令の数を減らせるような..GOTを経由するアクセスが連続しておこなわれるときはこうして一つのレジスタにGOTのアドレスいれとけばアクセスしやすいということ?後述のR_386_GOTOFFと同じような処理にするためにこうしてる?
次はR_386_PLT32です.これは共有ライブラリ内で,共有ライブラリの他の関数を呼ぶときに使われます.
(staticな関数を呼ぶときは通常の相対アドレスでの呼び出しになります)
例:c.c
int l(){ return 0; } int g(){ return l(); }
$ gcc -c -fPIC -o c.o c.c $ objdump -r -d c.o c.o: file format elf32-i386 Disassembly of section .text: 00000000 <l>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: b8 00 00 00 00 mov $0x0,%eax 8: 5d pop %ebp 9: c3 ret 0000000a <g>: a: 55 push %ebp b: 89 e5 mov %esp,%ebp d: 53 push %ebx e: 83 ec 04 sub $0x4,%esp 11: e8 fc ff ff ff call 12 <g+0x8> 12: R_386_PC32 __i686.get_pc_thunk.bx 16: 81 c3 02 00 00 00 add $0x2,%ebx 18: R_386_GOTPC _GLOBAL_OFFSET_TABLE_ 1c: e8 fc ff ff ff call 1d <g+0x13> 1d: R_386_PLT32 l // call l via plt 21: 83 c4 04 add $0x4,%esp 24: 5b pop %ebx 25: 5d pop %ebp 26: c3 ret Disassembly of section .text.__i686.get_pc_thunk.bx: 00000000 <__i686.get_pc_thunk.bx>: 0: 8b 1c 24 mov (%esp),%ebx 3: c3 ret
16行目で_GLOBAL_OFFSET_TABLE_の絶対アドレスを取得しているのは先ほどと同じです.ここで_GLOBAL_OFFSET_TABLE_のアドレスを求めているのはこの次に呼ぶPLTの関数でその値を使用するためです.1c行目が関数l()の呼び出しです.ここで,R_386_PLT32となっているのでl()を直接呼び出すのではなく
PLT経由で呼び出しています(PLT自体はリンク時に生成され,R_386_PLT32の位置にその生成したPLTへの相対アドレスが格納されます).これは後述する遅延バインディングをおこなうためです.
実際これをコンパイルすると関数gは以下のようになります.
00000494 <g>: 494: 55 push %ebp 495: 89 e5 mov %esp,%ebp 497: 53 push %ebx 498: 83 ec 04 sub $0x4,%esp 49b: e8 d7 ff ff ff call 477 <__i686.get_pc_thunk.bx> 4a0: 81 c3 54 1b 00 00 add $0x1b54,%ebx 4a6: e8 f1 fe ff ff call 39c <l@plt> 4ab: 83 c4 04 add $0x4,%esp 4ae: 5b pop %ebx 4af: 5d pop %ebp 4b0: c3 ret
ここでl()を直接呼び出すかわりにlの対応するPLTを呼び出します.PLTの内容は次のようになっています.
0000039c <l@plt>: 39c: ff a3 10 00 00 00 jmp *0x10(%ebx) 3a2: 68 08 00 00 00 push $0x8 3a7: e9 d0 ff ff ff jmp 37c <_init+0x30>
%ebxには直前の操作によって_GLOBAL_OFFSET_TABLE_の値が入っています.つまりこの最初の部分では
GOTのlに対応するエントリにジャンプするわけです.さて,_GLOBAL_OFFSET_TABLE_の値は
計算してみると0x1b54+0x4a0=0x1ff4で,objdumpで見ると次のようになっています.
00001ff4 <.got.plt>: 1ff4: 1c 1f sbb $0x1f,%al ... 1ffe: 00 00 add %al,(%eax) 2000: 92 xchg %eax,%edx 2001: 03 00 add (%eax),%eax 2003: 00 a2 03 00 00 b2 add %ah,-0x4dfffffd(%edx) 2009: 03 00 add (%eax),%eax
l@pltからのジャンプ先は0x10(%ebx)なので,0x1ff4+0x10=0x2004番地目に格納されているアドレスということになります.よく見るとこの値は0x03a2となっていて,つまりl@pltの2行目です.なんでこんな意味の分からないことをしているかというとこれは共有ライブラリの関数の遅延バインディングをおこなうためで,この後l@pltの3行目で呼ばれる関数によって先ほどのGOTの対応する箇所(0x2004番地)に関数l()の番地が書き込まれ,その上で関数l()が呼ばれます.これによって2回目以降の呼び出しはGOTを参照すればすぐに呼べるようになるわけです.多くのライブラリ関数を使うようになると初めのロード時に全ての呼び出し番地を解決していたら時間がかかるためこのように必要になってときのみアドレス解決をおこなうようになっています.ただし,遅延バインディングといってもGOTのエントリの初期値はPLTの絶対アドレスを持つ必要があるため,共有ライブラリロード時にGOTのエントリに対しライブラリがロードされたアドレスが加算されます(この場合何か他のシンボルのアドレスを参照したりするわけではなくただベースアドレスを加算するだけなので高速に処理できるようです).
ちなみに,共有ライブラリではなく共有ライブラリを利用する側のプログラムではpltは少し違います.
例えばこのプログラム
hello.c
#include <stdio.h> int main(){ printf("hello\n"); }
をコンパイルしてみます.
$ gcc -o hello.o -c hello.c $ objdump -rd hello.o hello.o: file format elf32-i386 Disassembly of section .text: 00000000 <main>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 e4 f0 and $0xfffffff0,%esp 6: 83 ec 10 sub $0x10,%esp 9: c7 04 24 00 00 00 00 movl $0x0,(%esp) c: R_386_32 .rodata 10: e8 fc ff ff ff call 11 <main+0x11> 11: R_386_PC32 puts 15: c9 leave 16: c3 ret
この状態ではprinf()は共有ライブラリ関数なのかどうかも分からない未解決の状態です
これをリンクすると,次のようになります.
$ gcc -o hello hello.o $ objdump -D hello 08048318 <puts@plt>: 8048318: ff 25 08 a0 04 08 jmp *0x804a008 804831e: 68 10 00 00 00 push $0x10 8048323: e9 c0 ff ff ff jmp 80482e8 <_init+0x30> ... 080483e4 <main>: 80483e4: 55 push %ebp 80483e5: 89 e5 mov %esp,%ebp 80483e7: 83 e4 f0 and $0xfffffff0,%esp 80483ea: 83 ec 10 sub $0x10,%esp 80483ed: c7 04 24 c0 84 04 08 movl $0x80484c0,(%esp) 80483f4: e8 1f ff ff ff call 8048318 <puts@plt> 80483f9: c9 leave 80483fa: c3 ret ... 08049ff4 <_GLOBAL_OFFSET_TABLE_>: 8049ff4: 20 9f 04 08 00 00 and %bl,0x804(%edi) 8049ffa: 00 00 add %al,(%eax) 8049ffc: 00 00 add %al,(%eax) 8049ffe: 00 00 add %al,(%eax) 804a000: fe 82 04 08 0e 83 incb -0x7cf1f7fc(%edx) 804a006: 04 08 add $0x8,%al 804a008: 1e push %ds 804a009: 83 .byte 0x83 804a00a: 04 08 add $0x8,%al ...
最適化によってprintfがputsになっていますが,mainの804834のところでputs@pltが呼ばれています.
puts@pltでは共有ライブラリの場合異なり,絶対アドレスで_GLOBAL_OFFSET_TABLE_内のエントリにジャンプしています.実行形式の場合ロードされるアドレスは既にこの時点で確定しているので共有ライブラリのようにわざわざ相対的にアクセスする必要がないわけです.逆にいうとR_386_PLT32の役割というのはリンカに対して_GLOBAL_OFFSET_TABLE_内の対応するエントリに相対的にジャンプするようなPLTのコードを生成せよと伝える役目があるのだと思います(たぶん
というか最初の例は-fPICでコンパイルしてるからPLTもPICになってると解釈する方がいいのかも.
▼わざわざPLTでGOTを経由することなく直接PLT内を再配置した方が間接ジャンプする必要も余分なGOTエントリを作る必要もなく,実行時のコストもプログラムの容量的にも優れてるんじゃ..と思いますがこれはプログラムコードであるPLTを実行可能かつ読み込み可能としてロードするためだと思います.また,こう
しておけばPLTも複数のプログラムから共有できるようになります.PLTを直接再配置するアーキテクチャもあるようです.
次はR_386_GOTOFFです.
これは共有ライブラリ内で文字列定数やstatic変数を参照するときに使います.文字列定数やstatic変数はライブラリ内で使用するもので外部に公開するわけではないのでGOTを経由する必要はありませんがどこにロードされるのか分からないためシンボルを参照するときは再配置が必要になります.
d.c
static int c = 3; int g(){ return c; }
$ gcc -c -fPIC -o d.o d.c $ objdump -d -r d.o d.o: file format elf32-i386 Disassembly of section .text: 00000000 <g>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: e8 fc ff ff ff call 4 <g+0x4> 4: R_386_PC32 __i686.get_pc_thunk.cx 8: 81 c1 02 00 00 00 add $0x2,%ecx a: R_386_GOTPC _GLOBAL_OFFSET_TABLE_ e: 8b 81 00 00 00 00 mov 0x0(%ecx),%eax 10: R_386_GOTOFF .data 14: 5d pop %ebp 15: c3 ret Disassembly of section .text.__i686.get_pc_thunk.cx: 00000000 <__i686.get_pc_thunk.cx>: 0: 8b 0c 24 mov (%esp),%ecx 3: c3 ret
一見R_386_GOT32の例と似ていますが,R_386_GOT32と違い間接参照せず直接変数cの値を求めています.R_386_GOTOFFにはシンボルのGOTからのオフセットが入っているので上の例のようにすれば変数にアクセスできます.(0xeの位置ので%eaxにcの値が入る.この例だとcをreturnにしてるだけなので%eaxに入れたら終わり).
▼これって自分の位置からデータまでの相対アドレスじゃだめなの..?
R_386_JMP_SLOT
これは共有ライブラリの関数を呼び出す実行ファイル内に存在します.共有ライブラリが他の共有ライブラリの関数を呼ぶ場合にはその共有ライブラリ内にも存在します(その場合はR_386_PLT32によって作られたPLTと対応します).
例えばある共有ライブラリ(ここではlibhoge.soとする)を利用する次の関数を考えます.
e.c
int main(){ f(); // 共有ライブラリの関数fを呼び出す return 0; }
これを次のようにコンパイルすると,
$ gcc -o e -lhoge
再配置情報は,
e: file format elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE ... 0804a008 R_386_JUMP_SLOT f
こんな感じで具体的に0x0804a008の箇所を見てみると
$ objdump -D e ... Disassembly of section .got.plt: 08049ff4 <_GLOBAL_OFFSET_TABLE_>: 8049ff4: 18 9f 04 08 00 00 sbb %bl,0x804(%edi) 8049ffa: 00 00 add %al,(%eax) 8049ffc: 00 00 add %al,(%eax) 8049ffe: 00 00 add %al,(%eax) 804a000: de 83 04 08 ee 83 fiadd -0x7c11f7fc(%ebx) 804a006: 04 08 add $0x8,%al 804a008: fe .byte 0xfe 804a009: 83 .byte 0x83 804a00a: 04 08 add $0x8,%al ...
と,_GLOBAL_OFFSET_TABLE_の一部をさしています.つまりこの位置が関数fに対応するGOTのエントリというわけです.実際この初期値は0x080483feで,この位置をobjdumpで調べてみると,
080483f8 <f@plt>: 80483f8: ff 25 08 a0 04 08 jmp *0x804a008 80483fe: 68 10 00 00 00 push $0x10 8048403: e9 c0 ff ff ff jmp 80483c8 <_init+0x30>
と,関数fに対応するPLT内を指しています.関数f()の1回目の呼び出しはこのPLTを経由しておこなわれ,このときにこのR_386_JUMP_SLOTに対応するアドレスが書き変わるので2回目以降はPLTを経由することなしにf()が呼ばれるというわけです.本当にこうなってるのかはgdbで簡単に確認できます(関数呼び出し前と後で対応するGOTのエントリを見るだけ).
R_386_GLOB_DAT / R_368_COPY
この2つは対のようなもので,R_386_GLOB_DATは共有ライブラリが,ライブラリ内の大域変数を参照するために用いられ,R_386_COPYはライブラリを利用するプログラムがライブラリ内の大域変数を使用するために用いられます.
foo.c
int a = 3; int f(){ return a; }
bar.c
extern int a; int main(){ a += 1; return 0; }
こんなそれぞれこんな感じにコンパイルします
$ gcc -fPIC -shared -o libfoo.so.1 foo.c $ gcc -fPIC -shared -o libfoo.so.1 -Wl,-soname=libfoo.so.1 foo.c $ ln -s libfoo.so.1 libfoo.so $ gcc -L ./ -lfoo -o bar bar.c
ここでまずbarの再配置情報を見てみます.
$ objdump -R bar
bar: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ff0 R_386_GLOB_DAT __gmon_start__
0804a010 R_386_COPY a
0804a000 R_386_JUMP_SLOT __gmon_start__
0804a004 R_386_JUMP_SLOT __libc_start_main
変数aに対する再配置情報がR_386_COPYとなっています.対応する位置0x804a010はどこかというと,これは実は.bssセクションになります.
$ readelf -S bar There are 30 section headers, starting at offset 0x1124: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al ... [25] .bss NOBITS 0804a010 001010 00000c 00 WA 0 0 4 ...
barは実行時にこの.bssセクションにaの値をコピーします.そしてそれ以降はそのコピーしたaに対して操作をおこなうわけです.この時点ではまだbarにはaの実態がないので初期化しない大域変数と同じような扱いをするわけです.ということで逆にライブラリ関数側でaを操作する場合にはbarの.bssセクション内の変数を操作する必要があります.これを可能にするのがR_386_GLOB_DATです.libfoo.soの再配置情報を見てみると(ここまできてreadelf使った方が情報多いことに気づいた..),
$ readelf -r libfoo.so Relocation section '.rel.dyn' at offset 0x2fc contains 5 entries: Offset Info Type Sym.Value Sym. Name 00002008 00000008 R_386_RELATIVE 00001fe4 00000106 R_386_GLOB_DAT 00000000 __gmon_start__ 00001fe8 00000206 R_386_GLOB_DAT 00000000 _Jv_RegisterClasses 00001fec 00000706 R_386_GLOB_DAT 0000200c a 00001ff0 00000306 R_386_GLOB_DAT 00000000 __cxa_finalize Relocation section '.rel.plt' at offset 0x324 contains 2 entries: Offset Info Type Sym.Value Sym. Name 00002000 00000107 R_386_JUMP_SLOT 00000000 __gmon_start__ 00002004 00000307 R_386_JUMP_SLOT 00000000 __cxa_finalize
fの中身を見てみると,
0000045c <f>: 45c: 55 push %ebp 45d: 89 e5 mov %esp,%ebp 45f: e8 10 00 00 00 call 474 <__i686.get_pc_thunk.cx> 464: 81 c1 90 1b 00 00 add $0x1b90,%ecx 46a: 8b 81 f8 ff ff ff mov -0x8(%ecx),%eax 470: 8b 00 mov (%eax),%eax 472: 5d pop %ebp 473: c3 ret
45fから46aでGOTのエントリにアクセスしています.そのアドレスは0x464+0x1b90-0x8=1efcとなっていてさっき調べた変数aに対応するR_386_GLOB_DATに対応しています.この場所をライブラリロード時に
書き換えることによってbarの.bss領域のaにアクセスするわけです.ちなみに変数に対しては遅延バインディングのようなものはありません.コピーされるaのデータは?というとこれはちゃんとlibfoo.soの.dataセクションに入っています.上のreadelfのaのところを見るとSym.Valueが200cになっていますがこれがaの値が格納されている箇所のアドレスです.
Disassembly of section .data: ... 0000200c <a>: 200c: 03 00 add (%eax),%eax ...
と,ここまで書いて本当にそうなってるのか疑問になったのでgdbで確かめてみます.(実行する際には共有ライブラリを使用するので export LD_LIBRARY_PATH="./" などとする必要があります)
$ gdb bar ... (gdb) b main Breakpoint 1 at 0x80484b7 (gdb) r Starting program: /home/m/tmp/bar Breakpoint 1, 0x080484b7 in main () (gdb) maintenance info sections Exec file: `/home/m/tmp/bar', file type elf32-i386. ... 0x804a010->0x804a01c at 0x00001010: .bss ALLOC ...
maintenanc info sectionsとするとセクションの配置されているアドレスが分かります..bssのアドレス
は当然先ほど調べたものと同じ0x804a010番地です.この番地を調べてみると,
(gdb) x/10wx 0x804a010 0x804a010 <a>: 0x00000003 0x00000000 0x00000000 0x00000000 0x804a020: 0x00000000 0x00000000 0x00000000 0x00000000 0x804a030: 0x00000000 0x00000000
と,無事に3がコピーされています.さて,次はlibfoo.soを調べてみます.disassemble fとすればfoo.c内の関数f()が表示されます.
(gdb) disassemble f Dump of assembler code for function f: 0x0012e45c <+0>: push %ebp 0x0012e45d <+1>: mov %esp,%ebp 0x0012e45f <+3>: call 0x12e474 <__i686.get_pc_thunk.cx> 0x0012e464 <+8>: add $0x1b90,%ecx 0x0012e46a <+14>: mov -0x8(%ecx),%eax 0x0012e470 <+20>: mov (%eax),%eax 0x0012e472 <+22>: pop %ebp 0x0012e473 <+23>: ret End of assembler dump.
ここで0x0012e470のところで変数aにアクセスしているので,このときの%eaxの値が0x804a010ならばいいわけです.%eaxに入っている値が格納されている番地は,0x0012e464+0x1b90-0x8 = 0x12ffcなので,この番地を見てみます.
(gdb) p/x 0x0012e464+0x1b90-0x8 $1 = 0x12ffec (gdb) x/10wx 0x0012e464+0x1b90-0x8 0x12ffec: 0x0804a010 0x00160500 0x00001f14 0xb7fff8a8 0x12fffc: 0x00123270 0x0012e37a 0x0012e38a 0x00130008 0x13000c <a>: 0x00000003 0x00000000
ちゃんと0x12ffcには0x0804a010が入ってました.
共有ライブラリで完全に共有されるのはテキスト領域のみで,static変数おかれたりする.data領域等はコピーオンライトでプロセスごとに作成されます.この部分も共有ライブラリロード時にそういう風な処理になってるんだと思います
R_386_RELATIVE
これは共有ライブラリとしてリンクが終わったオブジェクトファイル内に存在し,共有ライブラリ内でシンボルのアドレスを参照するときなどに使われます(正確にいうと-sharedをつけてコンパイルしたものの中.PICかどうかは関係ない.たぶん).具体的にいうと共有ライブラリ内でポインタが文字列定数のアドレスを指す場合などです.
例:e.c
#include <stdio.h> char *hello = "hello"; void f(){ printf("%s\n",hello); }
e.o: file format elf32-i386 RELOCATION RECORDS FOR [.text]: OFFSET TYPE VALUE 00000008 R_386_PC32 __i686.get_pc_thunk.bx 0000000e R_386_GOTPC _GLOBAL_OFFSET_TABLE_ 00000014 R_386_GOT32 hello 0000001e R_386_PLT32 puts RELOCATION RECORDS FOR [.data.rel.local]: OFFSET TYPE VALUE 00000000 R_386_32 .rodata
コンパイルしただけでは再配置はR_386_32ですが,これを-sharedでリンクして共有ライブラリを作ると,
$ gcc -shared -o libe.so.1 -Wl,-soname=libe.so.1 e.o $ objdump -R libe.so.1 e: file format elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 0000200c R_386_RELATIVE *ABS* 00002010 R_386_RELATIVE *ABS* 00001fe4 R_386_GLOB_DAT __gmon_start__ 00001fe8 R_386_GLOB_DAT _Jv_RegisterClasses 00001fec R_386_GLOB_DAT hello 00001ff0 R_386_GLOB_DAT __cxa_finalize 00002000 R_386_JUMP_SLOT __gmon_start__ 00002004 R_386_JUMP_SLOT puts 00002008 R_386_JUMP_SLOT __cxa_finalize
このようにR_386_RElATIVEが生成されています.__gmon_start__とかはリンク時にくっつもので,これはプロファイラなどに使われるようですがここでは無視します.ここで重要なのは上から2番のR_386_RELATIVEです.OFFSETが2010となっていますが,これは実は.dataセクションを指しており,この中に"hello"を指すアドレスが入っています.
具体的に確かめてみると,
$ readelf -S libe.so.1 There are 28 section headers, starting at offset 0x1114: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al .... [22] .data PROGBITS 0000200c 00100c 000008 00 WA 0 0 4 ...
このように0x200cから.dataセクションです.
objdumpでも確認してみると,
$ objdump -D libe.so.1 Disassembly of section .data: ... 00002010 <hello>: 2010: 24 05 and $0x5,%al 2010: R_386_RELATIVE *ABS* ...
と,初期値として0x0524が入っています(リトルエンディアン).同じくobjdumpの出力で
0x524番地を見てみると,
Disassembly of section .rodata: 00000524 <.rodata>: 524: 68 65 6c 6c 6f push $0x6f6c6c65 # 文字列hello
となっています.ということで変数helloの値はとりあえず0x0524なわけですが共有ライブラリロード時に
この値にライブラリがロードされた位置を足すことで実際のアドレスになるというわけです.ちなみに,ポインタではなく文字列をそのまま引数にした場合(つまり,printf("%s\n","hello!");とした場合)は再配置情報は通常ならR_386_32,PICならR_386_GOTOFFとなり.rodata内の文字列を直接指すようになります.char hello[] = "hello" のように配列にした場合も同様で直接.rodataを指します.さらに補足すると,グローバルな変数や関数のアドレスを参照する場合は普通にR_386_32です.
例:
f.c
int x = 3; int *px = &x;
これを次のようにコンパイルしてreadelfしてみると,
$ gcc -shared -fPIC -o f f.c $ redelf -r v Relocation section '.rel.dyn' at offset 0x2f0 contains 5 entries: Offset Info Type Sym.Value Sym. Name ... 00002010 00000401 R_386_32 0000200c x ...
となります..dataセクションを見ると,
Disassembly of section .data: ... 0000200c <x>: 200c: 03 00 add (%eax),%eax ... 00002010 <px>: 2010: 00 00 add %al,(%eax) ...
となっていて,0x2010番地のpxに0x200c番地のxのアドレスを格納することを指示する再配置情報になっています.
ということで長々と再配置について調べてきましたがこうしてみてみると共有ライブラリまわりのところは
本当にうまくできてるなと思います.まだちょっと理解の足らないとこがありますがのでこれ以上はソースを読まないと..
▼Linkers and Loadersもじっくり読んでみるかな-