詳解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);
}
}
};
主要な学び
- トレーシング: 全ての割り当てを記録することで後で分析可能
- バウンズチェック: ガード領域でオーバーフロー/アンダーフローを検出
- ビジュアライゼーション: メモリ状態を可視化して理解を深める
- 本番環境: メトリクスとログで継続的な監視
- ケーススタディ: 実際のバグパターンから学ぶ
次の演習では、これらの技法を組み合わせて実際のデバッグツールを構築します。