#+title: 3 -- O formato binário ELF #+author: Blau Araujo #+email: cursos@blauaraujo.com #+options: toc:3 * Objetivos - Compreender a estrutura do formato ELF. - Compreender a definição das seções do programa com a linguagem Assembly. - Explorar as seções do binário com ferramentas =readelf=, =objdump= e =hexdump=. - Relacionar o binário ELF com seu conteúdo em Assembly. * O que é o formato ELF O formato binário ELF, de /Executable and Linking Format/ (/Formato Executável e de Ligação/) foi originalmente desenvolvido e publicado como parte da /Interface Binária de Aplicações/ (ABI) do Unix System V Release 4 (SVR4). Desde então, tornou-se o padrão adotado pela maioria dos sistemas /Unix-like/ para representar arquivos objeto binários, como executáveis e bibliotecas. ** Principais tipos de arquivos objeto Os arquivos objeto, criados por montadores e editores de ligações (/link-editores/), são representações binárias de programas destinados a serem executados diretamente por um processador. As especificações do ELF para o Linux definem três tipos principais de arquivos objeto: - Arquivo relocável :: Contém código e dados preparados para serem ligados a outros objetos para criar um executável ou um objeto compartilhado. - Arquivo executável :: O arquivo de um programa que pode ser executado. - Arquivo objeto compartilhado :: Contém código e dados que podem ser ligados de duas formas: com outros arquivos relocáveis para criar um outro objeto, ou com um executável e outros objetos compartilhados para formar a imagem de um processo. * Formato do arquivo Os arquivos objeto participam tanto da link-edição quanto da execução de programas. Portanto, seu formato deve acomodar as estruturas necessárias para ambas as atividades. Na link-edição, o foco está nas seções e na tabela de cabeçalhos de seções, enquanto que, na execução, o carregador (/loader/) utiliza os cabeçalhos do programa para mapear segmentos na memória. #+begin_example LINK-EDIÇÃO (ARQUIVO) EXECUÇÃO (MEMÓRIA) ┌────────────────────────┐ ┌────────────────────────┐ │ CABEÇALHO ELF │ │ CABEÇALHO ELF │ ├────────────────────────┤ ├────────────────────────┤ │ TABELA DE CABEÇALHOS │ │ TABELA DE CABEÇALHOS │ │ DO PROGRAMA (OPCIONAL) │ │ DO PROGRAMA │ ├────────────────────────┤ ├────────────────────────┤ │ SEÇÃO 1 │ │ │ ├────────────────────────┤ │ SEGMENTO 1 │ │ SEÇÃO 2 │ │ │ ├────────────────────────┤ ├────────────────────────┤ │ ... │ │ │ ├────────────────────────┤ │ SEGMENTO 2 │ │ SEÇÃO N │ │ │ ├────────────────────────┤ ├────────────────────────┤ │ ... │ │ ... │ ├────────────────────────┤ ├────────────────────────┤ │ TABELA DE CABEÇALHOS │ │ TABELA DE CABEÇALHOS │ │ DE SEÇÕES │ │ DE SEÇÕES (OPCIONAL) │ └────────────────────────┘ └────────────────────────┘ #+end_example *Importante!* - O posicionamento real das tabelas de seções e do programa pode ser diferente de como está representado nos diagramas. - Do mesmo modo, as seções e os seguimentos não têm uma ordem específica. - Só o cabeçalho ELF tem uma posição fixa no arquivo. - Um segmento na memória pode conter várias seções do arquivo. No diagrama... - Cabeçalho ELF :: Escrito nos primeiros 52 ou 64 bytes do arquivo, o cabeçalho ELF contém um resumo da sua organização e diversas informações, como o formato do arquivo (32 ou 64 bits), a ordem de escrita dos bytes de dados (/little/ ou /big endian/), o tipo do arquivo objeto (se é relocável, executável ou compartilhado), a arquitetura do conjunto de instruções, entre outras definições. Os primeiros 4 bytes do cabeçalho ELF contêm a assinatura do formato, o seu /número mágico/: o byte =0x7F= seguido dos bytes dos caracteres =E=, =L= e =F= na tabela ASCII (=45 4C 46=). - Seções :: Contêm a organização dos dados do programa para efeito da edição das ligações no arquivo, como as instruções do código, dados globais contantes e variáveis, símbolos, informações de relocação, etc. - Tabela de cabeçalhos do programa :: Se existir, diz ao programa como montar uma imagem do programa quando ele for carregado na memória para execução. Logo, se o arquivo objeto for executável, ele terá que conter uma tabela de cabeçalhos do programa, o que é desnecessário em arquivos objeto relocáveis. - Tabela de cabeçalhos de seções :: Contém a descrição das seções do arquivo. Cada seção tem uma entrada na tabela com informações como seu nome, seu tamanho, a partir de onde pode ser encontrada no arquivo (/offset/), etc. Arquivos utilizados durante a edição de ligações precisam ter a tabela de cabeçalhos de seções, mas ela é desnecessária no caso de arquivos objeto que só serão ligados dinamicamente em tempo de execução. * Seções especiais Várias seções de um arquivo ELF são predefinidas para conter informações de controle utilizadas pelo sistema operacional. Na pŕática, programas executáveis são formados vários arquivos objeto e bibliotecas vinculados através do processo de ligação, que pode ser: - *Estática:* Vários arquivo objeto são combinados em um só arquivo executável. - *Dinâmica:* O objeto executável é ligado na memoria, em tempo de execução, com objetos compartilhados e bibliotecas disponíveis no sistema. No fim das contas, o resultado será sempre um programa completo na memória, a diferença está em quando o objeto executável é construído e em quem resolve e processa as ligações. No GNU/Linux: - *Ligações estáticas:* Processadas pelo editor de ligações (=ld=, da suíte GNU Binutils). - *Ligações dinâmicas:* Processadas pelo carregador e ligador dinâmico do sistema (=ld-linux=, da =glibc=). Seja a ligação realizada estaticamente, durante a construção do executável, ou dinamicamente, no momento da execução, o correto processamento das ligações depende da organização interna do arquivo ELF. Essa organização é descrita por suas seções especiais, entre as quais podemos destacar: | Seção | Descrição | |---------------+----------------------------------------------------------------------------------------------------------------------------------------| | =.bss= | Contém dados globais não inicializados ou inicializados com zero; ocupa espaço na memória, mas não ocupa espaço no arquivo executável. | | =.data= | Contém dados globais inicializados; ocupa espaço tanto no arquivo quanto na memória. | | =.rodata= | Contém dados que não podem ser alterados (/read only/), geralmente usada para constantes e strings literais. | | =.text= | Seção que contém o código executável do programa (instruções da CPU). | | =.symtab= | Tabela de símbolos completa, usada pelo ligador e depuradores para localizar símbolos. | | =.strtab= | Tabela de strings que armazena os nomes dos símbolos referenciados em =.symtab=. | * Tipos de segmentos Na execução de um programa ELF, o sistema operacional utiliza a Tabela de Cabeçalhos do Programa para saber quais partes do arquivo devem ser carregadas na memória e como tratá-las. Cada entrada nessa tabela descreve um segmento que representa uma região de interesse para o carregador, como código do programa, seus dados, informações de ligação dinâmica e o caminho do carregador e ligador dinâmico. Esses segmentos são identificados por tipos padronizados, cada um com uma finalidade específica no processo de carregamento e execução do programa. Aqui estão alguns tipos de segmentos: | Tipo | Descrição | |--------------+---------------------------------------------------------------------------| | =LOAD= | Segmento que deve ser carregado na memória, contendo código ou dados. | | =INTERP= | Indica o caminho do carregador dinâmico (=ld-linux=) para execução. | | =DYNAMIC= | Contém informações para ligação dinâmica, como símbolos e dependências. | | =GNU_STACK= | Define permissões da pilha (stack), como se pode ou não ser executável. | | =NOTE= | Armazena metadados usados pelo sistema operacional. | | =PHDR= | Contém a própria Tabela de Cabeçalhos do Programa, usada por depuradores. | | =GNU_EH_FRAME= | Informações para suporte à pilha de exceções (C, C++, etc.). | | =GNU_RELRO= | Segmento de dados que será tornado somente leitura após inicialização. | ** Exemplo de programa mínimo em Assembly #+begin_src nasm :tangle elf_min.asm section .text global _start _start: mov rax, 60 xor rdi, rdi syscall #+end_src ** Compilação #+begin_src sh nasm -f elf64 -o elf_min.o elf_min.asm ld -o elf_min elf_min.o #+end_src ** Inspeção do ELF #+begin_src sh file elf_min readelf -h elf_min readelf -S elf_min # Seções readelf -l elf_min # Segmentos (program headers) objdump -d elf_min hexdump -C elf_min | less #+end_src ** Versão em C para comparação #+begin_src C :tangle elf_min.c int main() { return 0; } #+end_src ** Exercícios 1. Compare os headers do binário gerado em C com o de Assembly. 2. Use `readelf -x .text` para inspecionar o código de máquina. 3. Modifique o binário com `hexedit` para alterar manualmente um byte do código. 4. Gere um programa com `.data` e `.bss` e localize essas seções no ELF. ** Referências - man 5 elf - https://refspecs.linuxfoundation.org/elf/elf.pdf - https://wiki.osdev.org/ELF