Debugging de Problemas de Memória em Zig: Guia Completo

Este é o artigo final da série Masterclass de Memória em Zig. Depois de dominar stack e heap, tipos de allocators, arena allocator e custom allocators, agora vamos aprender a diagnosticar e corrigir os bugs de memória mais comuns.

Os Tipos de Bugs de Memória

Antes de debugar, é importante reconhecer os tipos de problemas:

BugDescriçãoConsequência
Memory LeakMemória alocada nunca liberadaUso crescente de RAM
Use-After-FreeAcesso a memória já liberadaDados corrompidos, crash
Double FreeLiberar a mesma memória duas vezesCorrupção do heap
Buffer OverflowEscrever além dos limitesCorrupção de dados adjacentes
Dangling PointerPonteiro para memória inválidaComportamento indefinido

Ferramenta 1: GeneralPurposeAllocator como Detector

O GPA de Zig é seu melhor amigo para detectar problemas de memória. Ele detecta leaks, double-free e use-after-free automaticamente:

const std = @import("std");

fn exemploLeak(allocator: std.mem.Allocator) !void {
    const dados = try allocator.alloc(u8, 100);
    // BUG: esquecemos de liberar 'dados'!
    _ = dados;

    const outros = try allocator.alloc(u8, 200);
    allocator.free(outros); // Este foi liberado corretamente
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{
        .stack_trace_frames = 10, // Captura mais frames para debug
    }){};
    defer {
        const status = gpa.deinit();
        if (status == .leak) {
            std.debug.print("\n*** MEMORY LEAK DETECTADO ***\n", .{});
            std.debug.print("Verifique os stack traces acima para encontrar a origem.\n", .{});
        } else {
            std.debug.print("\nSem memory leaks!\n", .{});
        }
    }

    try exemploLeak(gpa.allocator());
}

Ao rodar, o GPA mostrará exatamente onde a memória foi alocada e não liberada, incluindo o stack trace.

Ferramenta 2: Padrão de Verificação com Defer

O padrão mais eficaz para prevenir leaks é usar defer sistematicamente:

const std = @import("std");

const Conexao = struct {
    host: []u8,
    porta: u16,
    allocator: std.mem.Allocator,
    ativa: bool,

    fn conectar(allocator: std.mem.Allocator, host: []const u8, porta: u16) !Conexao {
        const host_copia = try allocator.alloc(u8, host.len);
        @memcpy(host_copia, host);

        return Conexao{
            .host = host_copia,
            .porta = porta,
            .allocator = allocator,
            .ativa = true,
        };
    }

    fn desconectar(self: *Conexao) void {
        if (self.ativa) {
            self.allocator.free(self.host);
            self.ativa = false;
            std.debug.print("Conexão fechada e memória liberada\n", .{});
        }
    }
};

fn processarComConexao(allocator: std.mem.Allocator) !void {
    var conn = try Conexao.conectar(allocator, "localhost", 5432);
    defer conn.desconectar(); // SEMPRE limpar ao sair

    // Simular trabalho que pode falhar
    const dados = try allocator.alloc(u8, 1024);
    defer allocator.free(dados);

    std.debug.print("Conectado a {s}:{d}\n", .{ conn.host, conn.porta });

    // Mesmo que algo falhe aqui, defer garante a limpeza
    for (dados, 0..) |*b, i| {
        b.* = @intCast(i % 256);
    }
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer {
        const status = gpa.deinit();
        if (status == .leak) {
            std.debug.print("LEAK detectado!\n", .{});
        } else {
            std.debug.print("Sem leaks! Padrão defer funcionou.\n", .{});
        }
    }

    try processarComConexao(gpa.allocator());
}

Ferramenta 3: Valgrind com Zig

Zig compila para código nativo, então podemos usar Valgrind para análise avançada:

# Compilar em modo debug (padrão)
zig build-exe src/main.zig

# Rodar com Valgrind
valgrind --leak-check=full --show-leak-kinds=all ./main

# Para análise de cache (performance)
valgrind --tool=cachegrind ./main

# Para detectar acessos inválidos
valgrind --tool=memcheck --track-origins=yes ./main

Exemplo de programa para testar com Valgrind:

const std = @import("std");

pub fn main() !void {
    // Usar c_allocator para Valgrind funcionar melhor
    const allocator = std.heap.c_allocator;

    const dados = try allocator.alloc(u8, 100);
    defer allocator.free(dados);

    // Preencher dados
    for (dados, 0..) |*b, i| {
        b.* = @intCast(i);
    }

    // Alocação que "esquecemos" de liberar (para Valgrind detectar)
    const leak = try allocator.alloc(u8, 50);
    _ = leak;

    std.debug.print("Programa finalizado. Rode com valgrind para ver o leak.\n", .{});
}

Valgrind mostrará algo como:

==12345== 50 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2ABBD: malloc (vg_replace_malloc.c:380)
==12345==    ...

Ferramenta 4: Sanitizador de Endereços (ASan)

Zig suporta AddressSanitizer nativamente:

# Compilar com sanitizador
zig build-exe -fsanitize=address src/main.zig

# Rodar — erros serão reportados automaticamente
./main

O ASan detecta:

  • Buffer overflow na heap e stack
  • Use-after-free
  • Use-after-return
  • Memory leaks

Padrão: Tracking Allocator para Produção

Para monitorar memória em produção, crie um allocator de tracking leve:

const std = @import("std");

const MemoryTracker = struct {
    backing: std.mem.Allocator,
    alocacoes_ativas: usize,
    bytes_ativos: usize,
    pico_bytes: usize,
    total_alocacoes: usize,

    fn init(backing: std.mem.Allocator) MemoryTracker {
        return .{
            .backing = backing,
            .alocacoes_ativas = 0,
            .bytes_ativos = 0,
            .pico_bytes = 0,
            .total_alocacoes = 0,
        };
    }

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

    fn alloc(ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8 {
        const self: *MemoryTracker = @ptrCast(@alignCast(ctx));
        const result = self.backing.rawAlloc(len, ptr_align, ret_addr);
        if (result != null) {
            self.alocacoes_ativas += 1;
            self.bytes_ativos += len;
            self.total_alocacoes += 1;
            if (self.bytes_ativos > self.pico_bytes) {
                self.pico_bytes = self.bytes_ativos;
            }
        }
        return result;
    }

    fn resize(ctx: *anyopaque, buf: [*]u8, buf_align: u8, new_len: usize, ret_addr: usize) bool {
        const self: *MemoryTracker = @ptrCast(@alignCast(ctx));
        return self.backing.rawResize(buf, buf_align, new_len, ret_addr);
    }

    fn free_fn(ctx: *anyopaque, buf: [*]u8, buf_align: u8, ret_addr: usize) void {
        const self: *MemoryTracker = @ptrCast(@alignCast(ctx));
        if (self.alocacoes_ativas > 0) {
            self.alocacoes_ativas -= 1;
        }
        self.backing.rawFree(buf, buf_align, ret_addr);
    }

    fn relatorio(self: *const MemoryTracker) void {
        std.debug.print("\n=== Relatório de Memória ===\n", .{});
        std.debug.print("Alocações ativas: {d}\n", .{self.alocacoes_ativas});
        std.debug.print("Bytes ativos: {d}\n", .{self.bytes_ativos});
        std.debug.print("Pico de uso: {d} bytes\n", .{self.pico_bytes});
        std.debug.print("Total de alocações: {d}\n", .{self.total_alocacoes});

        if (self.alocacoes_ativas > 0) {
            std.debug.print("AVISO: {d} alocações ainda ativas!\n", .{self.alocacoes_ativas});
        }
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var tracker = MemoryTracker.init(gpa.allocator());
    const alloc = tracker.allocator();

    // Simular uso da aplicação
    var buffers: [5][]u8 = undefined;
    for (&buffers, 0..) |*buf, i| {
        buf.* = try alloc.alloc(u8, (i + 1) * 100);
    }

    tracker.relatorio();

    // Liberar alguns
    for (buffers[0..3]) |buf| {
        alloc.free(buf);
    }

    tracker.relatorio();

    // Liberar o restante
    for (buffers[3..]) |buf| {
        alloc.free(buf);
    }

    tracker.relatorio();
}

Checklist de Debugging de Memória

Quando encontrar um problema de memória, siga esta sequência:

  1. Ative o GPA com stack_trace_frames alto
  2. Verifique defer — cada alloc/create tem um defer free/defer destroy?
  3. Busque ponteiros escapando — dados alocados na stack retornados por referência
  4. Teste com Valgrind para análise detalhada
  5. Use ASan para buffer overflows
  6. Adicione tracking em produção para monitorar tendências

Erros Comuns e Soluções

Erro 1: Ponteiro para stack escapando

// ERRADO: retorna ponteiro para dado na stack
fn perigoso() *i32 {
    var x: i32 = 42;
    return &x; // x será destruído ao sair da função!
}

// CORRETO: alocar na heap e retornar
fn seguro(allocator: std.mem.Allocator) !*i32 {
    const x = try allocator.create(i32);
    x.* = 42;
    return x; // Caller deve chamar allocator.destroy(x)
}

Erro 2: Esquecer errdefer

const std = @import("std");

const Recurso = struct {
    a: []u8,
    b: []u8,

    fn init(allocator: std.mem.Allocator) !Recurso {
        const a = try allocator.alloc(u8, 100);
        // Se a alocação de 'b' falhar, 'a' vazaria!
        errdefer allocator.free(a);

        const b = try allocator.alloc(u8, 200);
        // Se chegamos aqui, ambos foram alocados com sucesso

        return Recurso{ .a = a, .b = b };
    }

    fn deinit(self: *Recurso, allocator: std.mem.Allocator) void {
        allocator.free(self.a);
        allocator.free(self.b);
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    var r = try Recurso.init(gpa.allocator());
    defer r.deinit(gpa.allocator());

    std.debug.print("Recurso inicializado com {d} + {d} bytes\n", .{ r.a.len, r.b.len });
}

O errdefer executa apenas se a função retornar um erro — perfeito para limpeza parcial.

Conclusão da Série

Ao longo desta masterclass, você aprendeu:

  1. Fundamentos — Como stack e heap funcionam em Zig
  2. Allocators — Cada tipo e quando usar
  3. Arena Allocator — O padrão mais poderoso para alocações em lote
  4. Custom Allocators — Como criar os seus próprios
  5. Debugging — Ferramentas e técnicas para encontrar bugs

Com esse conhecimento, você está equipado para escrever código Zig que é seguro, eficiente e livre de bugs de memória.

Leitura Complementar

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.