Binary Hacks の校正大会にて、あーセクションの話が少し説明不足で不親切だね、っていう話が出ました。あった方がいいかな、と思ったので、宣伝を兼ねて、ここに私が知っていることを書いておきます。
内容としては、 Binary Hacks に比べてかなりいい加減に書いています。例えば調べものは一切せずに書きます。著者の中で最もいい加減な私が本よりもいい加減に書いたということで、 Binary Hacks の全ての文章はこれよりはレベルが上、というようなサンプルだと思って下さい。宣伝を兼ねるということで、これ単体ではフォローせずに Binary Hacks のここを見てね、というポインタだけ示す部分が多いです。『』で囲まれた文字列は Binary Hacks の中のハック名に対応しています。
書いてる最中なので、気が向いたら内容を追加します。
詳しい参考文献としては Linkers&Loaders が良いでしょう。手元に無いので確認できませんが、この文章と内容が結構かぶる予定です。そもそも Binary Hacks 自体ともかなりかぶっています。具体的には『ELF入門』や『プログラムが main() にたどりつくまで』と強くかぶりますが、それぞれとは少しフォーカスの置き所が違う予定です。
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 コードを作ってやります。
#includeint 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
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 やメモリアドレスの情報がプログラムヘッダと一致していることに注目して下さい。
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 でシンボルの一覧を取得する』では動的シンボルを取得する場合について触れられていますが、その時使われるのはこの空間です。
再配置を行うための情報が含まれているセクションです。『オブジェクトファイルを自力でロードする』では、この情報を用いた再配置を、実際に自力で行っています。
ELF: プログラマの視点から: .init および .fini セクション が詳しいようですので、こちらを参照下さい。これらはリンカスクリプトがコンパイラと連携して作成しているセクションで、その過程を見るとリンカスクリプトについて学べるでしょう。これらの初期化処理などの実際の使われかたは、『プログラムが main() にたどりつくまで』や『main() の前に関数を呼ぶ』などでも触れられています。
.jcr は GCJ によって使われます。
『Linux の共有ライブラリを作るとき PIC でコンパイルするのはなぜか』や『PIE (位置独立実行形式) を作成する』で述べられている、 PIC, PIE に使われる Procedure Linkage Table が置かれる場所を示しています。
ちなみに、この情報はリンカが作ってるんだと思うのですが、リンカスクリプトで作れる内容では無いですし、どういったメカニズムでできているのか調べようと思って調べていません。 .eh_frame のように ld 本体にハードコードされてるんだと想像していますが。
いわゆる、プログラム本体です。
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 が入っているのでわかりやすいですね、リードオンリーな情報、つまり定数の情報が入っています。
C++ で例外が飛んだ場合の処理をするために、フレーム情報を保持しているセクションです。『G++ の例外処理を理解する Dwarf2 編』に詳細が述べられています。
初期化が必要で定数でない情報が入っています。 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 という数値が入っていることがわかります(リトルエンディアン)。
ゼロに初期化される、特別な初期値を持たない変数領域の情報が入っています。例えば、
int x;
というようなグローバル変数などです。この領域は、メモリ上のサイズは指定しているだけで、ファイル上では全く容量を使っていないことに注意して下さい。以下のコマンドは .bss セクションを objdump していますが、ファイル内には情報が無いため、何も表示されていません。
i@u ~/test> objdump -s -j .bss a.out a.out: ファイル形式 elf32-i386
デバッガの用いる、デバッグ情報の入る空間です。『libbfd でシンボルの一覧を取得する』や『libdwarf でデバッグ情報を取得する』などでこの一つである、 .debug_info セクションについて述べられています。
.dynsym などと同様にシンボル情報が入っていますが、こちらは静的リンクに用いる情報が入っています。 .o ファイルではこのセクションを消すと実質ゴミファイルになってしまいますが、実行ファイルではこのセクションを消しても特に問題はありませんし、実際 strip(1) はこのセクションを削除します。『libbfd でシンボルの一覧を取得する』などで、このセクションの扱いに触れています。静的にこれらの情報を見る方法は、『nm でオブジェクトファイルに含まれるシンボルをチェックする』などでも述べられています。
セクションには、プログラムを実行する時に必要な様々な情報が入っています。これはコンパイラ・アセンブラ・リンカのあわせ技で作成され、OS・デバッガ・リンカなどから使用されます。
SEA & FSIJ での発表資料 に結構色々書いてあります。
OMF について一言言うなら、 全てをレコードっていう単位に 押し込めた、 なんか ELF とは雰囲気大部違うものだってこと。
たぶん書きません。
たぶん書きません。
ƥեǤ Ҥϼͳ˻ѤƤƹޤ ¾ΤΤGPLǤФѤ˴ؤʸϸޤ ʤˤв륢ɥ쥹ء