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)である。
2. ELF ヘッダ — 全体の目次
先頭の ELF ヘッダ(32 ビットで 52 バイト)に、そのファイルの素性が全部書いてある。
readelf -h firmware.elf| フィールド | 意味 | 組み込みでの典型 |
|---|---|---|
EI_CLASS | 32 / 64 ビット | ELF32(Cortex-M) |
EI_DATA | エンディアン | リトル |
e_type | 種類 | EXEC(実行)、DYN(PIE / 共有ライブラリ)、REL(.o) |
e_machine | CPU | ARM、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_info | LOAD(読み込む)、NOTE など |
| 見るコマンド | readelf -S | readelf -l |
| 実機に載るか | 情報。ローダはセグメントを見る | これがフラッシュ / RAM に配置される |
セクションは「部品の一覧」、セグメントは「メモリへの積み方」と考えるとよい。 1 つの LOAD セグメントが複数のセクション(.text + .rodata)をまとめて含む。
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 の順にファイルにする。 このとき捨てられるもの:
- シンボル表・デバッグ情報(
.symtab、.debug_*)——だから生バイナリからは名前が復元できない .bss(初期値ゼロなのでファイルに置く意味がない。起動時に RAM で 0 埋め)- セクションの境界・名前・アドレス情報
逆に言えば、フラッシュから吸い出した生バイナリを ELF に戻すことはできない(情報が失われている)。生バイナリの解析が難しいのはこのためで、16 章の逆アセンブラは「失われた構造を推定し直す」作業をする。
8. AArch64 / Linux の ELF
Linux 機器(15 章)の実行ファイルも ELF だが、Cortex-M と違う点がある。
- 64 ビット(ELF64、
e_machine = AArch64) - 動的リンク:
.dynsym、.dynamic、.plt、.gotがあり、共有ライブラリ(.so)を実行時に結合する。lddで依存を見る readelf -dで必要な共有ライブラリ(NEEDED)が分かる- PIE(位置独立実行形式、
e_type = DYN)が普通。ロード先アドレスは実行時に決まる
9. 手を動かす
ELF の構造を組み立てる
この章のポイント
- ELF は「最も情報の多い」形式。コード・データ・シンボル・デバッグ情報を含む
- セクションは部品の一覧(
readelf -S)、セグメントはメモリへの積み方(readelf -l) .dataは VMA(RAM)と LMA(フラッシュ)が違う。.bssはファイルに入らない- シンボル付き ELF は
nm/objdump -d/addr2lineで名前とアドレスがつながる。リリース用に保管せよ objcopy -O binaryはシンボル・デバッグ情報・.bssを捨てる。生バイナリから ELF には戻せない