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 #+author: Blau Araujo
#+email: cursos@blauaraujo.com #+email: cursos@blauaraujo.com
#+options: toc:3 #+options: toc:3
* Objetivos * Objetivos
- Compreender o papel das linguagens de montagem. - Distinguir as etapas de tradução de códigos-fonte para código de máquina.
- Distinguir montagem, compilação e linkagem. - Conhecer algumas das formas como linguagens de programação são implementadas.
- Conhecer o funcionamento do NASM e do `ld`. - Utilizar o NASM para montar arquivos objeto de programas em Assembly.
- Utilizar ferramentas como `objdump` e `readelf`. - 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 * 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: respectivos operandos. Em Assembly 32 bits, eles corresponderiam a:
#+begin_example #+begin_example
b8 01 00 00 00 -> mov eax, 0x00000001 ; Registrar o valor 1 em EAX (syscall exit). 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 (estado de término). bb 00 00 00 00 -> mov ebx, 0x00000000 ; Registrar o valor 0 em EBX
cd 80 -> int 0x80 ; Executar a interrupção 0x80 (executa a syscall). cd 80 -> int 0x80 ; Executar a interrupção 0x80
#+end_example #+end_example
Nós falaremos sobre registradores e chamadas de sistemas mais adiante, mas Nós falaremos sobre registradores e chamadas de sistemas mais adiante no curso,
esta tradução "reversa" (de código de máquina para Assembly) mostra que, 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 seguindo as convenções de chamadas de sistema do Linux, nos permite ver que:
=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 - O valor =1= foi copiado no registrador =EAX=, para informar que eu queria
terminar (estado de término) e mandei executar a interrupção =0x80= que, executar a chamada de sistema =exit=;
em arquiteturas 32 bits, é como as chamadas de sistema são executadas. - 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 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 significa /sucesso/, para o sistema). Para confirmar, vamos dar permissão
@ -277,6 +284,8 @@ máquina.
/"como fazer"/. /"como fazer"/.
#+end_quote #+end_quote
* Sistemas de tradução de linguagens
Independentemente de estar escrito em uma linguagem de baixo ou alto nível, 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 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 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. - É uma linguagem fartamente documentada.
- Pode ser aprendida com muita facilidade por iniciantes. - Pode ser aprendida com muita facilidade por iniciantes.
* Exemplos comparativos * Um programa em Assembly
** Programa em Assembly
Aqui, nós temos um programa escrito em Assembly 64 bits (NASM) para imprimir 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=... 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 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 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, 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. no arquivo binário.
Montando e link-editando o programa: Montando e link-editando o programa:
@ -470,24 +477,57 @@ tudo correu bem e já podemos executar o programa:
Salve, simpatia! Salve, simpatia!
#+end_example #+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 #+begin_example
:~$ stat -c '%s' salve :~$ stat -c '%s' salve
8880 8880
#+end_example #+end_example
*** Informações gerais do arquivo ** Informações gerais do arquivo
Com o utilitário =file=:
#+begin_example #+begin_example
:~$ file salve :~$ file salve
salve: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, salve: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically
not stripped linked, not stripped
#+end_example #+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 #+begin_example
:~$ readelf -h salve :~$ readelf -h salve
@ -513,7 +553,7 @@ Cabeçalho ELF:
Índice de tabela de cadeias da secção: 5 Índice de tabela de cadeias da secção: 5
#+end_example #+end_example
*** Lista de seções ** Lista de seções do programa
#+begin_example #+begin_example
:~$ readelf -l salve :~$ readelf -l salve
@ -539,7 +579,7 @@ Cabeçalhos do programa:
02 .rodata 02 .rodata
#+end_example #+end_example
*** Conteúdo (em hexa) da seção .text ** Despejo do conteúdo (em hexa) da seção .text
#+begin_example #+begin_example
:~$ readelf -x .text salve :~$ readelf -x .text salve
@ -550,14 +590,7 @@ Despejo máximo da secção ".text":
0x00401020 4831ff0f 05 H1... 0x00401020 4831ff0f 05 H1...
#+end_example #+end_example
** Despejo do conteúdo (em hexa) da seção .rodata
#+begin_src sh
file salve
readelf -h salve
objdump -d salve
#+end_src
*** Conteúdo (em hexa) da seção .rodata
#+begin_example #+begin_example
:~$ readelf -x .rodata salve :~$ readelf -x .rodata salve
@ -567,10 +600,10 @@ Despejo máximo da secção ".rodata":
0x00402010 0a . 0x00402010 0a .
#+end_example #+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 Tentando manter a maior proximidade possível com o que foi escrito em Assembly,
seria uma possível versão equivalente em C... esta seria uma possível versão equivalente em C...
Arquivo: [[exemplos/02/salve.c][salve.c]] Arquivo: [[exemplos/02/salve.c][salve.c]]
@ -610,26 +643,28 @@ Compilando e executando:
Salve, simpatia! Salve, simpatia!
#+end_example #+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 #+begin_example
:~$ stat -c '%s' salvec :~$ du -b salvec
16032 16032
#+end_example #+end_example
*** Informações gerais do arquivo ** Informações gerais do arquivo
#+begin_example #+begin_example
:~$ file salvec :~$ file salvec
salvec: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, salvec: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically
interpreter /lib64/ld-linux-x86-64.so.2, linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=351e5395c71e0b758b9cc05a68f3813f8f130817, for GNU/Linux 3.2.0, BuildID[sha1]=351e5395c71e0b758b9cc05a68f3813f8f130817, for GNU/Linux 3.2.0,
not stripped not stripped
#+end_example #+end_example
*** Cabeçalho ELF ** Cabeçalho do formato ELF
#+begin_example #+begin_example
:~$ readelf -h salvec :~$ readelf -h salvec
@ -655,7 +690,7 @@ Cabeçalho ELF:
Índice de tabela de cadeias da secção: 30 Índice de tabela de cadeias da secção: 30
#+end_example #+end_example
*** Lista de seções ** Lista de seções do programa
#+begin_example #+begin_example
:~$ readelf -l salvec :~$ readelf -l salvec
@ -715,7 +750,7 @@ Cabeçalhos do programa:
13 .init_array .fini_array .dynamic .got 13 .init_array .fini_array .dynamic .got
#+end_example #+end_example
*** Conteúdo (hexa) da seção .text ** Despejo do conteúdo (em hexa) da seção .text
#+begin_example #+begin_example
:~$ readelf -x .text salvec :~$ readelf -x .text salvec
@ -740,7 +775,7 @@ Despejo máximo da secção ".text":
0x00001160 00e8dafe ffffbf00 000000e8 c0feffff ................ 0x00001160 00e8dafe ffffbf00 000000e8 c0feffff ................
#+end_example #+end_example
*** Conteúdo (hexa) da seção .rodata ** Despejo do conteúdo (em hexa) da seção .rodata
#+begin_example #+begin_example
:~$ readelf -x .rodata salvec :~$ readelf -x .rodata salvec
@ -751,14 +786,123 @@ Despejo máximo da secção ".rodata":
0x00002020 0a00 .. 0x00002020 0a00 ..
#+end_example #+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 * Referências
- man xxd - =man xxd=
- man 2 write - =man 2 write=
- man 2 exit - =man 2 exit=
- man readelf - =man bvi=
- man bvi - =man readelf=
- man stat - =man du=
- man file - =man stat=
- =man file=
- =man objdump=
- https://nasm.us/doc/ - https://nasm.us/doc/