Introdução ao GDB

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

ComandoVersão resumidaDescrição
runrO comando de execução faz com que a execução do programa comece desde o início
quitqSai do GDB
breakpoint localizaçãob localizaçãoO comando breakpoint define um ponto de interrupção em um determinado local. (linha, função, etc)
print expressãop expressãoIsso imprimirá o valor da expressão dada.
continuecContinua a execução após um breakpoint, até o próximo ou o término do programa.
stepsExecuta uma única linha após um breakpoint.
nextnExecuta uma única linha. Se esta linha for uma chamada de subprograma, execute e retorne da chamada.
listlLista algumas linhas ao redor da localização de origem atual.
backtracebtExibe um backtrace da cadeia de chamadas.

Controle de Execução

ComandoVersão resumidaDescrição
runrInicia a execução do programa
run argumentosr argumentosInicia a execução do programa com opções
run <stdin-file> stdout-filer < stdin-file > stdout-fileInicia a execução do programa com redirecionamento da E/S
continuecContinua a execução do programa até encontrar um breakpoint
killFinaliza o processo atual
quitqSai do GDB

Gerenciamento do breakpoint

ComandoDescrição
break nome-da-funçãoDefina um ponto de interrupção na função especificada
break número-linhaDefina um ponto de interrupção na linha especificada
break NomeClasse::nomeFuncaoDefine um ponto de interrupção numa função específica
break +offsetDefina um ponto de interrupção num número específico de linhas para a frente a partir da posição atual
break -offsetDefina 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çãoDefine um ponto de interrupção em uma função específica dentro de um arquivo
break nome-arquivo:número-linhaDefine um ponto de interrupção em um número específico de linha em um arquivo
break *endereçoDefina um ponto de interrupção no endereço de instrução especificado
break número-linha if condiçãoDefine um ponto de interrupção se a condição é alcançada
break linha thread número-threadDefine um ponto de interrupção em um thread especificado pelo um número de linha
tbreakDefine uma parada temporária (para uma vez apenas)
watch condiçãoSuspende a execução quando uma condição é executada
clearApaga os pontos de interrupção
clear funçãoApaga todos os pontos de interrupção em uma função
clear linha-númeroApaga os pontos de interrupção até um número de linha específico
deleteExclua todos os pontos de interrupção, pontos de observação ou pontos de captura
delete breakpoint-númeroExclua um ponto de interrupção específico
delete breakpoint-númerobreakpoint-númeroExclua um ponto de interrupção dentro de um intervalo especificado. exemplo: delete 1-4
disable breakpoint-númeroDesabilitar um ponto de interrupção especificado
disable breakpoint-númerobreakpoint-númeroDesabilitar um ponto de interrupção dentro de um intervalo especificado. exemplo: disable 1-4
enable breakpoint-númeroHabilita um ponto de interrupção especificado
enable breakpoint-númerobreakpoint-númeroHabilita um ponto de interrupção especificado dentro de um intervalo. exemplo: enable 1-4

Pilha de análise

ComandoVersão resumidaDescrição
backtracebtImprime o rastreio da pilha
backtrace fullImprime valores das variáveis locais
framefMostra o quadro da pilha corrente
frame númerof númeroMostra um número do quadro especifico na pilha
upMove um quadro único para cima
downMove um quadro único para baixo
up númeroMova um número especificado de quadros na pilha para cima
down númeroMova um número especificado de quadros na pilha para baixo
info frameLista endereço, linguagem, endereço de argumentos/variáveis locais e quais registros foram salvos em um quadro
info argsInformações de argumentos de um quadro selecionado
info localsInformações de argumentos de uma variável local selecionada
info catchInformaçõ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

ComandoDescrição
layout nextMostra o display seguinte.
layout prevMostra o display anterior
layout srcMostra o código fonte.
layout asmMostra o código assembly
layout splitMostra o fonte e o assembly
layout regsMostra a janela de registradores junto com o código fonte ou o assembly
focus next, prev, src, asm, regs, splitMantém o foco na janela chamada como referência.
refreshAtualiza a tela.
updateAtualiza a janela do código fonte e do ponto de execução corrente.
winheight name +countAumenta o tamanho da janela de acordo com o número de linhas passado como parâmetro.
winheight name -countDiminui 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:

ComandoVersão resumidaDescrição
info lineMostra a posição inicial e final de um código
info line numeroMostra a posição de um código objeto em uma linha específica
disassemble start_address end_addressMostra o código assembly de um código objeto específico, com os valores de memória inicial e final
stepisiPassa uma instrução assembly
nextiniPróxima instrução asembly
x addressExamina o conteúdo da memória
x/nfu addressExamina 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.

Leave a Reply

Your email address will not be published. Required fields are marked *