この記事は、
Resolving ELF Relocation Name / Symbols
の翻訳です。認めてくれたLeaf SRの人ありがとう!訳はがんばりましたが、間違ってる所もあるかもしれません。そこはご了承ください…。
共有オブジェクトのELFファイル内関数へのcall命令(たとえば、puts関数の呼び出しとか)は、直接関数のアドレスへ飛ぶのではなくて、PLT(Procedure Linkage Table)に飛ぶよ。PLTを使うと、関数の実際のアドレスを実行時に解決することができる。言い換えると、共有オブジェクトが実際にどこにロードされるかは実行時にならないと分からないので、PLTを使って実際のアドレスを解決している。
次のELFのサンプルを見てみよう:
ここに、ELFのテキスト・セグメント1内にある、アドレス0x804833cへの呼び出し命令がある:
$ objdump -d example-elf | grep 804833c | grep call
804843f: e8 f8 fe ff ff call 804833c
それで、次がこのサンプルファイルのPLTだ。さっきの命令で呼んでたアドレス0x804833cに注意してね。ここには実際には単なる*0x8049684へのジャンプ命令が並んでいる。
$ objdump -d example-elf | grep “section .plt:” -A 31
Disassembly of section .plt: 080482fc <__gmon_start__@plt-0x10>: 80482fc: ff 35 70 96 04 08 pushl 0x8049670 8048302: ff 25 74 96 04 08 jmp *0x8049674 8048308: 00 00 add %al,(%eax) ... 0804830c <__gmon_start__@plt>: 804830c: ff 25 78 96 04 08 jmp *0x8049678 8048312: 68 00 00 00 00 push $0x0 8048317: e9 e0 ff ff ff jmp 80482fc <_init+0x18> 0804831c <__libc_start_main@plt>: 804831c: ff 25 7c 96 04 08 jmp *0x804967c 8048322: 68 08 00 00 00 push $0x8 8048327: e9 d0 ff ff ff jmp 80482fc <_init+0x18> 0804832c <__stack_chk_fail@plt>: 804832c: ff 25 80 96 04 08 jmp *0x8049680 8048332: 68 10 00 00 00 push $0x10 8048337: e9 c0 ff ff ff jmp 80482fc <_init+0x18> 0804833c : 804833c: ff 25 84 96 04 08 jmp *0x8049684 8048342: 68 18 00 00 00 push $0x18 8048347: e9 b0 ff ff ff jmp 80482fc <_init+0x18> 0804834c : 804834c: ff 25 88 96 04 08 jmp *0x8049688 8048352: 68 20 00 00 00 push $0x20 8048357: e9 a0 ff ff ff jmp 80482fc <_init+0x18>
(注意:*は、C言語のポインタ参照と同じで、0x8049684に書いてある値、の意味)
*0x8049684が実際には何なのかを知りたければ、Global Offset Table (GOT)を探せばいい。こんな感じだ:
$ objdump -s example-elf | grep got.plt -A3
Contents of section .got.plt:
804966c 98950408 00000000 00000000 12830408 ................
804967c 22830408 32830408 42830408 52830408 "...2...B...R..
0x8049684には、42830408(リトルエンディアンで0x08048342)が書いてあった。ここでPLTに戻ってもう一回0x08048342を見てみると、jmp 命令でこのアドレスに飛んだあと、”push $0x18″という命令を実行することが分かる。0x18=24っていうのは、再配置テーブルへのオフセットアドレスだ。このpush命令に続いて、PLTの最初のアドレスへのジャンプである、”jmp 80482fc”が実行される。
この80482fcに書いてある命令列は、PLTの他の部分と大分ちがっているのはすぐ分かるだろう。最初の2つの命令、”pushl 0x8049670″と”jmp *0x8049674″は結構重要だ。
最初push命令はアドレス0x8049670をスタックにpushしていて、これはGOTを指している。次の(*0x8049674)へのジャンプだけど、このアドレスもGOT内のアドレスだ。この2つのアドレスは、ELFファイルの中ではどちらも”0″が書いてある。
というのも、これらは実行時に動的に埋められるからだ。実行時には、最初のアドレスの値は特定のライブラリが使われているかを識別するための番号になり、次のアドレスにはリンカーのシンボル解決ルーチンのアドレスが入る。これらのルーチンは前にスタックにpushされた0x18を、正しい場所を解決するために使う。
このテクニックは「遅延リンク(lazy linking)」と呼ばれている。再配置された関数のアドレス解決が、実行時に、しかも必要な時に、実際に関数が呼ばれた時だけ行われるからだ。
一度リンカーによって解決されると、リンカーはGOTエントリを書き換えて、最初の jmp *(アドレス)が、それまでのpush $0x18をするコードのアドレスではなく、解決された関数アドレスに直接ジャンプするようになる。
それじゃあ、どうやって関数のアドレスを解決するためのシンボル名を取ってくるんだろう?関数のアドレスを解決するには、’0x8049688’ではなくて、’sprintf’という文字列が必要だ。
SHT_RELというタイプを持っているセクションには、次のような構造体が並んでいる:
typedef struct { Elf32_Addr r_offset; /* Address */ Elf32_Word r_info; /* Relocation type and symbol index */ } Elf32_Rel;
さて、readelfでELFファイル内の再配置エントリを見てみよう2。
$ readelf -r /testbins/sha1
Relocation section '.rel.dyn' at offset 0x420 contains 3 entries: Offset Info Type Sym.Value Sym. Name 0804b54c 00001106 R_386_GLOB_DAT 00000000 __gmon_start__ 0804b598 00000505 R_386_COPY 0804b598 stderr 0804b59c 00000d05 R_386_COPY 0804b59c stdin Relocation section '.rel.plt' at offset 0x438 contains 2 entries: Offset Info Type Sym.Value Sym. Name 0804b55c 00000107 R_386_JUMP_SLOT 00000000 feof 0804b560 00000207 R_386_JUMP_SLOT 00000000 putchar
これらの情報はいったい何処から来たんだろう!?Offset/Info/Type/Value/Nameのそれぞれの情報、さっきの構造体に無かったよね?えっとね、これらの値のうち、殆どは実はr_infoフィールドから直接もってこれるか、r_infoフィールドを間接的に使うともってこれるんだ。
offsetの値はPLTの中を指していて、ここに再配置エントリの実体が置かれる。バイナリを逆アセンブルすると、PLTの中の正しいオフセットにある、このアドレスへjmpする命令が見えるはずだ。
今注目したいのは、r_infoフィールドだ。このフィールドに関係する、2つの(ほんとの事をいうと3つあって、3つ目は1と2を組み合わせると得られる)重要なマクロがelf.hに存在する:
#define ELF32_R_SYM(val) ( (val) >> 8) #define ELF32_R_TYPE(val) ( (val) & 0xff)
これらのマクロはr_infoからそれぞれ別の値を得るためのマクロだ。いっこずつ見てみよう。SHT_RELタイプをもつセクション(さっきからみてる、再配置セクション)は他にsh_linkっていうメンバを持っている。このsh_linkは重要で、これらの再配置情報のシンボル情報(関数名とかね)を持つ、ほかのセクションを指しているんだ。このsh_linkは、gccでは大抵「dynsym」というセクションを指している。
/bin/lsのdynsymを実際に呼んでみた結果がこれ(読みやすくするために、いくつか省略してるよ):
$ readelf -S /bin/ls
There are 26 section headers, starting at offset 0x126d8: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al ... [ 4] .dynsym DYNSYM 080484a0 0004a0 0006b0 10 A 5 1 4 ... [ 8] .rel.dyn REL 08049170 001170 000028 08 A 4 0 4 [ 9] .rel.plt REL 08049198 001198 0002f0 08 A 4 11 4 ...
セクション7と8のsh_linkメンバを見て。それぞれの4が、dynsym(dynamic symbol table)セクションを指している。この第4セクションが、再配置エントリに対応するシンボルネームが書いてあるセクションだ。ここまでやってきたことをまとめておくと、(1)SHT_RELタイプがついてるセクションを探して、(2)sh_linkメンバーの指し示すセクションをたどった。
よし、色々数字があるのは分かったから、あとはそれをどう組み合わせていけばいいんだろう。再配置テーブルのエントリを一個一個見ていって、r_infoフィールドの値を持ってきて、ELF32_R_SYM(val)マクロを使って値を取り出していけばいい。この数字はdynsymセクション(か、sh_linkの指している他のセクション)内のエントリに対応している。で、dynsymテーブルをパースしてエントリを調べれば、シンボルネームが解決できる。
他のマクロは何なんだろう?ELF32_R_TYPE(val)は再配置がどんなタイプなのかを教えてくれる。その値はelf.hにR_386_GOT32とかR_386_JMP_SLOTみたいなのが定義されている。この定義を使うと、再配置テーブルが関数のためのエントリなのかどうかとかが分かる(でも、rel.pltセクションの中のはだいたいそうだ3)。
ただし、ちょっと注意。全てのELFファイルがセクションヘッダを持っているわけじゃない。そういう時は、プログラムヘッダを使うと、動的なセグメントがどこかわかるし、再配置テーブルのアドレスとか、再配置テーブルのシンボルテーブルがどこかとかが分かる。
もしこのエントリに間違いがあったらぜひ教えてね!