第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);
}
};
まとめ
この章では、メモリデバッグテクニックについて学びました。
重要ポイント
次のステップ
次のExplanationセクションでは、これらのテクニックを実際のケーススタディで学びます。