Debugging em Zig: Técnicas e Ferramentas Completas

Debugar código é uma habilidade essencial para qualquer desenvolvedor. No Zig, você tem acesso a ferramentas poderosas para identificar e corrigir bugs — desde técnicas simples de print debugging até debuggers profissionais como GDB e LLDB.

Neste tutorial completo, você vai aprender todas as técnicas de debugging disponíveis no Zig: como usar o std.debug para diagnósticos rápidos, depurar com GDB/LLDB, analisar stack traces, debugar problemas de memória e integrar tudo com seu IDE favorito.

Pré-requisito: Conhecimento básico de Zig. Se você está começando, confira nosso Guia para Iniciantes.

Visão Geral do Debugging em Zig

Filosofia do Zig: Debugabilidade

O Zig foi projetado com debugabilidade em mente:

  1. Stack traces limpas — Sem overhead de runtime pesado
  2. Modo Debug seguro — Checks de segurança ativados por padrão
  3. Sem malloc escondido — Você controla toda alocação
  4. Erros explícitos — Error types ao invés de exceções
  5. Comportamento definido — Undefined behavior é detectado em Debug

Modos de Build e Debugging

ModoUsoDebugPerformance
DebugDesenvolvimento✅ Stack traces, bounds checksLento
ReleaseSafeProdução com segurança✅ Safety checksRápido
ReleaseFastMáxima velocidade❌ Sem checksMáximo
ReleaseSmallBinários pequenos❌ Sem checksRápido

💡 Dica: Sempre desenvolva no modo Debug para ter acesso completo a informações de debugging.

# Build com símbolos de debug (padrão)
zig build

# Build de release (sem símbolos de debug)
zig build -Doptimize=ReleaseFast

Ferramentas de Debugging Disponíveis

┌─────────────────────────────────────────────────────────────┐
│                    Debugging em Zig                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Rápido e Simples        Debugger Profissional              │
│  ┌──────────────┐        ┌──────────────┐                   │
│  │ std.debug    │        │ GDB          │                   │
│  │ print        │        │ LLDB         │                   │
│  │ dumpStackTrace│       │ VS Code      │                   │
│  │ assert       │        │ Breakpoints  │                   │
│  └──────────────┘        └──────────────┘                   │
│                                                             │
│  Análise de Memória                                         │
│  ┌──────────────┐                                           │
│  │ GeneralPurposeAllocator│                                  │
│  │ GPA.detectLeaks()      │                                  │
│  │ Valgrind (Linux)       │                                  │
│  └──────────────┘                                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

A forma mais rápida de debugar é usar std.debug.print. É simples, mas poderosa.

Básico: std.debug.print

const std = @import("std");

pub fn main() void {
    const valor = 42;
    
    // Print básico
    std.debug.print("Valor: {d}\n", .{valor});
    
    // Múltiplos valores
    const nome = "Zig";
    const versao = 0.15;
    std.debug.print("{s} v{d}\n", .{nome, versao});
    
    // Debug format (mostra estrutura completa)
    const ponto = .{ .x = 10, .y = 20 };
    std.debug.print("Ponto: {any}\n", .{ponto});
    // Saída: Ponto: struct{comptime x: comptime_int = 10, comptime y: comptime_int = 20}
}

Formatos de Print Úteis

const std = @import("std");

pub fn main() void {
    const num: i32 = 42;
    const hex: u32 = 0xDEADBEEF;
    const ptr: *i32 = @ptrFromInt(0x1000);
    const bin: u8 = 0b10101010;
    
    // Diferentes formatos
    std.debug.print("Decimal: {d}\n", .{num});      // 42
    std.debug.print("Hexadecimal: {x}\n", .{hex});  // deadbeef
    std.debug.print("Hex uppercase: {X}\n", .{hex});// DEADBEEF
    std.debug.print("Ponteiro: {*p}\n", .{ptr});    // 0x1000
    std.debug.print("Binário: {b}\n", .{bin});      // 10101010
    std.debug.print("Caractere: {c}\n", .{'A'});    // A
    std.debug.print("String: {s}\n", .{"hello"});   // hello
    std.debug.print("Any: {any}\n", .{num});        // 42
}

Asserts e Verificações

Asserts são essenciais para capturar bugs cedo:

const std = @import("std");

fn dividir(a: i32, b: i32) i32 {
    // Assert básico
    std.debug.assert(b != 0);  // Panic se b == 0
    
    return @divTrunc(a, b);
}

fn buscar(array: []const i32, indice: usize) i32 {
    // Assert com mensagem customizada
    std.debug.assert(indice < array.len);
    
    return array[indice];
}

// Asserts em tempo de compilação
comptime {
    std.debug.assert(@sizeOf(usize) >= @sizeOf(u32));
}

Dump de Stack Trace

Quando algo dá errado, veja onde:

const std = @import("std");

fn funcaoNivel3() void {
    std.debug.print("Em funcaoNivel3\n", .{});
    
    // Imprime stack trace atual
    const trace = @import("builtin").StackTrace;
    std.debug.dumpStackTrace(trace{});
}

fn funcaoNivel2() void {
    funcaoNivel3();
}

fn funcaoNivel1() void {
    funcaoNivel2();
}

pub fn main() void {
    funcaoNivel1();
}

Saída típica:

Em funcaoNivel3
first render trace:
/home/user/projeto/src/main.zig:6:31: 0x1033b60 in funcaoNivel3 (main)
/home/user/projeto/src/main.zig:13:18: 0x1033b80 in funcaoNivel2 (main)
/home/user/projeto/src/main.zig:17:18: 0x1033ba0 in funcaoNivel1 (main)
/home/user/projeto/src/main.zig:21:17: 0x1033bc0 in main (main)

Use comptime para prints de debug opcionais:

const std = @import("std");
const builtin = @import("builtin");

const debug_mode = builtin.mode == .Debug;

fn debugPrint(comptime fmt: []const u8, args: anytype) void {
    if (debug_mode) {
        std.debug.print("[DEBUG] " ++ fmt ++ "\n", args);
    }
}

pub fn main() void {
    debugPrint("Iniciando aplicação", .{});
    debugPrint("Config carregada: {any}", .{.{
        .host = "localhost",
        .port = 8080,
    }});
}

Debugging com GDB

GDB (GNU Debugger) é o debugger padrão para Linux. Funciona perfeitamente com código Zig.

Preparação: Build com Símbolos de Debug

# Build padrão já inclui símbolos
zig build

# Ou explicitamente
zig build-exe -ODebug src/main.zig

# Para C/C++ mixed projects
zig build -Doptimize=Debug

Comandos Básicos do GDB

# Iniciar GDB
gdb ./zig-out/bin/meu-programa

# Ou com arguments
gdb --args ./zig-out/bin/meu-programa arg1 arg2

Comandos essenciais:

ComandoAbreviaçãoDescrição
runrIniciar execução
breakbDefinir breakpoint
continuecContinuar execução
stepsEntrar na função
nextnPróxima linha (pula função)
finishfinTerminar função atual
printpImprimir valor
backtracebtStack trace
info localsVariáveis locais
quitqSair

Exemplo Prático com GDB

Código:

const std = @import("std");

fn fatorial(n: u32) u32 {
    if (n <= 1) return 1;
    return n * fatorial(n - 1);
}

pub fn main() void {
    const n = 5;
    const resultado = fatorial(n);
    std.debug.print("{d}! = {d}\n", .{n, resultado});
}

Sessão GDB:

# Build
zig build-exe -ODebug src/main.zig -o debug_app

# Iniciar GDB
gdb ./debug_app

# Dentro do GDB:
(gdb) break fatorial           # Breakpoint na função
Breakpoint 1 at 0x2370

(gdb) run                      # Executar
Starting program: /home/user/debug_app
Breakpoint 1, fatorial (n=5) at src/main.zig:4
4           if (n <= 1) return 1;

(gdb) print n                  # Inspecionar valor
$1 = 5

(gdb) continue                 # Continuar
Continuing.
Breakpoint 1, fatorial (n=4) at src/main.zig:4
4           if (n <= 1) return 1;

(gdb) bt                       # Stack trace
#0  fatorial (n=4) at src/main.zig:4
#1  0x0000555555556379 in fatorial (n=5) at src/main.zig:6
#2  0x000055555555639e in main () at src/main.zig:11

(gdb) continue                 # Continuar até o fim
Continuing.
5! = 120
[Inferior 1 (process 12345) exited normally]

(gdb) quit

Breakpoints Condicionais

# Breakpoint apenas quando n == 0
(gdb) break fatorial if n == 0

# Breakpoint na linha 6 apenas quando n > 10
(gdb) break src/main.zig:6 if n > 10

# Breakpoint temporário (para na primeira vez)
(gdb) tbreak fatorial

Watchpoints

Monitore quando uma variável muda:

# Parar quando resultado mudar
(gdb) watch resultado

# Parar quando a memória em um endereço mudar
(gdb) watch *(int*)0x7fffffffe000

Inspecionando Structs

const Pessoa = struct {
    nome: []const u8,
    idade: u32,
    ativo: bool,
};

const p = Pessoa{
    .nome = "Maria",
    .idade = 30,
    .ativo = true,
};
(gdb) print p
$1 = {
  nome = {
    ptr = 0x555555556004 "Maria",
    len = 5
  },
  idade = 30,
  ativo = true
}

(gdb) print p.idade
$2 = 30

(gdb) print p.nome.ptr
$3 = (const u8 *) 0x555555556004 "Maria"

Debugging com LLDB

LLDB é o debugger padrão no macOS e parte do LLVM. Também funciona no Linux.

Comandos LLDB vs GDB

GDBLLDBDescrição
breakbreakpoint set ou bDefinir breakpoint
runrun ou rExecutar
continuecontinue ou cContinuar
stepstep ou sEntrar função
nextnext ou nPróxima linha
printexpr ou pImprimir valor
backtracethread backtrace ou btStack trace
info localsframe variableVariáveis locais
quitquit ou qSair

Sessão LLDB Exemplo

# Iniciar LLDB
lldb ./zig-out/bin/meu-programa

# Sessão:
(lldb) breakpoint set --name fatorial
Breakpoint 1: where = debug_app`fatorial + 4 at main.zig:4, address = 0x00002370

(lldb) run
Process 12345 launched
Process 12345 stopped
* thread #1, name = 'debug_app', stop reason = breakpoint 1.1
    frame #0: 0x0000555555556370 debug_app`fatorial(n=5) at main.zig:4
   1    const std = @import("std");
   2
   3    fn fatorial(n: u32) u32 {
-> 4        if (n <= 1) return 1;
   5        return n * fatorial(n - 1);
   6    }

(lldb) expr n
(unsigned int) $0 = 5

(lldb) thread backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x100003720 debug_app`fatorial(n=5) at main.zig:4
    frame #1: 0x100003784 debug_app`main at main.zig:11
    frame #2: 0x100003d54 debug_app`start + 52

(lldb) continue
Process 12345 resuming
5! = 120
Process 12345 exited with status = 0

Stack Traces em Zig

Obtendo Stack Traces

Zig torna fácil obter informações de onde seu código está:

const std = @import("std");

fn funcaoC() void {
    std.debug.print("Em funcaoC\n", .{});
    
    // Capturar stack trace
    var address_buffer: [10]usize = undefined;
    const trace = std.debug.StackTrace{
        .instruction_addresses = &address_buffer,
        .index = 0,
    };
    
    // Em caso de erro, você pode capturar o trace
    @panic("Erro intencional para demonstração");
}

fn funcaoB() void {
    funcaoC();
}

fn funcaoA() void {
    funcaoB();
}

pub fn main() void {
    funcaoA();
}

Stack Trace de Erros

const std = @import("std");

fn podeFalhar() !void {
    return error.ErroExemplo;
}

pub fn main() !void {
    podeFalhar() catch |err| {
        std.debug.print("Erro capturado: {s}\n", .{@errorName(err)});
        
        // Em modo Debug, você tem stack trace automático
        if (@errorReturnTrace()) |trace| {
            std.debug.dumpStackTrace(trace.*);
        }
        
        return err;
    };
}

Saída em modo Debug:

Erro capturado: ErroExemplo
/home/user/projeto/src/main.zig:5:5: 0x1032a0 in podeFalhar (main)
    return error.ErroExemplo;
    ^
/home/user/projeto/src/main.zig:10:5: 0x1032f0 in main (main)
    podeFalhar() catch |err| {
    ^

Debugging de Memória

GeneralPurposeAllocator (GPA)

O GeneralPurposeAllocator é seu aliado para detectar problemas de memória:

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{
        .safety = true,        // Detecta use-after-free, double-free
        .thread_safety = true, // Thread-safe
    }){};
    defer {
        const deinit_status = gpa.deinit();
        if (deinit_status == .leak) {
            std.debug.print("⚠️  Memory leak detectado!\n", .{});
        }
    }
    
    const allocator = gpa.allocator();
    
    // Alocação
    const ptr = try allocator.alloc(u8, 100);
    defer allocator.free(ptr);
    
    // Se você esquecer o free, o GPA detecta no deinit
}

Detectando Memory Leaks

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();
    
    // ❌ Memory leak - esquecemos de liberar
    const leaky = try allocator.alloc(u8, 100);
    _ = leaky;  // Nunca usamos free
    
    // O deinit vai detectar
    const status = gpa.deinit();
    if (status == .leak) {
        std.log.err("Memory leak detectado!", .{});
    }
}

Saída:

error(gpa): memory address 0x7f3a4c000b90 leaked:
/home/user/projeto/src/main.zig:8:40: 0x103450 in main (main)
    const leaky = try allocator.alloc(u8, 100);
                                       ^

Detectando Use-After-Free

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    
    var ptr = try allocator.alloc(u8, 10);
    allocator.free(ptr);
    
    // ❌ Use-after-free - o GPA detecta
    ptr[0] = 42;  // Panic aqui!
}

Saída:

thread 12345 panic: Use of uninitialized memory
/home/user/projeto/src/main.zig:11:5: 0x1034a0 in main (main)
    ptr[0] = 42;
    ^

Detectando Double-Free

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    
    const ptr = try allocator.alloc(u8, 10);
    allocator.free(ptr);
    
    // ❌ Double-free - o GPA detecta
    allocator.free(ptr);  // Panic aqui!
}

Page Allocator para Valgrind

Para debugging avançado com Valgrind:

const std = @import("std");

pub fn main() !void {
    // PageAllocator é compatível com Valgrind
    var pa = std.heap.page_allocator;
    
    const ptr = try pa.alloc(u8, 4096);
    defer pa.free(ptr);
    
    // Use valgrind para detectar problemas
    // valgrind --leak-check=full ./meu-programa
}

Integração com VS Code

Configuração do Launch.json

Crie .vscode/launch.json:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug Zig",
            "type": "lldb",
            "request": "launch",
            "program": "${workspaceFolder}/zig-out/bin/${input:program}",
            "args": [],
            "cwd": "${workspaceFolder}",
            "preLaunchTask": "build"
        }
    ],
    "inputs": [
        {
            "id": "program",
            "type": "pickString",
            "description": "Qual programa debugar?",
            "options": ["meu-programa"],
            "default": "meu-programa"
        }
    ]
}

Configuração de Build Tasks

.vscode/tasks.json:

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "type": "shell",
            "command": "zig build",
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "presentation": {
                "echo": true,
                "reveal": "always",
                "focus": false,
                "panel": "shared"
            },
            "problemMatcher": ["$gcc"]
        },
        {
            "label": "test",
            "type": "shell",
            "command": "zig build test",
            "group": "test"
        }
    ]
}

Extensão Zig para VS Code

Instale a extensão oficial:

  1. Abra VS Code
  2. Ctrl+Shift+X (extensões)
  3. Procure “Zig”
  4. Instale “Zig” (ziglang.org)

Features:

  • Syntax highlighting
  • Autocomplete
  • Go to definition
  • Format on save (zig fmt)
  • Build tasks integradas

Breakpoints no VS Code

  1. Clique na margem esquerda para adicionar breakpoint (ponto vermelho)
  2. Pressione F5 para iniciar debugging
  3. Use F10 (step over), F11 (step into), Shift+F11 (step out)
  4. Variables panel mostra valores das variáveis
  5. Watch panel para monitorar expressões

Debugging de Erros Comuns

Erro: “attempt to use null value”

const std = @import("std");

pub fn main() void {
    var ptr: ?*i32 = null;
    
    // ❌ Erro: tentando dereferenciar null
    ptr.?.* = 42;  // Panic: attempt to use null value
}

Debug:

// ✅ Verifique antes de usar
if (ptr) |p| {
    p.* = 42;
} else {
    std.debug.print("ptr é null!\n", .{});
}

Erro: “index out of bounds”

const std = @import("std");

pub fn main() void {
    const array = [5]i32{1, 2, 3, 4, 5};
    const indice = 10;
    
    // ❌ Erro: índice fora dos limites
    const valor = array[indice];  // Panic aqui
}

Debug:

// ✅ Verifique bounds
std.debug.assert(indice < array.len);
const valor = array[indice];

Erro: “integer overflow”

const std = @import("std");

pub fn main() void {
    var x: u8 = 255;
    
    // ❌ Erro: overflow em modo Debug
    x += 1;  // Panic: integer overflow
}

Debug:

// ✅ Use funções saturadas ou checked
const y = std.math.add(u8, x, 1) catch |err| {
    std.debug.print("Overflow! err={}\n", .{err});
    return;
};

// Ou use wrapping se for intencional
x = @addWithOverflow(x, 1)[0];

Erro: “unreachable”

const std = @import("std");

fn parseNumero(input: []const u8) !i32 {
    if (input.len == 0) return error.Vazio;
    
    // ❌ Se chegar aqui com input inesperado
    unreachable;  // Panic!
}

pub fn main() !void {
    try parseNumero("abc");
}

Debug:

fn parseNumero(input: []const u8) !i32 {
    if (input.len == 0) return error.Vazio;
    if (std.mem.eql(u8, input, "abc")) return error.Invalido;
    
    // Agora unreachable é válido
    unreachable;
}

Técnicas Avançadas de Debug

Custom Panic Handler

const std = @import("std");

pub fn panic(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
    std.debug.print("\n╔════════════════════════════════════╗\n", .{});
    std.debug.print("║         PANIC DETECTADO!           ║\n", .{});
    std.debug.print("╚════════════════════════════════════╝\n", .{});
    std.debug.print("Mensagem: {s}\n", .{msg});
    
    if (error_return_trace) |trace| {
        std.debug.print("\nStack trace:\n", .{});
        std.debug.dumpStackTrace(trace.*);
    }
    
    std.process.exit(1);
}

pub fn main() void {
    @panic("Erro de demonstração");
}

Logging Estruturado

const std = @import("std");

const LogLevel = enum {
    debug,
    info,
    warn,
    err,
};

fn log(comptime level: LogLevel, comptime fmt: []const u8, args: anytype) void {
    const prefix = switch (level) {
        .debug => "[DEBUG]",
        .info => "[INFO]",
        .warn => "[WARN]",
        .err => "[ERROR]",
    };
    
    std.debug.print(prefix ++ " " ++ fmt ++ "\n", args);
}

pub fn main() void {
    log(.info, "Aplicação iniciada", .{});
    log(.debug, "Configuração carregada: {any}", .{{ .port = 8080 }});
    log(.warn, "Conexão lenta detectada", .{});
}

Time-based Debugging

const std = @import("std");

var timer: std.time.Timer = undefined;

fn startTimer() void {
    timer = std.time.Timer.start() catch unreachable;
}

fn logElapsed(comptime msg: []const u8) void {
    const elapsed_ns = timer.read();
    const elapsed_ms = @as(f64, @floatFromInt(elapsed_ns)) / 1_000_000.0;
    std.debug.print("[TIME] {s}: {d:.2}ms\n", .{msg, elapsed_ms});
}

pub fn main() !void {
    startTimer();
    
    // Código a ser medido
    var sum: u64 = 0;
    for (0..1_000_000) |i| {
        sum += i;
    }
    
    logElapsed("Loop de 1M iterações");
    std.debug.print("Soma: {d}\n", .{sum});
}

Checklist de Debugging

Quando seu código não funciona:

  • Build em modo Debugzig build (não ReleaseFast)
  • Adicione prints estratégicos — Antes e depois de operações suspeitas
  • Verifique inputs — Print todos os argumentos recebidos
  • Use asserts — Verifique pré-condições e pós-condições
  • Verifique erros — Todos os try e catch estão corretos?
  • Use GPA — Detecte memory leaks automaticamente
  • Leia stack traces — A última linha é onde o erro ocorreu
  • Debugger — GDB/LLDB para investigação profunda

Comandos Úteis:

# Build com informações de debug
zig build

# Run com diagnósticos
zig build run

# Testes
zig build test

# GDB
gdb ./zig-out/bin/meu-programa

# LLDB
lldb ./zig-out/bin/meu-programa

# Valgrind (Linux)
valgrind --leak-check=full ./zig-out/bin/meu-programa

# Strace (system calls)
strace ./zig-out/bin/meu-programa

Resumo

SituaçãoFerramenta/Técnica
Debug rápidostd.debug.print
Stack tracestd.debug.dumpStackTrace
Verificaçõesstd.debug.assert
Memory leaksGeneralPurposeAllocator
Debug profundoGDB ou LLDB
IDE integradoVS Code + LLDB
Erros de memóriaValgrind (Linux)

Próximos Passos

Agora que você domina debugging em Zig:

  1. 📊 Testes em Zig: Guia Completo — Escreva testes para prevenir bugs
  2. 💾 Gerenciamento de Memória em Zig — Entenda allocators em profundidade
  3. 🔧 Zig Build System — Configure builds otimizados
  4. 📚 Documentação de Debugging do Zig — Referência oficial

Tem dúvidas sobre debugging em Zig? Compartilhe com a comunidade!

Continue aprendendo Zig

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