読者です 読者をやめる 読者になる 読者になる

【お知らせ】プログラミング記事の投稿はQiitaに移行しました。

ELFの動的リンク(1)

これから2回に渡って、ELFの動的リンクについて見ていきます。

※ 試験的に文中の図はインラインSVGで描いています。(ソース

ELFファイルの中はセクションとセグメントで二重に分割されています。属性が共通するセクションをグループ化したものがセグメントです。セクションはリンカ、セグメントはローダで処理することを想定したブロックです。

ELFファイルの構造

ファイルの先頭にELFヘッダがあり、その直後にセグメントの構造を示したプログラムヘッダがあります。

readelfコマンドでプログラムヘッダを確認します。ここで分析するバイナリは以下のサンプルプログラムの stest/a.out です。

ELFファイル ELF header program headers .interp .hash .dynsym .dynstr .rel.plt .plt .text .dynamic .got.plt .shstrtab section headers
$ 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セクション

動的リンクに関係する情報が入っています。主に必要なセクションの場所やサイズを示しています。

ELFファイル ELF header program headers .interp .hash .dynsym .dynstr .rel.plt .plt .text .dynamic .got.plt .shstrtab section headers
$ 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も複数あります。それ以外は関係するセクションのアドレスとサイズです。セクションヘッダを見なくても必要なセクションが分かるようになっています。

.hashHASHセクションのアドレス
.dynsymSYMTABセクションのアドレス
SYMENTセクションに含まれるシンボル情報の1エントリのサイズ。セクションのサイズは示されないため、セクションに含まれるシンボル情報の個数は不明。
.dynstrSTRTABセクションのアドレス
STRSZセクションのサイズ
.rel.pltJMPRELセクションのアドレス
PLTRELSZセクションのサイズ
PLTRELセクション内に含まれる再配置情報の種類。RELとRELAのどちらか。
.got.pltPLTGOTセクションのアドレス

動的リンク

.hash .dynsym .dynstr .rel.plt .plt .text .dynamic .got.plt

.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>
実行ファイル .plt 共有ライブラリ .text インタプリタ リンク後 初回呼出時

プログラムから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や自作コードで実験した結果を基に書いています。意味の解釈などは仕様書の定義ではなく、動きから推測したものです。

次回は自作コードを見ながら動的リンク処理を追います。