C++ Depurar com o GDB

O programa quebrou com "Falha de segmentação" e não disse onde. Ou a média deu 8 quando deveria dar 8,33. Você pode encher o código de std::cout para descobrir o que está acontecendo, ou pode usar um depurador: um programa que roda o seu programa em câmera lenta, deixa você parar em qualquer linha e olhar o valor de cada variável. No Linux, o depurador clássico é o GDB (GNU Debugger). Nesta aula você vai usá-lo para achar a linha exata de um crash e para caçar um erro de conta, passo a passo.

Você vai aprender
  • Como preparar o programa para depuração: -g e sem otimização (-O0).
  • Como rodar dentro do GDB (gdb ./programa, run) e achar a linha de um crash com backtrace.
  • Como olhar variáveis com print e trocar de função com frame.
  • Como pausar o programa numa linha (break) e andar linha a linha (next e step).
  • Como seguir em frente (continue, finish) e sair (quit).
  • Que as IDEs têm depuradores com botões, que fazem a mesma coisa.

Todas as sessões desta aula são reais, feitas com o GDB 17 e o g++ 15 na máquina fictícia da Ana, na pasta ~/cpp/gdb. O GDB não funciona no editor do guia (ele precisa controlar um programa de verdade no seu sistema); para praticar, instale-o (no Ubuntu: sudo apt install gdb; veja Instalar no Linux). As missões desta página corrigem, no editor, os erros que o GDB encontrou.


A ideia: pausar o vídeo

Assistir a um programa rodar normalmente é como ver um vídeo de uma jogada em velocidade máxima: tudo acontece em milésimos de segundo, e você só vê o resultado. O depurador é o controle remoto:

No controle remotoNo GDB
Dar o playrun
Marcar um ponto para pausar sozinhobreak (um ponto de parada, breakpoint)
Avançar um quadronext / step
Dar zoom num detalheprint variável
Continuar até o próximo ponto marcadocontinue
"Como chegamos aqui?"backtrace

1. Preparar o programa: -g

O programa compilado normalmente não guarda os nomes das variáveis nem os números das linhas: o processador não precisa disso. Para o depurador poder dizer "linha 20, variável a", compile com a opção -g, que acrescenta essas informações de depuração ao executável:

$ g++ -std=c++23 -Wall -Wextra -g crash.cpp -o crash

E deixe as otimizações desligadas (é o padrão do g++, chamado -O0). Com -O2, o compilador reorganiza, junta e apaga linhas, e o passo a passo fica confuso (seção 6). Resumo: para depurar, -g sem -O2.


2. Achar a linha de um crash

Este programa procura alunos numa turma. A função buscar devolve um ponteiro para o aluno encontrado, ou nullptr se não achar (veja nullptr):

#include <iostream>
#include <string>
#include <vector>

struct Aluno {
    std::string nome;
    double nota = 0.0;
};

const Aluno* buscar(const std::vector<Aluno>& turma, const std::string& nome) {
    for (const auto& a : turma) {
        if (a.nome == nome) {
            return &a;
        }
    }
    return nullptr;
}

void mostrar(const Aluno* a) {
    std::cout << a->nome << ": " << a->nota << '\n';
}

int main() {
    std::vector<Aluno> turma = {{"Ana", 9.5}, {"Bruno", 7.0}};
    mostrar(buscar(turma, "Ana"));
    mostrar(buscar(turma, "Zeca"));
    std::cout << "Fim.\n";
}
$ ./crash
Ana: 9.5
Falha de segmentação     (imagem do núcleo gravada) ./crash
$ echo $?
139

O programa mostrou a Ana e morreu (sobre a falha de segmentação e o código 139, veja Comportamento Indefinido). Onde? Vamos perguntar ao GDB.

Rodar dentro do GDB: run

$ gdb -q ./crash
Reading symbols from ./crash...
(gdb) run
Starting program: /home/ana/cpp/gdb/crash
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1".
Ana: 9.5

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7d79764 in std::basic_ostream<char, std::char_traits<char> >& std::operator<< <char, std::char_traits<char>, std::allocator<char> >(std::basic_ostream<char, std::char_traits<char> >&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) ()
   from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
gdb -q ./crash
Abre o GDB com o programa. O -q (quiet, "quieto") esconde a mensagem de boas-vindas com a licença. O GDB mostra o seu prompt, (gdb), e espera comandos. O programa ainda não está rodando.
Reading symbols from ./crash...
Leu as informações do -g.
run
Começa a rodar o programa, dentro do GDB. As duas linhas sobre libthread_db são técnicas e aparecem sempre; pode ignorar.
Program received signal SIGSEGV, Segmentation fault.
"O programa recebeu o sinal SIGSEGV, falha de segmentação". Em vez de deixar o programa morrer, o GDB o congelou no exato instante do erro.
0x00007ffff7d79764 in std::basic_ostream… from …libstdc++.so.6
Onde ele congelou: dentro do operator<< que escreve um std::string, na biblioteca do C++ (libstdc++). Assustador, mas o erro quase nunca é da biblioteca: ela só recebeu algo estragado do seu código. Precisamos saber quem a chamou.

"Como chegamos aqui?": backtrace

(gdb) backtrace
#0  0x00007ffff7d79764 in std::basic_ostream<char, std::char_traits<char> >& std::operator<< <char, std::char_traits<char>, std::allocator<char> >(std::basic_ostream<char, std::char_traits<char> >&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) ()
   from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#1  0x0000555555556414 in mostrar (a=0x0) at crash.cpp:20
#2  0x0000555555556684 in main () at crash.cpp:26

O backtrace ("rastro para trás", ou bt) mostra a pilha de chamadas: quem chamou quem até chegar ao erro. Leia de baixo para cima:

  • #2 main () at crash.cpp:26: o main, na linha 26, chamou…
  • #1 mostrar (a=0x0) at crash.cpp:20: …a função mostrar, que na linha 20 chamou…
  • #0 … operator<< …: …o << da biblioteca, onde tudo quebrou.

A primeira linha que é do seu arquivo é a pista: crash.cpp:20. E o GDB ainda mostra o valor do parâmetro: a=0x0, ou seja, o ponteiro é nulo. Cada linha da pilha se chama quadro (frame), numerado a partir de 0.

Olhar as variáveis: frame e print

(gdb) frame 1
#1  0x0000555555556414 in mostrar (a=0x0) at crash.cpp:20
20	    std::cout << a->nome << ": " << a->nota << '\n';
(gdb) print a
$1 = (const Aluno *) 0x0
(gdb) print *a
❌️ Não é possível acessar a memória no endereço 0x0
(gdb) print turma
❌️ No symbol "turma" in current context.
(gdb) frame 2
#2  0x0000555555556684 in main () at crash.cpp:26
26	    mostrar(buscar(turma, "Zeca"));
(gdb) print turma
$2 = std::vector of length 2, capacity 2 = {{nome = "Ana", nota = 9.5}, {
    nome = "Bruno", nota = 7}}
(gdb) print turma[1].nome
$3 = "Bruno"
(gdb) quit
A debugging session is active.

	Inferior 1 [process 14] will be killed.

Quit anyway? (y or n) y
frame 1
"Vá para o quadro 1": a função mostrar. O GDB mostra a linha onde ela estava: a 20, que usa a->nome.
print a
Mostra o valor de uma variável (atalho: p a). Cada resultado ganha um número, $1, $2…, que você pode reusar. (const Aluno *) 0x0: um ponteiro para Aluno que vale zero, o nullptr.
print *a
Tenta ver o aluno apontado. O GDB responde (em português, porque o sistema da Ana está em português): não é possível acessar a memória no endereço 0. É exatamente o que o programa tentou fazer.
print turma → No symbol "turma" in current context.
"Não existe turma neste contexto": cada quadro só enxerga as variáveis da sua função (veja Escopo). A turma mora no main, o quadro 2.
frame 2 / print turma
No main, a turma existe, e o GDB mostra o vetor inteiro, com as structs dentro. Dá até para usar expressões: print turma[1].nome.
quit
Sai do GDB. Como o programa ainda estava congelado, ele pergunta se pode encerrá-lo: y.

Diagnóstico: na linha 26, o main buscou "Zeca", que não está na turma; buscar devolveu nullptr; mostrar usou o ponteiro nulo sem conferir. Sem um único cout a mais no código.

Missão: corrija o erro que o GDB encontrou: no começo de mostrar, se a for nullptr, mostre Aluno não encontrado e saia da função com return;. O programa deve chegar ao Fim.. (No editor, o ponteiro nulo passa calado: em vez da falha de segmentação, aparece uma linha : 0 no lugar do Zeca e o programa segue. O erro é o mesmo; só não dá sinal.)

#include <iostream>
#include <string>
#include <vector>

struct Aluno {
    std::string nome;
    double nota = 0.0;
};

const Aluno* buscar(const std::vector<Aluno>& turma, const std::string& nome) {
    for (const auto& a : turma) {
        if (a.nome == nome) {
            return &a;
        }
    }
    return nullptr;
}

void mostrar(const Aluno* a) {
    std::cout << a->nome << ": " << a->nota << '\n';
}

int main() {
    std::vector<Aluno> turma = {{"Ana", 9.5}, {"Bruno", 7.0}};
    mostrar(buscar(turma, "Ana"));
    mostrar(buscar(turma, "Zeca"));
    std::cout << "Fim.\n";
}
#include <iostream>
#include <string>
#include <vector>

struct Aluno {
    std::string nome;
    double nota = 0.0;
};

const Aluno* buscar(const std::vector<Aluno>& turma, const std::string& nome) {
    for (const auto& a : turma) {
        if (a.nome == nome) {
            return &a;
        }
    }
    return nullptr;
}

void mostrar(const Aluno* a) {
    if (a == nullptr) {
        std::cout << "Aluno não encontrado\n";
        return;
    }
    std::cout << a->nome << ": " << a->nota << '\n';
}

int main() {
    std::vector<Aluno> turma = {{"Ana", 9.5}, {"Bruno", 7.0}};
    mostrar(buscar(turma, "Ana"));
    mostrar(buscar(turma, "Zeca"));
    std::cout << "Fim.\n";
}

3. Atalho: tudo numa linha com -batch

Quando você só quer saber onde o programa quebrou, dá para passar os comandos direto na linha de comando. -batch roda os comandos e sai; cada -ex é um comando:

$ gdb -batch -ex run -ex backtrace ./crash
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1".
Ana: 9.5

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff7d79764 in std::basic_ostream<char, std::char_traits<char> >& std::operator<< <char, std::char_traits<char>, std::allocator<char> >(std::basic_ostream<char, std::char_traits<char> >&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#0  0x00007ffff7d79764 in std::basic_ostream<char, std::char_traits<char> >& std::operator<< <char, std::char_traits<char>, std::allocator<char> >(std::basic_ostream<char, std::char_traits<char> >&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#1  0x0000555555556414 in mostrar (a=0x0) at crash.cpp:20
#2  0x0000555555556684 in main () at crash.cpp:26

Guarde este comando: gdb -batch -ex run -ex backtrace ./programa é a forma mais rápida de transformar "Falha de segmentação" em "linha 20".

Sem -g, sem linhas

O mesmo programa compilado sem -g (crash-semg):

$ g++ -std=c++23 -Wall -Wextra crash.cpp -o crash-semg
$ gdb -batch -ex run -ex backtrace ./crash-semg
«… as mesmas linhas até o #0 …»
#1  0x0000555555556414 in mostrar(Aluno const*) ()
#2  0x0000555555556684 in main ()

O GDB ainda sabe os nomes das funções, mas não as linhas, nem os valores dos parâmetros. Por isso o -g.


4. Andar linha a linha: break, next, step

Nem todo erro derruba o programa. Este calcula a média de 7, 8 e 10, que é 8,33…, mas mostra 8:

#include <iostream>
#include <vector>

double media(const std::vector<int>& notas) {
    int soma = 0;
    for (int n : notas) {
        soma += n;
    }
    return soma / notas.size();
}

int main() {
    std::vector<int> notas = {7, 8, 10};
    double m = media(notas);
    std::cout << "Média: " << m << '\n';
}
$ g++ -std=c++23 -Wall -Wextra -g media.cpp -o media
$ ./media
Média: 8

Vamos assistir em câmera lenta. Primeiro, um ponto de parada no começo do main, e o programa roda até lá:

$ gdb -q ./media
Reading symbols from ./media...
(gdb) break main
Ponto de parada 1 at 0x13b1: file media.cpp, line 12.
(gdb) run
Starting program: /home/ana/cpp/gdb/media
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main () at media.cpp:12
12	int main() {

break main (atalho: b main) marca um ponto de parada na função main. Também dá para marcar por linha (break 9) ou por arquivo e linha (break media.cpp:9). O run rodou até o ponto e parou. A linha mostrada (12 int main() {) é a próxima a ser executada; ela ainda não rodou.

(gdb) next
13	    std::vector<int> notas = {7, 8, 10};
(gdb) next
14	    double m = media(notas);
(gdb) step
media (notas=std::vector of length 3, capacity 3 = {...}) at media.cpp:4
4	double media(const std::vector<int>& notas) {
(gdb) next
5	    int soma = 0;
(gdb) next
6	    for (int n : notas) {
(gdb) next
7	        soma += n;
(gdb) next
6	    for (int n : notas) {
(gdb) print n
$1 = 7
(gdb) print soma
$2 = 7
next
Executa a linha atual inteira e para na próxima (atalho: n). Se a linha chama uma função, a função roda toda, sem você entrar nela.
step
Como o next, mas entra na função chamada (atalho: s). Na linha 14, o step entrou em media, e o GDB mostrou até o parâmetro recebido.
7 → 6 → 7…
O laço: a linha 7 soma, a 6 pega o próximo número. Depois da primeira volta, n vale 7 e soma vale 7. Tudo certo até aqui.

Apertar next três vezes por volta cansa. Vamos marcar um ponto de parada na linha do return e deixar o laço terminar sozinho com continue:

(gdb) break 9
Ponto de parada 2 at 0x555555555341: file media.cpp, line 9.
(gdb) continue
Continuing.

Breakpoint 2, media (notas=std::vector of length 3, capacity 3 = {...})
    at media.cpp:9
9	    return soma / notas.size();
(gdb) print soma
$3 = 25
(gdb) print notas.size()
$4 = 3
(gdb) print soma / notas.size()
$5 = 8
(gdb) print soma / 3.0
$6 = 8.3333333333333339

Achamos! A soma está certa (25) e o tamanho também (3), mas soma / notas.size() dá 8: as duas partes são inteiras, então a divisão é inteira e joga fora o ",33" (veja Operadores Aritméticos). Com um lado double (3.0), o resultado sai certo. O print aceita qualquer expressão do C++, e isso permite testar a correção antes de mexer no código.

(gdb) finish
Run till exit from #0  media (
    notas=std::vector of length 3, capacity 3 = {...}) at media.cpp:9
0x00005555555553f6 in main () at media.cpp:14
14	    double m = media(notas);
Value returned is $7 = 8
(gdb) continue
Continuing.
Média: 8
[Inferior 1 (process 14) exited normally]
(gdb) quit
finish
Roda até a função atual terminar, volta para quem chamou e mostra o valor devolvido: Value returned is $7 = 8.
continue
Segue rodando até o próximo ponto de parada ou até o fim (atalho: c). exited normally = "terminou normalmente", com código 0. O "Inferior 1" é como o GDB chama o programa que ele está controlando.

Missão: corrija a divisão inteira que o GDB revelou, para a saída ser Média: 8.33333. Dica: converta a soma para double antes de dividir, com static_cast<double>(soma) (veja Conversão de Tipos).

#include <iostream>
#include <vector>

double media(const std::vector<int>& notas) {
    int soma = 0;
    for (int n : notas) {
        soma += n;
    }
    return soma / notas.size();
}

int main() {
    std::vector<int> notas = {7, 8, 10};
    double m = media(notas);
    std::cout << "Média: " << m << '\n';
}
#include <iostream>
#include <vector>

double media(const std::vector<int>& notas) {
    int soma = 0;
    for (int n : notas) {
        soma += n;
    }
    return static_cast<double>(soma) / notas.size();
}

int main() {
    std::vector<int> notas = {7, 8, 10};
    double m = media(notas);
    std::cout << "Média: " << m << '\n';
}

5. Cuidado: o step entra até na biblioteca

O step entra em qualquer função, inclusive nas da biblioteca padrão, que estão escondidas em linhas que parecem simples. Numa das tentativas desta aula, a Ana deu step na linha que cria o vetor:

(gdb) next
13	    std::vector<int> notas = {7, 8, 10};
(gdb) step
std::allocator<int>::allocator (this=<optimized out>)
    at /usr/include/c++/15/bits/allocator.h:168
168	      allocator() _GLIBCXX_NOTHROW { }
(gdb) next
main () at media.cpp:13
13	    std::vector<int> notas = {7, 8, 10};

Ela caiu dentro do código do std::vector (o arquivo allocator.h da biblioteca). Não há nada de errado aí; é só o lugar errado para procurar. Quando isso acontecer, use finish (ou alguns next) para voltar ao seu código. Regra prática: next por padrão; step só nas linhas que chamam funções suas.


6. Por que depurar sem -O2

Com otimização, o compilador reescreve o programa à vontade. O mesmo media.cpp, compilado com -O2 -g:

$ g++ -std=c++23 -Wall -Wextra -O2 -g media.cpp -o media-O2
$ gdb -q ./media-O2
Reading symbols from ./media-O2...
(gdb) break media.cpp:9
❌️ No compiled code for line 9 in file "media.cpp".
Make breakpoint pending on future shared library load? (y or [n]) n
(gdb) quit

No compiled code for line 9 = "não há código compilado para a linha 9": o -O2 juntou a linha 9 com outras, e ela deixou de existir como um lugar onde parar (a pergunta seguinte é sobre bibliotecas carregadas depois; responda n). Com otimização, também é comum o GDB mostrar <optimized out> ("removida pela otimização") no lugar de uma variável, e o next pular de um lado para o outro. Para depurar, compile com -g e sem -O2 (ou com -Og, uma otimização leve feita para não atrapalhar a depuração).


Depuradores com botões (IDEs)

As IDEs têm depuradores visuais (veja Editores e IDEs): você clica na margem para marcar um ponto de parada, usa botões para Step Over (o next), Step Into (o step), Step Out (o finish) e Continue, e vê as variáveis numa janela, sem digitar print. No Linux, a maioria delas (VS Code com a extensão C/C++, CLion, Qt Creator, Code::Blocks) usa o próprio GDB por baixo. No Windows, o Visual Studio tem um depurador próprio; no macOS, o Xcode usa o LLDB, primo do GDB. As ideias desta aula valem para todos.


Quando dá errado

"No symbol "x" in current context."

O que aconteceu: a variável não existe no quadro atual: ela é de outra função, ou de um bloco que já terminou, ou o programa nem está rodando.

Como resolver: veja os quadros com backtrace e mude com frame N. Se o programa não estiver rodando, marque um break e dê run.

"The program is not being run."

O que aconteceu: você deu next, step ou continue antes do run, ou depois de o programa terminar.

Como resolver: marque um ponto de parada (break main) e dê run.

O backtrace não mostra arquivos e linhas, só mostrar(Aluno const*) ()

O que aconteceu: o programa foi compilado sem -g.

Como resolver: compile de novo com -g e repita.

<optimized out>, "No compiled code for line", o next pulando linhas

O que aconteceu: o programa foi compilado com otimização (-O2).

Como resolver: compile com -g e sem -O2 (ou com -Og).

"gdb: comando não encontrado"

O que aconteceu: o GDB não está instalado.

Como resolver: no Ubuntu e no Debian, sudo apt install gdb.


Referência rápida

ComandoAtalhoO que faz
g++ … -g arquivo.cpp -o progCompila com informações de depuração (e sem -O2).
gdb -q ./progAbre o programa no GDB.
gdb -batch -ex run -ex backtrace ./progRoda e, se quebrar, mostra onde.
runrComeça a rodar.
break main / break 9 / break arq.cpp:9bMarca um ponto de parada.
nextnPróxima linha, sem entrar em funções.
stepsPróxima linha, entrando nas funções.
continuecSegue até o próximo ponto de parada ou o fim.
finishTermina a função atual e mostra o valor devolvido.
print exprpMostra uma variável ou expressão.
backtracebtMostra a pilha de chamadas.
frame NfVai para o quadro N da pilha.
listlMostra o código em volta da linha atual.
info localsMostra todas as variáveis locais.
quitqSai do GDB.

Apertar Enter sem digitar nada repete o último comando: ótimo para uma sequência de next.


Exercícios

1. Compilar para depurar

Complete a opção que acrescenta as informações de depuração:

g++ -std=c++23 -Wall -Wextra prog.cpp -o prog

2. Onde quebrou?

O programa parou com SIGSEGV dentro do GDB. Qual comando mostra a pilha de chamadas?

(gdb)

3. Parar numa linha

Complete para marcar um ponto de parada na linha 15 do arquivo loja.cpp:

(gdb)

4. A linha atual é double m = media(notas); e você quer ver o que acontece dentro de media. Qual comando?

5. No backtrace, o quadro #0 está dentro de libstdc++.so.6 e o #1 é mostrar (a=0x0) at crash.cpp:20. Onde provavelmente está o erro?


Resumo

  • Compile com -g e sem -O2 para depurar.
  • gdb -q ./prog abre; run roda; num crash, o GDB congela o programa no ponto do erro.
  • backtrace mostra quem chamou quem; a primeira linha com um arquivo seu é a pista. frame N muda de quadro; print mostra variáveis e expressões.
  • break pausa numa função ou linha; next anda sem entrar em funções; step entra; continue segue; finish sai da função atual; quit sai.
  • Atalho para crashes: gdb -batch -ex run -ex backtrace ./prog.
  • Os depuradores das IDEs fazem o mesmo, com botões.