.. | ||
README.org |
Curso prático de introdução ao GDB
- 3. Binários executáveis
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