Um dos grandes problemas dos alunos de graduação na área de tecnologia é que grande parte não utilizam algum tipo de depurador na época ainda de estudos. Postergando isso para quando os mesmo estão trabalhando. Segue uma dica de uso do sistema GDB (GNU Debugger), depurador que, como muitos outros, permite você ver profundamente, o que está acontecendo com um programa na sua execução.
De acordo com a documentação do GDB, ele pode fazer quatro tipos principais de coisas para ajudá-lo a identificar bugs:
- Inicie seu programa, especificando qualquer coisa que possa afetar seu comportamento.
- Faça seu programa parar em condições especificadas.
- Examine o que aconteceu, quando seu programa parar.
- Mude as instruções em seu programa, para que você possa experimentar corrigir os efeitos de um bug e continuar a aprender sobre outro.
Para início com GDB vamos utilizar o seguinte código:
/*
============================================================================
Name : main.c
Author : Anderson Moreira
Version :
Copyright : Your copyright notice
Description : Ansi-style to execute GDB
============================================================================
*/
#include <stdio.h>
#include <stdlib.h>
int main(void){
printf("Ola mundo!\n");
FILE *fp;
int a = 1;
a = a + 2;
fp = fopen("/home/alsm/out.txt", "w+");
fprintf(fp, "Este e um teste do fprintf...\n");
fputs("Este e um teste do fputs...\n", fp);
fclose(fp);
printf("a: %d\n", a);
return EXIT_SUCCESS;
}
Compile e Execute
Para compilar estamos utilizando o VSCODE, mas o mesmo também pode ser compilador em um terminal em que o GCC esteja instalado na máquina. Porém para depurar deve ser utilizada a flag “-g” que permite habilitar e usar informações extras.
GDB
Executando o Programa
Agora que temos o GDB aberto, pode começar a executar o programa usando o comando run
.
Ponto de interrupção (breakpoint)
Pontos de interrupção são lugares no código, por exemplo, uma linha no código, que você pode especificar e sempre que o computador atinge este ponto, ele faz uma pausa antes de executar a linha especificada e mostra o prompt do depurador. Para criar um ponto de interrupção usamos o comando break
.
No exemplo, estamos criando um breakpoint na linha 18
ou seja onde tem a = a + 2
;
Análise de Dados
Agora que temos um breakpoint, podemos executar o programa novamente e começar a analisar os dados.
Para ver o valor atual armazenado na variável a
usamos o comando print
.
Lembre-se que a linha atual ainda não foi executada. Então, se quisermos executar a linha atual e parar no próximo, executamos o comando next
.
Se imprimirmos a variável a
novamente, devemos ver um valor diferente.
Tudo funcionou como esperado, agora podemos continuar a execução usando o comando continue
, que retomará a execução do programa até que um breakpoint seja atingido.
Comandos Básicos
Comando | Versão resumida | Descrição |
run | r | O comando de execução faz com que a execução do programa comece desde o início |
quit | q | Sai do GDB |
breakpoint localização | b localização | O comando breakpoint define um ponto de interrupção em um determinado local. (linha, função, etc) |
print expressão | p expressão | Isso imprimirá o valor da expressão dada. |
continue | c | Continua a execução após um breakpoint, até o próximo ou o término do programa. |
step | s | Executa uma única linha após um breakpoint. |
next | n | Executa uma única linha. Se esta linha for uma chamada de subprograma, execute e retorne da chamada. |
list | l | Lista algumas linhas ao redor da localização de origem atual. |
backtrace | bt | Exibe um backtrace da cadeia de chamadas. |
Controle de Execução
Comando | Versão resumida | Descrição |
---|---|---|
run | r | Inicia a execução do programa |
run argumentos | r argumentos | Inicia a execução do programa com opções |
run <stdin-file> stdout-file | r < stdin-file > stdout-file | Inicia a execução do programa com redirecionamento da E/S |
continue | c | Continua a execução do programa até encontrar um breakpoint |
kill | Finaliza o processo atual | |
quit | q | Sai do GDB |
Gerenciamento do breakpoint
Comando | Descrição |
---|---|
break nome-da-função | Defina um ponto de interrupção na função especificada |
break número-linha | Defina um ponto de interrupção na linha especificada |
break NomeClasse::nomeFuncao | Define um ponto de interrupção numa função específica |
break +offset | Defina um ponto de interrupção num número específico de linhas para a frente a partir da posição atual |
break -offset | Defina um ponto de interrupção num número específico de linhas para a trás a partir da posição atual |
break nome-arquivo:função | Define um ponto de interrupção em uma função específica dentro de um arquivo |
break nome-arquivo:número-linha | Define um ponto de interrupção em um número específico de linha em um arquivo |
break *endereço | Defina um ponto de interrupção no endereço de instrução especificado |
break número-linha if condição | Define um ponto de interrupção se a condição é alcançada |
break linha thread número-thread | Define um ponto de interrupção em um thread especificado pelo um número de linha |
tbreak | Define uma parada temporária (para uma vez apenas) |
watch condição | Suspende a execução quando uma condição é executada |
clear | Apaga os pontos de interrupção |
clear função | Apaga todos os pontos de interrupção em uma função |
clear linha-número | Apaga os pontos de interrupção até um número de linha específico |
delete | Exclua todos os pontos de interrupção, pontos de observação ou pontos de captura |
delete breakpoint-número | Exclua um ponto de interrupção específico |
delete breakpoint-número–breakpoint-número | Exclua um ponto de interrupção dentro de um intervalo especificado. exemplo: delete 1-4 |
disable breakpoint-número | Desabilitar um ponto de interrupção especificado |
disable breakpoint-número–breakpoint-número | Desabilitar um ponto de interrupção dentro de um intervalo especificado. exemplo: disable 1-4 |
enable breakpoint-número | Habilita um ponto de interrupção especificado |
enable breakpoint-número–breakpoint-número | Habilita um ponto de interrupção especificado dentro de um intervalo. exemplo: enable 1-4 |
Pilha de análise
Comando | Versão resumida | Descrição |
---|---|---|
backtrace | bt | Imprime o rastreio da pilha |
backtrace full | Imprime valores das variáveis locais | |
frame | f | Mostra o quadro da pilha corrente |
frame número | f número | Mostra um número do quadro especifico na pilha |
up | Move um quadro único para cima | |
down | Move um quadro único para baixo | |
up número | Mova um número especificado de quadros na pilha para cima | |
down número | Mova um número especificado de quadros na pilha para baixo | |
info frame | Lista endereço, linguagem, endereço de argumentos/variáveis locais e quais registros foram salvos em um quadro | |
info args | Informações de argumentos de um quadro selecionado | |
info locals | Informações de argumentos de uma variável local selecionada | |
info catch | Informações de argumentos de um manipulador de exceções selecionada |
Depurar com arquivos de núcleo (core)
Um arquivo de núcleo (core dump) contém o espaço de endereço de um processo (memória) quando o mesmo finaliza a execução inesperadamente. Este arquivo é muito útil para depuração de erros, como por exemplo, falha de segmentação.
Gerando um arquivo de core dump
Muito sistemas por padrão tem a geração de arquivo core dump desabilitado. Podemos verificar executando o seguinte comando:
Para habilitar a criação deve executar o seguinte comando:
Programa exemplo
Para especificar o conceito de como utilizar arquivos core dump e GDB para uma melhor depuração de seus programas vamos escrever o seguinte programa.
#include <stdio.h>
int main() {
char* s = 0;
char c = s[0];
return 0;
}
Agora podemos compilar e executar o programa:
Você pode verificar o arquivo do core dump se o mesmo foi criado com algum comando de listagem (ls, dir, etc.).
Caso o arquivo dump não esteja sendo criado no linux, execute os seguintes comandos:
Desta forma todos os arquivos dump podem ser encontrados na pasta /tmp e funcionará os passos a seguir.
Agora que temos os arquivos de depuração, basta abrir o mesmo com o aplicativo do gdb.
$ gdb a.out a.dump
Lembrando que a.dump é o arquivo core dump criado.
A primeira coisa que deve fazer é utilizar o comando backtrace. Um programa quando executaé instanciado na memória uma área chamada stack (pilha) que contém informações sobre funções que estão sendo utilizadas e assim por diante. Cada item na pilha é mantido em um quadro (frame) e cada quadro contém informações necessárias em que as variáveis locais utilizam nas funções. O comando backtrace é utilizado para buscar informações da pilha quando o sinal SIGSEGV é acionado. Cada quadro na pilha tem um número onde 0 é a chamada mais recente.
O comando backtrace é essencial e possibilita ao programador uma boa ideia de onde está o problema
Interface gráfica do usuário
TUI
A Text User Interface (TUI) é uma possibilidade gráfica em terminal que o usuário pode verificar o código fonte, assembly, registradores e comandos do GDB em telas separadas. O modo TUI é habilitado por padrão quando utiliza a opção -tui juntamente com o comando do depurador.
Comandos do TUI
Comando | Descrição |
---|---|
layout next | Mostra o display seguinte. |
layout prev | Mostra o display anterior |
layout src | Mostra o código fonte. |
layout asm | Mostra o código assembly |
layout split | Mostra o fonte e o assembly |
layout regs | Mostra a janela de registradores junto com o código fonte ou o assembly |
focus next, prev, src, asm, regs, split | Mantém o foco na janela chamada como referência. |
refresh | Atualiza a tela. |
update | Atualiza a janela do código fonte e do ponto de execução corrente. |
winheight name +count | Aumenta o tamanho da janela de acordo com o número de linhas passado como parâmetro. |
winheight name -count | Diminui o tamanho da janela de acordo com o número de linhas passado como parâmetro. |
Exemplo de testes vamos utilizar o seguinte código:
#include <stdio.h>
int main() {
int a = 0;
a = 2;
printf("%s\n", "Ola GDB");
return 0;
}
Compile com os símbolos do GDB e execute
Inicialize o TUI
$ gdb -tui ./a.out
Você verá a seguinte tela
Adicione alguns pontos de interrupções e continue a execução para ver como este funciona com o TUI.
Após execute (digite o comando run) e imprima a variável “a” para acompanhar (digite o comando p a). Repita até finalizar a execução dos breakpoints.
Neste caso tudo funciona normalmente e podemos inclusive ver o assembly do código fonte (comando layout next).
Assembly com o GDB
O depurador também possibilita ao desenvolvedor executar o código assembly diretamente passo-a-passo. Principalmente para observar o comportamento de um determinado palicativo na memória, quais registradores este utiliza, referências, etc.
Os comandos relacionados são:
Comando | Versão resumida | Descrição |
---|---|---|
info line | Mostra a posição inicial e final de um código | |
info line numero | Mostra a posição de um código objeto em uma linha específica | |
disassemble start_address end_address | Mostra o código assembly de um código objeto específico, com os valores de memória inicial e final | |
stepi | si | Passa uma instrução assembly |
nexti | ni | Próxima instrução asembly |
x address | Examina o conteúdo da memória | |
x/nfu address | Examina o conteúdo da memória com um formato específico. n: número de itens para imprimir (padrão é 1), f: especifica o formato da saída i – instr, s-string, x-hex, d-sdecimal, u-udecimal, o-octal, t-binary, a-addr, c-char ,f-float, u: especifica o tamanho da unidade de dados b-byte, h-halfword, w-word, g-giant (8 bytes) |
O programa que utilizaremos para teste será este
#include <stdio.h>
int main() {
int a = 1;
a = a + 2;
printf("a: %d\n", a);
return 0;
}
Compile e execute
Crie um ponto de interrupção na função main.
Com o comando info line é possível ver em que posição de memória o código está alocado.
Podemos verificar o assembly gerado para o código e ver e a posição de memória corresponde ao que foi informado.
Podemos acompanhar a execução de uma instrução no decorrer do tempo usando o comando stepi.
Por fim podemos ver a importância da ferramenta, incluindo um novo ponto de interrupção na linha em que adiciona 2 a variável “a” e observar a mudança de valor na memória.