概要
メモリダンプ(Memory Dump)とは、マイコンや組み込みシステムのメモリ(RAM・フラッシュ・レジスタ等)の内容を、ある時点のスナップショットとして取得・保存する操作です。不具合解析(ポストモーテム分析)、セキュリティ調査、ファームウェアの動作確認などに使われます。
組み込みシステムでは、クラッシュ(HardFault・ウォッチドッグリセット等)の発生時にスタック内容・レジスタ状態・グローバル変数の値などをフラッシュメモリやシリアルポートに保存することで、フィールドで発生した問題の原因を事後解析できます。これを「コアダンプ」または「クラッシュダンプ」と呼びます。
JTAGやSWD経由のインサーキットデバッガを使えば、デバッガからメモリの任意アドレスを読み出してダンプファイルを生成できます。GDBのxコマンドやOpenOCDのdump_imageコマンドが代表的な手段です。
歴史・背景
1960〜70年代:メインフレームの時代から「コアダンプ(Core Dump)」の概念が存在しました。プログラムが異常終了した際に磁気コア記憶(main memory)の全内容を磁気テープにダンプし、事後解析する手法です。UNIX系OSでは異常終了時にcoreファイルが生成される伝統が現在も続いています。
1980〜90年代:組み込みシステムが普及するにつれ、フィールドで発生した故障を解析する必要性が高まりました。組み込みLinuxでは/proc/sys/kernel/core_patternでコアダンプの出力先を設定できるようになりました。
2000年代:JTAG/SWDデバッガが普及し、IDEから簡単にメモリダンプを取得できるようになりました。Windowsのミニダンプ(.dmpファイル)やLinuxのcoredumpが開発ツールと連携するようになりました。
2010年代以降:IoT機器の普及に伴い、フィールドのデバイスから自動でクラッシュダンプを収集してクラウドに送信し、メーカー側で解析する「フィールドデバッグ」の仕組みが重要になっています。セキュリティの観点からは、メモリダンプからの情報漏洩(パスワード・暗号鍵の露出)も問題になっています。
技術仕様
ARM Cortex-M のメモリマップ(ダンプ対象)
| アドレス範囲 | 領域 | 内容 |
|---|---|---|
| 0x00000000〜0x1FFFFFFF | コード領域 | フラッシュメモリ(実行コード) |
| 0x20000000〜0x3FFFFFFF | SRAM領域 | スタック・ヒープ・グローバル変数 |
| 0x40000000〜0x5FFFFFFF | ペリフェラル領域 | レジスタ(GPIO・UART等) |
| 0xE0000000〜0xFFFFFFFF | System/プライベート | CoreSight・デバッグ制御 |
ダンプデータのフォーマット
| フォーマット | 用途 | 特徴 |
|---|---|---|
| バイナリ(raw) | 最も一般的 | 圧縮効率が良い、ツールで直接解析可能 |
| Intel HEX(.hex) | フラッシュ書き込み用 | アドレス情報付きテキスト形式 |
| S-Record(.srec) | モトローラ系 | アドレス情報付きテキスト形式 |
| ELF コアダンプ | Linux/GDB向け | シンボル情報・スレッド情報付き |
| JSON | 人間可読 | レジスタ・変数名のデバッグ出力 |
動作原理
GDBによるメモリダンプ
# GDB + OpenOCD で接続中のターゲットのメモリをダンプ
arm-none-eabi-gdb firmware.elf -ex "target remote :3333"
# メモリ表示コマンド(x コマンド)
(gdb) x/10xw 0x20000000 # SRAMの先頭10ワードを16進数で表示
0x20000000: 0x20002000 0x08001234 0x00000000 0xDEADBEEF
0x20000010: 0x00000001 0x00000002 0x0000000A 0x3F800000
0x20000020: 0x00000000 0x00000000
# アドレス書式: x/[count][format][size] address
# count: 表示する要素数
# format: x(16進), d(10進), o(8進), t(2進), c(文字), s(文字列), i(逆アセンブル)
# size: b(1B), h(2B), w(4B), g(8B)
# フラッシュメモリのダンプ(512KB)
(gdb) dump binary memory flash_dump.bin 0x08000000 0x08080000
# SRAMのダンプ(128KB)
(gdb) dump binary memory sram_dump.bin 0x20000000 0x20020000
# スタック領域のみダンプ(シンボルを使ってアドレス解決)
(gdb) p &_estack # スタック終端アドレス確認
(gdb) dump binary memory stack_dump.bin 0x2001C000 0x20020000
OpenOCDによるメモリダンプ(デバッガなしで)
# OpenOCDのtelnet経由でダンプ
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg &
# telnetでOpenOCDに接続
telnet localhost 4444
# フラッシュ全体をダンプ
> dump_image /tmp/flash_full.bin 0x08000000 0x100000 # 1MB
# SRAM をダンプ(STM32F407: 128KB SRAM)
> dump_image /tmp/sram.bin 0x20000000 0x20000
# バイナリの内容確認(ホストで)
xxd /tmp/sram.bin | head -20
HardFault ハンドラでのクラッシュダンプ実装
組み込みシステムでフィールドクラッシュの情報を保存する実装例:
// hardfault_handler.c
#include "stm32f4xx.h"
#include <string.h>
// クラッシュダンプを格納する構造体
typedef struct {
uint32_t magic; // ダンプ識別子: 0xDEADC0DE
uint32_t r0, r1, r2, r3;
uint32_t r12;
uint32_t lr; // リンクレジスタ(呼び出し元アドレス)
uint32_t pc; // プログラムカウンタ(例外発生アドレス)
uint32_t xpsr; // プログラムステータスレジスタ
uint32_t msp; // メインスタックポインタ
uint32_t psp; // プロセススタックポインタ
uint32_t hfsr; // HardFaultステータスレジスタ
uint32_t cfsr; // 設定可能なFaultステータス
uint32_t bfar; // バスフォルトアドレス
uint32_t mmfar; // メモリ管理フォルトアドレス
uint8_t stack_snapshot[256]; // スタックのスナップショット
} CrashDump;
// バックアップSRAM(電源断後も保持)に格納
CrashDump crash_dump __attribute__((section(".backup_sram")));
// HardFault ハンドラ(アセンブリ → C)
void __attribute__((naked)) HardFault_Handler(void) {
__asm volatile (
"TST LR, #4 \n" // EXC_RETURN の bit2 でSP判定
"ITE EQ \n"
"MRSEQ R0, MSP \n" // EQ: MSP(割り込みコンテキスト)
"MRSNE R0, PSP \n" // NE: PSP(タスクコンテキスト)
"B HardFault_C \n"
);
}
void HardFault_C(uint32_t *stack_frame) {
// スタックフレームからレジスタを取得
crash_dump.magic = 0xDEADC0DE;
crash_dump.r0 = stack_frame[0];
crash_dump.r1 = stack_frame[1];
crash_dump.r2 = stack_frame[2];
crash_dump.r3 = stack_frame[3];
crash_dump.r12 = stack_frame[4];
crash_dump.lr = stack_frame[5];
crash_dump.pc = stack_frame[6];
crash_dump.xpsr = stack_frame[7];
// 追加情報
crash_dump.hfsr = SCB->HFSR;
crash_dump.cfsr = SCB->CFSR;
crash_dump.bfar = SCB->BFAR;
crash_dump.mmfar = SCB->MMFAR;
crash_dump.msp = __get_MSP();
crash_dump.psp = __get_PSP();
// スタックのスナップショット(前後128バイト)
memcpy(crash_dump.stack_snapshot,
(uint8_t*)stack_frame - 128, 256);
// リセット前にダンプをUART出力
send_crash_dump_uart(&crash_dump);
// システムリセット
NVIC_SystemReset();
}
// クラッシュダンプをシリアル出力
void send_crash_dump_uart(CrashDump *dump) {
printf("\r\n*** CRASH DUMP ***\r\n");
printf("PC: 0x%08lX\r\n", dump->pc);
printf("LR: 0x%08lX\r\n", dump->lr);
printf("HFSR: 0x%08lX\r\n", dump->hfsr);
printf("CFSR: 0x%08lX\r\n", dump->cfsr);
// PCからファームウェアの問題のある行を特定
// → addr2line コマンドで解析
// arm-none-eabi-addr2line -e firmware.elf 0x0800XXXX
}
addr2line による原因特定
# ダンプのPCアドレス(例: 0x08001ABC)から問題のある行を特定
arm-none-eabi-addr2line -e firmware.elf 0x08001ABC
# → /src/sensor_driver.c:142
# より詳細な情報(関数名とインライン情報)
arm-none-eabi-addr2line -fie firmware.elf 0x08001ABC
# sensor_read_temperature
# /src/sensor_driver.c:142
# 逆アセンブルで確認
arm-none-eabi-objdump -d firmware.elf | grep -A 20 "08001a[0-9a-f]*:"
用途・ユースケース
フィールド不具合の解析
製品出荷後にフィールドで発生したクラッシュを解析する際、ユーザーがデバッガを持っていないため、クラッシュダンプをUSB/シリアル/OTA経由でサーバーに送信してエンジニアが解析します:
# サーバー側: クラッシュダンプの解析スクリプト(Python)
import subprocess
import struct
def parse_crash_dump(binary_data):
"""クラッシュダンプのバイナリを解析"""
MAGIC = 0xDEADC0DE
fields = struct.unpack('<I' * 13, binary_data[:52])
magic, r0, r1, r2, r3, r12, lr, pc, xpsr, msp, psp, hfsr, cfsr = fields
if magic != MAGIC:
print("Invalid dump magic")
return
print(f"PC: 0x{pc:08X} -> ", end='')
# addr2line で行番号を取得
result = subprocess.run(
['arm-none-eabi-addr2line', '-fie', 'firmware.elf', f'0x{pc:08X}'],
capture_output=True, text=True
)
print(result.stdout.strip())
# HardFaultの種類を解析
if hfsr & (1 << 1):
print("VECTTBL: Vector table read error")
if cfsr & 0xFF:
print(f"MemManage Fault at 0x{fields[9]:08X}")
if cfsr & 0xFF00:
print("Bus Fault detected")
セキュリティ監査
暗号鍵やパスワードがRAMに残存していないかを確認するためにメモリダンプを使います:
# SRAMダンプからパスワード文字列を検索
strings sram_dump.bin | grep -i "password\|secret\|key"
# バイナリパターン検索(AES鍵の特徴的なパターン等)
xxd sram_dump.bin | grep -E "[0-9a-f]{32}"
実装・開発のポイント
スタックオーバーフローの検出
// スタックに "canary" パターンを配置して後から確認
#define STACK_CANARY 0xDEADBEEF
void stack_init(void) {
extern uint32_t _sstack; // スタック先頭(低アドレス)
extern uint32_t _estack; // スタック末端(高アドレス)
// スタック全体をカナリーパターンで埋める
uint32_t *p = &_sstack;
while (p < &_estack) {
*p++ = STACK_CANARY;
}
}
uint32_t stack_get_highwater_mark(void) {
extern uint32_t _sstack;
extern uint32_t _estack;
uint32_t *p = &_sstack;
while (*p == STACK_CANARY && p < &_estack) {
p++;
}
return (uint32_t)(&_estack) - (uint32_t)p; // 使用量(バイト)
}
ダンプファイルの比較(デグレード検出)
# 2つのファームウェアバージョンのフラッシュダンプを比較
diff <(xxd v1.2.bin) <(xxd v1.3.bin) | head -50
# 変更されたセクションを確認
arm-none-eabi-objdump -h firmware_v1.2.elf
arm-none-eabi-objdump -h firmware_v1.3.elf
他技術との比較
| 比較項目 | メモリダンプ(事後解析) | ブレークポイント(リアルタイム) | printfデバッグ |
|---|---|---|---|
| 実行停止 | 不要(ダンプ取得後に解析) | 必要 | 不要 |
| フィールド対応 | 可能(ダンプをファイルに保存) | 不可(デバッガ接続が必要) | 条件次第 |
| タイミング依存問題 | 対応しやすい | 停止で再現不可 | 可能(ログ記録) |
| 解析の深さ | 深い(全メモリ参照可能) | 深い(対話的) | 浅い(出力のみ) |
| セットアップ | 中(ダンプ取得方法の実装) | 中(デバッガ環境) | 低 |
| スクリプト対応 | 容易(バイナリ解析) | 困難 | テキスト解析で可 |