gdb-pratico/mods/03
2025-04-28 10:51:02 -03:00
..
README.org atualização das partes 1 2 e 3 2025-04-28 10:51:02 -03:00

Curso prático de introdução ao GDB

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:

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  +-----------------------+

Utilitários para examinar binários ELF

Utilitário readelf

Imprime cabeçalhos e seções de binários ELF.

Exemplos de uso:

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
Utilitário ldd

Lista dependências de bibliotecas compartilhadas.

Exemplo de uso:

ldd PROGRAMA       Lista as dependências dinâmicas do programa
Utilitário nm

Lista símbolos declarados no binário, como funções e variáveis globais.

Exemplo de uso:

nm PROGRAMA        Lista todos os símbolos no programa.
Utilitário objdump

Analisa, desmonta e imprime informações detalhadas sobre as seções de um programa.

Exemplo de uso:

objdump -d PROGRAMA     Desmonta e exibe o conteúdo das seções executáveis do programa.

Símbolos de depuração

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:

:~$ gcc -o demo demo.c
:~$ file demo
demo: ELF 64-bit LSB pie executable [...] not stripped

Com a opção -g:

:~$ gcc -g -o demo demo.c
:~$ file demo
demo: ELF 64-bit LSB pie executable [...] with debug_info, not stripped

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:

:~$ 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

Mas o próprio GDB informa se há símbolos de depuração ao iniciar…

Sem a opção -g:

:~$ gcc -o demo demo.c
:~$ gdb demo
Reading symbols from demo...
(No debugging symbols found in demo)

Com a opção -g:

:~$ gcc -g -o demo demo.c
:~$ gdb demo
Reading symbols from demo...

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:

:~$ gcc -g -o demo demo.c
:~$ gdb ./demo
Reading symbols from ./demo...
(gdb)

Listagem de símbolos

(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

Listagem de símbolos conhecidos

Funções:

(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

Variáveis:

(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

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…

(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;

Para exibir o layout do mapeamento de memória do processo:

(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]

Observando o conteúdo da pilha

Terminando a execução anterior (kill):

(gdb) k
[Inferior 1 (process 1030168) killed]

Executando novamente:

(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;

Avançar a execução até a chamada da função soma:

(gdb) n
10	    int y = 20;
(gdb) n
11	    int z = soma(x, y);

Inspecionar o conteúdo da pilha neste momento:

(gdb) bt
#0  main () at demo.c:11

Avançar a execução entrando na função soma:

(gdb) s
soma (a=10, b=20) at demo.c:4
4	    int resultado = a + b;

Inspecionar o conteúdo da pilha:

(gdb) bt
#0  soma (a=10, b=20) at demo.c:4
#1  0x0000555555555178 in main () at demo.c:11

Listagem de bibliotecas compartilhadas carregadas

(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