From 38aaa19866f30b223a5ae36d0ab8d092dd7e99f1 Mon Sep 17 00:00:00 2001 From: Blau Araujo Date: Sat, 17 May 2025 11:01:21 -0300 Subject: [PATCH] =?UTF-8?q?atualiza=C3=A7=C3=A3o=20da=20aula=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- curso/aula-02.org | 254 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 199 insertions(+), 55 deletions(-) diff --git a/curso/aula-02.org b/curso/aula-02.org index 86dcf92..da5e150 100644 --- a/curso/aula-02.org +++ b/curso/aula-02.org @@ -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, = como =movabs rdi, =. Contudo, os dados binários +ainda correspondem à operação =mov= (=48 BE =). +#+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/