ESP32の内部:アーキテクチャとファームウェア分析
ESP-WROOM-32チップの内部へのディープダイブ。Xtensa CPU、メモリアーキテクチャを探求し、ファームウェアをリバースエンジニアリングしてその秘密を解き明かす方法。
ESP32チップ#
Internet of Things (IoT) の世界において、Espressif ESP32の名は非常によく知られるようになりました。最近、私はESP-WROOM-32を手に入れ、こう思い始めました。「この中身は一体どうなっているのだろう?」と。これは単なる単純なマイクロコントローラではなく、40nmプロセスで製造された完全なSoC (System-on-a-Chip) なのです。
この小さなチップ一つに、私たちは以下を手にします:
- Wi-Fi (2.4 GHz帯)
- Bluetooth (クラシックとBLE)
- 2つの高性能CPUコア (Dual high-performance cores)
- 超低電力コプロセッサ (Ultra Low Power co-processor)
- 多数のペリフェラル (multiple peripherals)
この記事では、このチップのユニークな命令セットアーキテクチャから、メモリの管理方法、さらにはそのファームウェアを「ダンプ」して何を隠しているのかを探求していきます。
CPU:なぜARMではなくXtensaなのか?#
最初の興味深い点は、ESP32が(STM32やRaspberry Pi Picoのような)一般的なARMアーキテクチャを使用していないことです。代わりに、Tensilica Xtensa LX6 CPUを使用しています。
ARMやx86の固定された命令セットとは異なり、Xtensaはカスタマイズ可能なアーキテクチャです。これは柔軟性を示しています:チップ設計者(Espressifのような)は、デジタルシグナルプロセッシング(DSP)や暗号化などの特定のタスクに最適化するために、CPUにカスタム命令を追加することができ、別のチップを必要とせずにハードウェアを高速化するのに役立ちます。
対称型マルチプロセッシング (SMP)#
ESP32は、2つの完全に同一なXtensa LX6 CPUを搭載したデュアルコアシステムです。これらはほとんどのメモリとペリフェラルを共有しています。これは対称型マルチプロセッシング (SMP) アーキテクチャであり、FreeRTOS(リアルタイムオペレーティングシステム)によって管理されています。
しかし、実際には、これら2つのコアは非常にはっきりとタスクが分担されていることが多いです:
- CORE 0 (PRO_CPU): 「Protocol CPU」の略。このコアは通常、Wi-FiスタックやBluetoothスタックといったプロトコル負荷の高いタスクを実行します。これらのネットワークスタックはリアルタイムシステムであり、ミリ秒単位でパケットに応答しなければなりません。もし遅延すれば、接続は切断されます。
- CORE 1 (APP_CPU): 「Application CPU」の略。ここが通常、ユーザーのアプリケーションコードが実行される場所です。このコアは、Wi-Fi接続に干渉する恐れなく、何でも(Webサーバーの実行、センサーの読み取りなど)自由に行うことができます。1
FreeRTOSは、これら2つのコアを調整するマネージャーです。QueuesやSemaphoresのようなメカニズムを通じて、それらが安全に相互通信できるようにします。
ULP (Ultra-Low-Power) コプロセッサ#
2つのLX6コアの他に、ESP32には第3の脳があります:ULP (Ultra-Low-Power) コプロセッサです。
これは本質的にFSM (Finite State Machine) であり、esp32ulp-assemblerツールを使用してアセンブリでプログラムすることができます。ULPはRTC (Real-Time Clock) ドメインで動作し、2つのメインCPUがdeep sleepモードにある間も動作し続けることができます。
ULPの目的は、センサーの読み取り、ロジック条件のチェック、必要な時にメインCPUを起こすといった単純なタスクを実行することです。これは2つの別々のメモリ領域を使用します:
- RTC_FAST_MEM (8 KB): ULPのコード(命令)を格納します。
- RTC_SLOW_MEM (8 KB): データと状態を格納し、これはディープスリープ中も保持されます。
この設計のおかげで、ESP32は極めて低い消費電力を維持しつつ、定期的な測定や監視を行うことができ、これはバッテリー駆動のIoTアプリケーションにおいて決定的な要因となります。
メモリアーキテクチャ
ESP32は、ハーバード・アーキテクチャに従う2つのXtensa LX6 CPUを搭載したデュアルコアシステムです。つまり、命令用とデータ用に別々のバスを持っているということです。
すべてのメモリ(内蔵メモリ、外部メモリ)およびペリフェラルは、これらのCPUのデータバスおよび/または命令バス上に配置されています。
アドレス空間#
これは非常に重要であり、混同しやすい点です。ESP32は、データバスと命令バスの両方に32ビットのアドレス空間 (4GB) を持っています。これは4GBのRAMを持っているという意味ではありません。CPUが40億個の異なるアドレスを見ることができるという意味です。
物理的なコンポーネントをこの4GB空間にどのようにマッピングするかは、チップ設計者次第です。ESP32では、この4GB空間は以下のように分割されています:
- 1296 KB 内蔵メモリ用
- 19704 KB 外部メモリ用
- 512 KB ペリフェラル用
- 328 KB DMA (Direct Memory Access) 用
内蔵メモリ (オンチップ)#
これは物理的にESP32チップの内部にあるメモリで、非常に高速です。
- 448 KB 内部ROM: 非常に高速ですが、変更不可能です。これにはファーストステージブートローダー(チップ電源投入時に最初に実行されるプログラム)とコアライブラリ(一部のWi-Fi機能、ROM処理など)が含まれています。
- 520 KB 内部SRAM: 非常に高速です。ここが実際にコードが実行され、データが格納される場所です。IRAM(Instruction RAM、実行中のコードを保持するために使用)とDRAM(Data RAM、変数、スタック、ヒープなどのデータを保持するために使用)に分かれています。
- 8 KB RTC FAST Memory: ULPコプロセッサ用に使用されます。
- 8 KB RTC SLOW Memory: チップが
deep sleepモードにあってもデータを保持します。

外部メモリ (オフチップ)#
SPIメモリ(オフチップフラッシュ)は、外部メモリとして使用するために利用可能なアドレス空間にマッピングすることができます。内蔵メモリの一部は、この外部メモリ用の透過的キャッシュとして使用することができます。
- 最大16 MBのオフチップSPIフラッシュをサポート(ここにアプリケーションコードが保存されます。例えば、私のESP-WROOM-32は4MBです)。
- 最大8 MBのオフチップSPI SRAMをサポート(あまり一般的ではなく、RAM拡張用に使用されます)。
図1のブロック図はシステム構成を示しており、図2のブロック図はアドレスマップ構成を示しています。

ESP32は約520 KBのSRAMしかありませんが、プログラムは数MBにもなり、フラッシュ上に存在します。解決策は、SRAMの一部を命令キャッシュとして使用することです — これにより、CPUはプログラム全体をRAMにロードすることなく、フラッシュからコードを実行できます。
ESP32は、内蔵メモリ(SRAM)の一部を、外部フラッシュメモリ用の透過的キャッシュとして使用します。
CPU(例:APP_CPU)があるアドレスの命令を実行しようとすると、メモリコントローラは以下をチェックします:
- この命令はキャッシュ(IRAM)内にあるか?
- はいの場合:IRAMから即座に実行(非常に高速)。
- いいえの場合(キャッシュミス):CPUは一時停止。メモリコントローラはSPIフラッシュから命令のブロックを読み取り(低速)、古いブロックを上書きしてIRAMにロードします。
- CPUはIRAMからの実行を再開します。
このメカニズムにより、私たちは物理的なSRAM容量よりもはるかに大きなプログラムを実行することができます。特定のアドレスの詳細については、ESP32テクニカルリファレンスマニュアルを参照すべきです — そこにはすべてのレジスタアドレスがリストされています。
ブートプロセス#
では、これらすべてのコンポーネント(ROM、フラッシュ、SRAM)はどのように連携して動作するのでしょうか?すべてはチップに電源が投入されたときに始まります:
- ステージ1 (ROM): Xtensa CPUが起動し、固定アドレスからコードの実行を開始します。このアドレスは内部ROM (448 KB) を指しています。このROM内のプログラム(ファーストステージブートローダー)は変更できません。
- ブートモードの確認: ステージ1ブートローダーはストラップピンを読み取り、次に何をすべきかを決定します(例:フラッシュから起動するか、UART経由でのコードアップロードコマンドを待つか)。
- ステージ2 (Flash IRAM): 通常の起動では、フラッシュメモリ(通常はアドレス
0x1000)からセカンドステージブートローダーをIRAM (SRAM) にロードし、その実行を開始します。 - パーティションテーブルの読み取り: このステージ2ブートローダーはより賢いです。パーティションテーブル(通常は
0x8000)を見つけて読み取り、フラッシュのレイアウトを理解します — これは私たちが後で分析するのと同じパーティションテーブルです。 - アプリケーションのロード: 最後に、アプリケーションパーティション(例:
app0)を見つけ、私たちが議論したMMUとキャッシュのメカニズムを使用してこのアプリケーションをアドレス空間にマッピングし、あなたのコード(setup()関数が始まる)に制御を移します。
このプロセスは、ROM(起動用)、フラッシュ(ストレージ用)、SRAM(実行用)の関係を明確に示しています。
メモリマップドI/O (MMIO)#
マイクロコントローラアーキテクチャの興味深い点は:CPU (Xtensa) はGPIO、UART、SPIが何であるかを知らないということです。それが知っているのは、計算することと、メモリの読み書きをすることの2つだけです。
では、どうやってLEDを制御したり、UARTからデータを読み取ったりするのでしょうか? 答えはメモリマップドI/O (MMIO) です。
4GBのアドレス空間の一部が、ペリフェラルの物理ハードウェアに直接接続されています。
- 例:アドレス空間
0x3FF44000からは、GPIOハードウェアに直接接続されています。 - アドレス空間
0x3FF40000からは、UART0ハードウェアに直接接続されています。
プログラマーがdigitalWrite(LED_PIN, HIGH)のようなコードを書くと、基礎となるライブラリはそれを特定のメモリアドレスに値を書き込むコマンド(例:WRITE(0x3FF44008, 0x10))に変換します。CPUはただRAMに書き込んでいると思っていますが、ハードウェアがこの書き込みコマンドを傍受し、GPIOピンの状態を変更します。
このメカニズムにより、プログラマーは(Wi-FiからI2Cまで)すべてを、特定のメモリアドレスからの読み取りと書き込みという同じ単一のメカニズムを使用して制御することができます。
「Hello, Wi-Fi!」#
次に、Wi-Fiに接続するための簡単な「hello world」プログラムを実行してみましょう。私はPlatformIOを使ってコードを素早くコンパイルし、アップロードしました。
#include <Arduino.h>
#include <WiFi.h>
// あなたのWi-Fi認証情報に置き換えてください
const char *ssid = "YOUR-WIFI-NAME";
const char *password = "PASSWORD";
void setup()
{
Serial.begin(115200);
delay(1000);
Serial.println("Starting WiFi...");
WiFi.begin(ssid, password);
// 接続待ち
int attempts = 0;
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
attempts++;
if (attempts > 20)
{
Serial.println("\nFailed to connect to WiFi!");
return;
}
}
Serial.println("\nWiFi connected!");
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
}
void loop()
{
Serial.println("Logging data...");
delay(2000);
}コードをアップロードした後、シリアルモニタを開くと、この結果が得られました:
Starting WiFi...
.....
WiFi connected!
IP address: 192.168.1.105
Logging data...
Logging data...素晴らしい!動作しました。私はssidとpasswordの情報が今チップのどこにあるのか疑問に思い始めました。それは安全に保存されているのでしょうか?
リバースエンジニアリング:フラッシュの中身は?#
次に、リバースエンジニアリング (RE) を実行して、その4MBのフラッシュメモリの中に何があるかを見てみましょう。
フラッシュのダンプ#
私はesptool(Espressif自身のPythonツール)を使用して、フラッシュメモリの全内容を読み取り、ファイルに保存しました。
# アドレス 0x00000 から 4MB (0x400000 バイト) を読み取る
esptool.py --chip esp32 --port /dev/ttyUSB0 read_flash 0x00000 0x400000 flash.bin数分後、flash.binという名前の4MBのファイルが手に入りました。
フラッシュの分析#
このflash.binファイルには、ブートローダー、アプリケーションコード、そして...おそらくデータも含まれています。私はesp32knifeという別のツールを使って、このダンプファイルを分析しました。
esp32knifeを実行すると、最初に見つかるのは、パーティションテーブルとしても知られるフラッシュメモリマップです。私の場合、それはparsed/partitions.csvに保存されていました:
# ESP-IDF Partition Table
# Name, Type, SubType, Offset, Size, Flags
nvs,data,nvs,0x9000,20K,
otadata,data,ota,0xe000,8K,
app0,app,ota_0,0x10000,1280K,
app1,app,ota_1,0x150000,1280K,
spiffs,data,spiffs,0x290000,1408K,
coredump,data,coredump,0x3f0000,64K,主なパーティションがわかります:
- nvs (Non-Volatile Storage): Wi-Fi設定情報のような、キーバリューペアを保存するために使用されます。
- app0/app1 (OTA): ESP32はOver-The-Airアップデートをサポートしています。
app0がメインアプリケーション(私たちが今フラッシュしたもの)、app1が新しいアップデートが保存される場所です。 - spiffs: ファイル(画像、設定、Webページ)を保存するためのシンプルなファイルシステム。
「秘密」の探索#
興味深い部分はnvs (Non-Volatile Storage) パーティションにあります。ここが、WiFi.begin()関数が次回自動再接続するためにログイン認証情報を保存する場所です。
part.0.nvs.csvファイル(esp32knifeによって抽出された)をチェックすると、驚きがありました:
# Key, Type, Encoding, Value
...
wifi.ssid, data, string, "VEVOLVdJRklDVUFCQU4="
wifi.pwd, data, string, "TUFUS0hBVVdJRkk="
...これらのVEVOLV...やTUFUS...という値は見覚えがあります。それらは私のSSIDとパスワードがBase64でエンコードされたものです!
これらの文字列をコピーしてデコードしてみると:
echo "VEVOLVdJRklDVUFCQU4=" | base64 -dYOUR-WIFI-NAMEecho "TUFUS0hBVVdJRkk=" | base64 -dPASSWORD
それらは、NVSパーティション内に(デコード後は)平文としてそのまま保存されていました。
重要な注意: Base64はセキュリティ暗号化ではありません。これは単なるエンコーディング方式であり、NVSライブラリがすべてのデータ文字列(特殊文字を含むものも)をASCIIテキストとして安全に保存できるようにするために使用されます。私たちが見たように、チップへの物理的アクセスを持つ人なら誰でも、フラッシュをダンプし、これらの文字列を簡単にデコードできます。
データを保護したい場合は、ESP32のフラッシュ暗号化またはセキュアブート機能を有効にすべきです。
結論#
一見単純に見えるESP-WROOM-32チップからの旅は、私たちをそのハードウェアアーキテクチャの奥深くへと導きました。ESP32が2つのCPUコアのおかげで強力であるだけでなく、Xtensaアーキテクチャのおかげで柔軟でもあることを見てきました。520KBのSRAMで4MBのコードを実行する背後にあるメカニズム(キャッシュのおかげ)や、CPUが外部世界を制御する方法(MMIOのおかげ)を理解しました。
最後に、ファームウェアをダンプすることで、適切に暗号化されていない場合、Wi-Fiパスワードのような機密情報でさえも抽出可能であることを確認しました。ESP32は、まさに小さなチップに詰め込まれた複雑で強力なシステムです。
閲覧数
— 閲覧数
Nguyen Xuan Hoa
nguyenxuanhoakhtn@gmail.com
