Binary Analysis 05 · ELF ファイル — 実行ファイルの標準形式を読む

Chapter 05

ELF ファイル — 実行ファイルの標準形式を読む

この章のゴール.

ELF のヘッダ・セクション・セグメントの役割を区別でき、 readelf と objdump で「どのコードとデータが、どのアドレスに、どう配置されるか」を読めるようになること。 .elf から .bin への変換で何が捨てられるかを理解すること。

この章で使う既出の用語(定義は各リンク先). Ghidra(02 章 6 節)、エンディアン(03 章 2 節)、オフセット(03 章 1 節)、サイズ(04 章 7 節)

1. ELF とは何か

ELF(Executable and Linkable Format)は、Linux と多くの組み込みツールチェーン(arm-none-eabi-gcc など)が作る、実行ファイル・オブジェクトファイル・ライブラリの標準形式である。 組み込みでは、ビルドの成果物 firmware.elf がこれで、コード・データ・シンボル・デバッグ情報を全部含む「最も情報の多い」形である。 だから、ELF が手元にあるなら解析の大半は済んだも同然である。逆に、フラッシュから吸い出した生バイナリには ELF の情報はない。

先頭 4 バイトは必ず 7F 45 4C 46(.ELF)である。

ELF の全体構造ELF ヘッダ(52 B)7F 45 4C 46 で始まる0x00プログラムヘッダ表セグメント = メモリへの積み方.text / .rodata / .data …セクション = 部品セクションヘッダ表セクションの一覧.symtab / .debug_*シンボル・デバッグ情報最も情報が多い形コード・データ・シンボル・デバッグ情報を全部含む。ELF があれば解析の大半は済む。生バイナリにはこの情報がない先頭 4 バイトは必ず 7F 45 4C 46(.ELF)
コード・データ・シンボル・デバッグ情報を全部含む「最も情報の多い」形式

2. ELF ヘッダ — 全体の目次

先頭の ELF ヘッダ(32 ビットで 52 バイト)に、そのファイルの素性が全部書いてある。

readelf -h firmware.elf
フィールド意味組み込みでの典型
EI_CLASS32 / 64 ビットELF32(Cortex-M)
EI_DATAエンディアンリトル
e_type種類EXEC(実行)、DYN(PIE / 共有ライブラリ)、REL(.o)
e_machineCPUARM、AArch64、RISC-V、Xtensa(ESP32)
e_entry実行開始アドレスリセットハンドラ、または _start
e_phoff / e_shoffプログラムヘッダ表 / セクションヘッダ表の位置—

e_machine と EI_CLASS を確認すれば、「どの逆アセンブラの設定で読むか」(16 章)が決まる。

3. セクションとセグメント — 2 つの見方

ELF には、同じ中身に対する2 つの分け方がある。ここが最初の混乱どころである。

セクション(section)セグメント(segment / プログラムヘッダ)
目的リンク・デバッグのための細かい分類実行時にメモリへ載せるための大きな単位
例.text .rodata .data .bss .symtab .debug_infoLOAD(読み込む)、NOTE など
見るコマンドreadelf -Sreadelf -l
実機に載るか情報。ローダはセグメントを見るこれがフラッシュ / RAM に配置される

セクションは「部品の一覧」、セグメントは「メモリへの積み方」と考えるとよい。 1 つの LOAD セグメントが複数のセクション(.text + .rodata)をまとめて含む。

セクションとセグメント — 同じ中身の 2 つの見方セクション(readelf -S)リンク・デバッグのための細かい分類。.text / .rodata / .data / .bss / .symtab / .debug_info。部品の一覧セグメント(readelf -l)実行時にメモリへ載せる大きな単位。LOAD がフラッシュ / RAM に配置される。メモリへの積み方VMA と LMA の違い.data の初期値はフラッシュ(LMA = 配置アドレス)、実行時は RAM(VMA = 実行時アドレス)。起動時にコピー。.bss はファイルに入らない(起動時に 0 埋め)1 つの LOAD セグメントが複数のセクション(.text + .rodata)をまとめて含む「セクション = 部品の一覧」「セグメント = 積み方」
セクションは部品の一覧、セグメントはメモリへの積み方。.data は VMA と LMA が違う

4. 主なセクション

readelf -S firmware.elf         # セクション一覧(名前・種類・アドレス・サイズ・オフセット)
セクション中身実機での置き場所
.isr_vector / .vectorsベクタテーブル(07 章)フラッシュ先頭
.textプログラムのコード(機械語)フラッシュ
.rodata読み取り専用データ(文字列定数、テーブル、const)フラッシュ
.data初期値のある変数値はフラッシュ、実体は RAM(起動時にコピー)
.bss初期値ゼロの変数RAM(起動時に 0 埋め。ファイルには入らない)
.stack / .heapスタック・ヒープ領域の予約RAM
.symtab / .strtabシンボル表(関数・変数の名前とアドレス)ファイルのみ
.debug_*デバッグ情報(DWARF。行番号、型、変数)ファイルのみ
.commentビルドに使ったコンパイラの版ファイルのみ

VMA と LMA の違いは組み込みで重要である。 .data の変数は実行時には RAM にある(VMA、Virtual Memory Address = 実行時アドレス)が、初期値はフラッシュに焼かれている(LMA、Load Memory Address = 配置アドレス)。 起動時のスタートアップコードが LMA から VMA へコピーする。readelf -l の VirtAddr と PhysAddr がこの 2 つである。

5. シンボルを読む — 名前とアドレスの対応

シンボル表があれば、アドレスと関数名・変数名の対応が分かる。これが解析で最も強力な情報である。

nm -n firmware.elf              # アドレス順に関数・変数を並べる
nm -S --size-sort firmware.elf  # サイズの大きい順(どの関数が大きいか)
readelf -s firmware.elf         # シンボル表の詳細(型・束縛・セクション)
arm-none-eabi-addr2line -e firmware.elf 0x080012a4   # アドレス→ソース行(デバッグ情報が要る)

nm の各行は「アドレス 種別 名前」で、種別の文字は T(.text のグローバル関数)、t(ローカル関数)、D(初期値ありデータ)、B(.bss)、R(.rodata)など。 クラッシュしたアドレス(フォールト時の PC やスタックの戻りアドレス、17 章)を addr2line に渡すと、どの関数のどの行かが分かる。

リリースビルドはシンボルが削られていることが多い.

製品の ELF は strip でシンボルとデバッグ情報を削ってあることが多い(readelf -s がほぼ空)。 その場合はアドレスから名前を復元できず、16 章の逆アセンブルで関数を再発見することになる。 逆に、自社の開発用 ELF を保管しておくと、後の障害解析が桁違いに楽になる。「シンボル付き ELF をリリースごとに保存する」のは実務上の鉄則である。

6. コードを見る — objdump

arm-none-eabi-objdump -d firmware.elf              # 全コードを逆アセンブル
arm-none-eabi-objdump -d --start-address=0x8001200 --stop-address=0x8001300 firmware.elf
arm-none-eabi-objdump -S firmware.elf              # ソースcoと混ぜて表示(デバッグ情報が要る)
arm-none-eabi-objdump -t firmware.elf              # シンボル表
arm-none-eabi-objdump -s -j .rodata firmware.elf   # .rodata の中身を 16 進で

シンボル付き ELF に対する objdump -d は、関数名付きの逆アセンブルを出す。これは Ghidra を開くまでもない調査に十分速くて便利である。

7. ELF から実機イメージへ — 何が捨てられるか

フラッシュに焼くのは ELF ではなく、そこから必要な部分だけを抜き出した生バイナリ(.bin)か HEX(06 章)である。

arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
arm-none-eabi-objcopy -O ihex   firmware.elf firmware.hex
arm-none-eabi-size firmware.elf     # text / data / bss のサイズ

objcopy -O binary は、LOAD セグメントの中身だけを、LMA の順にファイルにする。 このとき捨てられるもの:

逆に言えば、フラッシュから吸い出した生バイナリを ELF に戻すことはできない(情報が失われている)。生バイナリの解析が難しいのはこのためで、16 章の逆アセンブラは「失われた構造を推定し直す」作業をする。

objcopy -O binary で何が捨てられるかfirmware.elf.text / .rodata / .data / .bss / .symtab / .debug_* / セクション情報objcopy-O binaryfirmware.binLOAD セグメントの中身だけ、LMA 順に捨てられるものシンボル表・デバッグ情報(→ 名前が復元できない)、.bss(起動時に 0 埋め)、セクションの境界と名前生バイナリから ELF には戻せない。だから生バイナリの解析は難しい(16 章で構造を推定し直す)
生バイナリはシンボル・デバッグ情報・.bss を捨てる。ELF には戻せない

8. AArch64 / Linux の ELF

Linux 機器(15 章)の実行ファイルも ELF だが、Cortex-M と違う点がある。

9. 手を動かす

ELF の構造を組み立てる


この章のポイント