atualização da aula 2
This commit is contained in:
parent
4321a16da0
commit
38aaa19866
1 changed files with 199 additions and 55 deletions
|
@ -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/
|
||||
|
|
Loading…
Add table
Reference in a new issue