第5章: メモリデバッグテクニック

学習目標

この章では、Zigにおける高度なメモリデバッグ技法を学びます。

この章で学ぶこと

  • メモリデバッグ戦略の概要
  • Valgrindとの統合
  • AddressSanitizerとZig
  • カスタムデバッグアロケータの実装
  • 本番環境でのメモリデバッグ戦略
  • メモリデバッグの重要性

    なぜデバッグが困難なのか

    const std = @import("std");
    
    // 問題1: 時間差攻撃型のバグ
    pub fn timeBomb() !void {
        var gpa = std.heap.GeneralPurposeAllocator(.{}){};
        defer _ = gpa.deinit();
    
        const allocator = gpa.allocator();
    
        const ptr = try allocator.alloc(u8, 100);
        allocator.free(ptr);
    
        // 他の処理が大量に実行される...
        var i: usize = 0;
        while (i < 1000) : (i += 1) {
            const temp = try allocator.alloc(u8, 50);
            defer allocator.free(temp);
        }
    
        // ずっと後でuse-after-freeが発生
        // ptr[0] = 42; // 遠く離れた場所でクラッシュ
    }
    
    // 問題2: 稀にしか起こらないバグ
    pub fn raceCondition() !void {
        // マルチスレッド環境で1000回に1回だけ発生
        // デバッグが非常に困難
    }
    
    // 問題3: 本番環境でのみ発生
    pub fn productionOnly() !void {
        // ローカル環境では再現しないが
        // 本番環境では確実にクラッシュ
    }
    

    デバッグの階層

    デバッグツールの階層:
    
    Level 1: コンパイル時チェック
    ├─ Zigの型システム
    ├─ 境界チェック
    └─ コンパイラ警告
    
    Level 2: 実行時チェック
    ├─ GPAのsafetyモード
    ├─ Assert文
    └─ カスタムデバッグコード
    
    Level 3: 外部ツール
    ├─ Valgrind (メモリチェック)
    ├─ AddressSanitizer (ASAN)
    └─ プロファイラー
    
    Level 4: 本番環境モニタリング
    ├─ ログ記録
    ├─ メトリクス収集
    └─ クラッシュレポート
    

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

    レイヤー型デバッグ

    const std = @import("std");
    
    // デバッグレイヤーを重ねる
    pub fn layeredDebugging() !void {
        // Base: GPA
        var gpa = std.heap.GeneralPurposeAllocator(.{}){};
        defer _ = gpa.deinit();
    
        // Layer 1: ログ記録
        var logging = LoggingAllocator.init(gpa.allocator());
    
        // Layer 2: 統計収集
        var stats = StatsAllocator.init(logging.allocator());
    
        // Layer 3: ガード追加
        var guarded = GuardedAllocator.init(stats.allocator());
    
        // 最終的なアロケータ
        const allocator = guarded.allocator();
    
        // 使用
        const buffer = try allocator.alloc(u8, 100);
        defer allocator.free(buffer);
    
        // 各レイヤーが独自のデバッグ情報を提供
    }
    
    const LoggingAllocator = struct {
        parent: std.mem.Allocator,
    
        pub fn init(parent: std.mem.Allocator) LoggingAllocator {
            return .{ .parent = parent };
        }
    
        pub fn allocator(self: *LoggingAllocator) 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(*LoggingAllocator, @ptrCast(@alignCast(ctx)));
    
            std.debug.print("[ALLOC] size={}, align={}, addr=0x{x}\n", .{
                len,
                @as(usize, 1) << @as(u6, @intCast(ptr_align)),
                ret_addr,
            });
    
            const result = self.parent.vtable.alloc(
                self.parent.ptr,
                len,
                ptr_align,
                ret_addr,
            );
    
            if (result) |ptr| {
                std.debug.print("[ALLOC] -> 0x{x}\n", .{@intFromPtr(ptr)});
            } else {
                std.debug.print("[ALLOC] -> FAILED\n", .{});
            }
    
            return result;
        }
    
        fn resize(ctx: *anyopaque, buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool {
            const self = @as(*LoggingAllocator, @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(*LoggingAllocator, @ptrCast(@alignCast(ctx)));
    
            std.debug.print("[FREE] ptr=0x{x}, size={}\n", .{
                @intFromPtr(buf.ptr),
                buf.len,
            });
    
            self.parent.vtable.free(self.parent.ptr, buf, buf_align, ret_addr);
        }
    };
    
    const StatsAllocator = struct {
        parent: std.mem.Allocator,
        total_allocated: usize = 0,
        total_freed: usize = 0,
    
        pub fn init(parent: std.mem.Allocator) StatsAllocator {
            return .{ .parent = parent };
        }
    
        pub fn allocator(self: *StatsAllocator) 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(*StatsAllocator, @ptrCast(@alignCast(ctx)));
            const result = self.parent.vtable.alloc(self.parent.ptr, len, ptr_align, ret_addr);
    
            if (result != null) {
                self.total_allocated += len;
            }
    
            return result;
        }
    
        fn resize(ctx: *anyopaque, buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool {
            const self = @as(*StatsAllocator, @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(*StatsAllocator, @ptrCast(@alignCast(ctx)));
            self.total_freed += buf.len;
            self.parent.vtable.free(self.parent.ptr, buf, buf_align, ret_addr);
        }
    };
    
    const GuardedAllocator = struct {
        parent: std.mem.Allocator,
    
        pub fn init(parent: std.mem.Allocator) GuardedAllocator {
            return .{ .parent = parent };
        }
    
        pub fn allocator(self: *GuardedAllocator) 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(*GuardedAllocator, @ptrCast(@alignCast(ctx)));
            // ガード領域を追加して割り当て
            _ = self;
            _ = len;
            _ = ptr_align;
            _ = ret_addr;
            return null;
        }
    
        fn resize(ctx: *anyopaque, buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool {
            const self = @as(*GuardedAllocator, @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(*GuardedAllocator, @ptrCast(@alignCast(ctx)));
            self.parent.vtable.free(self.parent.ptr, buf, buf_align, ret_addr);
        }
    };
    

    Valgrind統合

    Valgrindとは

    Valgrindは強力なメモリデバッグツールです。

    # Valgrindでプログラムを実行
    valgrind --leak-check=full --show-leak-kinds=all ./my_program
    
    # 出力例:
    # ==12345== HEAP SUMMARY:
    # ==12345==     in use at exit: 1,024 bytes in 1 blocks
    # ==12345==   total heap usage: 10 allocs, 9 frees, 5,120 bytes allocated
    # ==12345==
    # ==12345== 1,024 bytes in 1 blocks are definitely lost
    # ==12345==    at 0x4C2AB80: malloc (in /usr/lib/valgrind/...)
    # ==12345==    by 0x108A: main (test.zig:42)
    

    ZigでValgrindを使用

    const std = @import("std");
    
    pub fn main() !void {
        // c_allocatorを使用(ValgrindがC allocを追跡)
        const allocator = std.heap.c_allocator;
    
        const buffer = try allocator.alloc(u8, 1024);
        defer allocator.free(buffer);
    
        // メモリを使用
        @memset(buffer, 42);
    
        std.debug.print("Done\n", .{});
    }
    

    ビルドとテスト:

    # Cアロケータを使用してビルド
    zig build-exe test.zig -lc
    
    # Valgrindで実行
    valgrind --leak-check=full ./test
    
    # メモリリークがない場合:
    # ==12345== LEAK SUMMARY:
    # ==12345==    definitely lost: 0 bytes in 0 blocks
    # ==12345==    indirectly lost: 0 bytes in 0 blocks
    # ==12345==      possibly lost: 0 bytes in 0 blocks
    # ==12345==    still reachable: 0 bytes in 0 blocks
    

    Valgrind互換アロケータ

    const std = @import("std");
    
    // Valgrindアノテーションを使用
    pub const ValgrindAllocator = struct {
        parent: std.mem.Allocator,
    
        pub fn init(parent: std.mem.Allocator) ValgrindAllocator {
            return .{ .parent = parent };
        }
    
        pub fn allocator(self: *ValgrindAllocator) 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(*ValgrindAllocator, @ptrCast(@alignCast(ctx)));
            const result = self.parent.vtable.alloc(self.parent.ptr, len, ptr_align, ret_addr);
    
            if (result) |ptr| {
                // Valgrindにメモリブロックを定義として報告
                // VALGRIND_MALLOCLIKE_BLOCK(ptr, len, 0, 0);
                _ = ptr;
            }
    
            return result;
        }
    
        fn resize(ctx: *anyopaque, buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool {
            const self = @as(*ValgrindAllocator, @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(*ValgrindAllocator, @ptrCast(@alignCast(ctx)));
    
            // Valgrindに解放を報告
            // VALGRIND_FREELIKE_BLOCK(buf.ptr, 0);
    
            self.parent.vtable.free(self.parent.ptr, buf, buf_align, ret_addr);
        }
    };
    

    AddressSanitizer (ASAN)

    ASANとは

    AddressSanitizerは、GoogleのChromiumプロジェクトで開発された高速なメモリエラー検出ツールです。

    # ASANを有効にしてビルド
    zig build-exe -fsanitize=address test.zig
    
    # 実行
    ./test
    
    # エラー検出時の出力例:
    # =================================================================
    # ==12345==ERROR: AddressSanitizer: heap-use-after-free
    # READ of size 1 at 0x602000000010 thread T0
    #     #0 0x4a2345 in main test.zig:15
    #
    # 0x602000000010 is located 0 bytes inside of 100-byte region
    # freed by thread T0 here:
    #     #0 0x4a1234 in free
    #     #1 0x4a2340 in main test.zig:14
    

    ASANの利点

    検出できるエラー:
    ├─ Use-after-free
    ├─ Heap buffer overflow
    ├─ Stack buffer overflow
    ├─ Global buffer overflow
    ├─ Use-after-return
    ├─ Use-after-scope
    ├─ Double-free
    └─ Memory leaks
    
    パフォーマンス:
    ├─ Valgrindより10-100倍高速
    ├─ 実行時オーバーヘッド: 約2倍
    └─ メモリオーバーヘッド: 約2-3倍
    

    ZigでのASAN使用

    const std = @import("std");
    
    pub fn main() !void {
        var gpa = std.heap.GeneralPurposeAllocator(.{}){};
        defer _ = gpa.deinit();
    
        const allocator = gpa.allocator();
    
        // Use-after-freeを作成
        const buffer = try allocator.alloc(u8, 100);
        allocator.free(buffer);
    
        // ASANがエラーを検出
        // buffer[0] = 42;
    }
    

    ビルドオプション:

    # Debug build with ASAN
    zig build-exe -fsanitize=address -O Debug test.zig
    
    # Release build with ASAN (本番前テスト用)
    zig build-exe -fsanitize=address -O ReleaseSafe test.zig
    

    カスタムデバッグアロケータの実装

    センチネル値による破損検出

    const std = @import("std");
    
    pub const SentinelAllocator = struct {
        parent: std.mem.Allocator,
    
        const SENTINEL_BEFORE: u32 = 0xDEADBEEF;
        const SENTINEL_AFTER: u32 = 0xBEEFDEAD;
    
        pub fn init(parent: std.mem.Allocator) SentinelAllocator {
            return .{ .parent = parent };
        }
    
        pub fn allocator(self: *SentinelAllocator) 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(*SentinelAllocator, @ptrCast(@alignCast(ctx)));
    
            // 実際のサイズ: sentinel(4) + data + sentinel(4)
            const real_len = len + 8;
    
            const real_ptr = self.parent.vtable.alloc(
                self.parent.ptr,
                real_len,
                ptr_align,
                ret_addr,
            ) orelse return null;
    
            // センチネル値を配置
            const before = @as(*u32, @ptrCast(@alignCast(real_ptr)));
            before.* = SENTINEL_BEFORE;
    
            const after_ptr = real_ptr + 4 + len;
            const after = @as(*u32, @ptrCast(@alignCast(after_ptr)));
            after.* = SENTINEL_AFTER;
    
            // ユーザーデータ部分を返す
            return real_ptr + 4;
        }
    
        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(*SentinelAllocator, @ptrCast(@alignCast(ctx)));
    
            // センチネル値をチェック
            const user_ptr = buf.ptr;
            const real_ptr = user_ptr - 4;
    
            const before = @as(*u32, @ptrCast(@alignCast(real_ptr)));
            if (before.* != SENTINEL_BEFORE) {
                std.debug.panic(
                    "Buffer underrun detected! Expected 0x{x}, got 0x{x}",
                    .{ SENTINEL_BEFORE, before.* },
                );
            }
    
            const after_ptr = user_ptr + buf.len;
            const after = @as(*u32, @ptrCast(@alignCast(after_ptr)));
            if (after.* != SENTINEL_AFTER) {
                std.debug.panic(
                    "Buffer overflow detected! Expected 0x{x}, got 0x{x}",
                    .{ SENTINEL_AFTER, after.* },
                );
            }
    
            // 実際の解放
            const real_len = buf.len + 8;
            const real_buf = real_ptr[0..real_len];
            self.parent.vtable.free(self.parent.ptr, real_buf, buf_align, ret_addr);
        }
    };
    

    本番環境デバッグ戦略

    ログベースのデバッグ

    const std = @import("std");
    
    pub const ProductionAllocator = struct {
        parent: std.mem.Allocator,
        log_file: std.fs.File,
        enable_logging: bool,
    
        pub fn init(parent: std.mem.Allocator, log_file: std.fs.File) ProductionAllocator {
            return .{
                .parent = parent,
                .log_file = log_file,
                .enable_logging = true,
            };
        }
    
        pub fn allocator(self: *ProductionAllocator) 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(*ProductionAllocator, @ptrCast(@alignCast(ctx)));
    
            const result = self.parent.vtable.alloc(
                self.parent.ptr,
                len,
                ptr_align,
                ret_addr,
            );
    
            if (self.enable_logging) {
                const timestamp = std.time.milliTimestamp();
                const log_msg = std.fmt.allocPrint(
                    self.parent,
                    "[{}] ALLOC: size={}, ptr=0x{x}\n",
                    .{ timestamp, len, if (result) |p| @intFromPtr(p) else 0 },
                ) catch return result;
                defer self.parent.free(log_msg);
    
                _ = self.log_file.write(log_msg) catch {};
            }
    
            return result;
        }
    
        fn resize(ctx: *anyopaque, buf: []u8, buf_align: u8, new_len: usize, ret_addr: usize) bool {
            const self = @as(*ProductionAllocator, @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(*ProductionAllocator, @ptrCast(@alignCast(ctx)));
    
            if (self.enable_logging) {
                const timestamp = std.time.milliTimestamp();
                const log_msg = std.fmt.allocPrint(
                    self.parent,
                    "[{}] FREE: ptr=0x{x}, size={}\n",
                    .{ timestamp, @intFromPtr(buf.ptr), buf.len },
                ) catch {
                    self.parent.vtable.free(self.parent.ptr, buf, buf_align, ret_addr);
                    return;
                };
                defer self.parent.free(log_msg);
    
                _ = self.log_file.write(log_msg) catch {};
            }
    
            self.parent.vtable.free(self.parent.ptr, buf, buf_align, ret_addr);
        }
    };
    

    まとめ

    この章では、メモリデバッグテクニックについて学びました。

    重要ポイント

  • 多層防御: 複数のデバッグ手法を組み合わせる
  • 外部ツール: ValgrindとASANの活用
  • カスタムツール: 用途に応じたデバッグアロケータ
  • 本番環境: ログと監視の重要性
  • 次のステップ

    次のExplanationセクションでは、これらのテクニックを実際のケーススタディで学びます。

    理解度チェック

  • デバッグアロケータのレイヤー型設計の利点は?
  • ValgrindとASANの違いは何ですか?
  • センチネル値による破損検出の仕組みは?
  • 本番環境でのメモリデバッグ戦略は?
  • どのツールをどの場面で使用すべきですか?