GDB (GNU Debugger) é um programa que ajuda a saber o que está acontecendo dentro de um outro processo (debuggee), acompanhar sua execução, parar a execução, ver e alterar seu estado interno. Resumindo: é um debugger
. O GDB foi feito primariamente para debugar programas escritos em C/C++, mas possui suporte de nível variado para programas escritos em outras linguagens. O GDB funciona em vários Sistemas Operacionais e em várias arquiteturas de processadores, mas neste texto vou considerar o GDB debugando programas em C, executando em Linux, arquitetura x86. Não tenho aqui a pretensão de apresentar um guia abrangente e cobrir cada uma das peculiaridades e detalhes obscuros do GDB nem seu funcionamento interno, ao contrário, a intenção é mostrar como fazer o básico, através de exemplos práticos. Muitos dos resultados mostrados podem ser obtidos de formas diferentes da mostrada, ficando a escolha ao gosto do freguês. Por fim, os melhores lugares para encontrar informações detalhadas sobre todos os aspectos do GDB são:
# info gdb
# man gdb
Página do GDB.
Como qualquer debugger decente, o GDB possui as seguintes características:
Executar um programa, parando na primeira instrução ou linha de código.
Executar um programa passo a passo.
Criar e gerenciar breakpoints, inclusive breakpoints de expressões e breakpoints condicionais.
Alterar o estado da CPU, registradores, pilha.
Examinar e alterar o valor de variáveis, salvar trechos de memória para um arquivo.
Parar o programa em caso de erro (exceção).
Criar tracepoints
Atachar o debugger à um programa em execução.
Gerar/Debugar core dumps.
Criar scripts de debug usando Python.
Debugar um programa executando em um outro computador.
Des-debugar (é, eu inventei a palavra).
Gravar e reproduzir uma sessão.
Antes que seja tarde, um pequeno glossário para esclarecer alguns termos:
| Debugger |
Um programa auxiliar capaz de examinar/alterar o estado interno de um outro programa. |
| Debuggee |
O programa que está sendo debugado pelo Debugger. |
| GNU |
GNU is Not Unix. |
| GDB |
GNU Debugger, o programa em estudo |
| GCC |
GNU Compiler Collection, o compilador C/C++ usado nos exemplos |
| ELF |
Executable and Linkable Format, uma especificação de formato para arquivos binários executáveis e bibliotecas. Padrão no Linux. |
| COFF |
Common Object File Format, uma especificação de formato de arquivos binários executáveis e bibliotecas. Perdeu espaço no mundo Unix para o ELF, mais completo. |
| DWARF |
Formato para armazenamento de símbolos de debug. |
| Symbol, símbolo |
Um nome de variável ou função, normalmente dado pelo programador ao construir o código fonte. Debuggers modernos associam automaticamente endereços de memória a símbolos, facilitando o debug. |
| Section, seção |
Um formato de representação de arquivo binário como ELF ou COFF divide o arquivo em seções, que contém tipos diversos de informação. Uma ou mais seções podem estar dedicadas a armazenar informações de debug, como símbolos. |
| Session, sessão |
uma sessão de debug, uma rodada de execução de um programa que está sendo debugado. |
Vamos por logo a mão na massa: Usando o projeto esqueleto, fiz um programa para ser debugado, está aqui. Agora só precisamos iniciar uma sessão de debug usando GDB.
Aqui está o código fonte, para facilitar o acompanhamento:
/*
* Copyright (C) 2009 William Cesar de Oliveira
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <stdlib.h>
#define TEST_VALUE 100
int v_global = TEST_VALUE;
int main(int argc, char *argv[])
{
int i;
FILE *my_out = NULL;
my_out = stdout;
for (i = 0; i < TEST_VALUE; i++, v_global--)
fprintf(my_out, "i=%d global=%d\n", i, v_global);
return 0;
}
Para começar, compile o programa usando
# make clean release
E, depois execute o debugger com
# gdb ./gdb0
Com essa linha de comando o GDB vai carregar, mostrar seu o prompt e ficar aguardando um comando. Para fazer o programa executar,
gdb> run
E pronto, o programa debugado está executando sob a tutela do GDB. Fácil, não? Tanto a entrada como a saída padrão do programa (para onde vão os printf) são as mesmas do GDB, portanto, ao término do programa, o controle retorna para o GDB, mas o console continua o mesmo.
Quando qualquer condição anormal acontece no programa (um erro, fatal signal, exceção ou um breakpoint), o prompt do GDB retorna, e podemos examinar ou modificar o estado do programa debugado. Quando o programa debugado termina o controle também retorna para o GDB.
Logo quando o GDB inicia, é mostrada uma mensagem sobre o carregamento dos símbolos, no caso desse primeiro teste, a mensagem é:
Reading symbols from ./gdb0...(no debugging symbols found)...done.
Isso significa que o GDB não foi capaz de encontrar as informações necessárias para associar nomes de variáveis, funções e linhas com o programa em execução. Na prática, quer dizer que tudo o que você vai ver são instruções assembler, conteúdo de registradores e endereços de memória, sem nenhuma associação com os nomes usados no código fonte do programa.
O GDB ainda é útil para debugar programas que não contém os símbolos de debug, mas o processo fica muito mais difícil, exigindo conhecimentos da arquitetura do processador, um pouco do funcionamento interno do sistema operacional e muita paciência.
Podemos examinar o conteúdo de um binário qualquer usando o utilitário objdump. No caso desse programa de testes gdb0:
# objdump -h gdb0
gdb0: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .interp 0000001c 0000000000400200 0000000000400200 00000200 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .note.ABI-tag 00000020 000000000040021c 000000000040021c 0000021c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .note.gnu.build-id 00000024 000000000040023c 000000000040023c 0000023c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .gnu.hash 0000001c 0000000000400260 0000000000400260 00000260 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .dynsym 00000060 0000000000400280 0000000000400280 00000280 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .dynstr 0000003f 00000000004002e0 00000000004002e0 000002e0 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .gnu.version 00000008 0000000000400320 0000000000400320 00000320 2**1
CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .gnu.version_r 00000020 0000000000400328 0000000000400328 00000328 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
8 .rela.dyn 00000018 0000000000400348 0000000000400348 00000348 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
9 .rela.plt 00000030 0000000000400360 0000000000400360 00000360 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
10 .init 00000018 0000000000400390 0000000000400390 00000390 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
11 .plt 00000030 00000000004003a8 00000000004003a8 000003a8 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
12 .text 000001f8 00000000004003e0 00000000004003e0 000003e0 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
13 .fini 0000000e 00000000004005d8 00000000004005d8 000005d8 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
14 .rodata 00000020 00000000004005e8 00000000004005e8 000005e8 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
15 .eh_frame_hdr 00000024 0000000000400608 0000000000400608 00000608 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
16 .eh_frame 0000007c 0000000000400630 0000000000400630 00000630 2**3
CONTENTS, ALLOC, LOAD, READONLY, DATA
17 .ctors 00000010 00000000006006b0 00000000006006b0 000006b0 2**3
CONTENTS, ALLOC, LOAD, DATA
18 .dtors 00000010 00000000006006c0 00000000006006c0 000006c0 2**3
CONTENTS, ALLOC, LOAD, DATA
19 .jcr 00000008 00000000006006d0 00000000006006d0 000006d0 2**3
CONTENTS, ALLOC, LOAD, DATA
20 .dynamic 00000190 00000000006006d8 00000000006006d8 000006d8 2**3
CONTENTS, ALLOC, LOAD, DATA
21 .got 00000008 0000000000600868 0000000000600868 00000868 2**3
CONTENTS, ALLOC, LOAD, DATA
22 .got.plt 00000028 0000000000600870 0000000000600870 00000870 2**3
CONTENTS, ALLOC, LOAD, DATA
23 .data 00000004 0000000000600898 0000000000600898 00000898 2**2
CONTENTS, ALLOC, LOAD, DATA
24 .bss 00000010 00000000006008a0 00000000006008a0 0000089c 2**3
ALLOC
25 .comment 0000013f 0000000000000000 0000000000000000 0000089c 2**0
CONTENTS, READONLY
Nota-se que é um binário no formato ELF, 64 bits, para arquitetura x86_64. Vamos recompilar e debugar o mesmo programa novamente, agora usando
# make clean debug
Executando o objdump novamente, podemos ver que existem algumas seções que não estavam presente na versão release:
...
26 .debug_aranges 00000030 0000000000000000 0000000000000000 000009eb 2**0
CONTENTS, READONLY, DEBUGGING
27 .debug_pubnames 0000001b 0000000000000000 0000000000000000 00000a1b 2**0
CONTENTS, READONLY, DEBUGGING
28 .debug_info 000000da 0000000000000000 0000000000000000 00000a36 2**0
CONTENTS, READONLY, DEBUGGING
29 .debug_abbrev 00000075 0000000000000000 0000000000000000 00000b10 2**0
CONTENTS, READONLY, DEBUGGING
30 .debug_line 00000214 0000000000000000 0000000000000000 00000b85 2**0
CONTENTS, READONLY, DEBUGGING
31 .debug_str 000000ba 0000000000000000 0000000000000000 00000d99 2**0
CONTENTS, READONLY, DEBUGGING
32 .debug_macinfo 000048d6 0000000000000000 0000000000000000 00000e53 2**0
CONTENTS, READONLY, DEBUGGING
33 .debug_pubtypes 00000012 0000000000000000 0000000000000000 00005729 2**0
CONTENTS, READONLY, DEBUGGING
Como mencionado, essas seções servirão para ajudar um debugger como o GDB a relacionar os símbolos com os endereços de memória e nomes de função. Embora as informações de debug não precisem estar necessariamente embutidas no próprio binário, vamos usar esse modelo por ser mais simples.
Agora, veremos a seguinte mensagem quando o GDB carregar o programa:
# gdb ./gdb0
Reading symbols from ./gdb0...done.
Essa singela mensagem significa que o GDB vai ser capaz de correlacionar os símbolos do código fonte com o programa em execução.
Para construir um programa com os símbolos de debug, temos que instruir o compilador/linker para fazer isso. No caso do GCC, o comando -g está associado a debugging. Se usarmos o comando -ggdb, estaremos instruindo o GCC a adicionar informações de debug no formato ideal para o GDB. Outra opção que afeta o debugger é a otimização, o programa ideal para debug é o que foi compilado com todas as opções de otimização desligadas (no caso do GCC, -O0). Isso porque, durante a otimização, o compilador pode mover/remover varíaveis, funções e trechos de código, tornando mais difícil (mas não impossível) o processo de debug. Para mais informações, consulte o manual do GCC.
Um programa normalmente faz uso de bibliotecas carregadas dinamicamente, as famosas libs, sendo que a libc é usada em 99.9% dos programas. O GDB é capaz de carregar os símbolos tanto para o executável propriamente dito como para as libs, bastando que os símbolos estejam disponíveis para serem automaticamente carregados.
Voltando ao debug, usando start ao invés de run faremos com que o progama pare na primeira linha com side-effect do código fonte:
(gdb) start
Temporary breakpoint 1 at 0x400514: file main.c, line 28.
Starting program: ./gdb0
Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe1a8) at main.c:28
28 FILE *my_out = NULL;
O GDB colocou um breakpoint temporário na linha 28 antes de executar o programa. Um breakpoint é usado para indicar ao GDB que a execução do programa debugado deve ser suspensa e o controle deve ser passado ao GDB.
Como o programa está suspenso, podemos examinar a pilha de chamadas para a função atual (main):
(gdb) bt
#0 main (argc=1, argv=0x7fffffffe1a8) at main.c:28
Também podemos examinar a pilha incluindo as variáveis locais:
#0 main (argc=1, argv=0x7fffffffe1a8) at main.c:28
i = 0
my_out = 0x400420
Vemos que estamos na função main, que foi chamada com argc=1 e argv=0x7fffffffe1a8. Todo mundo sabe que argv é um array de ponteiros, mas será que o GDB sabe?
(gdb) whatis argv
type = char **
(gdb) whatis argc
type = int
(gdb) whatis my_out
type = FILE *
(gdb) whatis i
type = int
(gdb) whatis v_global
type = int
(gdb) whatis TEST_VALUE
type = int
(gdb) p TEST_VALUE
$1 = 100
Como podemos ver, o GDB também identifica e mostra macros, se o formato de armazenamento dos símbolos permitir.
Se quisermos informação detalhada sobre o tipo de uma variável, podemos usar o ptype:
(gdb) ptype my_out
type = struct _IO_FILE {
int _flags;
char *_IO_read_ptr;
char *_IO_read_end;
char *_IO_read_base;
char *_IO_write_base;
char *_IO_write_ptr;
char *_IO_write_end;
char *_IO_buf_base;
char *_IO_buf_end;
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset;
short unsigned int _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
__off64_t _offset;
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
size_t __pad5;
int _mode;
char _unused2[20];
} *
Note que apesar de FILE não ter sido definida em nosso código fonte, podemos ver a definição da struct.
Se quisermos examinar o conteúdo ou o endereço de memória de uma variável, por exemplo, v_global:
(gdb) p v_global
$1 = 100
O valor mostrado, 100, é o conteúdo de v_global. Para saber o endereço de v_global,
(gdb) p &v_global
$2 = (int *) 0x600924
Será que a inversa é possível, a partir de um endereço saber o nome da variável?
Para variáveis globais, a resposta é sim.
(gdb) info symbol 0x600924
v_global in section .data of gdb0
Para variáveis locais, no entanto...
(gdb) p &i
$3 = (int *) 0x7fffffffe0a4
(gdb) info symbol 0x7fffffffe0a4
No symbol matches 0x7fffffffe0a4
Agora que já examinamos algumas variáveis, agora vamos fazer o programa continuar para a próxima linha do código fonte. Uma linha de código fonte geralmente corresponde a várias instruções de máquina, mas não precisamos nos preocupar com isso, o GDB usa as anotações colocadas pelo compilador para calcular automaticamente onde o breakpoint será colocado.
(gdb) step
30 my_out = stdout;
Esse comando instrue o GDB para executar uma ou mais linhas de código fonte, sendo o default a execução de 1 linha. A linha mostrada após o comando é a próxima a ser executada.
Quando encontra uma chamada de função, o step segue para dentro daquela função. Existe um outro comando, o next, que funciona como o step, mas não segue uma função. Ao encontrar uma chamada de função, o next chama a função e para a execução após o retorno. Tanto step quanto next implementam a variação i, stepi e nexti, que funcionam como o comando original, mas executando uma instrução de máquina ao invés de uma linha de código.
No nosso programa de teste, verificando os valores das variáveis, obteremos:
(gdb) info locals
i = 0
my_out = 0x0
Só para lembrar, ao comandarmos step, a linha executada foi
28 FILE *my_out = NULL;
Por isso observamos que o valor de my_out agora é NULL (0x0).
Quando estamos executando um programa complexo, com muitos arquivos fonte e vários breakpoints, podemos ter problemas para lembrar qual o arquivo fonte que está relacionado ao contexto atual. Felizmente podemos resolver esse problema com:
(gdb) info source
Current source file is main.c
Compilation directory is gdb0
Located in ./gdb0/main.c
Contains 34 lines.
Source language is c.
Compiled with DWARF 2 debugging format.
Does not include preprocessor macro info.
Agora que já lembramos o que estávamos fazendo, vamos mandar o programa parar logo após o loop, na linha 33. Sabendo em qual linha queremos definir um breakpoint, temos 2 formas de informar isso ao GDB. Se a linha estiver no mesmo arquivo, como é o caso aqui, podemos simplesmente:
(gdb) break 33
Breakpoint 2 at 0x40056b: file main.c, line 33.
Isso significa que o GDB vai colocar um breakpoint na instrução que está no endereço 0x40056b, que corresponde à linha 33 do arquivo main.c.
Quando o breakpoint a ser definido não está no arquivo atual, ou quando o programa ainda não está em execução (e possui vários fontes), temos que informar o arquivo fonte, além do número da linha:
(gdb) break main.c:33
Breakpoint 1 at 0x40056b: file main.c, line 33.
Vamos então mandar o GDB continuar com a execução do programa:
(gdb) c
Continuing.
i=0 global=100
i=1 global=99
i=2 global=98
i=3 global=97
...
i=98 global=2
i=99 global=1
Breakpoint 1, main (argc=1, argv=0x7fffffffe1a8) at main.c:33
33 return 0;
O GDB suspendeu a execução do programa ANTES de executar a linha marcada. Vamos examinar o valor das variáveis:
(gdb) info locals
i = 100
my_out = 0x3cc7173780
(gdb) p v_global
$1 = 0
É, parece que funciona. Antes de continuar a execução, no entanto, vamos mudar o fluxo de execução, fazendo o programa redefinir qual a próxima linha/instrução a ser executada:
(gdb) jump 31
Continuing at 0x400527.
i=0 global=0
i=1 global=-1
i=2 global=-2
...
i=99 global=-99
Breakpoint 2, main (argc=1, argv=0x7fffffffe1a8) at main.c:33
33 return 0;
Veja que a variável v_global não foi reinicializada, por isso ficou com os valores negativos.
Por último, vamos deixar o programa terminar em paz:
(gdb) c
Continuing.
Program exited normally.
Uma extensão interessante de breakpoint é o breakpoint condicional, que só pára o programa quando uma determinada condição for satisfeita. Por exemplo, vamos adicionar um breakpoint condicional na linha 32, para que o breakpoint funcione somente quando o valor de i for 50 e, em seguida, mandaremos o programa executar:
(gdb) break main.c:32 if i == 50
Breakpoint 3 at 0x400530: file main.c, line 32.
(gdb) run
Starting program: ./gdb0
i=0 global=100
...
i=49 global=51
Breakpoint 3, main (argc=1, argv=0x7fffffffe1a8) at main.c:32
32 fprintf(my_out, "i=%d global=%d\n", i, v_global);
Examinando os valores das variáveis:
(gdb) info locals
i = 50
my_out = 0x3cc7173780
Vamos aproveitar o breakpoint para alterar o valor da variável i para 200:
(gdb) set variable i=200
(gdb) p i
$2 = 200
E agora mandamos o programa continuar:
(gdb) c
Continuing.
i=200 global=50
Se você não reiniciou o GDB, aquele breakpoint que colocamos na linha 33 continua lá...
Breakpoint 1, main (argc=1, argv=0x7fffffffe1a8) at main.c:33
33 return 0;
Examinando as variáveis novamente, teremos:
Breakpoint 1, main (argc=1, argv=0x7fffffffe1a8) at main.c:33
33 return 0;
(gdb) p i
$3 = 201
(gdb) p v_global
$4 = 49
E vamos terminar a execução normalmente:
(gdb) c
Continuing.
Program exited normally.
Mudamos o estado interno do programa durante a parada. O mecanismo de breakpoint permite fazer isso automaticamente, através da definição de uma lista de comandos a serem executados quando o breakpoint é ativado.
Vamos começar removendo o breakpoint da linha 32:
(gdb) info break
Num Type Disp Enb Address What
2 breakpoint keep y 0x000000000040056b in main at main.c:33
3 breakpoint keep y 0x0000000000400530 in main at main.c:32
stop only if i == 50
Queremos remover o breakpoint #3:
(gdb) delete 3
Agora vamos adicioná-lo novamente, definindo a condição para quando o valor de v_global for 47:
(gdb) break 32 if v_global == 47
Breakpoint 4 at 0x400530: file main.c, line 32.
Note que o slot 3 não é reutilizado. Poderíamos, ao invés de remover e adicionar, somente alterar as condições do breakpoint. Por exemplo, para alterar a condição do breakpoint 4 para parar quando v_global for 49:
(gdb) condition 4 v_global == 49
(gdb) info break
Num Type Disp Enb Address What
2 breakpoint keep y 0x000000000040056b in main at main.c:33
breakpoint already hit 1 time
4 breakpoint keep y 0x0000000000400530 in main at main.c:32
stop only if v_global == 49
Agora vamos fazer com que, quando o breakpoint 4 for ativado, o valor da variável i seja alterado para 200.
(gdb) commands 4
Type commands for when breakpoint 4 is hit, one per line.
End with a line saying just "end".
>set var i = 200
>end
E agora, vamos executar o programa para ver o resultado:
(gdb) run
Starting program: ./gdb0
i=0 global=100
...
i=50 global=50
Breakpoint 4, main (argc=1, argv=0x7fffffffe1a8) at main.c:32
32 fprintf(my_out, "i=%d global=%d\n", i, v_global);
Olhando para os valores das variáveis:
(gdb) p i
$1 = 200
(gdb) p v_global
$2 = 49
Podemos ver que o breakpoint executou os comandos que estavam previstos. Continuando com o programa:
(gdb) c
Continuing.
i=200 global=49
Breakpoint 2, main (argc=1, argv=0x7fffffffe1a8) at main.c:33
33 return 0;
Aquele breakpoint na linha 33 continua lá. Finalmente:
(gdb) c
Continuing.
Program exited normally.
Além de redefinir os valores de variáveis, também podemos chamar funções a partir lista de comandos associados ao breakpoint. Por exemplo, vamos alterar a lista de comandos do breakpoint da linha 32 e reiniciar o programa:
(gdb) info break
Num Type Disp Enb Address What
2 breakpoint keep y 0x000000000040056b in main at main.c:33
breakpoint already hit 1 time
4 breakpoint keep y 0x0000000000400530 in main at main.c:32
stop only if v_global == 49
breakpoint already hit 1 time
set var i = 200
(gdb) commands 4
Type commands for when breakpoint 4 is hit, one per line.
End with a line saying just "end".
>call puts("Hello Breakpoint!\n")
>end
(gdb) run
Starting program: ./gdb0
i=0 global=100
i=1 global=99
...
i=50 global=50
Breakpoint 4, main (argc=1, argv=0x7fffffffe1a8) at main.c:32
32 fprintf(my_out, "i=%d global=%d\n", i, v_global);
Hello Breakpoint!
$3 = 19
Lembre-se de que a função é chamada dentro do contexto do debuggee, usando o stack frame corrente. O valor 19 é o retorno da função puts. Quaisquer funções podem ser chamadas, estejam em bibliotecas ou no programa debugado.
É importante notar que a lista de comandos substituiu completamente a anterior, por isso, ao examinarmos o valor de i:
(gdb) p i
$4 = 51
Frequentemente queremos que o breakpoint seja ativado somente após algumas vezes depois que o programa possou por ele. O GDB já mantém um contador de vezes que um breakpoint foi ativado, e o comando ignore serve para isso. Vamos limpar a lista de breakpoints, adicionar um breakpoint na linha 32 e fazer com que o programa seja suspenso após passar pelo breakpoint por 7 vezes:
(gdb) delete breakpoints
Delete all breakpoints? (y or n) y
(gdb) break 32
Breakpoint 4 at 0x400530: file main.c, line 32.
(gdb) info break
Num Type Disp Enb Address What
4 breakpoint keep y 0x0000000000400530 in main at main.c:32
(gdb) ignore 4 7
Will ignore next 7 crossings of breakpoint 4.
Executando o programa novamente:
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: ./gdb0
i=0 global=100
i=1 global=99
i=2 global=98
i=3 global=97
i=4 global=96
i=5 global=95
i=6 global=94
Breakpoint 4, main (argc=1, argv=0x7fffffffe1a8) at main.c:32
32 fprintf(my_out, "i=%d global=%d\n", i, v_global);
Veja também enable, disable, finish, until e advance.
Chega de breakpoints, vamos limpar todos os breakpoints e reiniciar o programa. O assunto agora é watchpoints.
(gdb) delete breakpoints
Delete all breakpoints? (y or n) y
(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 5 at 0x400514: file main.c, line 28.
Starting program: ./gdb0
Temporary breakpoint 5, main (argc=1, argv=0x7fffffffe1a8) at main.c:28
28 FILE *my_out = NULL;
Um watchpoint nada mais é do que um "breakpoint de dados". O tipo de breakpoint que vimos até agora está relacionado à um endereço de memória de programa, e é ativado quando o registrador de instrução (IP na arquitetura x86) executa o endereço marcado (na verdade é mais tosco do que essa descrição poética, mas deixo essa descoberta para você). Um watchpoint funciona de modo parecido, mas é ativado pela avaliação de uma expressão, sem importar ONDE os termos da expressão foram alterados. Por exemplo:
(gdb) watch v_global == 90
Hardware watchpoint 2: v_global == 90
(gdb) c
Continuing.
i=0 global=100
i=1 global=99
i=2 global=98
i=3 global=97
i=4 global=96
i=5 global=95
i=6 global=94
i=7 global=93
i=8 global=92
i=9 global=91
Hardware watchpoint 2: v_global == 90
Old value = 0
New value = 1
0x0000000000400565 in main (argc=1, argv=0x7fffffffe188) at main.c:31
31 for (i = 0; i < TEST_VALUE; i++, v_global--)
Bom, você deve estar pensando: "eu poderia fazer o mesmo com um breakpoint condicional!". Como esse caso é muito simples, realmente isso poderia ser feito. Entretanto, imagine uma variável que pode ter o valor alterado em vários lugares diferentes, ou se você desejar que o watchpoint pare o programa somente quando uma certa variável for lida: nesse caso, watchpoint é a melhor (única?) opção.
Fazendo o programa continuar:
(gdb) c
Continuing.
i=10 global=90
Hardware watchpoint 2: v_global == 90
Old value = 1
New value = 0
0x0000000000400565 in main (argc=1, argv=0x7fffffffe188) at main.c:31
31 for (i = 0; i < TEST_VALUE; i++, v_global--)
E ele parou de novo, por causa da transição (houve uma referência à expressão).
Vamos apagar todos os breapoints novamente, adicionar um rwatch no lugar e executar o programa:
(gdb) delete breakpoints
Delete all breakpoints? (y or n) y
(gdb) rwatch v_global
Hardware read watchpoint 3: v_global
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: ./gdb0
Hardware read watchpoint 3: v_global
Value = 100
0x0000000000400536 in main (argc=1, argv=0x7fffffffe188) at main.c:32
32 fprintf(my_out, "i=%d global=%d\n", i, v_global);
Podemos ver que o programa parou na primeira vez que o valor de v_global foi lido (e não é no for
).
Se quiser, continue com c até cansar. Note que o watchpoint é tratado como breakpoint quando se trata de remoção.
Um detalhe importante é a diferença entre Hardware Watchpoint e Software Watchpoint. Quando usamos Hardware Watchpoint, o GDB é capaz de detectar e parar o programa na próxima instrução, funcionando mesmo que a referência/alteração seja feita em outras threads. Software Watchpoints, por outro lado, tem granularidade de statement. Entretanto, nada disso é grátis: Hardware watchpoints tem número limitado. Veja também tracepoint.
Depois dessa overdose de breakpoints, vamos descansar um pouco e ver como verificar o conteúdo dos registradores da CPU. Mas antes vamos aproveitar e deixar tudo limpinho:
(gdb) delete breakpoints
Delete all breakpoints? (y or n) y
(gdb) start
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Temporary breakpoint 5 at 0x400514: file main.c, line 28.
Starting program: ./gdb0
Temporary breakpoint 5, main (argc=1, argv=0x7fffffffe1a8) at main.c:28
28 FILE *my_out = NULL;
(gdb) info register
rax 0x3cc7175f60 261038235488
rbx 0x0 0
rcx 0x0 0
rdx 0x7fffffffe1b8 140737488347576
rsi 0x7fffffffe1a8 140737488347560
rdi 0x1 1
rbp 0x7fffffffe0c0 0x7fffffffe0c0
rsp 0x7fffffffe090 0x7fffffffe090
r8 0x3cc7174300 261038228224
r9 0x3cc6a0e4d0 261030470864
r10 0x7fffffffdf20 140737488346912
r11 0x3cc6e1ea20 261034732064
r12 0x400420 4195360
r13 0x7fffffffe1a0 140737488347552
r14 0x0 0
r15 0x0 0
rip 0x400514 0x400514 <main+16>
eflags 0x206 [ PF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
fctrl 0x37f 895
fstat 0x0 0
ftag 0xffff 65535
fiseg 0x0 0
fioff 0x0 0
foseg 0x0 0
fooff 0x0 0
fop 0x0 0
mxcsr 0x1f80 [ IM DM ZM OM UM PM ]
Para ver o conteúdo de todos os registradores, especialmente os de multimedia (se houver), comandamos
(gdb) info all-registers
rax 0x3cc7175f60 261038235488
rbx 0x0 0
rcx 0x0 0
rdx 0x7fffffffe1b8 140737488347576
rsi 0x7fffffffe1a8 140737488347560
rdi 0x1 1
rbp 0x7fffffffe0c0 0x7fffffffe0c0
rsp 0x7fffffffe090 0x7fffffffe090
r8 0x3cc7174300 261038228224
r9 0x3cc6a0e4d0 261030470864
r10 0x7fffffffdf20 140737488346912
r11 0x3cc6e1ea20 261034732064
r12 0x400420 4195360
r13 0x7fffffffe1a0 140737488347552
r14 0x0 0
r15 0x0 0
rip 0x400514 0x400514 <main+16>
eflags 0x206 [ PF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
st0 0 (raw 0x00000000000000000000)
st1 0 (raw 0x00000000000000000000)
st2 0 (raw 0x00000000000000000000)
st3 0 (raw 0x00000000000000000000)
st4 0 (raw 0x00000000000000000000)
st5 0 (raw 0x00000000000000000000)
st6 0 (raw 0x00000000000000000000)
st7 0 (raw 0x00000000000000000000)
fctrl 0x37f 895
fstat 0x0 0
ftag 0xffff 65535
fiseg 0x0 0
fioff 0x0 0
foseg 0x0 0
fooff 0x0 0
fop 0x0 0
xmm0 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0xff, 0x0 <repeats 15 times>}, v8_int16 = {0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0xff, 0x0, 0x0, 0x0}, v2_int64 = {0xff, 0x0}, uint128 = 0x000000000000000000000000000000ff}
xmm1 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x2f <repeats 16 times>}, v8_int16 = {0x2f2f, 0x2f2f, 0x2f2f, 0x2f2f, 0x2f2f, 0x2f2f, 0x2f2f, 0x2f2f}, v4_int32 = {0x2f2f2f2f, 0x2f2f2f2f, 0x2f2f2f2f, 0x2f2f2f2f}, v2_int64 = {
0x2f2f2f2f2f2f2f2f, 0x2f2f2f2f2f2f2f2f}, uint128 = 0x2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f}
xmm2 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 <repeats 16 times>}, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
xmm3 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v8_int16 = {0x0, 0x0, 0xff00, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0xff00, 0x0, 0x0}, v2_int64 = {
0xff0000000000, 0x0}, uint128 = 0x00000000000000000000ff0000000000}
xmm4 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 <repeats 16 times>}, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
xmm5 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 <repeats 16 times>}, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
xmm6 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 <repeats 16 times>}, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
xmm7 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 <repeats 16 times>}, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
xmm8 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 <repeats 16 times>}, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
xmm9 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 <repeats 16 times>}, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
xmm10 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 <repeats 16 times>}, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
xmm11 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 <repeats 16 times>}, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
xmm12 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 <repeats 16 times>}, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
xmm13 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 <repeats 16 times>}, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
xmm14 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 <repeats 16 times>}, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
xmm15 {v4_float = {0x0, 0x0, 0x0, 0x0}, v2_double = {0x0, 0x0}, v16_int8 = {0x0 <repeats 16 times>}, v8_int16 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, v4_int32 = {0x0, 0x0, 0x0, 0x0}, v2_int64 = {0x0, 0x0}, uint128 = 0x00000000000000000000000000000000}
mxcsr 0x1f80 [ IM DM ZM OM UM PM ]
Para examinar a FPU:
(gdb) info float
R7: Empty 0x00000000000000000000
R6: Empty 0x00000000000000000000
R5: Empty 0x00000000000000000000
R4: Empty 0x00000000000000000000
R3: Empty 0x00000000000000000000
R2: Empty 0x00000000000000000000
R1: Empty 0x00000000000000000000
=>R0: Empty 0x00000000000000000000
Status Word: 0x0000
TOP: 0
Control Word: 0x037f IM DM ZM OM UM PM
PC: Extended Precision (64-bits)
RC: Round to nearest
Tag Word: 0xffff
Instruction Pointer: 0x00:0x00000000
Operand Pointer: 0x00:0x00000000
Opcode: 0x0000
Alternativamente, se quisermos examinar o valor de um registrador específico:
(gdb) p/x $rax
$7 = 0x3cc7175f60
O /x nesse caso serve para que o resultado seja mostrado em formato hexadecimal. A especificação de formato pode ser usada em várias situações.
Para alterar o valor de RAX para NULL:
(gdb) set var $rax = 0
Agora, se pedirmos para ver novamente o valor de rax:
(gdb) p/x $rax
$8 = 0x0
Podemos alterar o valor de qualquer registrador. Claro que, dependendo do valor e do registrador, também poderemos provocar resultados catastróficos.
Às vezes pode ser útil ver as instruções de máquina decodificadas de uma região do programa. Quando não temos informação de debug disponível, essa pode ser a única forma. Por exemplo, para ver as instruções que compoe a função main:
(gdb) disas main
Dump of assembler code for function main:
0x0000000000400504 <main+0>: push %rbp
0x0000000000400505 <main+1>: mov %rsp,%rbp
0x0000000000400508 <main+4>: push %rbx
0x0000000000400509 <main+5>: sub $0x28,%rsp
0x000000000040050d <main+9>: mov %edi,-0x24(%rbp)
0x0000000000400510 <main+12>: mov %rsi,-0x30(%rbp)
0x0000000000400514 <main+16>: movq $0x0,-0x18(%rbp)
0x000000000040051c <main+24>: mov 0x20040d(%rip),%rax # 0x600930 <stdout@@GLIBC_2.2.5>
0x0000000000400523 <main+31>: mov %rax,-0x18(%rbp)
0x0000000000400527 <main+35>: movl $0x0,-0x1c(%rbp)
0x000000000040052e <main+42>: jmp 0x400565 <main+97>
0x0000000000400530 <main+44>: mov 0x2003ee(%rip),%ecx # 0x600924 <v_global>
0x0000000000400536 <main+50>: mov $0x400678,%ebx
0x000000000040053b <main+55>: mov -0x1c(%rbp),%edx
0x000000000040053e <main+58>: mov -0x18(%rbp),%rax
0x0000000000400542 <main+62>: mov %rbx,%rsi
0x0000000000400545 <main+65>: mov %rax,%rdi
0x0000000000400548 <main+68>: mov $0x0,%eax
0x000000000040054d <main+73>: callq 0x400410 <fprintf@plt>
0x0000000000400552 <main+78>: addl $0x1,-0x1c(%rbp)
0x0000000000400556 <main+82>: mov 0x2003c8(%rip),%eax # 0x600924 <v_global>
0x000000000040055c <main+88>: sub $0x1,%eax
0x000000000040055f <main+91>: mov %eax,0x2003bf(%rip) # 0x600924 <v_global>
0x0000000000400565 <main+97>: cmpl $0x63,-0x1c(%rbp)
0x0000000000400569 <main+101>: jle 0x400530 <main+44>
0x000000000040056b <main+103>: mov $0x0,%eax
0x0000000000400570 <main+108>: add $0x28,%rsp
0x0000000000400574 <main+112>: pop %rbx
0x0000000000400575 <main+113>: leaveq
0x0000000000400576 <main+114>: retq
End of assembler dump.
Para descobrir onde está uma linha específica, por exemplo, a linha 32 do main.c:
(gdb) info line 32
Line 32 of "main.c" starts at address 0x400530 <main+44> and ends at 0x400552 <main+78>.
Por fim, se quisermos que a função main seja decodificada juntamente com uma anotação mostrando onde cada linha começa:
(gdb) disas /m main
Dump of assembler code for function main:
26 {
0x0000000000400504 <main+0>: push %rbp
0x0000000000400505 <main+1>: mov %rsp,%rbp
0x0000000000400508 <main+4>: push %rbx
0x0000000000400509 <main+5>: sub $0x28,%rsp
0x000000000040050d <main+9>: mov %edi,-0x24(%rbp)
0x0000000000400510 <main+12>: mov %rsi,-0x30(%rbp)
27 int i;
28 FILE *my_out = NULL;
0x0000000000400514 <main+16>: movq $0x0,-0x18(%rbp)
29
30 my_out = stdout;
0x000000000040051c <main+24>: mov 0x20040d(%rip),%rax # 0x600930 <stdout@@GLIBC_2.2.5>
0x0000000000400523 <main+31>: mov %rax,-0x18(%rbp)
31 for (i = 0; i < TEST_VALUE; i++, v_global--)
0x0000000000400527 <main+35>: movl $0x0,-0x1c(%rbp)
0x000000000040052e <main+42>: jmp 0x400565 <main+97>
0x0000000000400552 <main+78>: addl $0x1,-0x1c(%rbp)
0x0000000000400556 <main+82>: mov 0x2003c8(%rip),%eax # 0x600924 <v_global>
0x000000000040055c <main+88>: sub $0x1,%eax
0x000000000040055f <main+91>: mov %eax,0x2003bf(%rip) # 0x600924 <v_global>
0x0000000000400565 <main+97>: cmpl $0x63,-0x1c(%rbp)
0x0000000000400569 <main+101>: jle 0x400530 <main+44>
32 fprintf(my_out, "i=%d global=%d\n", i, v_global);
0x0000000000400530 <main+44>: mov 0x2003ee(%rip),%ecx # 0x600924 <v_global>
0x0000000000400536 <main+50>: mov $0x400678,%ebx
0x000000000040053b <main+55>: mov -0x1c(%rbp),%edx
0x000000000040053e <main+58>: mov -0x18(%rbp),%rax
0x0000000000400542 <main+62>: mov %rbx,%rsi
0x0000000000400545 <main+65>: mov %rax,%rdi
0x0000000000400548 <main+68>: mov $0x0,%eax
0x000000000040054d <main+73>: callq 0x400410 <fprintf@plt>
33 return 0;
0x000000000040056b <main+103>: mov $0x0,%eax
34 }
0x0000000000400570 <main+108>: add $0x28,%rsp
0x0000000000400574 <main+112>: pop %rbx
0x0000000000400575 <main+113>: leaveq
0x0000000000400576 <main+114>: retq
End of assembler dump.
Agora que já sabemos como colocar breakpoints, examinar e alterar o valor de variáveis, vamos ver como fazemos para bisbilhotar uma região de memória. Por exemplo, para examinar o valor da região de memória próxima à variável stdout, podemos usar o comando x:
(gdb) x/16xb 0x3cc7173d70
0x3cc7173d70 <stdout>: 0x80 0x37 0x17 0xc7 0x3c 0x00 0x00 0x00
0x3cc7173d78 <stderr>: 0x60 0x38 0x17 0xc7 0x3c 0x00 0x00 0x00
Veja que o GDB adiciona o nome do símbolo mais próximo da área examinada, se houver. Sabemos que stdout é um ponteiro, então os 8 primeiros bytes correspondem ao endereço da área onde está a struct (em little endian, invertido):
0x80 0x37 0x17 0xc7 0x3c 0x00 0x00 0x00
Colocando na ordem correta, temos:
0000003cc7173780.
Comparando com o valor de stdout:
(gdb) p stdout
$13 = (struct _IO_FILE *) 0x3cc7173780
O comando x aceita alguns parâmetros, que especificam a quantidade (16), o tipo de formatação (x=hexadecimal) e o tamanho dos dados (b=byte) que queremos ver no dump.
Os exemplos acima usaram o GDB para carregar e executar o programa, mas não tem de ser assim. Podemos fazer o GDB debugar um programa que já está em execução. Por exemplo, vamos fazer uma pequena alteração no programa gdb0, e chamá-lo de gdb1, para que fique assim:
/*
* Copyright (C) 2009 William Cesar de Oliveira
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdio.h>
#include <stdlib.h>
#define TEST_VALUE 100
int v_global = TEST_VALUE;
int main(int argc, char *argv[])
{
int i;
FILE *my_out = NULL;
sleep(60);
my_out = stdout;
for (i = 0; i < TEST_VALUE; i++, v_global--)
fprintf(my_out, "i=%d global=%d\n", i, v_global);
return 0;
}
Com essa alteração, o programa vai esperar 60 segundos antes de continuar. Assim, teremos tempo de executar o GDB (espero). Agora, executamos o programa em segundo plano:
# ./gdb1 &
O shell vai colocar o número do PID do programa. Se não o fizer, podemos descobrir com:
# ps -C gdb1
PID TTY TIME CMD
13566 pts/3 00:00:00 gdb1
Em seguida, usamos o PID do processo (nesse caso, 13566) para dizer ao GDB qual o processo queremos debugar:
gdb --pid 13566
O GDB vai iniciar e tomar conta do processo a ser debugado, interrompendo a execução na próxima instrução. A partir desse ponto, podemos continuar com o processo de debug como estávamos fazendo anteriormente, não há distinção entre as duas sessões.
Normalmente quando um bug aparece não estamos lá para ver, muito menos rodando o programa no GDB. Com sorte, às vezes temos disponível um core dump para ser analisado, e podemos trabalhar com a chamada análise post-mortem. Quando ocorre um erro de execução, por exempo, se o sistema estiver corretamente configurado, um core dump poderá ser gerado. Podemos também obter um core dump de um programa que está em execução ou de um programa que está sendo debugado.
Escrevi o exemplo 3 para provocar um core dump devido à um sinal não manipulado. Se quisermos provocar um core dump, podemos usar o programa gcore com o PID do processo que queremos obter o core dump. A última opção é o comando gcore dentro do gdb, o que pode ser útil quando queremos um instantâneo da sessão para análise posterior.
Vamos ver o que acontece quando executamos o programa de testes:
# ./gdb2
Segmentation fault (core dumped)
Por padrão o core dump será gerado no diretório corrente no momento da geração do core, com o nome core.PID.
Agora vamos instruir o GDB para carregar o core dump e os símbolos, supondo que o PID do processo que gerou o core dump seja 17247:
# gdb --core=core.17247 --symbols=./gdb2
Reading symbols from ./gdb2...done.
Core was generated by `./gdb2'.
Program terminated with signal 11, Segmentation fault.
#0 0x00000000004005f2 in main (argc=1, argv=0x7fff35aad778) at main.c:40
40 *bug = 0;
Com isso já sabemos em qual linha foi gerada a falha. Podemos inspecionar e alterar os valores das variáveis, dos registradores, etc.
Para saber mais como o core é gerado e como controlar a geração do arquivo, veja man 5 core.
Dicas
Os nomes dos comandos podem ser abreviados, desde que não entrem em conflito com outro comando. Por exemplo, para o comando disassemble, podemos digitar somente disas.
Podemos fazer com que um comando seja sempre executado quando o processo for interrompido, usando display. O comando display é muito parecido com o p (print), mas será executado automaticamente cada vez que o programa debugado for paralisado.
Outro ponto interessante é como o GDB interpreta e localiza o nome de um símbolo, já que em C são permitidas variáveis com o mesmo nome em escopos diferentes. O contexto atual é o da função que está em execução (ou melhor, a pilha atual). Não é possível referenciar variáveis locais em funções que não estão em execução nem fazem parte da pilha de chamadas, mas é possível especificar o contexto, usando ::. Por exemplo, considere o seguinte trecho de código num arquivo chamado teste.c:
int my_test = 1234;
int f_test(int my_test)
{
return my_test * 2;
}
int main(int argc, char *argv[])
{
int my_test;
for (i = 0; i < 100; i++)
print(hello world (%d)\n", my_test, f_test(my_test));
return 0;
}
Supondo um breakpoint em f_test, se quisermos ver o valor da variável global my_test, devemos
p &'teste.c'::my_test
Por outro lado, para referenciar a variável local my_test, dentro da função main, podemos
p &main::my_test
Se solicitarmos informações sobre my_test sem definir o escopo, veremos o my_test da pilha atual (o my_test declado dentro de f_test):
p &my_test
De dentro do GDB, digite help para obter ajuda ou a descrição resumida de algum comando, por exemplo, para obter um breve descrição do comando break:
(gdb) help break
Set breakpoint at specified line or function.
break [LOCATION] [thread THREADNUM] [if CONDITION]
LOCATION may be a line number, function name, or "*" and an address.
If a line number is specified, break at start of code for that line.
If a function is specified, break at start of code for that function.
If an address is specified, break at that exact address.
With no LOCATION, uses current execution address of selected stack frame.
This is useful for breaking on return to a stack frame.
THREADNUM is the number from "info threads".
CONDITION is a boolean expression.
Multiple breakpoints at one place are permitted, and useful if conditional.
Do "help breakpoints" for info on other commands dealing with breakpoints.
Antes de finalizar, três assuntos interessantes para pesquisa: variáveis de conveniência, scripts e debug remoto.
Por último, se você ainda não descobriu, para sair do GDB:
quit