ELF 実行ファイル形式
Executable and Linkable Format — Linux/Unix のバイナリと共有オブジェクトの形式。
- MIME タイプ
- application/x-elf
- 拡張子
- .elf, .so, .o, .out
- カテゴリ
- 実行ファイル・バイトコード
マジックナンバー(ファイル署名)
| 0x00000000 | 7F454C46 | ·ELF | 0x7F 'ELF' |
ELF 実行ファイル形式について
ELF は Linux、Android のネイティブ層、BSD 系、そして大半の組み込みツールチェーンにおける実行ファイル・共有ライブラリ・オブジェクトファイル・コアダンプの形式です。先頭 16 バイトは e_ident という自己記述的な前置きで、マジックナンバーに続いて 32 ビットか 64 ビットかを示す 1 バイト、リトルエンディアンかビッグエンディアンかを示す 1 バイトが並びます。1 つのパーサーが、動かしたことのないマシン向けのバイナリまで読めるのはこの設計のおかげです。e_ident より後ろは 2 つのテーブルに分かれます。カーネルがイメージをメモリに写像するのに使うプログラムヘッダと、リンカやデバッガが使うセクションヘッダで、後者はストリップされたバイナリなら失われていても動きます。
ヘッダーの構造(バイト単位)
| オフセット | 長さ | フィールド | 意味 |
|---|---|---|---|
| 0 | 4 | EI_MAG0..3 (magic) | 7F 45 4C 46。あえて ASCII でないバイトの後ろに ELF の 3 文字を置いてあるので、テキストモード転送で壊れたり 7 ビットに切り詰められたりしたファイルは即座に一致しなくなります。 |
| 4 | 1 | EI_CLASS | 1 が ELFCLASS32、2 が ELFCLASS64。この 1 バイトが、ヘッダ以降のすべてのアドレスフィールドの幅を決めます。32 ビットのファイルでは e_entry、e_phoff、e_shoff がそれぞれ 4 バイトに縮み、オフセット 24 から先がすべて 12 バイト手前にずれます。 |
| 5 | 1 | EI_DATA | 1 が 2 の補数のリトルエンディアン、2 がビッグエンディアン。ファイル全体のあらゆる多バイトフィールドの読み方を支配します。リトルエンディアン前提のパーサーにとって MIPS や古い PowerPC のバイナリが意味不明に見えるのはこのためです。 |
| 6 | 1 | EI_VERSION | ELF ヘッダのバージョンで、常に 1 です。0 や 1 より大きい値は「新しい ELF が出た」ではなく「ヘッダが壊れている」という意味になります。 |
| 7 | 1 | EI_OSABI | 0 は UNIX System V で、実際に大半の Linux ツールチェーンが吐くのはこれです。3 が GNU/Linux、9 が FreeBSD、12 が OpenBSD。効いてくるのは OS 固有のセクション型やシンボル型を解釈するときだけなので、Linux のバイナリで 0 か 3 かが分かれるのは正常であって欠陥ではありません。 |
| 8 | 1 | EI_ABIVERSION | 上で示した ABI のサブバージョン。ほぼ必ず 0 です。 |
| 9 | 7 | EI_PAD | e_ident を 16 バイトに揃えるためのゼロ埋め。ここが非ゼロなら、手でパッチを当てたか偽造したヘッダである確かな臭いです。 |
| 16 | 2 | e_type | 1 は REL(再配置可能なオブジェクトファイル)、2 は EXEC(アドレス固定の実行ファイル)、3 は DYN で、共有オブジェクトとこの 10 年に作られた位置独立実行ファイルの両方が含まれます。4 は CORE(クラッシュダンプ)です。ヘッダだけでは .so と PIE を区別できません。それには dynamic セクションの DF_1_PIE が要ります。 |
| 18 | 2 | e_machine | 対象 CPU: x86-64 が 62、AArch64 が 183、i386 が 3、32 ビット ARM が 40、RISC-V が 243。アーキテクチャ違いのエラーでライブラリが読み込まれないとき、確認すべきはこのフィールドです。 |
| 20 | 4 | e_version | オブジェクトファイルのバージョンで、これも常に 1。EI_VERSION を別の幅で重複させたもので、元の ABI の名残です。 |
| 24 | 8 | e_entry | イメージを写像したあとカーネルが飛び込む仮想アドレス。32 ビットのファイルでは同じオフセットに 4 バイトで入ります。共有オブジェクトは自前のエントリポイントを持たないので、通常ここは 0 です。 |
| 32 | 8 | e_phoff | プログラムヘッダテーブルのファイルオフセット。実際にはこれが 64、つまりヘッダの直後から始まります。ローダーが最初に読む 1 ページの中にセグメント情報が欲しいからです。 |
| 40 | 8 | e_shoff | セクションヘッダテーブルのファイルオフセットで、通常はファイルの一番最後にあります。0 ならセクションがストリップされたということ、ファイル末尾より先を指しているなら、それが途中で切れたバイナリの典型的な姿です。 |
| 48 | 4 | e_flags | プロセッサ固有のフラグ。x86-64 では空ですが、ARM や RISC-V では浮動小数点 ABI や命令セットのオプションを符号化しています。soft-float と hard-float の不一致でリンクが失敗するのはここが理由です。 |
| 52 | 2 | e_ehsize | このヘッダのサイズ。64 ビットのファイルなら 64、32 ビットなら 52。安上がりな自己整合チェックになっていて、EI_CLASS と食い違うなら、そこから先を信用するのはやめたほうがいいです。 |
| 54 | 2 | e_phentsize | プログラムヘッダ 1 エントリのバイト数。64 ビットなら 56、32 ビットなら 32。ABI で固定されているので、違う値は珍しい構成ではなくファイルの破損を意味します。 |
| 56 | 2 | e_phnum | プログラムヘッダの個数。ふつうは 10 個そこそこです。特別な値 0xFFFF は本当の個数が最初のセクションヘッダにあることを示し、巨大なセグメントテーブルを持つバイナリのための回避策です。 |
| 58 | 2 | e_shentsize | セクションヘッダ 1 エントリのバイト数。64 ビットなら 64、32 ビットなら 40。 |
| 60 | 2 | e_shnum | セクションヘッダの個数。e_shentsize を掛けて e_shoff を足すと、セクションテーブルが末尾にある無傷のファイルではちょうどファイルサイズになります。切り詰めを確認する一番速い方法です。 |
| 62 | 2 | e_shstrndx | セクション名を保持するセクションのインデックス。これが無ければどのセクションも名前を持てないので、番号だけで名前を表示しないツールはたいてい、このインデックスが範囲外のファイルを読んでいます。 |
似た形式との見分け方
| 紛らわしい形式 | 見分け方 |
|---|---|
| PE(Windows 実行ファイル) | Windows 側の対応物で、混同はバイトレベルではなく実務上の問題です。クロスコンパイルの成果物が同じディレクトリに落ちてくるからです。PE は 4D 5A('MZ')で始まり、本当のヘッダはオフセット 0x3C にある 4 バイトのポインタの先に隠しています。ELF は 7F 45 4C 46 と名乗り、すべてを固定オフセットに置きます。実は .dll だった .so は、オフセット 0 の時点で失敗します。 |
| Mach-O 実行ファイル | macOS で同じ仕事をする形式です。Mach-O は CF FA ED FE(64 ビットのリトルエンディアン)か CE FA ED FE(32 ビット)で始まり、7F 45 4C 46 になることはありません。ユニバーサルバイナリなら CA FE BA BE です。どちらの形式もオブジェクトファイルの拡張子は .o、ライブラリは .dylib と .so なので、拡張子は何も証明せず、先頭 4 バイトがすべてを証明します。 |
| ar archive (.a) | 静的ライブラリは ELF ファイルではありません。メンバーが ELF オブジェクトである ar アーカイブです。先頭は 8 バイトの 21 3C 61 72 63 68 3E 0A('!<arch>' と改行)で、最初の 7F 45 4C 46 は最初のメンバーヘッダの後ろ、60 バイトほど先に現れます。.a に対して「ELF ファイルではない」と報告するツールは正しいのです。 |
| Core dump | コアファイルは同じヘッダを持つ本物の ELF ファイルなので、file コマンドが ELF と答えるのは当然です。分かれ目はオフセット 16 の e_type で、4 なら CORE。この種のファイルはプログラムヘッダを持つ一方でセクションヘッダもシンボルも持たないため、デバッガには元のバイナリを一緒に渡す必要があります。 |
| PIE | 共有オブジェクトと位置独立実行ファイルは、どちらも e_type が 3(DYN)と報告するので、ヘッダでは本当に区別できません。誰かが見落としている近道がある、という話ではないのです。区別の材料は dynamic セクションの DT_FLAGS_1 エントリにある DF_1_PIE で、readelf が 'Position-Independent Executable file' と表示する前に確認しているのもまさにこれです。 |
よくある壊れ方
- ストリップとセクション破損の取り違え。ストリップされたバイナリは e_shoff を 0 にしても完璧に動きます。カーネルはプログラムヘッダしか読まないからです。一方、壊れたほうは e_shoff がファイル末尾より先を指したままです。どちらも readelf は有用なことを何も表示しないので、どちらを手にしているかは e_shoff の値だけが教えてくれます。
- 途中切れ。セクションヘッダテーブルは通常 ELF ファイルの末尾にあるので、中断したダウンロードが最初に失うのはここです。プログラムヘッダはパースでき、エントリポイントも妥当に見え、セクションを使うツールだけが全滅します。e_shoff + e_shnum × e_shentsize を実際のファイルサイズと比べてください。
- テキストモード転送。改行を変換するクライアントを通してバイナリをコピーすると 0D 0A がすべて 0A になり、最初に現れた箇所から後ろが全部ずれます。マジックナンバーは残ることが多いので、ファイルは ELF と識別され続けたまま、プログラムヘッダがでたらめなオフセットを指します。
- EI_CLASS や EI_DATA のバイトが違う。4 バイト目か 5 バイト目を反転させてもヘッダは「正当」なままです。パーサーは以降のフィールドを間違った幅か間違ったエンディアンで読み、壊れていると認めるかわりに、途方もないオフセットとあり得ないセクション数を報告します。
- オフセットを直さずにその場でパッチした。.rodata の文字列をもっと長いものに書き換える、どこかにバイトを挿入する。どちらもセクションテーブルとプログラムヘッダテーブルの位置を動かしますが、e_shoff と e_phoff は更新されません。バイナリは読み込まれ、もはやコードでない場所へ飛ぶまでは動きます。
この形式の現在地
Linux のプログラム、共有ライブラリ、カーネルモジュールはすべて ELF ですし、APK の lib/ に入って Android が配るものも、ほとんどの組み込みツールチェーンが raw イメージや Intel HEX に落とす前に吐くものも ELF です。Unix 上では競合と呼べるものがありません。a.out と COFF は 90 年代半ばに消えましたし、配布形式として ELF の外側に来たもの(コンテナイメージ、Flatpak、APK)も結局は ELF ファイルを詰めたアーカイブです。届いていないのは PE を使う Windows と、Mach-O を使う Apple のプラットフォームだけ。だからクロスコンパイルのビルドツリーには 3 種類が同居し、見分けるのは先頭 4 バイトだけです。WebAssembly 周辺でも、ツールチェーンは .wasm モジュールを出力する前に内部で ELF を使います。emscripten のビルドディレクトリが、出荷されることのない ELF オブジェクトで埋まっているのはそのためです。
ELF 実行ファイルをこのサイトで開く
どれもブラウザ内で動きます。ファイルが端末から出ることはありません。
関連する形式
ELF 実行ファイルと混同しやすい形式です。先頭バイトを見比べてください。
| 形式 | 先頭バイト | 拡張子 |
|---|---|---|
| PE(Windows 実行ファイル) | 4D 5A | .exe |
| Mach-O 実行ファイル | CF FA ED FE | .dylib |
| WebAssembly | 00 61 73 6D | .wasm |
| Android DEX | 64 65 78 0A | .dex |