GCC を起動すると、前処理(preprocessing)、コンパイル、アセンブル、リンクの 4 段階で処理が行われます。全体的(overall)オプションを使うとこの一連の処理を途中の段階で停止することができます。次のプログラム hello.c を例にとってこの課程を覗いてみましょう。
#include <stdio.h> main() { printf("hello, world\n"); }
まず hello.c の前処理だけしてその出力を覗いてみましょう。コマンドラインから、次のように入力します。
$ gcc -E hello.c | sed '/^[ ]*$/d' | less
ヘッダファイル stdio.h が読みこまれて展開され、データ型の定義や、関数のプロトタイプ宣言がされているのが分かります。次に処理を全処理、コンパイルで止めてみます。コマンドラインから次のように入力してください。
$ gcc -S hello.c
アセンブルコードファイル hello.s が作成されているのが分かります。次のようにして中味を覗いて下さい。
$ less hello.s
今度は、前処理、コンパイル、アセンブルまで行ってみましょう。次のように入力します。
$ gcc -c hello.c
作成されたオブジェクトファイル hello.o はバイナリーファイルなので od で見ます
$ od -c hello.o
最後に一気に実行ファイルまで作ってみましょう。
$ gcc hello.c
a.out という実行ファイルができています。od で中を見ると、hello.o に比べて実行形式にするための付加情報が多いのが分かります。
$ od -c a.out
a.out に含まれる文字列の情報を取りだすには、strings コマンドを使います。
$ strings a.out
a.out がどのような共有ライブラリーを使っているかは ldd で分かります。
$ ldd a.out
objdump を使うと ELF ファイルの情報を得ることができます。例えばヘッダー情報は -f オプションを、セクション情報は -s オプションを使います。
$ objdump -fs a.out | less
ファイルの逆アセンブルもできます。
$ objdump -d a.out | less
readelf で ELF ファイルの情報を得ることができます。
$ readelf -a a.out | less
GCC の処理で作成されたファイルのうち、hello.o と a.out は ELF バイナリーファイルです。このうち短い方の hello.o を例に取って ELF の構造を探険してみましょう。ELF のファイルフォーマットは大きくは次のように四つの部分からなっているようです。
それぞれの詳細な定義はヘッダファイル /usr/include/elf.h にありますので、それを手がかりに ELF hello.o の中味を探索してみます。
まず ELF ファイルには ELF ヘッダーが置かれます。ELF ファイルヘッダー は elf.h で構造体として定義されています。これを見ると、ELF ファイルヘッダーの先頭部分、これはEFLファイルそのものの先頭部分でもありますが、その16バイトは Magic number and other info です。hello.oの次の部分です。
0000000 177 E L F 001 001 001 \0 \0 \0 \0 \0 \0 \0 \0 \0
第0バイトから第3バイトまでがマジックナンバーになります。第4バイトは File class byte index で 1 ですから 32-bit objects です。第5バイトは Data encoding byte index で値が 1 なので 2's complement, little endian です。第6バイトは File version byte indexで値が 1 ですから Current version です。第7バイトは OS ABI identification で、値が 0 なので、Unix System V ABI です。第8バイトは ABI version、第9バイトは Byte index of padding bytes です。これらは皆 /usr/include/elf.h に定義してあります。
;次の2バイトは オブジェクト ファイルの種別を表しています。リトルエンディアンなので先に現れたほうが下位バイトです。次の例では ELF ファイルの種別コードは 1 すなわちリロケータブルファイルになります。
0000020 001 \0 003 \0 001 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0 \0
次の2バイトは Architectureで、この場合 3 ですから Intel 80386 です。次の4バイトが Object file version で、この場合 1 なので Current version になります。次の4バイトは Entry point vertual address で、ここでは 0 です。次の4バイトが Program headder file offset でこれも 0 です。
次の行の先頭4バイトが Section header file offset で、先頭から8進数で 0344 バイトから Section header が始まることを示しています。次の4バイトは Processer specific flags でここでは 0。次の2バイトは ELF headder size in bytes で 4(=52)。次の2バイトが Program header entry size で 0、次の2バイトが Program header entry count、次の2バイトが Section header entry size です。
0000040 344 \0 \0 \0 \0 \0 \0 \0 4 \0 \0 \0 \0 \0 ( \0
4行目の先頭2バイトは Section header entry count で、\v(=40)。次の2バイトが Section header string table index で \b(=11)。ここで ELF file header は終わりです。
0000060 \v \0 \b \0 U 211 345 203 354 \b 203 304 364 h \0 \0
それでは今解読した内容をreadelf -h hello.oと比較してみてください。
ELF ファイルの各セクションについての情報はセクションヘッダに記述されています。セクションヘッダーを見るには readelf の -S オプションを利用します。readelf -S hello.oを見て下さい。セクションヘッダーの実体は次の構造体ELf32_Shdrの配列です。セクションヘッダーの開始位置は ELF ヘッダーの e_shoff (Section header table file offset) フィールドの内容から知ることができます。その他 ELF の解読に必要な情報は /usr/include/elf.h に宣言されたシンボルテーブルの構造体Elf32_Symの構造の情報やreadelf -s hello.oによるシンボルテールの情報があります。
セクションヘッダーの情報をもとに hello.o の内容を解読したものがhello_x.txtです。ここでは od の出力を16進表示にしていますが、od -A x -t x1 hello.o で得ることができます。
この結果から、hello.o は次のような構造になっているのがわかります。
ELF ヘッダー .text セクション:機械語に翻訳されたコード .data セクション:空 .bss セクション:空 .note セクション: .rodata セクション:プログラムで使用する文字列定数 .comment セクション:コメント .shstrtab セクション:セクション名の文字列のテーブル。セクションヘッダで使用 セクションヘッダ:各セクションの情報を保持する構造体の配列になっています。 .symtab セクション:シンボルテーブル .strtab セクション:関数名などプログラムで使用する文字列 .rel.text セクション: