conteúdo da aula 3

This commit is contained in:
Blau Araujo 2025-05-19 11:04:24 -03:00
parent 95ad4cc85b
commit 9780039c11
2 changed files with 434 additions and 45 deletions

View file

@ -15,10 +15,10 @@
* 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.
e de Ligação/), foi originalmente desenvolvido e publicado como parte da ABI
(/Interface Binária de Aplicações/) 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
@ -145,19 +145,19 @@ 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=. |
| 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 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
@ -178,53 +178,410 @@ Aqui estão alguns tipos de segmentos:
| =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. |
* Definindo seções ELF em NASM para Linux 64 bits
** Exemplo de programa mínimo em Assembly
Nós podemos definir qualquer seção ELF em Assembly NASM com a /diretiva/ =section=,
ou com seu sinônimo =segment=, oriundo de uma terminologia mais antiga para
arquiteturas x86 de 16 e 32 bits, onde o conceito de segmentação de memória era
explícito. Em ambientes modernos, =section= é mais comum, pois reflete a
nomenclatura utilizada no formato ELF e que está padronizada no editor de
ligações =ld=, no /loader/ =ld-linux= e em ferramentas como =readelf= e =objdump=.
#+begin_src asm :tangle exemplos/03/sections.asm
; Arquivo : sections.asm
; Descrição : Demonstra a definição das seções .rodata, .data, .bss e .text
; Montagem : nasm -f elf64 sections.asm
; Link-edição: ld sections.o -o sections
section .rodata
msg db "Eu sou imutável!", 0x0a
len equ $ - msg
section .data
contador dq 0 ; preenche 8 bytes (qword) com 0x00 em 'contador'
section .bss
buffer resb 32 ; reserva 32 bytes no endereço 'buffer'
#+begin_src nasm :tangle elf_min.asm
section .text
global _start
global _start ; ponto de entrada do programa
_start:
mov rax, 60
xor rdi, rdi
syscall
mov rax, 1 ; syscall: write
mov rdi, 1 ; fd 1 = stdout
mov rsi, msg ; endereço da mensagem
mov rdx, len ; tamanho da mensagem
syscall
mov rax, [contador] ; copia o dado no endereço 'contador' para rax
inc rax ; incrementa em 1 o valor em rax
mov [contador], rax ; copia o valor em rax para o endereço 'contador'
mov rax, 60 ; syscall: exit
xor rdi, rdi ; estado de término = 0
syscall
#+end_src
** Compilação
Antes de falarmos das seções do programa, é importante saber que, no caso de
programas em Assembly link-editados para serem carregados pelo sistema como
executáveis ELF, a ordem das seções no código-fonte só é mantida no arquivo
objeto montado (=.o=). Mas, quando o objeto é link-editado para gerar o arquivo
executável, o link-editor (=ld=) reorganiza as seções de modo a atender as
especificações do formato ELF.
#+begin_src sh
nasm -f elf64 -o elf_min.o elf_min.asm
ld -o elf_min elf_min.o
Montando e executando o programa:
#+begin_example
:~$ nasm -f elf64 sections.asm
:~$ ld sections.o -o sections
:~$ ./sections
Eu sou imutável!
#+end_example
Com o utilitário =readelf=, nós podemos listar os cabeçalhos do programa:
#+begin_example
$ readelf -l sections
Tipo de ficheiro Elf é EXEC (ficheiro executável)
Entry point 0x401000
There are 4 program headers, starting at offset 64
Cabeçalhos do programa:
Tipo Desvio EndVirtl EndFís
TamFich TamMem Bndrs Alinh
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000120 0x0000000000000120 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x0000000000000038 0x0000000000000038 R E 0x1000
LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
0x0000000000000012 0x0000000000000012 R 0x1000
LOAD 0x0000000000002014 0x0000000000403014 0x0000000000403014
0x0000000000000008 0x000000000000002c RW 0x1000
Secção para mapa do segmento:
Secções do segmento...
00
01 .text
02 .rodata
03 .data .bss
#+end_example
Onde:
- A seção =.text= inicia no byte =0x1000= do arquivo e ocupa 56 bytes (=0x38=).
- A seção =.rodata= inicia no byte =0x2000= do arquivo e ocupa 18 bytes (=0x12=).
- A seção =.data= inicia no byte =0x2014= do arquivo e ocupa 8 bytes.
- A seção =.bss= é indicada para mapeamento na execução, mas não ocupa
espaço no arquivo binário.
#+begin_quote
*Nota:* Repare que as seções =.text= e =.rodata= só têm permissão de leitura (=R=),
enquanto =.data= e =.bss= têm permissão de leitura e escrita (=RW=).
#+end_quote
Nós podemos localizar a definição da seção =.bss= listando a tabela de cabeçalhos
de seção, utilizada para orientar o mapeamento do executável nos segmentos da
memória:
#+begin_example
:~$ readelf -S sections
There are 8 section headers, starting at offset 0x2188:
Cabeçalhos de secção:
[Nr] Nome Tipo Endereço Desvio
Tam. Tam.Ent Bands Lig. Info Alinh
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000401000 00001000
0000000000000038 0000000000000000 AX 0 0 16
[ 2] .rodata PROGBITS 0000000000402000 00002000
0000000000000012 0000000000000000 A 0 0 4
[ 3] .data PROGBITS 0000000000403014 00002014
0000000000000008 0000000000000000 WA 0 0 4
[ 4] .bss NOBITS 000000000040301c 0000201c
0000000000000024 0000000000000000 WA 0 0 4
[ 5] .symtab SYMTAB 0000000000000000 00002020
00000000000000f0 0000000000000018 6 6 8
[ 6] .strtab STRTAB 0000000000000000 00002110
000000000000003e 0000000000000000 0 0 1
[ 7] .shstrtab STRTAB 0000000000000000 0000214e
0000000000000034 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
#+end_example
Aqui, nós podemos ver que =.bss= deve ocupar um espaço de 36 bytes na memória
(=0x24=), a partir do endereço de /offset/ 0x40301c. A diferença entre os 32 bytes
reservados no programa e os 36 bytes que vemos na tabela, é o resultado do
alinhamento de 4 bytes aplicado pelo =ld=, como podemos ver na última coluna
da tabela.
** Uma nota sobre alinhamento de dados
Em sistemas Linux 64 bits, é comum que dados sejam alinhados a endereços
múltiplos de 8 bytes, o que corresponde à largura dos registradores e garante
acesso eficiente e seguro à memória. Se pegarmos o /offset/ da seção =.bss=
(=0x201c=) e somarmos os 32 bytes reservados no programa, nós veremos que o
próximo dado teria que ser escrito no /offset/ =0x203c= (8252, em base 10), que
não é múltiplo de 8 (~8252/8=1031.5~). Somando 4 ao próximo endereço, nós
chegamos ao /offset/ =0x2040= (8256, em base 10), que é um múltiplo de 8
(~8256/8=1032.0~).
No entanto, observe que as seções =.rodata= e =.data= também tiveram alinhamento,
mas seus tamanhos não são diferentes daqueles definidos no código-fonte. Isso
aconteceu porque, diferente de =.bss=, os dados em =.rodata= e em =.data= foram
inicializados e, por definição, =.bss= é uma região reservada para dados não
inicializados -- logo, o tamanho da seção inclui os bytes de alinhamento.
#+begin_quote
*Nota:* O nome =.bss= vem da linguagem Fortran e significa /Block Started by Symbol/
(/bloco iniciado por um símbolo/, em tradução livre).
#+end_quote
** Inspecionando a seção .rodata
#+begin_src asm
section .rodata
msg db "Eu sou imutável!", 0x0a
len equ $ - msg
#+end_src
** Inspeção do ELF
Aqui, o endereço de rótulo =msg= receberá a cadeia de bytes definida com a
diretiva de /definição de bytes/ (=db=). Na montagem, esses bytes serão escritos
na seção =.rodata=, onde não poderão ser alterados.
#+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
Como visto na tabela de cabeçalhos do programa, a seção =.rodata= está no
/offset/ =0x002000= do arquivo e tem permissão apenas para leitura (=R=):
#+begin_example
Tipo Desvio EndVirtl EndFís
TamFich TamMem Bndrs Alinh
...
LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
0x0000000000000012 0x0000000000000012 R 0x1000
...
#+end_example
Seu conteúdo também pode ser visualizado com a opção =-x= do =readelf=:
#+begin_example
:~$ readelf -x .rodata sections
Despejo máximo da secção ".rodata":
0x00402000 45752073 6f752069 6d7574c3 a176656c Eu sou imut..vel
0x00402010 210a !.
#+end_example
Ou com o =xxd=, utilizando as opções =-s= (/skip/) e =-l= (/length/):
#+begin_example
:~$ xxd -s 0x2000 -l 32 sections
00002000: 4575 2073 6f75 2069 6d75 74c3 a176 656c Eu sou imut..vel
00002010: 210a 0000 0000 0000 0000 0000 0000 0000 !...............
#+end_example
** Inspeção da seção .data
#+begin_src asm
section .data
contador dq 0 ; preenche 8 bytes (qword) com 0x00 em 'contador'
#+end_src
** Versão em C para comparação
Neste exemplo, o valor =0= será escrito na seção =.data=, no endereço indicado
pelo rótulo =contador=, e ocupara 8 bytes (diretiva =dq=), resultando em 8 bytes
no arquivo (e na memória) preenchidos com zeros.
#+begin_src C :tangle elf_min.c
int main() {
return 0;
}
No NASM, os dados podem ser definidos com tamanhos de:
- 1 byte: =db=, de /define bytes/
- 2 bytes: =dw=, de /define word/
- 4 bytes: =dd=, de /define double word/
- 8 bytes =dq=, de /define quad word/
Como visto na tabela de cabeçalhos do programa, a seção =.data= inicia no
/offset/ =0x2014= do arquivo e tem permissões de leitura e escrita (=RW=):
#+begin_example
Tipo Desvio EndVirtl EndFís
TamFich TamMem Bndrs Alinh
...
LOAD 0x0000000000002014 0x0000000000403014 0x0000000000403014
0x0000000000000008 0x000000000000002c RW 0x1000
...
#+end_example
Seu conteúdo pode ser visualizado com a opção =-x= do =readelf=:
#+begin_example
:~$ readelf -x .data sections
Despejo máximo da secção ".data":
0x00403014 00000000 00000000 ........
#+end_example
Ou com o =xxd=, sabendo seu tamanho:
#+begin_example
:~$ xxd -s 0x2014 -l 8 sections
00002014: 0000 0000 0000 0000 ........
#+end_example
Ainda no exempĺo, nós escrevemos uma rotina para incrementar o valor no
endereço identificado pelo rótulo =contador=:
#+begin_src asm
mov rax, [contador] ; copia o dado no endereço 'contador' para rax
inc rax ; incrementa em 1 o valor em rax
mov [contador], rax ; copia o valor em rax para o endereço 'contador'
#+end_src
** Exercícios
#+begin_quote
Em Assembly, isso é o mais próximo que podemos chegar do conceito de
/variáveis/, próprio de linguagens de alto nível.
#+end_quote
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.
O efeito dessa rotina só pode ser examinado com o programa em execução, por
exemplo, com o depurador GDB (GNU Debugger). Mas, antes, o programa terá que
ser montado com símbolos de depuração (opção =-g= do =nasm=):
** Referências
- man 5 elf
#+begin_example
:~$ nasm -g -f elf64 sections.asm
:~$ ld sections.o -o sections
#+end_example
Carregando o programa com o GDB:
#+begin_example
:~$ gdb sections
Reading symbols from sections...
#+end_example
Listando o código-fonte (=list=) para descobrir as linhas antes e depois da
alteração do dado em =contador=:
#+begin_example
(gdb) list
1 ; Arquivo : sections.asm
2 ; Descrição : Demonstra a definição das seções .rodata, .data, .bss e .text
3 ; Montagem : nasm -f elf64 sections.asm
4 ; Link-edição: ld sections.o -o sections
5
6 section .rodata
7 msg db "Eu sou imutável!", 0x0a
8 len equ $ - msg
9
10 section .data
(gdb)
11 contador dq 0 ; preenche 8 bytes (qword) com 0x00 em 'contador'
12
13 section .bss
14 buffer resb 32 ; reserva 32 bytes no endereço 'buffer'
15
16 section .text
17 global _start ; ponto de entrada do programa
18
19 _start:
20 mov rax, 1 ; syscall: write
(gdb)
21 mov rdi, 1 ; fd 1 = stdout
22 mov rsi, msg ; endereço da mensagem
23 mov rdx, len ; tamanho da mensagem
24 syscall
25
26 mov rax, [contador] ; copia o dado no endereço 'contador' para rax
27 inc rax ; incrementa em 1 o valor em rax
28 mov [contador], rax ; copia o valor em rax para o endereço 'contador'
29
30 mov rax, 60 ; syscall: exit
(gdb)
31 xor rdi, rdi ; estado de término = 0
32 syscall
#+end_example
Definindo os pontos de parada (=break=) nas linhas 26 e 30:
#+begin_example
(gdb) break 26
Breakpoint 1 at 0x40101b: file sections.asm, line 26.
(gdb) break 29
Breakpoint 2 at 0x40102e: file sections.asm, line 30.
#+end_example
Executando o programa (=run=):
#+begin_example
(gdb) run
Starting program: /home/blau/git/pbn/curso/exemplos/03/sections
Eu sou imutável!
Breakpoint 1, _start () at sections.asm:26
26 mov rax, [contador] ; copia o dado no endereço 'contador' para rax
#+end_example
Imprimindo o valor no endereço =contador= (8 bytes equivale ao tipo =long=):
#+begin_example
(gdb) print (long *)contador
$1 = (long *) 0x0
#+end_example
Continuando a execução (=continue=):
#+begin_example
(gdb) continue
Continuing.
Breakpoint 2, _start () at sections.asm:30
30 mov rax, 60 ; syscall: exit
#+end_example
Verificando a alteração do valor no endereço =contador=:
#+begin_example
(gdb) print (long *)contador
$2 = (long *) 0x1
#+end_example
* Exercícios propostos
1. Crie um exemplo em C onde seja possível observar as seções =.text=, =.rodata=,
=.data= e =.bss=.
2. Com o arquivo objeto do exemplo =sections.asm=, tente interpretar as
informações presentes no seu cabeçalho ELF (a descrição dos campos
pode ser encontrada na página "ELF" da wiki OS Dev, nas referências).
3. Em seguida, interprete o cabeçalho ELF do arquivo executável e anote
as diferenças, se houver.
** Desafio
Sabendo que o NASM pode montar binários puros, tente criar um programa que
resulte em um arquivo executável 64 bits utilizando apenas as especificações
ELF para definir os bytes de seu conteúdo, que deverá ser funcional sem a
edição de ligações. O programa não precisa fazer nada além de terminar com
estado de saída 42.
Para montar e dar permissões de execução:
#+begin_example
:~$ nasm -f bin -o elf_exit42 elf_exit42.asm
:~$ chmod +x elf_exit42
#+end_example
Para testá-lo:
#+begin_example
$ ./elf_exit42
$ echo $?
42
#+end_example
* Referências
- =man 5 elf=
- [[https://wiki.osdev.org/ELF][OS Dev: ELF]]
- https://refspecs.linuxfoundation.org/elf/elf.pdf
- https://wiki.osdev.org/ELF

View file

@ -0,0 +1,32 @@
; Arquivo : sections.asm
; Descrição : Demonstra a definição das seções .rodata, .data, .bss e .text
; Montagem : nasm -f elf64 sections.asm
; Link-edição: ld sections.o -o sections
section .rodata
msg db "Eu sou imutável!", 0x0a
len equ $ - msg
section .data
contador dq 0 ; preenche 8 bytes (qword) com 0x00 em 'contador'
section .bss
buffer resb 32 ; reserva 32 bytes no endereço 'buffer'
section .text
global _start ; ponto de entrada do programa
_start:
mov rax, 1 ; syscall: write
mov rdi, 1 ; fd 1 = stdout
mov rsi, msg ; endereço da mensagem
mov rdx, len ; tamanho da mensagem
syscall
mov rax, [contador] ; copia o dado no endereço 'contador' para rax
inc rax ; incrementa em 1 o valor em rax
mov [contador], rax ; copia o valor em rax para o endereço 'contador'
mov rax, 60 ; syscall: exit
xor rdi, rdi ; estado de término = 0
syscall