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もじっくり読んでみるかな-