parcial da aula 3

This commit is contained in:
Blau Araujo 2025-05-18 11:50:09 -03:00
parent 13353156c1
commit 3695f289ef

230
curso/aula-03.org Normal file
View file

@ -0,0 +1,230 @@
#+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