atualização da aula 2

This commit is contained in:
Blau Araujo 2025-05-17 11:01:21 -03:00
parent 4321a16da0
commit 38aaa19866

View file

@ -1,15 +1,19 @@
#+title: 2 -- Linguagens de Montagem e a Compilação
#+title: 2 -- Linguagens, montagem e compilação
#+author: Blau Araujo
#+email: cursos@blauaraujo.com
#+options: toc:3
* Objetivos
- Compreender o papel das linguagens de montagem.
- Distinguir montagem, compilação e linkagem.
- Conhecer o funcionamento do NASM e do `ld`.
- Utilizar ferramentas como `objdump` e `readelf`.
- Distinguir as etapas de tradução de códigos-fonte para código de máquina.
- Conhecer algumas das formas como linguagens de programação são implementadas.
- Utilizar o NASM para montar arquivos objeto de programas em Assembly.
- Utilizar o ligador =ld= para gerar binários executáveis a partir de objetos.
- Utilizar o =gcc= para compilar programas em C.
- Conhecer algumas ferramentas do GNU/Linux para inspecionar arquivos binários.
- Utilizar o programa =objdump= para desmontar conteúdos de binários executáveis.
* Do código-fonte ao binário
@ -158,18 +162,21 @@ Aqui, estão os códigos de operação (/OpCodes/) da arquitetura x86 e seus
respectivos operandos. Em Assembly 32 bits, eles corresponderiam a:
#+begin_example
b8 01 00 00 00 -> mov eax, 0x00000001 ; Registrar o valor 1 em EAX (syscall exit).
bb 00 00 00 00 -> mov ebx, 0x00000000 ; Registrar o valor 0 em EBX (estado de término).
cd 80 -> int 0x80 ; Executar a interrupção 0x80 (executa a syscall).
b8 01 00 00 00 -> mov eax, 0x00000001 ; Registrar o valor 1 em EAX
bb 00 00 00 00 -> mov ebx, 0x00000000 ; Registrar o valor 0 em EBX
cd 80 -> int 0x80 ; Executar a interrupção 0x80
#+end_example
Nós falaremos sobre registradores e chamadas de sistemas mais adiante, mas
esta tradução "reversa" (de código de máquina para Assembly) mostra que,
seguindo as convenções de chamadas de sistema do Linux, eu copiei o valor
=1= no registrador =EAX= para informar que queria executar a chamada de sistema
=exit=, copiei =0= em =EBX= para definir o valor retornado pelo programa ao
terminar (estado de término) e mandei executar a interrupção =0x80= que,
em arquiteturas 32 bits, é como as chamadas de sistema são executadas.
Nós falaremos sobre registradores e chamadas de sistemas mais adiante no curso,
mas esta tradução "reversa" (de código de máquina para Assembly) mostra que,
seguindo as convenções de chamadas de sistema do Linux, nos permite ver que:
- O valor =1= foi copiado no registrador =EAX=, para informar que eu queria
executar a chamada de sistema =exit=;
- O valor =0= foi copiado no registrador =EBX=, para definir o valor que seria
retornado pelo programa ao terminar (estado de término);
- E a interrupção =0x80= deveria ser executada para invocar a chamada de sistema
definida em =EAX= com o argumento em =EBX=.
Resumindo, meu programa não faz nada além de terminar com estado =0= (que
significa /sucesso/, para o sistema). Para confirmar, vamos dar permissão
@ -277,6 +284,8 @@ máquina.
/"como fazer"/.
#+end_quote
* Sistemas de tradução de linguagens
Independentemente de estar escrito em uma linguagem de baixo ou alto nível,
todo programa precisa, em algum momento, ser convertido em código de máquina
para que possa ser executado pela CPU. Isso pode ocorrer por meio de tradução
@ -419,9 +428,7 @@ Neste curso, a escolha do montador NASM deve-se a alguns motivos:
- É uma linguagem fartamente documentada.
- Pode ser aprendida com muita facilidade por iniciantes.
* Exemplos comparativos
** Programa em Assembly
* Um programa em Assembly
Aqui, nós temos um programa escrito em Assembly 64 bits (NASM) para imprimir
a mensagem =Salve, simpatia!= no terminal e sair com estado de término =0=...
@ -452,7 +459,7 @@ Por enquanto, não importa entender o que está no código, mas observar seu
aspecto geral que, claramente, define passo a passo o que a CPU deve fazer
e organiza os dados e as instruções nas seções que deverão ocupar no
arquivo binário resultante. A mensagem que queremos imprimir, por exemplo,
está na seção chamada de =.rodata=, que é onde dados constantes são escritos
está na seção chamada de =.rodata=, que é onde dados constantes serão escritos
no arquivo binário.
Montando e link-editando o programa:
@ -470,24 +477,57 @@ tudo correu bem e já podemos executar o programa:
Salve, simpatia!
#+end_example
** Inspeção do arquivo binário
A partir daqui, vamos utilizar diversas ferramentas disponíveis para o GNU/Linux
que nos ajudarão o obter informações sobre arquivos binários de programas.
*** Tamanho do binário em bytes
** Tamanho do binário em bytes
Com o utilitário =ls=:
#+begin_example
:~$ ls -l salve
-rwxrwxr-x 1 blau blau 8880 mai 16 10:15 salve
#+end_example
Com o utilitário =du=:
#+begin_example
:~$ du -b salve
8880
#+end_example
Com o utilitário =stat=:
#+begin_example
:~$ stat -c '%s' salve
8880
#+end_example
*** Informações gerais do arquivo
** Informações gerais do arquivo
Com o utilitário =file=:
#+begin_example
:~$ file salve
salve: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked,
not stripped
salve: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically
linked, not stripped
#+end_example
*** Cabeçalho ELF
Com o utilitário =stat=:
#+begin_example
:~$ stat salve
Arquivo: salve
Tamanho: 8880 Blocos: 24 bloco de E/S: 4096 regular file
Dispositivo: 8,18 Inode: 4856471 Ligações: 1
Acesso: (0775/-rwxrwxr-x) Uid: ( 1000/ blau) Gid: ( 1000/ blau)
Acesso: 2025-05-16 10:15:50.611903536 -0300
Modificação: 2025-05-16 10:15:46.279952507 -0300
Alteração: 2025-05-16 10:15:46.279952507 -0300
Criação: 2025-05-16 10:15:46.275952553 -0300
#+end_example
** Cabeçalho do formato ELF
#+begin_example
:~$ readelf -h salve
@ -513,7 +553,7 @@ Cabeçalho ELF:
Índice de tabela de cadeias da secção: 5
#+end_example
*** Lista de seções
** Lista de seções do programa
#+begin_example
:~$ readelf -l salve
@ -539,7 +579,7 @@ Cabeçalhos do programa:
02 .rodata
#+end_example
*** Conteúdo (em hexa) da seção .text
** Despejo do conteúdo (em hexa) da seção .text
#+begin_example
:~$ readelf -x .text salve
@ -550,14 +590,7 @@ Despejo máximo da secção ".text":
0x00401020 4831ff0f 05 H1...
#+end_example
#+begin_src sh
file salve
readelf -h salve
objdump -d salve
#+end_src
*** Conteúdo (em hexa) da seção .rodata
** Despejo do conteúdo (em hexa) da seção .rodata
#+begin_example
:~$ readelf -x .rodata salve
@ -567,10 +600,10 @@ Despejo máximo da secção ".rodata":
0x00402010 0a .
#+end_example
** Versão equivalente em C
* Uma versão equivalente em C
Tentando ser o mais próximo possível ao que foi escrito em Assembly, esta
seria uma possível versão equivalente em C...
Tentando manter a maior proximidade possível com o que foi escrito em Assembly,
esta seria uma possível versão equivalente em C...
Arquivo: [[exemplos/02/salve.c][salve.c]]
@ -610,26 +643,28 @@ Compilando e executando:
Salve, simpatia!
#+end_example
** Inspeção do binário produzido com o código em C
Apesar de fazer a mesma coisa praticamente do mesmo modo, o arquivo binário
gerado a partir de um código em C tem diferenças importantes, como veremos
a seguir.
*** Tamanho do binário em bytes:
** Tamanho do binário em bytes:
#+begin_example
:~$ stat -c '%s' salvec
:~$ du -b salvec
16032
#+end_example
*** Informações gerais do arquivo
** Informações gerais do arquivo
#+begin_example
:~$ file salvec
salvec: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2,
salvec: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically
linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=351e5395c71e0b758b9cc05a68f3813f8f130817, for GNU/Linux 3.2.0,
not stripped
#+end_example
*** Cabeçalho ELF
** Cabeçalho do formato ELF
#+begin_example
:~$ readelf -h salvec
@ -655,7 +690,7 @@ Cabeçalho ELF:
Índice de tabela de cadeias da secção: 30
#+end_example
*** Lista de seções
** Lista de seções do programa
#+begin_example
:~$ readelf -l salvec
@ -715,7 +750,7 @@ Cabeçalhos do programa:
13 .init_array .fini_array .dynamic .got
#+end_example
*** Conteúdo (hexa) da seção .text
** Despejo do conteúdo (em hexa) da seção .text
#+begin_example
:~$ readelf -x .text salvec
@ -740,7 +775,7 @@ Despejo máximo da secção ".text":
0x00001160 00e8dafe ffffbf00 000000e8 c0feffff ................
#+end_example
*** Conteúdo (hexa) da seção .rodata
** Despejo do conteúdo (em hexa) da seção .rodata
#+begin_example
:~$ readelf -x .rodata salvec
@ -751,14 +786,123 @@ Despejo máximo da secção ".rodata":
0x00002020 0a00 ..
#+end_example
* Desmontagem comparativa
Com a opção =-d=, o utilitário =objdump= pode desmontar arquivos binários objeto ou
executáveis, ou seja, ele exibe um código em Assembly que corresponde ao código
de máquina encontrado no binário. Por exemplo, com o binário do nosso programa
=salve.asm=, nós podemos executar:
#+begin_example
:~$ objdump -d salve
salve: formato de ficheiro elf64-x86-64
Desmontagem da secção .text:
0000000000401000 <_start>:
401000: b8 01 00 00 00 mov $0x1,%eax
401005: bf 01 00 00 00 mov $0x1,%edi
40100a: 48 be 00 20 40 00 00 movabs $0x402000,%rsi
401011: 00 00 00
401014: ba 11 00 00 00 mov $0x11,%edx
401019: 0f 05 syscall
40101b: b8 3c 00 00 00 mov $0x3c,%eax
401020: 48 31 ff xor %rdi,%rdi
401023: 0f 05 syscall
#+end_example
O resultado foi a impressão de um código em Assembly escrito com a sintaxe
AT&T. Se quisermos que a desmontagem seja feita com a sintaxe Intel, nós
precisaremos incluir a opção =-M intel=:
#+begin_example
:~$ objdump -d -M intel salve
salve: formato de ficheiro elf64-x86-64
Desmontagem da secção .text:
0000000000401000 <_start>:
401000: b8 01 00 00 00 mov eax,0x1
401005: bf 01 00 00 00 mov edi,0x1
40100a: 48 be 00 20 40 00 00 movabs rsi,0x402000
401011: 00 00 00
401014: ba 11 00 00 00 mov edx,0x11
401019: 0f 05 syscall
40101b: b8 3c 00 00 00 mov eax,0x3c
401020: 48 31 ff xor rdi,rdi
401023: 0f 05 syscall
#+end_example
Se colocarmos o código desmontado ao lado do código original, nós veremos que
eles são praticamente os mesmos:
| Código original | Código desmontado | Diferença |
|-----------------+----------------------+------------------------------------------------------------------------|
| =mov rax, 1= | =mov eax, 0x1= | Utilizados apenas 4 bytes de RAX (EAX) |
| =mov rdi, 1= | =mov edi, 0x1= | Utilizados apenas 4 bytes de RDI (EDI) |
| =mov rsi, msg= | =movabs rsi, 0x402000= | Rótulo =msg= substituído pelo seu endereço absoluto |
| =mox rdx, len= | =mov edx, 0x11= | Macro =len= substituída pelo seu valor (~0x11~ = ~17~) |
| =syscall= | =syscall= | |
| =mov rax, 60= | =move eax, 0x3c= | Utilizados apenas 4 bytes de RAX (EAX) |
| =mov rdi, 0= | =xor rdi, rdi= | O valor =0= foi obtido por uma operação XOR bit a bit com o operador RDI |
| =syscall= | =syscall= | |
Essas diferenças não foram "inventadas" pelo =objdump=: elas estão no binário
gerado pelo montador NASM e resultam do preprocessamento do fonte e de várias
estratégias de otimização, como:
- Antes da montagem começar, a expressão =equ=, no rótulo =len=, é avaliada e o
valor resultante é substituído em todas as ocorrências de =len= no fonte.
- Rótulos de dados, como =msg=, são substituídos pelo deslocamento de endereços
(/offset/) que representam (=0x402000=, no nosso arquivo binário).
- Por que ocupar 8 bytes (RAX) com um valor numérico que pode ser escrito com
apenas 4 bytes (EAX)?
- Por que ocupar 5 bytes (=mov rdi, 0=) se o mesmo resultado pode ser obtido com
apenas 3 bytes (=xor rdi, rdi=)?
- O mnemônico =movabs= é uma convenção de alguns montadores 64 bits (como o GAS)
para explicitar que o valor copiado para o registrador deve ser escrito com
8 bytes e não pode ser otimizado para 4 bytes.
#+begin_quote
*Nota:* O NASM trata automaticamente a necessidade, ou não, de utilizar os 64bits
do registrador e não precisamos utilizar =movabs=. Na desmontagem, porém, o =objdump=
tenta deixar explícito que é isso que está acontecendo no binário e, portanto,
escreve =mov rdi, <valor>= como =movabs rdi, <valor>=. Contudo, os dados binários
ainda correspondem à operação =mov= (=48 BE <valor com 8 bytes>=).
#+end_quote
* Exercícios propostos
** 1. Desmonte e analise o programa em C
Desmonte o binário executável =salvc=, pesquise e analise as causas das muitas
diferenças encontradas em relação à desmontagem do programa =salve=.
** 2. Desmonte e analise o programa em código de máquina
Com o comando abaixo, desmonte o binário =ok.bin=, pesquise e analise o código
em assembly exibido:
#+begin_example
objdump -b binary -m i386 -M intel -D ok.bin
#+end_example
No mínimo, tente localizar e analisar o código relativo ao que o programa
efetivamente faz.
* Referências
- man xxd
- man 2 write
- man 2 exit
- man readelf
- man bvi
- man stat
- man file
- =man xxd=
- =man 2 write=
- =man 2 exit=
- =man bvi=
- =man readelf=
- =man du=
- =man stat=
- =man file=
- =man objdump=
- https://nasm.us/doc/