開発・デバッグ・テスト

メモリダンプ

不具合解析のためメモリ内容を取得する。

概要

メモリダンプ(Memory Dump)とは、マイコンや組み込みシステムのメモリ(RAM・フラッシュ・レジスタ等)の内容を、ある時点のスナップショットとして取得・保存する操作です。不具合解析(ポストモーテム分析)、セキュリティ調査、ファームウェアの動作確認などに使われます。

組み込みシステムでは、クラッシュ(HardFault・ウォッチドッグリセット等)の発生時にスタック内容・レジスタ状態・グローバル変数の値などをフラッシュメモリやシリアルポートに保存することで、フィールドで発生した問題の原因を事後解析できます。これを「コアダンプ」または「クラッシュダンプ」と呼びます。

JTAGSWD経由のインサーキットデバッガを使えば、デバッガからメモリの任意アドレスを読み出してダンプファイルを生成できます。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〜0x3FFFFFFFSRAM領域スタック・ヒープ・グローバル変数
0x40000000〜0x5FFFFFFFペリフェラル領域レジスタ(GPIO・UART等)
0xE0000000〜0xFFFFFFFFSystem/プライベート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デバッグ
実行停止不要(ダンプ取得後に解析)必要不要
フィールド対応可能(ダンプをファイルに保存)不可(デバッガ接続が必要)条件次第
タイミング依存問題対応しやすい停止で再現不可可能(ログ記録)
解析の深さ深い(全メモリ参照可能)深い(対話的)浅い(出力のみ)
セットアップ中(ダンプ取得方法の実装)中(デバッガ環境)
スクリプト対応容易(バイナリ解析)困難テキスト解析で可

関連用語

参考リンク