gdb-pratico/mods/03/README.org

447 lines
16 KiB
Org Mode
Raw Normal View History

2025-04-26 11:45:42 -03:00
#+title: Curso prático de introdução ao GDB
#+author: Blau Araujo
#+email: blau@debxp.org
2025-04-28 10:51:02 -03:00
* 3. Binários executáveis
** Objetivos
- Entender o que é um programa para o sistema operacional;
- Conhecer os elementos do formato de binários executáveis ELF;
- Investigar os símbolos de depuração;
- Descobrir como programas são executados.
** O que são programas
Do ponto de vista do sistema operacional GNU/Linux (e de sistemas UNIX-like em
geral), um programa é sempre um arquivo binário contendo um conjunto de instruções
que a CPU do computador é capaz de executar. Isso não significa que todo programa
é escrito para ser transformado em um arquivo binário e, só então, ser executado!
É preciso entender que, o que nós escrevemos e chamamos de "programa", pode
não ser exatamente o que o sistema operacional vai executar.
Para nós, programas podem ser...
- Escritos diretamente na linguagem que a CPU é capaz de interpretar e executar
(código de máquina, com baixo nível de abstração), e apenas armazenados como
arquivos binários;
- Escritos usando mnemônicos e pseudoinstruções relativamente simples (linguagem
de montagem, ou assembly), e então montados em código de máquina para gerar
arquivos binários executáveis;
- Escritos em linguagens com expressões e regras de sintaxe mais próximas da
escrita humana (alto nível de abstração), e depois traduzidos para código de
máquina por meio de um processo de compilação;
- Escritos em linguagens de alto nível, projetadas para serem lidas e
interpretadas em tempo de execução por outro programa (o interpretador), que é
um binário executável.
Mas o sistema operacional só executa arquivos binários que seguem uma convenção
específica de formato.
*** O que significa executar
Em sistemas GNU/Linux e UNIX-like, executar um programa significa:
- Criar uma estrutura de dados para gerenciar a execução do programa (processo);
- Carregar o conteúdo do binário executável na memória;
- Carregar na memória o conteúdo de outros arquivos necessários para o
funcionamento do programa, como bibliotecas compartilhadas;
- Disponibilizar um espaço de endereços de memória para uso exclusivo do
programa;
- Dar acesso a dados do ambiente de execução, como variáveis exportadas e
argumentos passados na invocação do programa;
- Conceder acesso a recursos diversos do sistema, como dispositivos de terminal
e descritores de arquivos;
- Administrar a passagem do controle da CPU para o programa;
- Monitorar o término do programa, mantendo sua estrutura de processo até que o
término seja completamente tratado.
Para que o sistema possa realizar todas essas etapas corretamente, é necessário
que o conteúdo do binário executável siga uma convenção específica de organização
de dados. Essa convenção define, por exemplo, onde se encontram as instruções do
programa, os dados que ele utiliza e as informações sobre bibliotecas externas.
No sistema GNU/Linux, o formato padrão utilizado para isso é o ELF (/Executable
and Linkable Format/).
** O formato ELF
Um arquivo no formato ELF possui uma estrutura interna bem definida que informa
ao sistema onde estão -- e onde devem ser carregados na memória -- os dados e os
elementos essenciais para a execução do programa, entre eles:
- As instruções do programa em código de máquina (seção =.text=);
- Dados constantes (=.rodata=);
- Dados de variáveis globais e estáticas (seções =.data= e =.bss=);
- Tabelas de símbolos do programa e de seções;
- Tabelas de símbolos de depuração (se incluídas).
*** Passagem do arquivo para a memória
O conteúdo do binário no formato ELF é organizado em seções descritas em várias
tabelas. Quando um processo é iniciado para executar o programa, essas seções são
mapeadas para segmentos de memória correspondentes em seu espaço de endereços.
Isso é feito por uma biblioteca do sistema chamada de /loader/ (carregador).
No GNU/Linux, o /loader/ é a biblioteca compartilhada =ld-linux= e, entre suas várias
atribuições, nós podemos destacar:
- Carregar o programa na memória, mapeando as seções do arquivo ELF para os
segmentos apropriados no espaço de endereços do processo.
- Configurar o ambiente de execução, incluindo a configuração de uma estrutura
de pilha (/stack/) no espaço de endereços do processo.
- Resolver dependências de bibliotecas compartilhadas, carregando-as no espaço
de endereços do processo, se for necessário.
*** Layout de memória
Após o carregamento do binário e a configuração do ambiente de execução, o espaço
de endereços do processo estará organizado desta forma:
#+begin_example
ENDEREÇO MAIS ALTO +-----------------------+ - Dados de ambiente
| | - Argumentos de linha de comando
| PILHA | - Endereços de retorno
| | - Dados locais de chamadas de funções
+-----------------------+
| ↓ |
| |
| |
+-----------------------+
| MMAP | - Mapeamento de bibliotecas compartilhadas
+-----------------------+
| |
| HEAP | - Dados dinâmicos do programa
| |
+-----------------------+
| SEGMENTO .BSS | - Dados globais e estáticos não inicializados
+-----------------------+
| SEGMENTO .DATA | - Dados globais e estáticos inicializados
+-----------------------+
| SEGMENTO .RODATA | - Dados constantes
+-----------------------+
| SEGMENTO .TEXT | - Instruções e funções do programa
ENDEREÇO MAIS BAIXO +-----------------------+
#+end_example
*** Utilitários para examinar binários ELF
**** Utilitário =readelf=
Imprime cabeçalhos e seções de binários ELF.
Exemplos de uso:
#+begin_example
readelf -h PROGRAMA Informações do cabeçalho ELF
readelf -l PROGRAMA Lista de cabeçalhos do programa
readelf -S PROGRAMA Lista dos cabeçalhos de seções do arquivo
readelf -x SEÇÂO PROGRAMA Exibe o conteúdo de uma seção em hexadecimal
#+end_example
**** Utilitário =ldd=
Lista dependências de bibliotecas compartilhadas.
Exemplo de uso:
#+begin_example
ldd PROGRAMA Lista as dependências dinâmicas do programa
#+end_example
**** Utilitário =nm=
Lista símbolos declarados no binário, como funções e variáveis globais.
Exemplo de uso:
#+begin_example
nm PROGRAMA Lista todos os símbolos no programa.
#+end_example
**** Utilitário =objdump=
Analisa, desmonta e imprime informações detalhadas sobre as seções de um
programa.
Exemplo de uso:
#+begin_example
objdump -d PROGRAMA Desmonta e exibe o conteúdo das seções executáveis do programa.
#+end_example
** Símbolos de depuração
2025-04-26 11:45:42 -03:00
Símbolos são nomes associados a elementos do programa, como funções e
variáveis. Quando os símbolos de depuração são incluídos no binário
(compilando com =-g=, por exemplo), o GDB é capaz de:
- Exibir nomes legíveis, em vez de endereços de memória;
- Acompanhar o fluxo do programa com o código-fonte;
- Receber definições de /breakpoints/ por nomes de funções;
- Fazer a associação de códigos binários com as linhas do código-fonte;
- Acessar nomes de variáveis, tipos, estruturas, etc.
Nós podemos verificar se o programa foi compilado com os símbolos de
depuração com o utilitário =file=...
Sem a opção =-g=:
#+begin_example
:~$ gcc -o demo demo.c
:~$ file demo
demo: ELF 64-bit LSB pie executable [...] not stripped
#+end_example
Com a opção =-g=:
#+begin_example
:~$ gcc -g -o demo demo.c
:~$ file demo
demo: ELF 64-bit LSB pie executable [...] with debug_info, not stripped
#+end_example
Repare que, desta vez, nós temos a informação =with debug_info= no final
da linha.
Nós também podemos verificar se há símbolos de depuração a partir das
informações no formato ELF:
#+begin_example
:~$ readelf -S demo | grep debug
[28] .debug_aranges PROGBITS 0000000000000000 00003037
[29] .debug_info PROGBITS 0000000000000000 00003067
[30] .debug_abbrev PROGBITS 0000000000000000 00003180
[31] .debug_line PROGBITS 0000000000000000 0000324a
[32] .debug_str PROGBITS 0000000000000000 000032ba
[33] .debug_line_str PROGBITS 0000000000000000 00003367
#+end_example
Mas o próprio GDB informa se há símbolos de depuração ao iniciar...
Sem a opção =-g=:
#+begin_example
:~$ gcc -o demo demo.c
:~$ gdb demo
Reading symbols from demo...
(No debugging symbols found in demo)
#+end_example
Com a opção =-g=:
#+begin_example
:~$ gcc -g -o demo demo.c
:~$ gdb demo
Reading symbols from demo...
#+end_example
2025-04-28 10:51:02 -03:00
** Inspecionando a execução de programas
Com o GDB, nós podemos obter várias informações relacionadas à execução de
programas.
Utilizando o programa =demo.c= como exemplo:
#+begin_example
:~$ gcc -g -o demo demo.c
:~$ gdb ./demo
Reading symbols from ./demo...
(gdb)
#+end_example
*** Listagem de símbolos
#+begin_example
(gdb) info files
Symbols from "/home/blau/git/gdb-pratico/mods/01/demo".
Local exec file:
`/home/blau/git/gdb-pratico/mods/01/demo', file type elf64-x86-64.
Entry point: 0x1050
0x0000000000000350 - 0x0000000000000370 is .note.gnu.property
0x0000000000000370 - 0x0000000000000394 is .note.gnu.build-id
0x0000000000000394 - 0x00000000000003b0 is .interp
0x00000000000003b0 - 0x00000000000003d4 is .gnu.hash
0x00000000000003d8 - 0x0000000000000480 is .dynsym
0x0000000000000480 - 0x000000000000050f is .dynstr
0x0000000000000510 - 0x000000000000051e is .gnu.version
0x0000000000000520 - 0x0000000000000550 is .gnu.version_r
0x0000000000000550 - 0x0000000000000610 is .rela.dyn
0x0000000000000610 - 0x0000000000000628 is .rela.plt
0x0000000000001000 - 0x0000000000001017 is .init
0x0000000000001020 - 0x0000000000001040 is .plt
0x0000000000001040 - 0x0000000000001048 is .plt.got
0x0000000000001050 - 0x000000000000119b is .text
0x000000000000119c - 0x00000000000011a5 is .fini
0x0000000000002000 - 0x0000000000002013 is .rodata
0x0000000000002014 - 0x0000000000002048 is .eh_frame_hdr
0x0000000000002048 - 0x0000000000002114 is .eh_frame
0x0000000000002114 - 0x0000000000002134 is .note.ABI-tag
0x0000000000003dd0 - 0x0000000000003dd8 is .init_array
0x0000000000003dd8 - 0x0000000000003de0 is .fini_array
0x0000000000003de0 - 0x0000000000003fc0 is .dynamic
0x0000000000003fc0 - 0x0000000000003fe8 is .got
0x0000000000003fe8 - 0x0000000000004008 is .got.plt
0x0000000000004008 - 0x0000000000004018 is .data
0x0000000000004018 - 0x0000000000004020 is .bss
#+end_example
*** Listagem de símbolos conhecidos
Funções:
#+begin_example
(gdb) info functions
All defined functions:
File demo.c:
8: int main();
3: int soma(int, int);
Non-debugging symbols:
0x0000000000001000 _init
0x0000000000001030 printf@plt
0x0000000000001040 __cxa_finalize@plt
0x0000000000001050 _start
0x0000000000001080 deregister_tm_clones
0x00000000000010b0 register_tm_clones
0x00000000000010f0 __do_global_dtors_aux
0x0000000000001130 frame_dummy
0x000000000000119c _fini
#+end_example
Variáveis:
#+begin_example
(gdb) info variables
All defined variables:
Non-debugging symbols:
0x0000000000002000 _IO_stdin_used
0x0000000000002014 __GNU_EH_FRAME_HDR
0x0000000000002110 __FRAME_END__
0x0000000000002114 __abi_tag
0x0000000000003dd0 __frame_dummy_init_array_entry
0x0000000000003dd8 __do_global_dtors_aux_fini_array_entry
0x0000000000003de0 _DYNAMIC
0x0000000000003fe8 _GLOBAL_OFFSET_TABLE_
0x0000000000004008 __data_start
0x0000000000004008 data_start
0x0000000000004010 __dso_handle
0x0000000000004018 __TMC_END__
0x0000000000004018 __bss_start
0x0000000000004018 _edata
0x0000000000004018 completed
0x0000000000004020 _end
#+end_example
*** Examinando o estado da memória
A inspeção da memória só pode ser feita com o início de um processo associado
à execução do programa, portanto...
#+begin_example
(gdb) b main
Breakpoint 1 at 0x115b: file demo.c, line 9.
(gdb) r
Starting program: /home/blau/git/gdb-pratico/mods/01/demo
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, main () at demo.c:9
9 int x = 10;
#+end_example
Para exibir o layout do mapeamento de memória do processo:
#+begin_example
(gdb) info proc mappings
process 1029871
Mapped address spaces:
Start Addr End Addr Size Offset Perms File
0x0000555555554000 0x0000555555555000 0x1000 0x0 r--p /home/blau/git/gdb-pratico/mods/01/demo
0x0000555555555000 0x0000555555556000 0x1000 0x1000 r-xp /home/blau/git/gdb-pratico/mods/01/demo
0x0000555555556000 0x0000555555557000 0x1000 0x2000 r--p /home/blau/git/gdb-pratico/mods/01/demo
0x0000555555557000 0x0000555555558000 0x1000 0x2000 r--p /home/blau/git/gdb-pratico/mods/01/demo
0x0000555555558000 0x0000555555559000 0x1000 0x3000 rw-p /home/blau/git/gdb-pratico/mods/01/demo
0x00007ffff7da3000 0x00007ffff7da6000 0x3000 0x0 rw-p
0x00007ffff7da6000 0x00007ffff7dce000 0x28000 0x0 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7dce000 0x00007ffff7f33000 0x165000 0x28000 r-xp /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7f33000 0x00007ffff7f89000 0x56000 0x18d000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7f89000 0x00007ffff7f8d000 0x4000 0x1e2000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7f8d000 0x00007ffff7f8f000 0x2000 0x1e6000 rw-p /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7f8f000 0x00007ffff7f9c000 0xd000 0x0 rw-p
0x00007ffff7fbf000 0x00007ffff7fc1000 0x2000 0x0 rw-p
0x00007ffff7fc1000 0x00007ffff7fc5000 0x4000 0x0 r--p [vvar]
0x00007ffff7fc5000 0x00007ffff7fc7000 0x2000 0x0 r-xp [vdso]
0x00007ffff7fc7000 0x00007ffff7fc8000 0x1000 0x0 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7fc8000 0x00007ffff7ff0000 0x28000 0x1000 r-xp /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ff0000 0x00007ffff7ffb000 0xb000 0x29000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ffb000 0x00007ffff7ffd000 0x2000 0x34000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ffd000 0x00007ffff7ffe000 0x1000 0x36000 rw-p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ffe000 0x00007ffff7fff000 0x1000 0x0 rw-p
0x00007ffffffde000 0x00007ffffffff000 0x21000 0x0 rw-p [stack]
#+end_example
*** Observando o conteúdo da pilha
Terminando a execução anterior (=kill=):
#+begin_example
(gdb) k
[Inferior 1 (process 1030168) killed]
#+end_example
Executando novamente:
#+begin_example
(gdb) r
Starting program: /home/blau/git/gdb-pratico/mods/01/demo a b c
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, main () at demo.c:9
9 int x = 10;
#+end_example
Avançar a execução até a chamada da função =soma=:
#+begin_example
(gdb) n
10 int y = 20;
(gdb) n
11 int z = soma(x, y);
#+end_example
Inspecionar o conteúdo da pilha neste momento:
#+begin_example
(gdb) bt
#0 main () at demo.c:11
#+end_example
Avançar a execução entrando na função =soma=:
#+begin_example
(gdb) s
soma (a=10, b=20) at demo.c:4
4 int resultado = a + b;
#+end_example
Inspecionar o conteúdo da pilha:
#+begin_example
(gdb) bt
#0 soma (a=10, b=20) at demo.c:4
#1 0x0000555555555178 in main () at demo.c:11
#+end_example
*** Listagem de bibliotecas compartilhadas carregadas
#+begin_example
(gdb) info sharedlibrary
From To Syms Read Shared Object Library
0x00007ffff7fc8000 0x00007ffff7fef2d1 Yes /lib64/ld-linux-x86-64.so.2
0x00007ffff7dce400 0x00007ffff7f3217d Yes /lib/x86_64-linux-gnu/libc.so.6
#+end_example