pbn/curso/aula-03.org
2025-05-18 11:50:09 -03:00

11 KiB
Raw Blame History

3 O formato binário ELF

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.

     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)  │
  └────────────────────────┘     └────────────────────────┘

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

section .text
    global _start

_start:
    mov rax, 60
    xor rdi, rdi
    syscall

Compilação

nasm -f elf64 -o elf_min.o elf_min.asm
ld -o elf_min elf_min.o

Inspeção do ELF

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

Versão em C para comparação

int main() {
    return 0;
}

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.