Chapter 18
差分と壊れたデータ — 2 つを比べ、破損を見抜く
この章のゴール.
2 つのダンプ(更新前後・正常と異常・機器間)を比べて変わった場所だけを取り出せるようになること。 電源断やフラッシュ劣化で壊れたダンプを、破損か自分の解釈違いかを切り分けて扱えること。
この章で使う既出の用語(定義は各リンク先). メモリマップ(01 章 3 節)、CRC(03 章 8 節)、アラインメント(03 章 4 節)、エンディアン(03 章 2 節)、オフセット(03 章 1 節)、ロードアドレス(07 章 10 節)、MCUboot(08 章 1 節)、バージョン(08 章 3 節)、LittleFS(09 章 2 節)、メタデータペア(09 章 4 節)、NVM3(10 章 1 節)、NVS(11 章 1 節)、読み方(11 章 2 節)、タイムスタンプ(12 章 3 節)、U-Boot(15 章 1 節)
1. なぜ差分が強力か
「ゼロから全部を理解する」のは大変だが、「2 つを比べて違いだけを見る」のは桁違いに速い。
- 更新前後: どのアドレスが書き換わったか = 更新が触った領域。バージョン番号・設定・コードのどこが変わったか
- 正常機と異常機: 違う場所に異常の原因がある可能性が高い
- 操作の前後: ある操作(設定変更、鍵の登録)が、フラッシュのどこに何を書いたかが分かる。未知の構造を解く最短路
- 同一ロットの複数機: 共通部分は固定値・コード、違う部分は個体ごとのデータ(シリアル、鍵、較正値)
2. バイト単位の差分
cmp -l a.bin b.bin | wc -l # 違うバイト数
cmp -l a.bin b.bin | head # 位置(10 進、1 始まり)と値(8 進)
radiff2 a.bin b.bin # 16 進で見やすい差分
radiff2 -s a.bin b.bin # 類似度(%)
vbindiff a.bin b.bin # 並べて対話的にサイズが違う / ずれている場合、単純なバイト比較は「1 か所ずれると以降全部違う」と出て使えない。 その場合はブロック単位・構造単位で比べるか、radiff2 のアラインメント機能、bsdiff の差分を使う。
3. 差分を意味に変える
違うバイトの一覧が出たら、それを 01 章のメモリマップに重ねる。
a = open("before.bin","rb").read()
b = open("after.bin","rb").read()
runs, start = [], None
for i in range(min(len(a),len(b))):
if a[i] != b[i]:
if start is None: start = i
else:
if start is not None: runs.append((start, i)); start = None
if start is not None: runs.append((start, len(a)))
for s,e in runs:
print(f"{s:#08x}-{e:#08x} ({e-s} B) {a[s:e][:8].hex()} -> {b[s:e][:8].hex()}")変化した区間が、どの領域(コード・設定・ログ・バージョン)かを見て、意味を推定する。
- 小さな連続した変化 = カウンタ・バージョン・タイムスタンプ
- 4 KB / 64 KB 境界に揃った大きな変化 = フラッシュブロックの書き換え(設定更新、ログ追記)
- コード領域の変化 = ファーム更新
4. リバースエンジニアリングとしての差分
操作を 1 つだけ変えて、前後をダンプする——これは未知の構造を解く最強の手法である。
例: NVM3 / NVS(10 章・11 章)で、あるキーの意味を知りたい。
- 設定を既定にして全体をダンプ(before)
- その設定を 1 つだけ変える(例: 送信出力を 10 → 15)
- 再度ダンプ(after)
- 差分を取ると、変わった数バイトが、その設定の格納場所。値も 10→15 に対応して変わっている
これを設定項目ごとに繰り返せば、ドキュメントのないストレージの地図が埋まる。
5. 壊れたダンプ — まず切り分ける
ダンプが「理屈に合わない」とき、原因は 3 つ。
| 原因 | 兆候 | 対処 |
|---|---|---|
| 読み出しの失敗 | 2 回読むと違う(04 章)、特定区間だけ 0 / 0xFF | 読み直す。配線・電源・クロックを疑う |
| 実際の破損(電源断・劣化) | CRC / ハッシュが合わない、追記の途中で切れている | 破損として扱う(次項) |
| 自分の解釈違い | 1 か所だけ辻褄が合わない | エンディアン・オフセット・アラインメント・構造の版を疑う(最も多い) |
「まず自分を疑う」のが鉄則である。「壊れている」と決める前に、エンディアン(03 章)、ロードアドレス(07 章)、構造体の版、パディングを確かめる。壊れているように見えて、実は読み方が違うだけのことが大半である。
6. 破損の扱い方
本当に壊れている場合の対処。
- CRC / ハッシュで健全性を確認する。MCUboot の SHA-256(08 章)、ログの CRC(12 章)、U-Boot 環境の CRC32(15 章)。合う部分は信頼、合わない部分は破損
- 追記型は最後が切れている前提(10 章〜12 章)。最後の不完全なレコードは捨て、最新の完全なレコードまでを使う
- 冗長性を使う: LittleFS のメタデータペア(09 章)、NVM3 の二重化、ミラー領域。片方が壊れてももう片方が生きている
- 部分的に救う: 全体を復元できなくても、「知りたい 1 つの値」が読める部分にあれば十分なことが多い(01 章の「時間を区切る」)
7. CRC を合わせて検証する
構造の末尾の CRC(03 章)を、実際に計算して照合すると、「自分の構造の理解が正しいか」「壊れていないか」の両方を確かめられる。
import binascii
# 例: レコードの [0:len-4] が本体、末尾 4 バイトが CRC-32 だと仮説
body, crc_stored = rec[:-4], int.from_bytes(rec[-4:], "little")
crc_calc = binascii.crc32(body) & 0xFFFFFFFF
print("一致" if crc_calc == crc_stored else "不一致(構造か CRC 種別が違う、または破損)")合わなければ、CRC の種別(CRC-16 か 32 か)、初期値、ビット反転、対象範囲を変えて試す(crcmod で総当たり)。それでも合わなければ破損を疑う。