Chapter 07
Cortex-M のイメージ — ベクタテーブルから構造を復元する
この章のゴール.
シンボルのない生バイナリを見て、ここが Cortex-M のファームウェアだと判断でき、 ベクタテーブルからスタック位置・エントリポイント・割り込みハンドラを読み取れるようになること。 Thumb コードとリテラルプール、フラッシュとRAMのアドレスを区別できること。
この章で使う既出の用語(定義は各リンク先). メモリマップ(01 章 3 節)、オフセット(03 章 1 節)、サイズ(04 章 7 節)、ブートローダ(04 章 5 節)、ELF(05 章 1 節)
1. 手がかりのない生バイナリ
フラッシュから吸い出した .bin には、ELF のようなヘッダもシンボルもない。ただのバイト列である。 しかし Cortex-M(Arm のマイコン向け CPU)のファームウェアには、必ず決まった形で始まるという強力な手がかりがある。それがベクタテーブルである。
2. ベクタテーブル — 先頭に必ずある表
Cortex-M では、フラッシュの先頭(またはリセット時にマップされる先頭)に、32 ビットの値が並んだ表が置かれる。これがベクタテーブルで、リセットや割り込みが起きたときに CPU が「どこへ飛ぶか」を書いてある。
| オフセット | 内容 | 読み方 |
|---|---|---|
| 0x00 | 初期スタックポインタ(MSP) | RAM の範囲のアドレス(0x2000xxxx など)。スタックの初期値 |
| 0x04 | リセットハンドラ | 起動後に最初に実行されるコードのアドレス(フラッシュ範囲、最下位ビットが 1) |
| 0x08 | NMI ハンドラ | |
| 0x0C | HardFault ハンドラ | |
| 0x10〜0x3C | その他のシステム例外 | MemManage、BusFault、UsageFault、SVC、PendSV、SysTick |
| 0x40〜 | 外部割り込み(IRQ)ハンドラ | 周辺機器ごと。数十〜200 個 |
これが「Cortex-M のイメージだ」と判断する決め手である。
- オフセット 0 の u32 が RAM の範囲(典型的には 0x20000000〜0x2003FFFF など)にある
- オフセット 4 以降の u32 がフラッシュの範囲(0x08000000 台や 0x00000000 台)にあり、すべて奇数(最下位ビット = 1)
xxd -e -g4 -l 64 fw.bin
# 20000400 080081c5 08008181 08008183 ...
# ↑MSP ↑Reset ↑NMI ↑HardFault3. なぜハンドラのアドレスは奇数なのか — Thumb ビット
Cortex-M は Thumb 命令セット(16/32 ビット幅の Arm 命令)だけを実行する。 分岐先アドレスの最下位ビットが 1 なら「Thumb で実行せよ」という約束で、この 1 は「Thumb ビット」と呼ばれる。 だからベクタテーブルの各ハンドラアドレスは、実際のコード位置 + 1 になっている。
- 表の値が
0x080081C5なら、実際のコードは 0x080081C4 から始まる(偶数に直す) - 逆アセンブラにアドレスを渡すときも、最下位ビットを落とす
この「全部奇数」という性質が、ベクタテーブルを見分ける第 2 の手がかりになる。
4. ロードアドレスを突き止める
生バイナリのオフセット 0 が、実機のどのアドレスに焼かれているか(ロードアドレス)が分からないと、テーブルのアドレスと実際の位置がつながらない。
手がかり:
- リセットハンドラのアドレス(オフセット 4 の値、Thumb ビットを落とす)は、このイメージの中を指しているはず。その値が 0x08008000 台なら、イメージはフラッシュの 0x08008000 付近に焼かれている
- ダンプを取ったとき(04 章)に記録したアドレス
- チップのメモリマップ(フラッシュの開始アドレス)
- ブートローダの後ろに置かれるアプリは、0x08000000 ではなくオフセットした位置(0x08008000 など)に焼かれる。MCUboot なら 0x8000 + ヘッダ分(08 章)
ロードアドレスを L とすると、テーブルの値 V(Thumb ビットを落とした偶数)に対応するファイル内オフセットは V - L である。 これが合っていれば、リセットハンドラのオフセットに実際のコードがあるはずで、逆アセンブルして確かめられる。
import struct
d = open("fw.bin","rb").read()
sp, reset = struct.unpack_from("<II", d, 0)
print("MSP =", hex(sp), " Reset =", hex(reset & ~1))
# reset の上位から load address を推定
load = (reset & 0xFFFF0000) # ざっくり。実際は既知の値で
print("推定ロードアドレス ≈", hex(load))5. 割り込みハンドラの数からチップを推定する
ベクタテーブルの長さ(0x40 以降に並ぶ IRQ ハンドラの数)は、チップの割り込み源の数に対応する。 未使用の割り込みは、共通のデフォルトハンドラ(無限ループ)を指すことが多く、テーブルに同じアドレスが繰り返し現れる。
08008000: 20000400 080081c5 08008181 08008183
08008010: 08008185 08008187 08008189 00000000
08008020: 00000000 00000000 00000000 0800818b ← 0 が並ぶのは予約領域
...
08008080: 0800a201 0800a201 0800a201 0800a3f5 ← 同じ 0800a201 が繰り返す = デフォルトハンドラ「同じアドレスが繰り返し出てくる行」が見えたら、そこがデフォルトハンドラを指す未使用割り込みで、テーブルはそのあたりまで。 繰り返されるアドレス(デフォルトハンドラ)は、たいてい B .(自分へ無限ジャンプ、0xE7FE)という 2 バイトである。
6. コードとデータを見分ける
ベクタテーブルの後ろは、コードとデータが混在する。逆アセンブラ(16 章)に任せるのが本筋だが、目視でも当たりはつく。
- Thumb コード: 16 ビット命令が多い。
push {..., lr}(関数の入口、B5xx)、pop {..., pc}(出口、BDxx)、bl(関数呼び出し、F?xx F?xxの 4 バイト)が頻出 - リテラルプール: コードの合間に、32 ビットの定数やアドレスが島のように挟まる。Cortex-M は大きな即値を命令に埋め込めないので、近くに置いた定数を
ldr r0, [pc, #off]で読む。だからコードの中にポインタらしき 0x0800xxxx / 0x2000xxxx / 0x4000xxxx(周辺レジスタ)が点在する - 文字列:
.rodataに固まっている。stringsで位置が分かる - テーブル: 規則的に増える値の並び(ジャンプテーブル、設定の配列)
7. RAM ダンプの読み方
RAM(0x20000000 付近)のダンプは、止まった瞬間の状態である。フラッシュと違い、内容は実行のたびに変わる。
.data: 初期化済み変数。フラッシュの初期値から始まり、実行で変化している.bss: 0 で始まった変数- スタック: 高いアドレスから低いアドレスへ伸びる。関数の戻りアドレス(0x0800xxxx の奇数)が点在する。これを拾うと呼び出しの履歴が復元できる(17 章のスタックトレース)
- ヒープ:
mallocの管理構造(サイズ、次ブロックへのポインタ)が規則的に並ぶ - 周辺レジスタ(0x40000000 付近): RAM ではなくハードウェアのレジスタ。ダンプに含まれれば、止まった瞬間のデバイス状態
スタック上で 0x0800xxxx の奇数値を探すと、それは関数の戻りアドレスの候補である。addr2line(ELF があれば)や逆アセンブラで、どの関数から呼ばれたかをたどれる。
8. ESP32・RISC-V の場合
- ESP32(Xtensa / RISC-V): ベクタテーブルの形が Cortex-M と違う。フラッシュ先頭にはブートローダ、その後にパーティションテーブル(0x8000)、アプリイメージ(
esp_image_header_tというマジック0xE9で始まるヘッダ)が並ぶ。esptool.py image_info app.binが構造を解く - RISC-V のマイコン: ベクタは実装依存(CLINT / CLIC)。リセットベクタは決まったアドレス(0x80000000 など)だが、テーブルの形はチップによる
9. 手を動かす
ベクタテーブルを読む
この章のポイント
- Cortex-M イメージの先頭はベクタテーブル。オフセット 0 = RAM 範囲の MSP、オフセット 4 = フラッシュ範囲・奇数のリセットハンドラが決め手
- ハンドラアドレスが奇数なのは Thumb ビット。実コードは −1 の偶数位置
- ロードアドレスはリセットハンドラの値から推定する。ファイルオフセット = アドレス − ロードアドレス
- 繰り返す同じアドレスは未使用割り込みのデフォルトハンドラ。テーブルの終わりの目印
- コード中に点在する 0x0800/0x2000/0x4000 のポインタはリテラルプール。RAM ダンプのスタックには戻りアドレスが並ぶ