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:
| Bug | Descrição | Consequência |
|---|---|---|
| Memory Leak | Memória alocada nunca liberada | Uso crescente de RAM |
| Use-After-Free | Acesso a memória já liberada | Dados corrompidos, crash |
| Double Free | Liberar a mesma memória duas vezes | Corrupção do heap |
| Buffer Overflow | Escrever além dos limites | Corrupção de dados adjacentes |
| Dangling Pointer | Ponteiro para memória inválida | Comportamento 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:
- Ative o GPA com
stack_trace_framesalto - Verifique
defer— cadaalloc/createtem umdefer free/defer destroy? - Busque ponteiros escapando — dados alocados na stack retornados por referência
- Teste com Valgrind para análise detalhada
- Use ASan para buffer overflows
- 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:
- Fundamentos — Como stack e heap funcionam em Zig
- Allocators — Cada tipo e quando usar
- Arena Allocator — O padrão mais poderoso para alocações em lote
- Custom Allocators — Como criar os seus próprios
- 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
- Otimização de Performance — Otimize com base no que aprendeu sobre memória
- Testes Avançados — Teste sistematicamente para bugs de memória
- Desenvolvimento Web com Zig — Aplique gerenciamento de memória em servidores
- Builtins de Memória — Referência de funções built-in