Depurando com o GDB
Aprendendo a usar o depurador GDB do projeto GNU.
O GDB é um depurador de linha de comando que faz parte do projeto GNU. O Mingw-w64 já instala o GDB junto com o GCC, e no Linux ele pode ser instalado pelo pacote gdb
:
O GDB pode ser usado para depurar código tanto visualizando o Assembly como também o código-fonte. Para isso é necessário compilar o binário adicionando informações de depuração, com o GCC basta adicionar a opção -g3
ao compilar. Exemplo:
E pode rodar o GDB passando o caminho do binário assim:
O caminho do binário é opcional. Caso especificado o GDB já inicia com esse binário como alvo para depuração, mas existem comandos do GDB que podem ser usados para escolher um alvo conforme será explicado mais abaixo.
O GDB funciona com comandos, quando você o inicia ele te apresenta um prompt onde você pode ir inserindo comandos para executar determinadas ações. Mais abaixo irei apresentar os principais comandos e como utilizá-los.
Esse depurador suporta depurar código de diversas linguagens de programação (incluindo C++, Go e Rust), mas aqui será demonstrado seu uso somente em um código escrito em C. O seguinte código será usado para demonstração:
E será compilado da seguinte forma:
A opção -g
é usada para adicionar informações de depuração ao executável. Esse 3
seria o nível de informações que serão adicionadas, onde 3 é o maior nível.
Para mais informações consulte a documentação do GCC.
Expressões
Determinadas instruções do GDB recebem uma expressão como argumento onde é possível usar qualquer tipo de constante, variável ou operador da linguagem que está sendo depurada (neste caso C). Isso inclui casts, strings literais, macros e até mesmo chamadas de funções. Logo a expressão interpretada é quase idêntica a uma expressão que você escreveria na linguagem que está sendo depurada (no nosso caso C).
Também é possível referenciar o valor de algum registrador na expressão usando o prefixo $
, como $rax
por exemplo. Na imagem abaixo é uma demonstração usando o comando print
:
Comandos
O GDB aceita abreviações dos comandos, onde ele identifica o comando a ser executado de acordo com suas primeiras letras ou abreviações definidas pelo depurador. Por exemplo o comando breakpoint
pode ser executado também como break
, br
ou apenas b
.
Ao apertar enter sem digitar nenhum comando o GDB irá reexecutar o último comando que você executou.
quit
Finaliza o GDB. A expressão opcional é avaliada e o resultado dela é usado como código de saída. Se a expressão não for passada o GDB sai com código 0
.
file
Usa o arquivo binário especificado como alvo para depuração. O programa é procurado no diretório atual ou em qualquer caminho registrado na variável de ambiente PATH.
attach e detach
O comando attach
faz o attach no processo de ID especificado. Já o comando detach
desfaz o attach no processo que está atualmente conectado.
Você também pode iniciar a execução do GDB com a opção -p
para ele já inicializar fazendo attach em um processo, como em:
breakpoint
Se o comando for executado sem qualquer argumento o breakpoint será adicionado na instrução atual.
LOCATION é a posição onde o breakpoint deve ser inserido e pode ser o número de uma linha, endereço ou posição explícita.
Ao especificar o número da linha, o nome do arquivo e o número da linha são separados por :
. Se não especificar o nome do arquivo o breakpoint será adicionado a linha do arquivo atual. Exemplos:
Onde o primeiro adicionaria o breakpoint na linha 15 do arquivo atual, e o segundo adicionaria na linha 17 do arquivo test.c
.
O endereço pode ser simplesmente o nome de uma função ou então uma expressão, onde nesse caso é necessário usar *
como prefixo ao símbolo ou endereço de memória. Como em:
No primeiro caso um breakpoint seria adicionado a função main. No segundo caso o endereço da primeira instrução da função main seria somado com 8, e o endereço resultante seria onde o breakpoint seria inserido. Já no terceiro caso o breakpoint seria inserido no endereço 0x12345
.
Também é possível especificar para qual thread o breakpoint deve ser inserido, onde por padrão o breakpoint é válido para todas as threads. Exemplo:
Isso adicionaria o breakpoint somente para a thread de ID 2.
É possível usar o comando info threads
para obter a lista de threads e seus números de identificação.
E por fim dá para adicionar uma condição de parada ao breakpoint. Onde CONDITION é uma expressão booleana. Exemplo:
Onde no contexto do nosso código de exemplo, a
seria o primeiro parâmetro da função add.
clear
Remove um breakpoint no local especificado. LOCATION funciona da mesma forma que no comando breakpoint
.
Caso LOCATION não seja especificado remove o breakpoint na posição atual.
run
O comando run
inicia (ou reinicia) a execução do programa alvo. Opcionalmente pode-se passar argumentos de linha de comando para o programa. Caso os argumentos não sejam especificados, os mesmos argumentos utilizados na última execução de run
serão utilizados.
Nos argumentos é possível usar o caractere curinga *
, ele será expandido pela shell do sistema. Também é possível usar os redirecionadores <
, >
ou >>
.
kill
Finaliza a execução do programa que está sendo depurado.
start, starti
O uso desses dois comandos é idêntico ao uso de run
. Porém o comando start
inicia a execução do programa parando no começo da função main. Já o starti
inicia parando na primeira instrução do programa.
next, nexti
O comando next
(ou apenas n
) executa uma linha de código. Se N for especificado ele executa N linhas de código. Já o comando nexti
(ou apenas ni
) executa uma ou N instruções Assembly.
Os dois comandos atuam como um step over, ou seja, não entram em chamadas de procedimentos.
step, stepi
O step
(ou s
) executa uma ou N linhas de código. Já o stepi
(ou si
) executa uma ou N instruções Assembly. Os dois comandos entram em chamadas de procedimentos.
jump
Salta (modifica RIP) para o ponto do código especificado. Onde LOCATION é idêntico ao caso do comando breakpoint onde é possível especificar um número de linha ou endereço.
advance
Esse comando continua a execução do programa até o ponto do código especificado, daí para a execução lá. Assim como na instrução jump
, o comando advance
(ou adv
) recebe um LOCATION como argumento.
O comando advance
também para quando a função atual retorna.
finish
Executa até o retorno da função atual. Quando a função retorna é criada uma variável (como no caso do comando print) com o valor de retorno da função.
continue
Continua a execução normal do programa.
record e reverse-*
Quando o programa já está em execução você pode executar o comando record full
para iniciar a gravação das instruções executadas e record stop
para parar de gravar.
Quando há a gravação é possível executar o programa em ordem reversa usando os comandos: reverse-step
(rs
), reverse-stepi
(rsi
), reverse-next
(rn
), reverse-nexti
(rni
) e reverse-continue
(rc
).
Esses comandos fazem a mesma coisa que os comandos normais, porém executando o programa ao reverso. Cada instrução revertida tem suas modificações na memória ou registradores desfeitas. Conforme demonstra a imagem abaixo.
Outros subcomandos de record
são:
record goto
Salta para uma determinada instrução que foi gravada. Pode-se usar record goto begin
para voltar ao início da gravação (desfazendo todas as instruções), record goto end
para ir para o final da gravação ou record goto N
onde N seria o número da instrução na gravação para saltar para ela.
record save <filename>
Salva os logs de execução no arquivo.
record restore <filename>
Restaura os logs de execução a partir do arquivo.
thread
O comando thread
pode ser usado para trocar entre threads do processo. Você pode usar o comando info threads
para listar as threads do processo e obter seus ID. Exemplo:
Isso trocaria para a thread de ID 2. Esse comando também tem os seguintes subcomandos:
thread apply
Executa um comando na thread especificada.
thread name
Define um nome para a thread atual, facilitando a identificação dela.
thread find
Recebe uma expressão regular como argumento que é usada para listar as threads cujo o nome coincida com a expressão regular. O comando exibe o ID das threads listadas.
print
O comando print
(ou p
) exibe no terminal o resultado da expressão passada como argumento. Opcionalmente pode-se especificar o formato de saída, onde os formatos são os mesmos utilizados no comando x. Exemplo:
Repare que a cada execução do comando print
ele define uma variável ($1
, $2
etc.) que armazena o resultado da expressão do comando. Você também pode usar o valor dessas variáveis em uma expressão e assim reaproveitar o resultado de uma execução anterior do comando. Os símbolos $
e $$
se referem aos valores da última e penúltima execução do comando, respectivamente. Exemplo:
Existe também o operador binário @
que pode ser usado para tratar o valor no endereço especificado como uma array. O formato do uso desse operador é array@size
, passando à esquerda o primeiro elemento da array.
Onde o tipo de cada elemento da array é definido de acordo com o tipo do objeto que está sendo referenciado. Na imagem abaixo é demonstrado o uso desse operador para visualizar todo o conteúdo da array argv.
printf
Esse comando pode ser usado de maneira semelhante a função printf da libc. Cada argumento é separado por vírgula e o primeiro argumento é a format string que suporta quase todos os formatos suportados pela função printf. Os demais argumentos são expressões.
Exemplo de uso:
dprintf
Esse comando insere um breakpoint no código onde, toda vez que ele é alcançado, o comando printf
é executado e depois a execução continua. O uso desse comando é semelhante ao do comando printf
. Exemplo:
No nosso código de exemplo, isso inseria o dynamic printf na linha 7 que está dentro da função add. Conforme a imagem abaixo demonstra:
x
O comando x
serve para ver valores na memória. O argumento FMT (opcional) é o número de valores a serem exibidos, seguido de uma letra indicando o formato do valor seguido de uma letra que indica o tamanho do valor. Por padrão exibe apenas um valor caso o número não seja especificado. O formato e tamanho padrão é o mesmo utilizado na última execução do comando x
.
As letras de formato são: o
(octal), x
(hexadecimal), d
(decimal), u
(decimal não-sinalizado), t
(binário), f
(float), a
(endereço), i
(instrução), c
(caractere de 1 byte), s
(string) e z
(hexadecimal com zeros à esquerda).
Ao usar o formato i
será feito o disassembly do código no endereço. O número de valores é usado para especificar o número de instruções para fazer o disassembly.
Exemplo:
As letras de tamanho são: b
(byte), h
(metade de uma palavra), w
(palavra) e g
(giant, 8 bytes). Na arquitetura x86-64 uma palavra é 32-bit (4 bytes).
Exemplos:
disassembly
O comando disassembly
(ou disas
) pode ser usado para exibir o disassembly de uma função ou range de endereço. O argumento ADDRESS (opcional) é uma expressão, sem esse argumento ele faz o disassembly na posição ou função atual.
Também é possível especificar um range de endereços para exibir o dissasembly das instruções, separando o endereço inicial e final por vírgula. Se usar o +
no segundo argumento separado por vírgula, ele é considerado como o tamanho em bytes do range iniciado em start.
Exemplos:
O argumento MODIFIER é uma (ou mais) das seguintes letras:
s
- Exibe também as linhas de código correspondentes as instruções em Assembly.r
- Também exibe o código de máquina em hexadecimal.
Exemplo:
Por padrão o disassembly é feito em sintaxe AT&T, mas você pode modificar para sintaxe Intel com o comando: set disassembly-flavor intel
list
Exibe a listagem de código na linha ou início da função especificada. Um endereço também pode ser especificado usando um *
como prefixo, as linhas de código correspondentes ao endereço serão exibidas.
Caso list
seja executado sem argumentos mais linhas são exibidas a partir da última linha exibida pela última execução de list
.
O número de linhas exibido é por padrão 10, mas esse valor pode ser alterado com o comando set listsize <number-of-lines>
.
backtrace
O comando backtrace
(ou bt
) exibe o stack backtrace atual. O argumento COUNT é o número máximo de stack frames que serão exibidos. Se for um número negativo exibe os primeiros stack frames.
Exemplo:
frame
Sem argumentos exibe o stack frame selecionado. Caso seja especificado um número como argumento, seleciona e exibe o stack frame indicado pelo número. Esse número pode ser consultado com o comando backtrace
.
Esse comando tem os seguintes subcomandos:
frame address
Exibe o stack frame no endereço especificado.
frame apply
O comando frame apply
executa o mesmo comando em um ou mais stack frames. Esse subcomando é útil, por exemplo, para ver o valor das variáveis locais que estão em uma função de outro stack frame além do atual.
COUNT é o número de frames onde o comando será executado. Por exemplo frame apply 2 p x
executaria o comando print
nos últimos 2 frames (o atual e o anterior).
O frame apply all
executa o comando em todos os frames. Já o frame apply level
executa o comando em um frame específico. exemplo:
frame function
Exibe o stack frame da função especificada.
frame level
Exibe o stack frame do número especificado.
info
O comando info
contém diversos subcomandos para exibir informações sobre o programa que está sendo depurado. Abaixo será listado apenas os subcomandos principais.
info registers
Exibe os valores dos registradores. Pode-se passar como argumento uma lista (separada por espaço) dos registradores para exibir. Sem argumentos exibe o valor de todos os registradores de propósito geral, registradores de segmento e EFLAGS. Exemplo:
info frame
O uso desse subcomando é semelhante ao uso do comando frame e contém os mesmos subcomandos. A diferença é que ele exibe todas as informações relacionadas ao stack frame. Enquanto o comando frame
apenas exibe informações de um ponto de vista de alto-nível.
info args
Exibe os argumentos passados para a função do stack frame atual. Se NAMEREGEXP for especificado exibe apenas os argumentos cujo o nome coincida com a expressão regular.
info locals
Uso idêntico ao de info args
só que exibe o valor das variáveis locais.
info functions
Exibe todas as funções cujo o nome coincida com a expressão regular. Se o argumento não for especificado lista todas as funções.
info breakpoints
Exibe os breakpoints definidos no programa.
info source
Exibe informações sobre o código-fonte atual.
info threads
Lista as threads do processo.
display e undisplay
Esse comando pode ser usado da mesma maneira que o comando print. Ele registra uma expressão para ser exibida a cada vez que a execução do processo faz uma parada. Exemplo:
Isso exibiria o disassembly de 7 instruções a partir de RIP a cada passo executado.
Se display
for executado sem argumentos ele exibe todas as expressões registradas para auto-display.
Enquanto o comando undisplay
remove a expressão com o número especificado. Sem argumentos remove todas as expressões registradas por display
.
source
Carrega o arquivo especificado e executa os comandos no arquivo como um script.
Quando o GDB inicia ele faz o source automático do script de nome .gdbinit
presente na sua pasta home. Exceto se o GDB for iniciado com a flag --nh
.
help
O comando help
, sem argumentos, lista as classes de comandos. É possível rodar help CLASS
para obter a lista de comandos daquela classe.
Também é possível rodar help COMMAND
para obter ajuda para um comando específico, pode-se inclusive usar abreviações. E também é possível obter ajuda para subcomandos, conforme exemplos:
Text User Interface (TUI)
É possível usar o GDB com uma interface textual permitindo que seja mais agradável acompanhar a execução enquanto observa o código-fonte. Para isso basta iniciar o GDB com a flag -tui
, como em:
Atalhos de teclado
Single Key Mode
Quando se está no modo Single Key é possível executar alguns comandos pressionando uma única tecla, conforme tabela abaixo:
Qualquer outra tecla alterna temporariamente para o modo de comandos. Após um comando ser executado ele retorna para o modo Single Key.
Last updated