詳解5: メモリデバッグの実践

この章の目標

この章を終えると、以下ができるようになります:

  • デバッグアロケータのパターンを実装できる
  • センチネル値とカナリアによる破損検出を理解できる
  • スタックトレースキャプチャの仕組みを説明できる
  • メモリビジュアライゼーション技法を使える
  • 本番環境のメモリ問題をデバッグできる
  • 実際のメモリバグを特定して修正できる

なぜメモリデバッグの深い理解が重要か

実践的なシナリオ

const std = @import("std");

// ケース1: 製品リリース直前のバグ
// 本番環境でのみ発生、ローカルでは再現しない
pub fn productionMysteryBug() !void {
    // 1000リクエストに1回クラッシュ
    // スタックトレースなし
    // ログにも何も残らない
    //
    // どうデバッグする?
}

// ケース2: パフォーマンス劣化
// メモリ使用量が時間とともに増加
// 明らかなリークはない
// でもメモリが返却されない
pub fn mysteriousMemoryGrowth() !void {
    // デバッグツールが必要
}

// ケース3: 断続的なクラッシュ
// マルチスレッド環境
// データ競合が疑われる
// でも再現が困難
pub fn intermittentCrash() !void {
    // 適切なツールで追跡
}

デバッグアロケータのパターン

パターン1: トレーシングアロケータ

全ての割り当て/解放を記録し、後で分析できるようにします。

const std = @import("std");

pub const TracingAllocator = struct {
    parent: std.mem.Allocator,
    traces: std.ArrayList(AllocationTrace),
    next_id: usize,
    mutex: std.Thread.Mutex,

    pub const AllocationTrace = struct {
        id: usize,
        ptr: usize,
        size: usize,
        alignment: u8,
        stack_trace: [8]usize,
        timestamp_ns: u64,
        freed: bool,
        free_timestamp_ns: u64,
    };

    pub fn init(parent: std.mem.Allocator) !TracingAllocator {
        return .{
            .parent = parent,
            .traces = std.ArrayList(AllocationTrace).init(parent),
            .next_id = 1,
            .mutex = .{},
        };
    }

    pub fn deinit(self: *TracingAllocator) void {
        self.traces.deinit();
    }

    pub fn allocator(self: *TracingAllocator) std.mem.Allocator {
        return .{
            .ptr = self,
            .vtable = &.{
                .alloc = alloc,
                .resize = resize,
                .free = free,
            },
        };
    }

    fn alloc(ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8 {
        const self = @as(*TracingAllocator, @ptrCast(@alignCast(ctx)));

        const result = self.parent.vtable.alloc(
            self.parent.ptr,
            len,
            ptr_align,
            ret_addr,
        ) orelse return null;

        // トレースを記録
        self.mutex.lock();
        defer self.mutex.unlock();

        const trace = AllocationTrace{
            .id = self.next_id,
            .ptr = @intFromPtr(result),
            .size = len,
            .alignment = ptr_align,
            .stack_trace = captureStackTrace(ret_addr),
            .timestamp_ns = @intCast(std.time.nanoTimestamp()),
            .freed = false,
            .free_timestamp_ns = 0,
        };

        self.next_id += 1;
        self.traces.append(trace) catch {};

        return result;
    }

    fn resize(ctx: *anyopaque, buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool {
        const self = @as(*TracingAllocator, @ptrCast(@alignCast(ctx)));
        return self.parent.vtable.resize(self.parent.ptr, buf, buf_align, new_len, ret_addr);
    }

    fn free(ctx: *anyopaque, buf: []u8, buf_align: u8, ret_addr: usize) void {
        const self = @as(*TracingAllocator, @ptrCast(@alignCast(ctx)));

        // トレースを更新
        self.mutex.lock();
        defer self.mutex.unlock();

        const ptr = @intFromPtr(buf.ptr);
        for (self.traces.items) |*trace| {
            if (trace.ptr == ptr and !trace.freed) {
                trace.freed = true;
                trace.free_timestamp_ns = @intCast(std.time.nanoTimestamp());
                break;
            }
        }

        self.parent.vtable.free(self.parent.ptr, buf, buf_align, ret_addr);
    }

    // レポート生成
    pub fn generateReport(self: *TracingAllocator, writer: anytype) !void {
        self.mutex.lock();
        defer self.mutex.unlock();

        try writer.print("\n=== Allocation Trace Report ===\n", .{});
        try writer.print("Total allocations: {}\n", .{self.next_id - 1});

        var leaked: usize = 0;
        var leaked_bytes: usize = 0;

        for (self.traces.items) |trace| {
            if (!trace.freed) {
                leaked += 1;
                leaked_bytes += trace.size;
            }
        }

        try writer.print("Leaked allocations: {}\n", .{leaked});
        try writer.print("Leaked bytes: {}\n\n", .{leaked_bytes});

        if (leaked > 0) {
            try writer.print("Leaked Allocations:\n", .{});
            for (self.traces.items) |trace| {
                if (!trace.freed) {
                    try writer.print("  #{}: {} bytes at 0x{x}\n", .{
                        trace.id,
                        trace.size,
                        trace.ptr,
                    });
                    try writer.print("    Allocated at:\n", .{});
                    for (trace.stack_trace, 0..) |addr, i| {
                        if (addr == 0) break;
                        try writer.print("      #{}: 0x{x}\n", .{ i, addr });
                    }
                }
            }
        }
    }

    fn captureStackTrace(ret_addr: usize) [8]usize {
        var trace: [8]usize = undefined;
        trace[0] = ret_addr;
        var i: usize = 1;
        while (i < 8) : (i += 1) {
            trace[i] = 0;
        }
        return trace;
    }
};

パターン2: バウンズチェッキングアロケータ

メモリの前後にガード領域を設け、オーバーフロー/アンダーフローを検出します。

const std = @import("std");

pub const BoundsCheckingAllocator = struct {
    parent: std.mem.Allocator,

    const GUARD_SIZE = 16;
    const GUARD_PATTERN: [GUARD_SIZE]u8 = [_]u8{0xDE} ** GUARD_SIZE;

    const Header = struct {
        magic: u32,
        size: usize,
        alignment: u8,
        guard: [GUARD_SIZE]u8,
    };

    const Footer = struct {
        guard: [GUARD_SIZE]u8,
        magic: u32,
    };

    const HEADER_MAGIC: u32 = 0x48454144; // "HEAD"
    const FOOTER_MAGIC: u32 = 0x464F4F54; // "FOOT"

    pub fn init(parent: std.mem.Allocator) BoundsCheckingAllocator {
        return .{ .parent = parent };
    }

    pub fn allocator(self: *BoundsCheckingAllocator) std.mem.Allocator {
        return .{
            .ptr = self,
            .vtable = &.{
                .alloc = alloc,
                .resize = resize,
                .free = free,
            },
        };
    }

    fn alloc(ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8 {
        const self = @as(*BoundsCheckingAllocator, @ptrCast(@alignCast(ctx)));

        // 実際のサイズ: Header + User Data + Footer
        const total_size = @sizeOf(Header) + len + @sizeOf(Footer);

        const raw_ptr = self.parent.vtable.alloc(
            self.parent.ptr,
            total_size,
            ptr_align,
            ret_addr,
        ) orelse return null;

        // ヘッダーを設定
        const header = @as(*Header, @ptrCast(@alignCast(raw_ptr)));
        header.* = .{
            .magic = HEADER_MAGIC,
            .size = len,
            .alignment = ptr_align,
            .guard = GUARD_PATTERN,
        };

        // フッターを設定
        const footer_ptr = raw_ptr + @sizeOf(Header) + len;
        const footer = @as(*Footer, @ptrCast(@alignCast(footer_ptr)));
        footer.* = .{
            .guard = GUARD_PATTERN,
            .magic = FOOTER_MAGIC,
        };

        // ユーザーデータ部分を返す
        return raw_ptr + @sizeOf(Header);
    }

    fn resize(ctx: *anyopaque, buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool {
        _ = ctx;
        _ = buf;
        _ = buf_align;
        _ = new_len;
        _ = ret_addr;
        return false; // リサイズ非サポート
    }

    fn free(ctx: *anyopaque, buf: []u8, buf_align: u8, ret_addr: usize) void {
        const self = @as(*BoundsCheckingAllocator, @ptrCast(@alignCast(ctx)));

        const user_ptr = buf.ptr;
        const raw_ptr = user_ptr - @sizeOf(Header);

        // ヘッダーをチェック
        const header = @as(*Header, @ptrCast(@alignCast(raw_ptr)));

        if (header.magic != HEADER_MAGIC) {
            std.debug.panic(
                "Corrupted header! Expected magic 0x{x}, got 0x{x}",
                .{ HEADER_MAGIC, header.magic },
            );
        }

        // ガード領域をチェック
        for (header.guard, 0..) |byte, i| {
            if (byte != GUARD_PATTERN[i]) {
                std.debug.panic(
                    "Buffer underrun detected at header offset {}! Expected 0x{x}, got 0x{x}",
                    .{ i, GUARD_PATTERN[i], byte },
                );
            }
        }

        // フッターをチェック
        const footer_ptr = user_ptr + header.size;
        const footer = @as(*Footer, @ptrCast(@alignCast(footer_ptr)));

        if (footer.magic != FOOTER_MAGIC) {
            std.debug.panic(
                "Corrupted footer! Expected magic 0x{x}, got 0x{x}",
                .{ FOOTER_MAGIC, footer.magic },
            );
        }

        for (footer.guard, 0..) |byte, i| {
            if (byte != GUARD_PATTERN[i]) {
                std.debug.panic(
                    "Buffer overflow detected at footer offset {}! Expected 0x{x}, got 0x{x}",
                    .{ i, GUARD_PATTERN[i], byte },
                );
            }
        }

        // 実際の解放
        const total_size = @sizeOf(Header) + header.size + @sizeOf(Footer);
        const real_buf = raw_ptr[0..total_size];
        self.parent.vtable.free(self.parent.ptr, real_buf, buf_align, ret_addr);
    }
};

メモリビジュアライゼーション

ASCIIアートによるメモリ表示

const std = @import("std");

pub const MemoryVisualizer = struct {
    pub fn visualizeAllocation(
        writer: anytype,
        ptr: [*]const u8,
        size: usize,
        bytes_per_line: usize,
    ) !void {
        try writer.print("\nMemory at 0x{x} ({} bytes):\n", .{ @intFromPtr(ptr), size });
        try writer.print("┌", .{});
        var i: usize = 0;
        while (i < bytes_per_line * 3 + 2) : (i += 1) {
            try writer.print("─", .{});
        }
        try writer.print("┬", .{});
        i = 0;
        while (i < bytes_per_line) : (i += 1) {
            try writer.print("─", .{});
        }
        try writer.print("┐\n", .{});

        var offset: usize = 0;
        while (offset < size) {
            // アドレス
            try writer.print("│ 0x{x:0>8}:  ", .{@intFromPtr(ptr) + offset});

            // 16進ダンプ
            var j: usize = 0;
            while (j < bytes_per_line and offset + j < size) : (j += 1) {
                try writer.print("{x:0>2} ", .{ptr[offset + j]});
            }

            // パディング
            while (j < bytes_per_line) : (j += 1) {
                try writer.print("   ", .{});
            }

            try writer.print("│ ", .{});

            // ASCII表示
            j = 0;
            while (j < bytes_per_line and offset + j < size) : (j += 1) {
                const byte = ptr[offset + j];
                if (std.ascii.isPrint(byte)) {
                    try writer.print("{c}", .{byte});
                } else {
                    try writer.print(".", .{});
                }
            }

            try writer.print(" │\n", .{});
            offset += bytes_per_line;
        }

        try writer.print("└", .{});
        i = 0;
        while (i < bytes_per_line * 3 + 2) : (i += 1) {
            try writer.print("─", .{});
        }
        try writer.print("┴", .{});
        i = 0;
        while (i < bytes_per_line) : (i += 1) {
            try writer.print("─", .{});
        }
        try writer.print("┘\n", .{});
    }

    pub fn visualizeMemoryLayout(writer: anytype) !void {
        try writer.print("\n", .{});
        try writer.print("Memory Layout Visualization:\n", .{});
        try writer.print("\n", .{});
        try writer.print("┌────────────────────────────────────┐\n", .{});
        try writer.print("│ Header (32 bytes)                  │\n", .{});
        try writer.print("│  ├─ Magic: 0xDEADBEEF             │\n", .{});
        try writer.print("│  ├─ Size: 1024                    │\n", .{});
        try writer.print("│  └─ Stack Trace: [4]usize         │\n", .{});
        try writer.print("├────────────────────────────────────┤\n", .{});
        try writer.print("│ User Data (1024 bytes)            │\n", .{});
        try writer.print("│ [████████████████████████████████] │\n", .{});
        try writer.print("├────────────────────────────────────┤\n", .{});
        try writer.print("│ Footer (8 bytes)                  │\n", .{});
        try writer.print("│  └─ Magic: 0xBEEFDEAD             │\n", .{});
        try writer.print("└────────────────────────────────────┘\n", .{});
    }
};

実際のケーススタディ

ケース1: メモリリークの追跡

const std = @import("std");

// 問題のあるコード
pub fn leakyWebServer() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{
        .safety = true,
        .verbose_log = true,
    }){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    // リクエストハンドラ
    var request_count: usize = 0;
    while (request_count < 100) : (request_count += 1) {
        // リクエストごとに処理
        const request_buffer = try allocator.alloc(u8, 4096);
        errdefer allocator.free(request_buffer);

        // リクエストを処理
        try handleRequest(request_buffer);

        // ❌ バグ: 正常終了時にfreeを忘れている
        // allocator.free(request_buffer);
    }
}

fn handleRequest(buffer: []u8) !void {
    _ = buffer;
    // リクエスト処理
}

// デバッグと修正
pub fn fixedWebServer() !void {
    var tracing = try TracingAllocator.init(std.heap.page_allocator);
    defer tracing.deinit();

    const allocator = tracing.allocator();

    var request_count: usize = 0;
    while (request_count < 100) : (request_count += 1) {
        const request_buffer = try allocator.alloc(u8, 4096);
        defer allocator.free(request_buffer); // ✓ 修正

        try handleRequest(request_buffer);
    }

    // レポートを生成
    var buffer = std.ArrayList(u8).init(std.heap.page_allocator);
    defer buffer.deinit();

    try tracing.generateReport(buffer.writer());
    std.debug.print("{s}\n", .{buffer.items});
}

ケース2: バッファオーバーフローの検出

const std = @import("std");

// 問題のあるコード
pub fn unsafeStringCopy() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    const dest = try allocator.alloc(u8, 10);
    defer allocator.free(dest);

    const src = "This is a very long string that will overflow";

    // ❌ バグ: 境界チェックなし
    // @memcpy(dest, src[0..dest.len]); // これは安全
    // でも手動でコピーすると...
    var i: usize = 0;
    while (i < src.len) : (i += 1) {
        // dest[i] = src[i]; // オーバーフロー!
    }
}

// デバッグと修正
pub fn safeStringCopy() !void {
    var bounds_checker = BoundsCheckingAllocator.init(std.heap.page_allocator);
    const allocator = bounds_checker.allocator();

    const dest = try allocator.alloc(u8, 10);
    defer allocator.free(dest);

    const src = "This is a very long string";

    // ✓ 修正: 安全なコピー
    const copy_len = @min(dest.len, src.len);
    @memcpy(dest[0..copy_len], src[0..copy_len]);
}

本番環境デバッグのワークフロー

ステップ1: 問題の特定

const std = @import("std");

pub const ProductionDebugger = struct {
    allocator: std.mem.Allocator,
    metrics: Metrics,

    const Metrics = struct {
        total_allocated: std.atomic.Value(usize),
        total_freed: std.atomic.Value(usize),
        active_allocations: std.atomic.Value(usize),
        peak_memory: std.atomic.Value(usize),
    };

    pub fn init(allocator: std.mem.Allocator) ProductionDebugger {
        return .{
            .allocator = allocator,
            .metrics = .{
                .total_allocated = std.atomic.Value(usize).init(0),
                .total_freed = std.atomic.Value(usize).init(0),
                .active_allocations = std.atomic.Value(usize).init(0),
                .peak_memory = std.atomic.Value(usize).init(0),
            },
        };
    }

    pub fn recordAllocation(self: *ProductionDebugger, size: usize) void {
        _ = self.metrics.total_allocated.fetchAdd(size, .seq_cst);
        _ = self.metrics.active_allocations.fetchAdd(1, .seq_cst);

        const current = self.metrics.total_allocated.load(.seq_cst) -
            self.metrics.total_freed.load(.seq_cst);

        // ピーク値を更新
        var peak = self.metrics.peak_memory.load(.seq_cst);
        while (current > peak) {
            peak = self.metrics.peak_memory.cmpxchgWeak(
                peak,
                current,
                .seq_cst,
                .seq_cst,
            ) orelse break;
        }
    }

    pub fn recordFree(self: *ProductionDebugger, size: usize) void {
        _ = self.metrics.total_freed.fetchAdd(size, .seq_cst);
        _ = self.metrics.active_allocations.fetchSub(1, .seq_cst);
    }

    pub fn getMetrics(self: *ProductionDebugger) MetricsSnapshot {
        return .{
            .total_allocated = self.metrics.total_allocated.load(.seq_cst),
            .total_freed = self.metrics.total_freed.load(.seq_cst),
            .active_allocations = self.metrics.active_allocations.load(.seq_cst),
            .peak_memory = self.metrics.peak_memory.load(.seq_cst),
        };
    }

    pub const MetricsSnapshot = struct {
        total_allocated: usize,
        total_freed: usize,
        active_allocations: usize,
        peak_memory: usize,

        pub fn print(self: MetricsSnapshot, writer: anytype) !void {
            try writer.print("=== Memory Metrics ===\n", .{});
            try writer.print("Total allocated:  {} bytes\n", .{self.total_allocated});
            try writer.print("Total freed:      {} bytes\n", .{self.total_freed});
            try writer.print("Active:           {} allocations\n", .{self.active_allocations});
            try writer.print("Peak memory:      {} bytes\n", .{self.peak_memory});
            try writer.print("Current usage:    {} bytes\n", .{
                self.total_allocated - self.total_freed,
            });
        }
    };
};

自己チェック問題

問題1: トレーシングアロケータの実装

問題: TracingAllocatorを拡張して、最も頻繁に割り当てられるサイズを追跡してください。

解答例

pub const EnhancedTracingAllocator = struct {
    // ... 既存のフィールド
    size_histogram: std.AutoHashMap(usize, usize),

    pub fn recordSize(self: *EnhancedTracingAllocator, size: usize) !void {
        const entry = try self.size_histogram.getOrPut(size);
        if (entry.found_existing) {
            entry.value_ptr.* += 1;
        } else {
            entry.value_ptr.* = 1;
        }
    }

    pub fn printTopSizes(self: *EnhancedTracingAllocator, count: usize) !void {
        // サイズでソートして上位N個を表示
    }
};

問題2: メモリ破損の検出

問題: BoundsCheckingAllocatorに、解放時だけでなく、任意のタイミングでガード領域をチェックする機能を追加してください。

解答例

pub fn checkAllAllocations(self: *BoundsCheckingAllocator) !void {
    // 全ての有効な割り当てのガード領域をチェック
    // 破損があればレポート
}

問題3: 本番環境監視

問題: メモリ使用量が閾値を超えた時にアラートを発生させる機能を実装してください。

解答例

pub const AlertingAllocator = struct {
    threshold: usize,
    callback: *const fn (current: usize, threshold: usize) void,

    pub fn checkThreshold(self: *AlertingAllocator, current: usize) void {
        if (current > self.threshold) {
            self.callback(current, self.threshold);
        }
    }
};

主要な学び

  • トレーシング: 全ての割り当てを記録することで後で分析可能
  • バウンズチェック: ガード領域でオーバーフロー/アンダーフローを検出
  • ビジュアライゼーション: メモリ状態を可視化して理解を深める
  • 本番環境: メトリクスとログで継続的な監視
  • ケーススタディ: 実際のバグパターンから学ぶ

次の演習では、これらの技法を組み合わせて実際のデバッグツールを構築します。