#+title: Curso prático de introdução ao GDB #+author: Blau Araujo #+email: blau@debxp.org * 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 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 ** 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