Inside the ESP32: Architecture & Firmware Analysis
Hành trình khám phá sâu bên trong chip ESP-WROOM-32, tìm hiểu về CPU Xtensa, kiến trúc bộ nhớ, và cách reverse-engineering firmware để tìm ra những bí mật.
Chip ESP32#
Trong thế giới Internet of Things (IoT), cái tên Espressif ESP32 đã trở nên quá quen thuộc. Gần đây, tôi có trong tay một con ESP-WROOM-32 và bắt đầu tò mò: Điều gì thực sự nằm bên trong nó? Nó không chỉ là một vi điều khiển đơn thuần; nó là một SoC (System-on-a-Chip) hoàn chỉnh được sản xuất trên tiến trình 40nm.
Chỉ trên một con chip nhỏ, chúng ta có:
- Wi-Fi (băng tần 2.4 GHz)
- Bluetooth (Classic và BLE)
- Hai lõi CPU hiệu năng cao (Dual high-performance cores)
- Một bộ đồng xử lý siêu tiết kiệm điện (Ultra Low Power co-processor)
- Rất nhiều thiết bị ngoại vi (multiple peripherals)
Trong bài viết này, chúng ta sẽ cùng khám phá con chip này, từ kiến trúc tập lệnh (instruction set) độc đáo của nó, cách nó quản lý bộ nhớ, cho đến việc "dump" firmware ra để xem nó che giấu những gì.
CPU: Tại sao là Xtensa mà không phải ARM?#
Điều đầu tiên gây tò mò là ESP32 không sử dụng kiến trúc ARM phổ biến (như trong STM32 hay Raspberry Pi Pico). Thay vào đó, nó sử dụng CPU Tensilica Xtensa LX6.
Không giống như các tập lệnh cố định của ARM hay x86, Xtensa là một kiến trúc có thể tùy biến. Điều này thể hiện sự linh hoạt: Các nhà thiết kế chip (như Espressif) có thể thêm các lệnh tùy chỉnh (custom instructions) vào CPU để tối ưu hóa cho các tác vụ cụ thể, ví dụ như xử lý tín hiệu số (DSP) hoặc mã hóa, giúp tăng tốc phần cứng mà không cần thêm một chip riêng.
Symmetric Multiprocessing (SMP)#
ESP32 là một hệ thống lõi kép với hai CPU Xtensa LX6 hoàn toàn giống hệt nhau. Chúng chia sẻ hầu hết bộ nhớ và ngoại vi. Đây là một kiến trúc Symmetric Multiprocessing (SMP) và được quản lý bởi FreeRTOS (một hệ điều hành thời gian thực).
Tuy nhiên, trong thực tế, hai lõi này thường được phân chia nhiệm vụ rất rõ ràng:
- CORE 0 (PRO_CPU): Viết tắt của "Protocol CPU". Lõi này thường chạy các tác vụ nặng về giao thức như Wi-Fi stack và Bluetooth stack. Các stack mạng này là các hệ thống thời gian thực (real-time), chúng phải phản hồi các gói tin trong vài mili giây. Nếu chúng bị trễ, kết nối sẽ bị rớt.
- CORE 1 (APP_CPU): Viết tắt của "Application CPU". Đây là nơi thường chạy code ứng dụng của người dùng. Lõi này có thể tự do làm bất cứ điều gì (chạy web server, đọc cảm biến,...) mà không sợ làm ảnh hưởng đến kết nối Wi-Fi. 1
FreeRTOS chính là người quản lý, điều phối hai lõi này. Nó cho phép chúng giao tiếp an toàn với nhau thông qua các cơ chế như Queues và Semaphores.
Bộ đồng xử lý (Coprocessor) ULP#
Bên cạnh hai lõi LX6, ESP32 còn có một bộ não thứ ba: ULP (Ultra-Low-Power) Coprocessor — viết tắt của bộ đồng xử lý siêu tiết kiệm năng lượng.
Đây thực chất là một FSM (Finite State Machine – Máy trạng thái hữu hạn) có thể lập trình bằng assembly thông qua công cụ esp32ulp-assembler. ULP hoạt động trong miền RTC (Real-Time Clock domain), cho phép nó tiếp tục chạy ngay cả khi hai CPU chính đang ở chế độ deep sleep.
Mục đích của ULP là thực hiện các tác vụ đơn giản như đọc cảm biến, kiểm tra điều kiện logic, và đánh thức CPU chính khi cần thiết. Nó sử dụng hai vùng bộ nhớ riêng:
- RTC_FAST_MEM (8 KB): chứa mã lệnh của ULP.
- RTC_SLOW_MEM (8 KB): lưu dữ liệu và trạng thái, vẫn được duy trì trong chế độ ngủ sâu.
Nhờ thiết kế này, ESP32 có thể thực hiện các phép đo hoặc giám sát định kỳ mà vẫn duy trì mức tiêu thụ điện cực thấp — một yếu tố quan trọng trong các ứng dụng IoT chạy bằng pin.
Kiến trúc Bộ nhớ#
ESP32 là một hệ thống dual-core với hai CPU Xtensa LX6 theo kiến trúc Harvard (Harvard Architecture), nghĩa là nó có các bus riêng biệt cho lệnh (instruction) và dữ liệu (data).
Tất cả bộ nhớ (embedded memory, external memory) và các thiết bị ngoại vi đều nằm trên data bus và/hoặc instruction bus của các CPU này.
Address Space#
Đây là điểm cực kỳ quan trọng và dễ gây nhầm lẫn. ESP32 có không gian địa chỉ 32-bit (4GB) cho cả data bus và instruction bus. Điều này không có nghĩa là nó có 4GB RAM. Nó có nghĩa là CPU có thể nhìn thấy 4 tỷ địa chỉ khác nhau.
Việc các thành phần vật lý được ánh xạ vào không gian 4GB này như thế nào là do nhà thiết kế chip quyết định. Trên ESP32, không gian 4GB này được chia nhỏ như sau:
- 1296 KB cho Bộ nhớ nhúng (Embedded Memory)
- 19704 KB cho Bộ nhớ ngoài (External Memory)
- 512 KB cho Ngoại vi (Peripherals)
- 328 KB cho DMA (Direct Memory Access)
Embedded Memory (On-Chip)#
Đây là bộ nhớ nằm vật lý bên trong chip ESP32, siêu nhanh.
- 448 KB Internal ROM: Rất nhanh, nhưng không thể sửa đổi. Nó chứa First-Stage Bootloader (chương trình đầu tiên chạy khi chip có điện) và các thư viện cốt lõi (như một số hàm Wi-Fi, xử lý ROM).
- 520 KB Internal SRAM: Cực kỳ nhanh. Đây là nơi code thực sự chạy và dữ liệu được lưu trữ. Nó được chia thành IRAM (Instruction RAM, dùng để chứa mã lệnh đang chạy) và DRAM (Data RAM, dùng để chứa dữ liệu như biến, stack, heap).
- 8 KB RTC FAST Memory: Dùng cho ULP Coprocessor.
- 8 KB RTC SLOW Memory: Giữ được dữ liệu ngay cả khi chip ở chế độ
deep sleep.

External Memory (Off-Chip)#
Bộ nhớ SPI (Off-Chip Flash) có thể được ánh xạ vào không gian địa chỉ sẵn có để sử dụng như external memory. Một phần embedded memory có thể được sử dụng làm transparent cache cho bộ nhớ ngoài này.
- Hỗ trợ lên đến 16 MB off-Chip SPI Flash (đây là nơi chứa code ứng dụng của bạn, như con ESP-WROOM-32 của tôi có 4MB).
- Hỗ trợ lên đến 8 MB off-Chip SPI SRAM (ít phổ biến hơn, dùng để mở rộng RAM).
Sơ đồ khối trong Hình 1 minh họa cấu trúc hệ thống và sơ đồ khối trong Hình 2 minh họa cấu trúc bản đồ địa chỉ.

ESP32 chỉ có khoảng 520 KB SRAM, nhưng chương trình có thể lớn tới vài MB nằm trên Flash. Giải pháp là sử dụng một phần SRAM làm instruction cache — cho phép CPU thực thi mã lệnh từ Flash mà không cần tải toàn bộ vào RAM.
ESP32 sử dụng một phần của Embedded Memory (SRAM) làm transparent cache cho bộ nhớ Flash bên ngoài.
Khi CPU (ví dụ, APP_CPU) cố gắng thực thi một lệnh tại một địa chỉ, bộ điều khiển bộ nhớ sẽ kiểm tra:
- Lệnh này có trong cache (IRAM) không?
- Nếu có: Thực thi ngay lập tức từ IRAM (rất nhanh).
- Nếu không (Cache miss): CPU tạm dừng. Bộ điều khiển bộ nhớ sẽ đọc một khối (block) lệnh từ SPI Flash (chậm) và nạp nó vào IRAM, ghi đè lên khối cũ.
- CPU tiếp tục thực thi lệnh từ IRAM.
Cơ chế này cho phép chúng ta chạy các chương trình lớn hơn nhiều so với dung lượng SRAM vật lý. Để biết chi tiết các địa chỉ cụ thể, bạn nên tham khảo ESP32 Technical Reference Manual—nó liệt kê tất cả các địa chỉ thanh ghi.
Boot Process#
Vậy làm thế nào để tất cả các thành phần này (ROM, Flash, SRAM) làm việc cùng nhau? Mọi thứ bắt đầu khi chip được cấp điện:
- Giai đoạn 1 (ROM): CPU Xtensa tỉnh dậy và bắt đầu thực thi mã lệnh từ một địa chỉ cố định. Địa chỉ này trỏ đến Internal ROM (448 KB). Chương trình trong ROM này (First-Stage Bootloader) không thể bị thay đổi.
- Kiểm tra Chế độ Boot: Bootloader Giai đoạn 1 sẽ đọc các chân cắm (strapping pins) để quyết định phải làm gì tiếp theo (ví dụ: boot từ Flash hay chờ lệnh nạp code qua UART).
- Giai đoạn 2 (Flash IRAM): Ở chế độ boot bình thường, nó sẽ tải Second-Stage Bootloader từ bộ nhớ Flash (thường ở địa chỉ
0x1000) vào IRAM (SRAM) và bắt đầu chạy nó. - Đọc Bảng Phân vùng: Bootloader Giai đoạn 2 này thông minh hơn. Nó sẽ tìm và đọc Bảng Phân vùng (Partition Table) (thường ở
0x8000) để hiểu được cấu trúc của Flash — chính là bảng phân vùng mà chúng ta sẽ phân tích ở phần sau. - Tải Ứng dụng: Cuối cùng, nó tìm phân vùng ứng dụng (ví dụ
app0), sử dụng cơ chế MMU và Cache mà chúng ta đã nói ở trên để ánh xạ ứng dụng này vào không gian địa chỉ, và chuyển quyền điều khiển cho code của bạn (hàmsetup()bắt đầu).
Quá trình này minh họa rõ ràng mối liên hệ giữa ROM (dùng để khởi động), Flash (dùng để lưu trữ), và SRAM (dùng để thực thi).
Memory-Mapped I/O (MMIO)#
Một điều thú vị về kiến trúc vi điều khiển là: CPU (Xtensa) không hề biết GPIO, UART, hay SPI là gì. Nó chỉ biết đúng 2 việc: Tính toán, và Đọc/Ghi Bộ nhớ.
Vậy làm sao nó điều khiển được đèn LED hay đọc dữ liệu từ UART? Câu trả lời là Memory-Mapped I/O (MMIO).
Một phần của không gian địa chỉ 4GB được nối thẳng vào phần cứng vật lý của các ngoại vi.
- Ví dụ: Không gian địa chỉ từ
0x3FF44000được nối thẳng vào phần cứng GPIO. - Không gian địa chỉ từ
0x3FF40000được nối thẳng vào phần cứng UART0.
Khi lập trình viên viết code digitalWrite(LED_PIN, HIGH), thư viện cơ bản sẽ dịch nó thành một lệnh ghi giá trị vào một địa chỉ bộ nhớ cụ thể (ví dụ: WRITE(0x3FF44008, 0x10)). CPU chỉ nghĩ rằng nó đang ghi vào RAM, nhưng phần cứng sẽ bắt lệnh ghi này và thay đổi trạng thái chân GPIO.
Cơ chế này cho phép lập trình viên điều khiển mọi thứ (từ Wi-Fi đến I2C) bằng cùng một cơ chế duy nhất: đọc và ghi vào các địa chỉ bộ nhớ cụ thể.
"Hello, Wi-Fi!"#
Tiếp theo, hãy thực hiện một chương trình "hello world" đơn giản để kết nối Wi-Fi. Tôi đã sử dụng PlatformIO để biên dịch và nạp code cho nhanh.
#include <Arduino.h>
#include <WiFi.h>
// Thay bằng thông tin Wi-Fi của bạn
const char *ssid = "TEN-WIFI-CUA-BAN";
const char *password = "MAT-KHAU-WIFI";
void setup()
{
Serial.begin(115200);
delay(1000);
Serial.println("Starting WiFi...");
WiFi.begin(ssid, password);
// Chờ kết nối
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);
}Sau khi nạp code, tôi mở Serial Monitor và nhận được kết quả:
Starting WiFi...
.....
WiFi connected!
IP address: 192.168.1.105
Logging data...
Logging data...Tuyệt vời! Nó đã hoạt động. Tôi bắt đầu tò mò về thông tin ssid và password bây giờ đang nằm ở đâu trong chip? Liệu nó có được lưu trữ an toàn không?
Reverse Engineering: Bên trong Flash có gì?#
Tiếp theo, hãy thực hiện Reverse Engineering (RE) để xem bên trong bộ nhớ Flash 4MB kia có gì.
Dump Flash#
Tôi sử dụng esptool (một công cụ Python của chính Espressif) để đọc toàn bộ nội dung của bộ nhớ flash và lưu nó vào một file.
# Đọc 4MB (0x400000 bytes) từ địa chỉ 0x00000
esptool.py --chip esp32 --port /dev/ttyUSB0 read_flash 0x00000 0x400000 flash.binSau vài phút, tôi có file flash.bin dung lượng 4MB.
Phân tích Flash#
File flash.bin này chứa mọi thứ: bootloader, code ứng dụng, và... có thể cả dữ liệu nữa. Tôi dùng một công cụ khác là esp32knife để phân tích file dump này.
Khi chạy esp32knife, điều đầu tiên chúng ta tìm thấy là bản đồ của bộ nhớ flash, hay còn gọi là Bảng Phân vùng (Partition Table). Trong trường hợp của tôi, nó được lưu trong 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,Chúng ta có thể thấy các phân vùng chính:
- nvs (Non-Volatile Storage): Dùng để lưu các cặp key-value, như thông tin cấu hình Wi-Fi.
- app0/app1 (OTA): ESP32 hỗ trợ Over-The-Air updates.
app0là ứng dụng chính (chúng ta vừa nạp),app1là nơi chứa bản cập nhật mới. - spiffs: Một hệ thống file đơn giản để lưu trữ file (ảnh, config, web page).
Tìm kiếm "Bí mật"#
Điều thú vị nằm ở phân vùng nvs (Non-Volatile Storage). Đây là nơi chương trình WiFi.begin() lưu trữ thông tin đăng nhập để tự động kết nối lại lần sau.
Khi kiểm tra file part.0.nvs.csv (được trích xuất bởi esp32knife), tôi đã tìm thấy một điều bất ngờ:
# Key, Type, Encoding, Value
...
wifi.ssid, data, string, "VEVOLVdJRklDVUFCQU4="
wifi.pwd, data, string, "TUFUS0hBVVdJRkk="
...Các giá trị VEVOLV... và TUFUS... kia trông rất quen. Chúng chính là SSID và Mật khẩu của tôi, đã được mã hóa Base64!
Khi tôi sao chép các chuỗi này và giải mã:
echo "VEVOLVdJRklDVUFCQU4=" | base64 -dTEN-WIFI-CUA-BANecho "TUFUS0hBVVdJRkk=" | base64 -dMAT-KHAU-WIFI
Chúng ở ngay đó, được lưu trữ dưới dạng văn bản (sau khi giải mã) ngay trong phân vùng NVS.
Lưu ý quan trọng: Base64 không phải là mã hóa bảo mật. Nó chỉ là một phương thức mã hóa biểu diễn (encoding), được thư viện NVS sử dụng để đảm bảo mọi chuỗi dữ liệu (kể cả chứa ký tự đặc biệt) có thể được lưu trữ an toàn dưới dạng văn bản ASCII. Như chúng ta thấy, bất kỳ ai có quyền truy cập vật lý vào chip đều có thể dump flash và giải mã các chuỗi này một cách dễ dàng.
Nếu muốn bảo vệ dữ liệu, bạn nên bật tính năng Flash Encryption hoặc Secure Boot của ESP32.
Kết luận#
Hành trình từ một con chip ESP-WROOM-32 tưởng chừng đơn giản đã dẫn chúng ta đi sâu vào kiến trúc phần cứng. Chúng ta đã thấy ESP32 không chỉ mạnh mẽ nhờ hai lõi CPU, mà còn linh hoạt nhờ kiến trúc Xtensa. Chúng ta đã hiểu được cơ chế đằng sau việc chạy code 4MB trên SRAM 520KB (nhờ cache), và cách CPU điều khiển thế giới bên ngoài (nhờ MMIO).
Cuối cùng, bằng cách dump firmware, chúng ta thấy rằng ngay cả những thông tin nhạy cảm như mật khẩu Wi-Fi cũng có thể bị trích xuất nếu không được mã hóa cẩn thận. ESP32 thực sự là một hệ thống phức tạp và mạnh mẽ được gói gọn trong một con chip nhỏ bé.
lượt xem
— lượt xem
Nguyen Xuan Hoa
nguyenxuanhoakhtn@gmail.com
