オブジェクトファイルについて

since: 2006-11-01 update: 2006-11-14 count:

はじめに

Binary Hacks の校正大会にて、あーセクションの話が少し説明不足で不親切だね、っていう話が出ました。あった方がいいかな、と思ったので、宣伝を兼ねて、ここに私が知っていることを書いておきます。

内容としては、 Binary Hacks に比べてかなりいい加減に書いています。例えば調べものは一切せずに書きます。著者の中で最もいい加減な私が本よりもいい加減に書いたということで、 Binary Hacks の全ての文章はこれよりはレベルが上、というようなサンプルだと思って下さい。宣伝を兼ねるということで、これ単体ではフォローせずに Binary Hacks のここを見てね、というポインタだけ示す部分が多いです。『』で囲まれた文字列は Binary Hacks の中のハック名に対応しています。

書いてる最中なので、気が向いたら内容を追加します。

詳しい参考文献としては Linkers&Loaders が良いでしょう。手元に無いので確認できませんが、この文章と内容が結構かぶる予定です。そもそも Binary Hacks 自体ともかなりかぶっています。具体的には『ELF入門』や『プログラムが main() にたどりつくまで』と強くかぶりますが、それぞれとは少しフォーカスの置き所が違う予定です。

ELF について

GNU/Linux などでは ELF というバイナリフォーマットが使われています。このフォーマットは実行ファイルや、共有オブジェクト(拡張子.so)、普通のオブジェクトファイル(拡張子.o)で、基本的には共通のものとなっています。例外は静的ライブラリ(拡張子.a)だけで、 .a は .o をただ ar という tar に似たツールで一つのファイルにして、(時に)検索用のインデックスをつけただけのものです。最終的に作りたいものは実行ファイルであり、その実行ファイルを作るための情報が共通しているため、同じフォーマットで記述するのは合理的と言えるでしょう。

ELF の詳細については、 Binary Hacks の『ELF 入門』などを参照下さい。と、それだけなのも不親切なので極めて簡単に言うと、 ELF ヘッダとプログラムヘッダとセクションヘッダがあるという話です。いずれにせよ readelf -a などで詳しく見ることができます。 readelf と elf.h はよく対応している(というか readelf は elf.h の構造体をそのまま出力している)ので、あわせて見ると参考になるでしょう。

ELF ヘッダには ELF 全体の情報であり、プログラムヘッダのサイズなど、一般的な情報が入っています。

プログラムヘッダについて

プログラムヘッダには実行開始時にメモリにロードされるべきデータについての情報が入っています。具体的には、プログラムコードや初期化済みグローバル変数の領域などがこれにあたります。

少し具体的に見てみます。適当に hello world を作って、 readelf を用いてプログラムヘッダの情報を出力させると、以下のようなものでした。

Program Headers:
  タイプ       オフセット 仮想Addr   物理Addr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
  INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1
      [要求されるプログラムインタプリタ: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x0046c 0x0046c R E 0x1000
  LOAD           0x00046c 0x0804946c 0x0804946c 0x00104 0x00108 RW  0x1000
  DYNAMIC        0x000480 0x08049480 0x08049480 0x000c8 0x000c8 RW  0x4
  NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

バイナリハックにおける「具体的に見る」、とは、本当にバイナリを見ることです。 INTERP と書いてあるものは FileSiz が 13Byte と、非常に短いのでこれを「見て」やることにしましょう。

ファイルの中身を見る

まずファイルの中身を見てやります。 INTERP のところを見ると、オフセットが 0x134 であり、 FileSiz が 0x13 とのことです。 Binary Hacks 中の『od でバイナリファイルをダンプする』で紹介した od コマンドを使うと簡単です。

% od -t x1z -j $((0x114)) -N $((0x13)) a.out
0000424 2f 6c 69 62 2f 6c 64 2d 6c 69 6e 75 78 2e 73 6f  >/lib/ld-linux.so<
0000444 2e 32 00                                         >.2.<
0000447

readelf が出力していた「/lib/ld-linux.so.2」という情報が入っていることがわかります。 od のそれぞれのオプションは本なりマニュアルを参照下さい。 $((0x114)) は shell によって10進数に展開されます。追記: sayさんから od は $(()) でくくらなくても 16進数表記解釈してくれるよ、と教えていただきました。削除することも考えましたが、 dd なんかを使う場合はこのテクニックは便利なので、一応残しておきます。sayさん、ご指摘ありがとうございました。

「objdump でオブジェクトファイルをダンプする」で紹介した objdump を使うとこれはもっと簡単にできます。以下のようにするだけです。

% objdump -s -j .interp a.out
a.out:     ファイル形式 elf32-i386

セクション .interp の内容:
 8048114 2f6c6962 2f6c642d 6c696e75 782e736f  /lib/ld-linux.so
 8048124 2e3200                               .2.

同じく /lib/ld-linux.so.2 という内容がきちんと入っていますね。

メモリの中身を見る

さて、先程からたびたび、「0x08048114」という数値が出てきています。プログラムヘッダの内容は実行時にメモリにマップされると説明しましたが、この数値はどのアドレスにマップされているかを示すものです。これも、実際に「見て」みましょう。以下のような C コードを作ってやります。

#include 
int main() {
    char *p = (char *)0x08048114;
    printf("%s\n", p);
}

見ての通り、 0x08048114 にある内容をそのまま文字列として出力させているだけです。コンパイルして実行すると、以下のようになります。

% gcc see_interp.c
% ./a.out
/lib/ld-linux.so.2

ここでも、きちんと /lib/ld-linux.so.2 という内容が出てきています。

プログラムヘッダのまとめ

プログラムヘッダはプログラムの実行開始時にメモリにマップすべきデータについての情報が入っています。ここでは、実際にメモリにデータがマップされていることを確認しました。

ところで、この /lib/ld-linux.so.2 というファイルは特殊なファイルで、『プログラムが main() にたどりつくまで』や『ldd で共有ライブラリの依存関係をチェックする』などで述べられています。

0x08048... という数値は、 GNU/Linux で作業をしていると馴染みの深いもので、通常はこのあたりにプログラム自身がマップされるからです。このあたりについては、『ロードしている共有ライブラリをチェックする』などでも述べられています。

セクションヘッダについて

プログラムヘッダにはメモリにロードされるデータの情報が入っていると説明しました。ではメモリにロードされることの無い .o ファイルにはプログラムヘッダは無いのではないか、と予想されます。実際、 .o ファイルはプログラムヘッダを持っていません。でも .o はリンク時に必要なシンボルテーブルなどが必要だし、メモリにマップする必要のない、デバッグ情報なども ELF には埋まってるはずです。などなど、オブジェクトファイルの情報を全てカバーするのがセクションヘッダです。

セクションヘッダは、 ELF 内の各データに対して、セクション名と、そのファイル内でのオフセット、サイズ、メモリに置かれるならその場合のアドレスといった情報に、いくつかの属性情報が加わったものです。それ自体は特に詳しいフォーマットなどはなく、この名前のセクションはこういう目的で使う、というような約束が決まっているだけです。 ELF 内のデータを記述するものですので、前述のプログラムヘッダもセクションヘッダに情報として書かれていたりします。

セクションヘッダについては、いくつか例を見ていくことにします。以下に readelf コマンドで出力されたセクションヘッダを貼っておきます。

Section Headers:
  [番] 名前              タイプ          アドレス Off    サイズ ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        08048114 000114 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048128 000128 000020 00   A  0   0  4
  [ 3] .hash             HASH            08048148 000148 000028 04   A  4   0  4
  [ 4] .dynsym           DYNSYM          08048170 000170 000050 10   A  5   1  4
  [ 5] .dynstr           STRTAB          080481c0 0001c0 00004a 00   A  0   0  1
  [ 6] .gnu.version      VERSYM          0804820a 00020a 00000a 02   A  4   0  2
  [ 7] .gnu.version_r    VERNEED         08048214 000214 000020 00   A  5   1  4
  [ 8] .rel.dyn          REL             08048234 000234 000008 08   A  4   0  4
  [ 9] .rel.plt          REL             0804823c 00023c 000018 08   A  4  11  4
  [10] .init             PROGBITS        08048254 000254 000017 00  AX  0   0  4
  [11] .plt              PROGBITS        0804826c 00026c 000040 04  AX  0   0  4
  [12] .text             PROGBITS        080482b0 0002b0 000188 00  AX  0   0 16
  [13] .fini             PROGBITS        08048438 000438 00001c 00  AX  0   0  4
  [14] .rodata           PROGBITS        08048454 000454 000014 00   A  0   0  4
  [15] .eh_frame         PROGBITS        08048468 000468 000004 00   A  0   0  4
  [16] .ctors            PROGBITS        0804946c 00046c 000008 00  WA  0   0  4
  [17] .dtors            PROGBITS        08049474 000474 000008 00  WA  0   0  4
  [18] .jcr              PROGBITS        0804947c 00047c 000004 00  WA  0   0  4
  [19] .dynamic          DYNAMIC         08049480 000480 0000c8 08  WA  5   0  4
  [20] .got              PROGBITS        08049548 000548 000004 04  WA  0   0  4
  [21] .got.plt          PROGBITS        0804954c 00054c 000018 04  WA  0   0  4
  [22] .data             PROGBITS        08049564 000564 00000c 00  WA  0   0  4
  [23] .bss              NOBITS          08049570 000570 000004 00  WA  0   0  4
  [24] .comment          PROGBITS        00000000 000570 0001c7 00      0   0  1
  [25] .debug_aranges    PROGBITS        00000000 000738 000058 00      0   0  8
  [26] .debug_pubnames   PROGBITS        00000000 000790 000025 00      0   0  1
  [27] .debug_info       PROGBITS        00000000 0007b5 00019f 00      0   0  1
  [28] .debug_abbrev     PROGBITS        00000000 000954 000062 00      0   0  1
  [29] .debug_line       PROGBITS        00000000 0009b6 00013d 00      0   0  1
  [30] .debug_str        PROGBITS        00000000 000af3 0000d2 01  MS  0   0  1
  [31] .shstrtab         STRTAB          00000000 000bc5 000127 00      0   0  1
  [32] .symtab           SYMTAB          00000000 00123c 000500 10     33  61  4
  [33] .strtab           STRTAB          00000000 00173c 0002ad 00      0   0  1

.interp

i@u ~/test> objdump -s -j .interp a.out

a.out:     ファイル形式 elf32-i386

セクション .interp の内容:
 8048114 2f6c6962 2f6c642d 6c696e75 782e736f  /lib/ld-linux.so
 8048124 2e3200                               .2.

先の章で見た、 /lib/ld-linux.so.2 というファイル名が格納されている位置は、セクションヘッダでも指定されています。このオフセット 0x114 やメモリアドレスの情報がプログラムヘッダと一致していることに注目して下さい。

.dynsym, .dynstr

i@u ~/test> objdump -s -j .dynsym -j .dynstr a.out

a.out:     ファイル形式 elf32-i386

セクション .dynsym の内容:
 8048170 00000000 00000000 00000000 00000000  ................
 8048180 1a000000 00000000 9a010000 12000000  ................
 8048190 2e000000 00000000 9f010000 12000000  ................
 80481a0 1f000000 58840408 04000000 11000e00  ....X...........
 80481b0 01000000 00000000 00000000 20000000  ............ ...
セクション .dynstr の内容:
 80481c0 005f5f67 6d6f6e5f 73746172 745f5f00  .__gmon_start__.
 80481d0 6c696263 2e736f2e 36007075 7473005f  libc.so.6.puts._
 80481e0 494f5f73 7464696e 5f757365 64005f5f  IO_stdin_used.__
 80481f0 6c696263 5f737461 72745f6d 61696e00  libc_start_main.
 8048200 474c4942 435f322e 3000               GLIBC_2.0.

シンボルの動的ロードに必要な情報が入っている場所を指定するセクションです。『libbfd でシンボルの一覧を取得する』では動的シンボルを取得する場合について触れられていますが、その時使われるのはこの空間です。

.rel.*

再配置を行うための情報が含まれているセクションです。『オブジェクトファイルを自力でロードする』では、この情報を用いた再配置を、実際に自力で行っています。

.init, .fini, .ctors, .dtors, .jcr

ELF: プログラマの視点から: .init および .fini セクション が詳しいようですので、こちらを参照下さい。これらはリンカスクリプトがコンパイラと連携して作成しているセクションで、その過程を見るとリンカスクリプトについて学べるでしょう。これらの初期化処理などの実際の使われかたは、『プログラムが main() にたどりつくまで』や『main() の前に関数を呼ぶ』などでも触れられています。

.jcr は GCJ によって使われます。

.plt

『Linux の共有ライブラリを作るとき PIC でコンパイルするのはなぜか』や『PIE (位置独立実行形式) を作成する』で述べられている、 PIC, PIE に使われる Procedure Linkage Table が置かれる場所を示しています。

ちなみに、この情報はリンカが作ってるんだと思うのですが、リンカスクリプトで作れる内容では無いですし、どういったメカニズムでできているのか調べようと思って調べていません。 .eh_frame のように ld 本体にハードコードされてるんだと想像していますが。

.text

いわゆる、プログラム本体です。

.rodata

i@u ~/test> objdump -s -j .rodata a.out

a.out:     ファイル形式 elf32-i386

セクション .rodata の内容:
 8048454 03000000 01000200 68656c6c 6f20776f  ........hello wo
 8048464 726c6400                             rld.

hello world が入っているのでわかりやすいですね、リードオンリーな情報、つまり定数の情報が入っています。

.eh_frame

C++ で例外が飛んだ場合の処理をするために、フレーム情報を保持しているセクションです。『G++ の例外処理を理解する Dwarf2 編』に詳細が述べられています。

.data

初期化が必要で定数でない情報が入っています。 C で言うと初期化されているグローバル変数などです。例えば、

int x = 0x1234567

などというグローバル変数があると、

i@u ~/test> objdump -s -j .data a.out

a.out:     ファイル形式 elf32-i386

セクション .data の内容:
 8049564 00000000 00000000 78940408 67452301  ........x...gE#.

というように、 67452301 という数値が入っていることがわかります(リトルエンディアン)。

.bss

ゼロに初期化される、特別な初期値を持たない変数領域の情報が入っています。例えば、

int x;

というようなグローバル変数などです。この領域は、メモリ上のサイズは指定しているだけで、ファイル上では全く容量を使っていないことに注意して下さい。以下のコマンドは .bss セクションを objdump していますが、ファイル内には情報が無いため、何も表示されていません。

i@u ~/test> objdump -s -j .bss a.out

a.out:     ファイル形式 elf32-i386

.debug_*

デバッガの用いる、デバッグ情報の入る空間です。『libbfd でシンボルの一覧を取得する』や『libdwarf でデバッグ情報を取得する』などでこの一つである、 .debug_info セクションについて述べられています。

.symtab, .strtab

.dynsym などと同様にシンボル情報が入っていますが、こちらは静的リンクに用いる情報が入っています。 .o ファイルではこのセクションを消すと実質ゴミファイルになってしまいますが、実行ファイルではこのセクションを消しても特に問題はありませんし、実際 strip(1) はこのセクションを削除します。『libbfd でシンボルの一覧を取得する』などで、このセクションの扱いに触れています。静的にこれらの情報を見る方法は、『nm でオブジェクトファイルに含まれるシンボルをチェックする』などでも述べられています。

セクションヘッダのまとめ

セクションには、プログラムを実行する時に必要な様々な情報が入っています。これはコンパイラ・アセンブラ・リンカのあわせ技で作成され、OS・デバッガ・リンカなどから使用されます。

最小 Hello world! を追及

SEA & FSIJ での発表資料 に結構色々書いてあります。

58B!!!

76Byte Hello world

without ASM

GCC 拡張でのセクション指定

main の自己書き換え、空気のように扱われる main

OMF フォーマット

OMF について一言言うなら、 全てをレコードっていう単位に 押し込めた、 なんか ELF とは雰囲気大部違うものだってこと。

omfutils

PE フォーマット

たぶん書きません。

Mach-O フォーマット

たぶん書きません。


home / index

ƥ󥯥ե꡼Ǥ Ҥϼͳ˻ѤƤƹޤ ¾ΤΤGPLǤФѤ˴ؤʸϸޤ ʤˤв᡼륢ɥ쥹ء

shinichiro.hamaji _at_ gmail.com / shinichiro.h