これから2回に渡って、ELFの動的リンクについて見ていきます。
※ 試験的に文中の図はインラインSVGで描いています。(ソース)
ELFファイルの中はセクションとセグメントで二重に分割されています。属性が共通するセクションをグループ化したものがセグメントです。セクションはリンカ、セグメントはローダで処理することを想定したブロックです。
ELFファイルの構造
ファイルの先頭にELFヘッダがあり、その直後にセグメントの構造を示したプログラムヘッダがあります。
readelfコマンドでプログラムヘッダを確認します。ここで分析するバイナリは以下のサンプルプログラムの stest/a.out です。
$ readelf -l a.out (略) Program Headers: Type Offset (略) MemSiz Flg Align PHDR 0x000034 .... 0x000a0 R E 0x4 INTERP 0x0000d4 .... 0x00013 R 0x1 [Requesting program interpreter: (略)] LOAD 0x000000 .... 0x001d5 R E 0x1000 LOAD 0x0001d8 .... 0x00098 RW 0x1000 DYNAMIC 0x0001d8 .... 0x00088 RW 0x4 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .hash .dynsym .dynstr .rel.plt .plt .text 03 .dynamic .got.plt 04 .dynamic
プログラムヘッダで Type=LOAD と示されているのがセグメントです。図では太い赤線で囲っています。ヘッダやセクションなどを含んでいます。セグメントに含まれているセクションについてはreadelfコマンドでも"Section to Segment mapping"として表示されています。
セクションヘッダは一番後ろにあり、その直前にセクション名を格納している.shstrtabがあります。ローダはセクションヘッダを使わないため、.shstrtab以降はロードしません。
プログラムヘッダにはセグメントの他に、ローダにとって重要な情報も示しています。図では黄色で塗りつぶしています。INTERPやDYNAMICはセクションとして独立していますが、セクションヘッダを見なくてもこれらの情報にアクセスできるようにするため、プログラムヘッダからたどれるようになっています。
セクションの役割
.textと.shstrtab以外は動的リンクに関係したセクションです。
.interp | 動的リンクを実際に処理するインタプリタ |
.hash | シンボル名のハッシュテーブル |
.dynsym | シンボルテーブル |
.dynstr | シンボル名の文字列テーブル |
.rel.plt | 動的リンクのために書き替えが必要なアドレスのリスト。アドレスとシンボルをペアにして関連付けている。 |
.plt | 動的リンクされた関数などを.got.pltからアドレスを取得して呼び出すコード。リンカが自動生成する定型的なコード。 |
.text | C言語などで書いたプログラムをコンパイルしたコード。 |
.dynamic | 動的リンクに必要な情報を集めたテーブル(次で説明) |
.got.plt | 動的リンクされた関数などのアドレステーブル。ここをインタプリタで書き替えることにより、動的リンクを実現する。 |
.shstrtab | セクション名の文字列テーブル |
.dynamicセクション
動的リンクに関係する情報が入っています。主に必要なセクションの場所やサイズを示しています。
$ readelf -d a.out Dynamic section at offset 0x1d8 contains 12 entries: Tag Type Name/Value 0x0001 (NEEDED) Shared library: [libc.so] 0x0004 (HASH) 0xe8 0x0005 (STRTAB) 0x160 0x0006 (SYMTAB) 0x110 0x000a (STRSZ) 49 (bytes) 0x000b (SYMENT) 16 (bytes) 0x0015 (DEBUG) 0x0 0x0003 (PLTGOT) 0x1260 0x0002 (PLTRELSZ) 8 (bytes) 0x0014 (PLTREL) REL 0x0017 (JMPREL) 0x194 0x0000 (NULL) 0x0
NEEDEDは動的リンクに必要なライブラリの名前を示します。複数のライブラリがリンクされていれば、NEEDEDも複数あります。それ以外は関係するセクションのアドレスとサイズです。セクションヘッダを見なくても必要なセクションが分かるようになっています。
.hash | HASH | セクションのアドレス |
.dynsym | SYMTAB | セクションのアドレス |
SYMENT | セクションに含まれるシンボル情報の1エントリのサイズ。セクションのサイズは示されないため、セクションに含まれるシンボル情報の個数は不明。 | |
.dynstr | STRTAB | セクションのアドレス |
STRSZ | セクションのサイズ | |
.rel.plt | JMPREL | セクションのアドレス |
PLTRELSZ | セクションのサイズ | |
PLTREL | セクション内に含まれる再配置情報の種類。RELとRELAのどちらか。 | |
.got.plt | PLTGOT | セクションのアドレス |
動的リンク
.rel.plt(JMPREL)には動的リンクのために書き替えるアドレス(.got.plt内)とシンボル(.dynsym内)をペアにしたリストがあります。これをたどって.got.pltを書き換えることで、動的リンクが実現できます。
.rel.pltを起点とした参照関係を右図に示します。
※ 図を単純化するため、無関係なヘッダやセクションは省略しています。
プログラム実行時には書き替えられた.got.pltを参照して.textから共有ライブラリを呼び出します。この流れを緑の矢印で示しました。
.rel.pltから読み取れる情報をreadelfで表示します。
$ readelf -r a.out Relocation section '.rel.plt' at offset 0x194 contains 1 entries: Offset Info Type Sym.Value Sym. Name 0000126c 00000107 R_386_JUMP_SLOT 00000000 putchar
Offsetは.got.pltを指しています。.pltに含まれるコードが.got.pltからアドレスを読み取ってジャンプすることで、共有ライブラリ内の関数を呼び出しています。このコードはリンカにより生成されたコードです。
$ objdump -d -M intel -j .plt a.out (略) Disassembly of section .plt: 0000019c <putchar@plt-0x10>: 19c: ff b3 04 00 00 00 push DWORD PTR [ebx+0x4] 1a2: ff a3 08 00 00 00 jmp DWORD PTR [ebx+0x8] 1a8: 00 00 add BYTE PTR [eax],al ... 000001ac <putchar@plt>: 1ac: ff a3 0c 00 00 00 jmp DWORD PTR [ebx+0xc] 1b2: 68 00 00 00 00 push 0x0 1b7: e9 e0 ff ff ff jmp 19c <putchar@plt-0x10>
プログラムからputcharを呼ぶと、共有ライブラリの関数ではなく、いったん.plt内のputchar@pltが呼ばれます。位置独立コードではebxは.got.pltのアドレスを指しており、その中でputcharに割り当てられているアドレスを読み取ってジャンプします。
19cや1b2のコードは無駄に見えますが、これは遅延リンクに使われます。遅延リンクはプログラムのロード時に共有ライブラリへのリンクを処理するのではなく、共有ライブラリ内の関数が初めて呼ばれたときにリンクを処理する仕組みです。
[ebx+0xc]のデフォルト値は1b2で、.got.pltが書き換えられていなければそのまま戻ってきます。スタックに0と[ebx+4]を積んで[ebx+8]に飛びます。0は.rel.pltのオフセット、[ebx+4]はインタプリタ(動的リンカ)により設定されたパラメータ、[ebx+8]はインタプリタ内の動的リンクを処理するコードのアドレスです。[ebx+4]と[ebx+8]は事前にインタプリタにより値をセットしておく必要があります。
参考
今回の記事はELFの仕様書を見ながら、readelfや自作コードで実験した結果を基に書いています。意味の解釈などは仕様書の定義ではなく、動きから推測したものです。
次回は自作コードを見ながら動的リンク処理を追います。