Binary Analysis 11 · NVS 系ストレージ — ESP-IDF NVS・Zephyr NVS・キー値の共通形

Chapter 11

NVS 系ストレージ — ESP-IDF NVS・Zephyr NVS・キー値の共通形

この章のゴール.

ESP-IDF NVS と Zephyr NVS を読み解け、 10 章の NVM3 と合わせて「追記型キー値ストアの共通パターン」を身につけること。 対象が変わっても「エントリ・世代・最新版」という同じ見方が使えることを確認する。

この章で使う既出の用語(定義は各リンク先). CRC(03 章 8 節)、オフセット(03 章 1 節)、固定長(03 章 5 節)、QEMU(04 章 6 節)、マップ(04 章 2 節)、テーブル(07 章 6 節)、文字列(07 章 6 節)、Storage(08 章 2 節)、NVM3(10 章 1 節)、ページ(10 章 4 節)

1. 「NVS」という名前の重複に注意

NVS(Non-Volatile Storage)は一般名詞で、複数の別物がこの名を使う。

名前提供元対象チップ
ESP-IDF NVSEspressifESP32 系
Zephyr NVSZephyr RTOS汎用(Nordic、STM32、NXP…)
Zephyr NVS の後継 ZMSZephyr(新しい版)汎用

構造は違うが、追記型のキー値ストアという思想は 10 章の NVM3 と共通である。まず ESP-IDF NVS を詳しく、次に Zephyr NVS、最後に共通パターンをまとめる。

2. ESP-IDF NVS の構造

ESP32 のフラッシュは、先頭付近のパーティションテーブル(オフセット 0x8000、マジック 0xAA 0x50)で領域が分かれる。その中の nvs タイプのパーティションが NVS である。

esptool.py read_flash 0x9000 0x6000 nvs.bin      # パーティションテーブルで示された nvs 領域
# パーティションテーブル自体を読む
esptool.py read_flash 0x8000 0xC00 pt.bin
gen_esp32part.py pt.bin                           # 人が読める表に

NVS 領域は 4 KB のページに分かれ、各ページは次の構成。

部分大きさ内容
ページヘッダ32 バイト状態(active / full / freeing)、シーケンス番号、CRC
エントリ状態ビットマップ32 バイト各エントリが「空 / 書込済 / 消去済」のどれか(2 ビット × 126)
エントリ32 バイト × 126実際のキー値データ

各エントリ(32 バイト)の形:

オフセット内容
0x00Namespace インデックス(キーをグループ分けする番号)
0x01Type(u8/i8/…/u32/i64/string/blob)
0x02Span(このエントリが占める 32 バイト単位の数。長い値は複数エントリ)
0x03ChunkIndex(blob を分割したときの断片番号)
0x04CRC32
0x08Key(最大 15 文字の NUL 終端文字列 + 予約)
0x18Value(小さい値はここに直接。大きい値は後続エントリを指す)
ESP-IDF NVS — 4 KB ページと 32 バイトエントリページヘッダ 32B状態・シーケンス番号・CRC状態ビットマップ 32B各エントリ 空/書込済/消去済エントリ 32B × 126各エントリ(32 バイト)+0x00 Namespace +0x01 Type +0x02 Span +0x03 ChunkIndex+0x04 CRC32 +0x08 Key(最大15文字) +0x18 Value読み方: シーケンス番号でページの新旧 → 状態ビットマップで「書込済」だけ → Namespace+Key+Type+Value を読む → 同じ Namespace+Key は最新を採用 → string/blob は Span 分の後続エントリを連結
ESP-IDF NVS: 4KB ページ + 状態ビットマップ + 32 バイトエントリ(Namespace + 文字列キー)

読み方:

  1. ページヘッダのシーケンス番号で、ページの新旧順を決める
  2. エントリ状態ビットマップで「書込済」のエントリだけを見る(消去済みは飛ばす)
  3. 各エントリの Namespace・Key・Type・Value を読む
  4. 同じ Namespace + Key が複数あれば、新しいページ・後ろのエントリが最新(また追記型)
  5. Type が string / blob なら、Span 分の後続エントリをつないで値を組み立てる

公式ツール

ESP-IDF 5.x には解析ツールがある(版により名前が違う)。

python nvs_tool.py nvs.bin                       # キー値を一覧(ESP-IDF 5.x)
# 生成側(参考)
python nvs_partition_gen.py generate in.csv out.bin 0x6000

esp-idf-nvs-partition-gen とコミュニティ製の nvs_read / パーサも使える。

3. Zephyr NVS の構造

Zephyr NVS は、10・11 章の中で最もシンプルな追記型で、16 ビットの ID をキーにする(キーは文字列ではなく番号。NVM3 に近い)。

構造の要点:

Zephyr NVS — 両端から詰めるデータ(前方から)ATE(後方から)互いに向かって伸ばす。ATE = 16 ビット ID・オフセット・長さ・CRC の固定長記録キーは 16 ビット ID(文字列でなく番号)。後の ATE ほど手前に積まれ、最新の ATE が有効。ソース subsys/fs/nvs/nvs.c の struct nvs_ate が仕様書。後継が ZMS
Zephyr NVS は 16 ビット ID。データは前方、ATE は後方から両端で詰める

Zephyr のソース(subsys/fs/nvs/nvs.c)に ATE 構造体(struct nvs_ate)の定義があり、これが仕様書になる。 後継の ZMS(Zephyr Memory Storage)は、消去操作を必要としないメモリ(RRAM・MRAM)や大容量フラッシュも扱えるように設計が拡張されている。

4. 追記型キー値ストアの共通パターン

10 章の NVM3、この章の ESP-IDF NVS と Zephyr NVS は、細部は違っても同じ 5 つの部品を持つ。一度この型を覚えると、初めて見る独自ストレージも読める。

部品NVM3ESP-IDF NVSZephyr NVS
区画を分ける単位ページ(消去ブロック)4 KB ページセクタ
世代の順序消去カウンタページのシーケンス番号ATE の並び順(後方から)
キー20 ビット番号Namespace + 文字列16 ビット ID
エントリの状態型 / 削除マーカ状態ビットマップATE の有無・CRC
最新版の決定後に書かれたもの後に書かれたもの後の ATE

解析の共通手順:

  1. 区画を消去ブロック(ページ / セクタ)に区切る
  2. 各ブロックの世代番号を読み、新旧を並べる
  3. 各ブロックから有効なエントリを拾う(状態フラグ・削除マーカを見て、消去済みは飛ばす)
  4. 全エントリを集め、同じキーは最新世代のものを採用
  5. 値の型に従って中身を解釈する(03 章)
追記型キー値ストアの共通パターン5 つの共通部品区画を分ける単位(ページ / セクタ)世代の順序(消去カウンタ / シーケンス / ATE 順)キー(20bit / 文字列 / 16bit)エントリの状態(型・削除マーカ・ビットマップ・CRC)最新版の決定(後に書かれたもの)共通の 5 手順① 区画を消去ブロックに区切る ② 各ブロックの世代番号で新旧を並べる ③ 有効なエントリを拾う(消去済みは飛ばす) ④ 同じキーは最新世代を採用 ⑤ 値の型に従い解釈(3 章)一度この型を覚えると、NVM3 も ESP-IDF NVS も Zephyr NVS も、初見の独自ストアも読めるここでも古い値が履歴として残る(10・13 章)。ソースが仕様書
細部は違っても、区画・世代・キー・状態・最新版の 5 部品と 5 手順は共通

5. 履歴とセキュリティ

10 章と同じく、追記型なので過去の値が残る。

6. マウントできる実装もある

ESP-IDF NVS には FUSE 経由のツールが、Zephyr は QEMU / ネイティブ実行でストレージを再現する手段があるが、解析目的ならダンプを直接パースするのが速くて確実である。専用ツール(nvs_tool.py)→ 手動パースの順に試す。

7. 手を動かす

3 つのストアを同じ型で読む


この章のポイント