Chapter 10
NVM3 — Silicon Labs の設定ストレージを読む
この章のゴール.
NVM3 が「追記だけで更新する鍵値ストア」であることを理解し、 ダンプからオブジェクトの列を拾い、同じキーの最新値を復元できるようになること。 なぜこの作りなのか(フラッシュの制約)を、10 章以降のログ・NVS にも通じる形で理解すること。
この章で使う既出の用語(定義は各リンク先). メモリマップ(01 章 3 節)、CRC(03 章 8 節)、オフセット(03 章 1 節)、サイズ(04 章 7 節)、バージョン(08 章 3 節)
1. NVM3 とは何か
NVM3(Non-Volatile Memory version 3)は、Silicon Labs(EFR32 / EFM32、Zigbee・Thread・Bluetooth の SoC)が使う不揮発データストレージのライブラリである。 設定値・較正値・ネットワーク鍵・Zigbee のバインディングなどを、キー(20 ビットの番号)と値(バイト列)の組として、内蔵フラッシュの一区画に保存する。
PSA 編 13 章の ITS / PS が「PSA の標準ストレージ API」だったのに対し、NVM3 は「Silicon Labs 実装の中身」である。同じ機器で、PSA ITS の裏側が NVM3 になっていることもある。
2. 追記型の設計 — なぜそうなるか
09 章で見たように、フラッシュは「消去はブロック単位、書き込みは 1→0 だけ」という制約を持つ。 キー 0x4001 の値を更新するたびに、そのキーの場所を消して書き直すのは、消去ブロック全体を巻き込み、寿命を縮める。
そこで NVM3(と、11 章の NVS、12 章のログ)は共通の戦略を取る。
更新は「上書き」ではなく「新しい版を末尾に追記」。古い版は残したまま、最新の版だけを有効とみなす。
- 各更新は、区画の空いている場所に新しいオブジェクトとして書かれる
- 同じキーの古いオブジェクトはそのまま残る(消さない)。ただし「無効」の印がつく、または「後のものが優先」という規則で無視される
- 区画が埋まってきたら、有効な最新版だけを別のブロックへ集めて、古いブロックを消す(ガベージコレクション、リペイジング)
この設計が、解析にとって重要な意味を持つ。
古い値がフラッシュに残っている.
追記型なので、過去の設定値・過去の鍵・削除したはずのデータが、区画の中に履歴として残っていることが多い。 これは障害解析では宝(設定がいつ変わったか追える)だが、セキュリティ監査では危険(消したはずの鍵が読める、13 章)でもある。 「最新値」だけでなく「そのキーの全履歴」を拾えることを意識する。
3. まず公式ツールを試す
01 章の原則どおり、自分で解く前に Simplicity Commander を試す。
commander nvm3 parse nvm3_dump.bin --tokengroup all
# または device から直接
commander nvm3 read --device EFR32MG21 --outfile nvm3.binこれが動けば、キーと値の一覧が得られる。動かない(ダンプが部分的、区画オフセットが不明、壊れている)ときに、以下の手動解析に進む。
4. NVM3 の構造
NVM3 の区画は、ページ(フラッシュの消去ブロックに対応、通常複数)に分かれ、各ページはページヘッダの後ろにオブジェクトが詰まっている。
ページヘッダ
各ページの先頭に、そのページの世代を示す消去カウンタ(erase count)やバージョンが入る。 リペイジングのたびにカウンタが増えるので、どのページが新しいかが分かる。
オブジェクト
各オブジェクトは、おおむね次の形のヘッダ + データである(バージョンで詳細は異なる)。
| 部分 | 内容 |
|---|---|
| オブジェクトヘッダ | キー(20 ビット)、型(データ / カウンタ / 削除マーカ)、長さ、フラグ |
| データ | 値そのもの(小さい値はヘッダに埋め込まれることも) |
| (末尾) | CRC など整合性の情報 |
オブジェクトは4 バイト境界に揃えて次々と追記される。だから区画を先頭から順にたどれば、オブジェクトの列を復元できる。
5. 手で拾う手順
公式ツールが使えないときの、原理どおりの読み方。
- 区画の範囲を特定する。フラッシュ全体のどこが NVM3 か(メモリマップ、
stringsで近くにヒントがないか、規則的なページ境界) - ページヘッダを見つけ、消去カウンタを読む。ページサイズ(消去ブロック、多くは 8 KB / 2 KB)ごとに区切る
- 各ページで、ヘッダの後ろからオブジェクトヘッダをパースし、キー・型・長さを読み、長さ分進んで次のオブジェクトへ。4 バイト境界に整列
- 0xFF が続くところがそのページの空き(未使用)。そこでページ終わり
- 全オブジェクトを集め、同じキーは「後に書かれたもの(新しいページ・後ろの位置)」を最新とする。削除マーカがあれば、そのキーは削除済み
# 概念コード(実際のヘッダ形式はバージョンに合わせる)
def parse_page(page, page_base):
objs, off = [], HEADER_LEN
while off < len(page):
if page[off:off+4] == b'\xff\xff\xff\xff': # 空き
break
key, otype, length = parse_obj_header(page, off) # ← 要実装
val = page[off+HDR : off+HDR+length]
objs.append((page_base+off, key, otype, val))
off += align4(HDR + length)
return objs実際のヘッダのビット割り当ては、NVM3 のソース(nvm3_*.c、Silicon Labs が公開)を仕様書として読むのが確実である。ここでも「ライブラリのソースが最強の仕様書」(01 章)が効く。
6. キーの意味を知る
キーは 20 ビットの番号で、それ自体に意味はない。何のキーかは、そのアプリケーションが決める。
- Zigbee / Thread / BLE スタックは、決まった範囲のキーをネットワーク鍵・アドレス・バインディングに使う。SDK のヘッダ(
*_token.h、sl_token_manager)にキーの定義がある - アプリ独自のキーは、そのファームウェアのソースにある
- 値の中身(構造体)も、キーごとにアプリが決める。値だけ取り出しても、03 章の手順で構造を解く必要がある
7. 障害解析での使いどころ
- 設定がおかしい機器: 最新値と履歴を比べ、いつ・何が異常な値に変わったかを追う
- 鍵が消えた: 削除マーカやリペイジングの失敗(電源断)で、有効な版が失われていないか
- 寿命: 消去カウンタが異常に大きければ、書き込みが多すぎて摩耗している
8. 手を動かす
追記型ストレージから最新値を復元する
この章のポイント
- NVM3 は Silicon Labs のキー値ストア。更新は上書きせず末尾に追記、最新版を有効とする
- 追記型なので、古い値・消したはずのデータが履歴として残る(障害解析の宝、監査の危険)
- まず Simplicity Commander
nvm3 parse。動かなければページ → オブジェクトを手でたどる - ページヘッダの消去カウンタで新旧、オブジェクトはキー・型・長さ、4 バイト境界で連続
- キーの意味は SDK のトークン定義とアプリのソースで引く