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.
- Como preparar o programa para depuração:
-ge sem otimização (-O0). - Como rodar dentro do GDB (
gdb ./programa,run) e achar a linha de um crash combacktrace. - Como olhar variáveis com
printe trocar de função comframe. - Como pausar o programa numa linha (
break) e andar linha a linha (nextestep). - 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 remoto | No GDB |
|---|---|
| Dar o play | run |
| Marcar um ponto para pausar sozinho | break (um ponto de parada, breakpoint) |
| Avançar um quadro | next / step |
| Dar zoom num detalhe | print variável |
| Continuar até o próximo ponto marcado | continue |
| "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_dbsã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 umstd::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: omain, na linha 26, chamou…#1 mostrar (a=0x0) at crash.cpp:20: …a funçãomostrar, 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 usaa->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 paraAlunoque vale zero, onullptr. - 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
turmaneste contexto": cada quadro só enxerga as variáveis da sua função (veja Escopo). Aturmamora nomain, o quadro 2. - frame 2 / print turma
- No
main, aturmaexiste, 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, ostepentrou emmedia, 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,
nvale 7 esomavale 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
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.
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.
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).
O que aconteceu: o GDB não está instalado.
Como resolver: no Ubuntu e no Debian, sudo apt install gdb.
Referência rápida
| Comando | Atalho | O que faz |
|---|---|---|
g++ … -g arquivo.cpp -o prog | Compila com informações de depuração (e sem -O2). | |
gdb -q ./prog | Abre o programa no GDB. | |
gdb -batch -ex run -ex backtrace ./prog | Roda e, se quebrar, mostra onde. | |
run | r | Começa a rodar. |
break main / break 9 / break arq.cpp:9 | b | Marca um ponto de parada. |
next | n | Próxima linha, sem entrar em funções. |
step | s | Próxima linha, entrando nas funções. |
continue | c | Segue até o próximo ponto de parada ou o fim. |
finish | Termina a função atual e mostra o valor devolvido. | |
print expr | p | Mostra uma variável ou expressão. |
backtrace | bt | Mostra a pilha de chamadas. |
frame N | f | Vai para o quadro N da pilha. |
list | l | Mostra o código em volta da linha atual. |
info locals | Mostra todas as variáveis locais. | |
quit | q | Sai 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:
2. Onde quebrou?
O programa parou com SIGSEGV dentro do GDB. Qual comando mostra a pilha de chamadas?
3. Parar numa linha
Complete para marcar um ponto de parada na linha 15 do arquivo loja.cpp:
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
-ge sem-O2para depurar. gdb -q ./progabre;runroda; num crash, o GDB congela o programa no ponto do erro.backtracemostra quem chamou quem; a primeira linha com um arquivo seu é a pista.frame Nmuda de quadro;printmostra variáveis e expressões.breakpausa numa função ou linha;nextanda sem entrar em funções;stepentra;continuesegue;finishsai da função atual;quitsai.- Atalho para crashes:
gdb -batch -ex run -ex backtrace ./prog. - Os depuradores das IDEs fazem o mesmo, com botões.