diff --git a/curso/aula-03.org b/curso/aula-03.org index 73f8747..7f11bd6 100644 --- a/curso/aula-03.org +++ b/curso/aula-03.org @@ -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 diff --git a/curso/exemplos/03/sections.asm b/curso/exemplos/03/sections.asm new file mode 100644 index 0000000..6b8794f --- /dev/null +++ b/curso/exemplos/03/sections.asm @@ -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