#+title: 0 -- Introdução à linguagem Assembly (NASM) #+author: Blau Araujo #+email: cursos@blauaraujo.com #+options: toc:3 * Objetivos - Apresentar as características e os elementos básicos da linguagem Assembly. - Diferenciar conceitos da programação em alto e baixo nível. - Conhecer as características da implementação NASM para Linux 64 bits. - Aprender as instruções e diretivas essenciais para começar a programar. - Criar executar um primeiro programa em Assembly. * O que é Assembly Assembly é uma linguagem de programação que oferece formas de representar, em texto legível, os códigos numéricos das operações que o hardware da máquina é capaz de processar. A rigor, qualquer linguagem faz isso, mas um programa em Assembly se diferencia por expressar instruções que correspondem, cada uma, a apenas um passo daquilo que o processador (a CPU) deve fazer -- sem abstrações e numa relação praticamente direta com o código de máquina. ** Linguagem dependente da arquitetura Como representa diretamente as instruções executadas por um processador, a linguagem Assembly depende necessariamente das características da arquitetura desse processador. Em especial, cada CPU é projetada com seu próprio conjunto de instruções (ISA, do inglês /Instruction Set Architecture/). A ISA é, por definição, a própria interface entre o hardware e o software de um sistema computacional. É ela que define, entre outras coisas: - Quais operações a CPU pode executar; - Quais registradores estão disponíveis; - Como as instruções são codificadas em binário; - Como os operandos são manipulados; - Como a memória pode ser utilizada. Sendo assim, não há apenas uma linguagem Assembly, a não ser como um termo genérico que engloba diversas linguagens, todas baseadas na representação simbólica das operações de arquiteturas específicas. No contexto dos nossos estudos, a linguagem Assembly que utilizaremos é aquela voltada à arquitetura Intel x86_64, tal como implementada no /montador/ NASM (/Netwide Assembler/), e é a ela que estaremos nos referindo todas as vezes que dissermos /"Assembly"/. ** Linguagem de montagem A palavra /"assembly"/ vem do inglês e significa /"montagem"/. Portanto, o nome deriva da primeira etapa do processo que transforma os programas escritos na linguagem (os /códigos-fonte/) nos códigos binários executáveis pela máquina (/código de máquina/). Esse processo se chama /"montagem"/ e é feita por um programa chamado /"montador"/ (ou /"assembler"/, em inglês). #+begin_quote *Importante!* A linguagem se chama /"Assembly"/, não /"assembler"/, que é como chamamos o programa que implementa a linguagem e faz a montagem do código de máquina. #+end_quote ** Classificações da linguagem O Assembly pode ser classificado como uma linguagem... *** De baixo nível de abstração (segunda geração) Trabalha diretamente com os recursos da arquitetura da máquina, como registradores, endereços de memória e instruções específicas da CPU. Não há camadas intermediárias entre o código e o hardware. *** Imperativa As instruções são dadas em uma sequência explícita, descrevendo passo a passo o que deve ser feito. O controle do fluxo é manual, baseado em saltos e comparações, sem abstrações de alto nível. *** De tipagem inexistente Não há distinção formal entre tipos de dados. Tudo é interpretado como sequências de bytes, cabendo a quem programa decidir como os dados devem ser lidos ou manipulados. *** Com gerenciamento manual de memória Não há alocação automática ou coleta de lixo. O programador deve lidar com endereços de memória diretamente, reservando, acessando e liberando espaços manualmente, de acordo com a necessidade. *** Compilada via montagem O código Assembly não é interpretado nem compilado. Em vez disso, ele é traduzido diretamente para código de máquina pelo montador (/assembler/), que gera um binário executável diretamente ou um arquivo binário do tipo /objeto/. Para que tenhamos um executável de fato, ainda pode ser necessário realizar uma etapa de /ligação/, em que utilizamos um /editor de ligações/ para adequar o binário montado às especificações do formato de executáveis do sistema operacional. *** Paradigmas Também pode ser considerada uma linguagem procedural, mas com ressalvas, já que é uma linguagem que só oferece instruções e, portanto, não impõe nenhum paradigma. Isso quer dizer que nós podemos escrever programas de forma procedural, mas também de forma totalmente linear e até orientada a macros. *** De uso específico Apesar de ser uma linguagem completa, o Assembly é geralmente usado para finalidades bem específicas, como a implementação de trechos críticos de desempenho, rotinas de acesso direto ao hardware, criação de módulos de sistemas operacionais ou interação com o sistema em níveis muito mais baixos. Raramente é usado para o desenvolvimento de aplicações inteiras, principalmente devido à sua complexidade, verbosidade e baixa portabilidade. ** Comparação com outras linguagens Por ser uma linguagem de segunda geração, o Assembly opera em um nível muito mais próximo do hardware do que linguagens de alto nível. Isso traz uma série de diferenças fundamentais em relação à forma como os programas são escritos, estruturados e compreendidos. Aqui estão algumas dessas diferenças... *** Não há estruturas de controle Em Assembly nós não temos estruturas de controle abstratas, como =if=, =case=, =for= ou =while=. Em vez disso, todo o controle de fluxo do programa tem que ser implementado com instruções de comparação e saltos para outras partes do programa. *** Não há o conceito de variáveis Os dados são associados diretamente a registradores da CPU ou a endereços de memória identificados por /rótulos/. Toda a manipulação de dados é feita por meio de instruções de cópia, como resultado de operações lógicas e aritméticas ou em definições que serão processadas durante a montagem do arquivo binário. *** Ponteiros, só por analogia Como não há variáveis, também não existe exatamente o conceito de /ponteiro/, a não ser por analogia. Os rótulos, por exemplo, representam endereços de memória, e nós até podemos pensar neles como os ponteiros e vetores da linguagem C, mas isso pode causar algumas confusões conceituais e deve ser evitado. *** Em vez de tipos, quantidades de bytes Novamente: todos os dados são escritos diretamente em registradores ou na memória. Se usarmos registradores, as suas capacidades determinarão quantos bytes poderão (ou deverão) ser escritos. A escrita direta na memória, por outro lado, não tem um limite fixo, mas depende da quantidade de memória endereçável e da maneira como o programa a utiliza. #+begin_quote *Nota:* uma das principais finalidades do conceito de /tipos/ é a delimitação do acesso aos dados. Por exemplo, declarar uma variável como do tipo =int= limita o acesso a apenas 4 bytes (em arquiteturas x86_64), mas não existe essa forma de restrição no Assembly. Nós somos livres para ler ou escrever quantos bytes quisermos, em qualquer endereço, desde que tenhamos permissão do sistema. #+end_quote ** Por que aprender Assembly? *** Entendimento do funcionamento de computadores Aprender Assembly proporciona uma visão detalhada do que realmente acontece durante a execução de um programa, como o ciclo de execução de instruções, o uso de registradores, o papel da memória RAM, o funcionamento da pilha e tantos outros conhecimentos fundamentais para a compreensão da arquitetura e da organização de sistemas computacionais modernos. *** Depuração e análise Ferramentas de depuração, análise, segurança e engenharia reversa são muito mais eficazes nas mãos de quem entende Assembly, especialmente quando tudo que temos são o código de máquina ou a /desmontagem/ do binário de um programa. *** Integração com C É comum utilizar Assembly para otimizações pontuais em programas escritos em C, como em trechos críticos de desempenho ou quando se deseja acesso direto a instruções específicas da arquitetura. Também é necessário para a escrita de rotinas de sistema, drivers ou na manipulação direta de hardware, especialmente em ambientes com recursos limitados ou sem suporte a bibliotecas padrão. *** Aprendizado Assembly é uma tremenda ferramenta didática em disciplinas como arquitetura e organização de computadores, sistemas operacionais, design de linguagens e construção de compiladores. Com ele, nós podemos demonstrar e observar diretamente conceitos que, de outra forma, seriam ocultados por várias camadas de abstração. ** Principais sintaxes Embora as instruções de máquina sejam definidas pela arquitetura da CPU, a maneira como essas instruções são escritas em Assembly pode variar bastante, dependendo da sintaxe adotada pelo montador. Para as arquiteturas x86 e x86_64, destacam-se duas sintaxes principais: a sintaxe /Intel/ e a sintaxe /AT&T/. *** Sintaxe AT&T A sintaxe AT&T foi popularizada no contexto dos sistemas Unix e do projeto GNU. Ela é usada por padrão pelo montador GAS (/GNU Assembler/) e é a sintaxe adotada implicitamente pelo GCC ao gerar código Assembly. Os operandos seguem a ordem /origem → destino/, com prefixos =%= obrigatórios para registradores e =$= para valores (ex.: =movl $1, %rax=). *"Salve, simpatia!" em sintaxe AT&T (GAS):* Arquivo: [[exemplos/00/salve-att.s][salve-att.s]] #+begin_src asm :tangle exemplos/00/salve-att.s # salve-att.s # Montar com: as salve-att.s -o salve-att.o # Linkar com: ld salve-att.o -o salve-att .section .data msg: .ascii "Salve, simpatia!\n" len = . - msg .section .text .global _start _start: # write(1, msg, len) mov $1, %rax # syscall: write mov $1, %rdi # stdout lea msg(%rip), %rsi # endereço da mensagem mov $len, %rdx # tamanho da mensagem syscall # exit(0) mov $60, %rax # syscall: exit xor %rdi, %rdi # status 0 syscall #+end_src *** Sintaxe Intel Mais comum em livros didáticos, manuais da Intel e tutoriais voltados ao desenvolvimento em baixo nível, especialmente em ambientes /bare-metal/. Ela apresenta os operandos na ordem /destino ← origem/ (ex.: =mov rax, 1=), não usa prefixos nos registradores e é adotada por montadores como o NASM, que utilizaremos nos nossos estudos. *"Salve, simpatia!" em sintaxe Intel (NASM):* Arquivo: [[exemplos/00/salve-intel.asm][salve-intel.asm]] #+begin_src asm :tangle exemplos/00/salve-intel.asm ; salve-intel.asm ; Montar com: nasm -f elf64 salve-intel.asm ; Linkar com: ld salve-intel.o -o salve-intel section .data msg db "Salve, simpatia!", 10 ; 10 = '\n' len equ $ - msg section .text global _start _start: ; write(1, msg, len) mov rax, 1 ; syscall número 1: write mov rdi, 1 ; stdout mov rsi, msg ; endereço da mensagem mov rdx, len ; tamanho da mensagem syscall ; exit(0) mov rax, 60 ; syscall número 60: exit xor rdi, rdi ; status 0 syscall #+end_src *** Compatibilidade Embora o GAS (executável =as=) use a sintaxe AT&T por padrão, ele também aceita código em sintaxe Intel com a diretiva =.intel_syntax= no início do código-fonte, geralmente acompanhada de =noprefix=. Alguns compiladores e depuradores, como o GCC e o GDB, também oferecem suporte a ambas as sintaxes -- por exemplo, com a opção =-masm=intel=, no GCC, e o comando =set disassembly-flavor=, no GDB. * O Netwide Assembler (NASM) O /Netwide Assembler/ (NASM) é um montador livre muito utilizado na programação em Assembly, especialmente para arquiteturas x86 e x86_64. Ele é conhecido por sua sintaxe clara e direta e por oferecer suporte a formatos de saída para várias plataforma. | Característica | Descrição | |--------------------------+---------------------------------------------------------------------------| | *Sintaxe* | Intel (não suporta AT&T) | | *Arquiteturas suportadas* | x86 (16/32 bits), x86_64 (64 bits) | | *Formatos de saída* | ELF, COFF, Mach-O, bin, RDOFF | | *Processo de montagem* | Gera binários brutos (/raw/) e arquivos objeto (exige link-edição separada) | | *Controle de código* | Total controle sobre instruções, endereços e seções | | *Pré-processamento* | Diretivas simples; sem pré-processador complexo como no C | | *Suporte a macros* | Sim, com argumentos e controle condicional | | *Integração com o sistema* | Compatível com chamadas de sistema Linux, Windows e Unix em geral | | *Otimizações* | Não realiza otimizações nem análises semânticas | | *Documentação* | Completa e com muitos exemplos | ** Sintaxe de instruções A sintaxe geral de uma instrução em NASM segue uma forma bastante regular e compreensível: #+begin_example [rótulo:] instrução operando1, operando2 ; comentário #+end_example Onde... | Elemento | Obrigatório | Exemplos | Descrição | |------------+-------------+-------------------+----------------------------------------------------------------------------------------------| | *Rótulo* | Opcional | =loop_start:= | Nome associado a um endereço. Pode ser usado como destino de saltos. | | *Instrução* | Sim | =mov=, =add=, =jmp= | A operação a ser executada. | | *Operando 1* | Depende | =rax=, =[msg]= | Primeiro operando: geralmente o destino (registrador ou memória). | | *Operando 2* | Depende | =rbx=, =1=, =[valor]= | Segundo operando: geralmente a origem dos dados (registrador, memória ou um valor imediato). | | *Comentário* | Opcional | =; isso é um salto= | Começa com =;= e vai até o fim da linha. Ignorado pelo montador. | *** Diretivas para definições de dados Além das instruções, mas ainda seguindo a estrutura geral da sintaxe, o NASM oferece várias diretivas para definir dados ou reservar espaços na memória, como: | Diretiva | Uso | Ação | Equivalente aproximado em C | |----------+---------------+------------------------------------------+------------------------------------| | =db= | =db 0x48= | Define um byte (define byte) | =char x = 0x48;= | | =dw= | =dw 1234h= | Define uma *word* (2 bytes) | =short x = 0x1234;= | | =dd= | =dd 1.0= | Define uma *double word* (4 bytes) | =int x = 1;= / =float x = 1.0f;= | | =dq= | =dq 3.14159= | Define uma *quad word* (8 bytes) | =double x = 3.14159;= | | =dt= | =dt 1.23e400= | Define uma *ten-byte* (80 bits, float ext) | =long double x = …= (FPU específico) | | =resb= | =resb 64= | Reserva 64 bytes (sem inicializar) | =char buffer[64];= (sem valor) | | =resw= | =resw 8= | Reserva 8 words (16 bytes) | =short arr[8];= | | =resd= | =resd 4= | Reserva 4 dwords (16 bytes) | =int arr[4];= | | =resq= | =resq 2= | Reserva 2 qwords (16 bytes) | =long arr[2];= | Essas diretivas podem ou não ser antecedidas de rótulos para indicar onde esses dados serão inseridos na memória, por exemplo: #+begin_src asm msg: db "Salve, simpatia!", 0 ; 'msg' é um rótulo para o endereço do primeiro byte buffer: resb 64 ; 'buffer' é um rótulo para a área reservada #+end_src É permitido definir e reservar espaços sem rótulos, mas os dados só poderão ser acessados com base em cálculos de posições relativas. ** Sintaxe de metaprogramação da montagem A sintaxe de metaprogramação da montagem refere-se ao conjunto de diretivas, macros e comandos especiais fornecidos pelo montador para escrever código que gera ou manipula outro código durante a montagem. Essa sintaxe vai além das instruções padrão da CPU e possibilita: - Definir constantes simbólicas e macros reutilizáveis. - Realizar substituições textuais antes da montagem real. - Implementar estruturas condicionais e laços para controle do fluxo de montagem. - Gerar código de forma dinâmica e programável, facilitando a automação e a abstração. Na tabela, nós temos alguns exemplos de diretivas e comandos usados na sintaxe de metaprogramação de montagem do NASM: | Diretiva / Comando | Categoria | Descrição breve | Exemplo simples | |--------------------+-----------------------+----------------------------------------------------+----------------------| | =equ= | Definição simbólica | Define uma constante simbólica imutável | =MAXLEN equ 32= | | ~=~ | Definição simbólica | Define símbolo numérico, pode ser redefinido | ~counter = 0~ | | =%define= | Macros simbólicas | Define uma macro simples (substituição textual) | =%define MSG "Olá!"= | | =%assign= | Macros simbólicas | Define símbolo numérico que pode ser alterado | =%assign i 10= | | =%macro= | Macros com parâmetros | Define macros com parâmetros e corpo expansível | =%macro PRINT_MSG 0= | | =%ifdef= / =%ifndef= | Controle condicional | Compila bloco se símbolo estiver (ou não) definido | =%ifdef DEBUG= | | =%include= | Inclusão de arquivos | Insere outro arquivo fonte no ponto da montagem | =%include "utils.inc"= | | =%rep= / =%endrep= | Laços e repetição | Repete um bloco de código um número fixo de vezes | =%rep 4= | ** Operadores e símbolos No NASM, a montagem de código também envolve o uso de operadores e símbolos especiais que permitem calcular endereços, manipular valores e controlar a geração do programa, tudo em tempo de montagem. Aqui estão alguns exemplos: | Símbolo | Descrição | Uso / Exemplo | |-----------------+---------------------------------------------------+-----------------------------------------------| | =$= | Endereço da instrução atual | =jmp $+5= — salta 5 bytes à frente | | =$$= | Endereço do início do segmento ou bloco atual | =jmp $$-$= — salto relativo ao início do bloco | | =$+n= / =$-n= | Endereço relativo deslocado =n= bytes | =jmp $-10= — volta 10 bytes | | =%= | Indica que um símbolo é um valor numérico (macro) | =%define X 10= | | =:= | Define ou qualifica rótulos | =loop_start:= | | =*= | Multiplicação em expressões | =len equ 4 * 10= | | =+=, =-=, =/=, =<<=, =>>= | Operadores aritméticos e bitwise | =size equ 1 << 4= | | =[...]= | Acesso indireto à memória (endereços) | =mov eax, [ebx]= | | =?= | Operador ternário em macros | =%if ?(cond) ... %endif= | ** Seções no NASM Quando escrevemos um programa em Assembly, é preciso organizar o código e os dados de forma que o sistema operacional possa carregá-lo corretamente na memória. Essa organização é feita por meio das /seções/ (ou /segmentos/), que dividem o programa em partes distintas com propósitos diferentes. Mas isso não é apenas uma convenção de organização: a separação das seções é exigida pelo formato do executável (como ELF no Linux ou PE no Windows) e é fundamental para que o /carregador/ do sistema (/loader/) saiba onde colocar cada parte do programa na memória. No NASM, nós usamos a diretiva =section= para declarar as divisões do código, que podem ser: - =section .text=: armazena o código executável do programa (as instruções); - =section .data=: contém dados inicializados (variáveis com valores definidos); - =section .rodata=: usada para dados constantes somente leitura (como strings fixas); - =section .bss=: reserva espaço para dados não inicializados (variáveis com valor padrão, geralmente zero). ** Importação e exportação de símbolos Símbolos são nomes associados a endereços e valores em um programa. Eles podem representar rótulos de rotinas, posições de dados na memória ou constantes definidas em tempo de montagem. Em Assembly, esses símbolos servem como pontos de referência tanto para o código quanto para o montador e o editor de ligações (/link-editor/). Quando escrevemos programas em NASM que serão ligados com outros módulos, como bibliotecas externas ou arquivos objeto, nós precisamos informar ao editor de ligações quais símbolos devem ser /exportados/ para esses módulos e quais devem ser /importados/ de outros lugares. No NASM, isso é feito com as diretivas =global= (importação) e =extern= (exportação). No GNU/Linux, especialmente quando não usamos uma linguagem de mais alto nível (como C), nós precisamos especificar manualmente o ponto de entrada do programa — ou seja, a função onde o sistema operacional deve começar a execução. Por padrão, o Linux espera que o ponto de entrada seja chamado de =_start=, que deve ser exportado com a diretiva: #+begin_src asm global _start #+end_src Essa instrução diz ao NASM que o símbolo =_start= deve ser incluído na tabela de símbolos do arquivo objeto montado. Assim, o editor de ligações (=ld=) poderá encontrá-lo. Nó código, =_start= geralmente é escrito no início da seção =.text=: #+begin_example global _start section .text _start: ; O código de início do programa vem aqui... #+end_example ** Tipos de operandos em instruções Toda instrução Assembly opera sobre um ou mais valores que nós chamamos de operandos. Eles indicam a origem e o destino dos dados e podem representar: - Registradores; - Endereços de memória; - Valores imediatos (constantes escritas diretamente no código). A combinação entre esses tipos de operandos determina a validade e o comportamento das instruções. Por exemplo, uma instrução =mov= pode copiar... - Dados de um registrador para outro registrador; - Um valor imediato para um registrador; - Dados em um endereço de memória para um registrador; - Dados de um registrador para um endereço de memória. Mas não pode, por exemplo, copiar dados de um registrador, ou de um endereço de memória, para um valor imediato, e o NASM exige que essas combinações estejam de acordo com a sintaxe e a semântica definida pela arquitetura. * Instruções essenciais A arquitetura x86_64 tem centenas de instruções, mas nós só precisamos conhecer algumas delas para começar a acompanhar os exemplos dos próximos tópicos -- com o tempo, nós acrescentaremos organicamente algumas outras ao nosso arsenal da linguagem Assembly. ** Instrução =mov= A instrução =mov= é usada para copiar valores de um operando para outro. É uma das instruções mais utilizadas em Assembly, pois quase todo código precisa manipular dados em registradores, na memória ou com valores imediatos. *Forma geral:* #+begin_example mov destino, origem #+end_example O valor de =origem= é copiado para =destino=. #+begin_quote *Importante!* Isso não é uma troca de dados entre operandos, é uma cópia de um operando para outro. #+end_quote *Combinações válidas:* | Origem | Destino | Exemplo | Observações | |-------------+-------------+----------------+-------------------------------------------| | Imediato | Registrador | =mov rax, 42= | Valor literal (imediato) para registrador | | Registrador | Registrador | =mov rbx, rax= | Cópia entre registradores | | Registrador | Memória | =mov [var], rax= | Salva conteúdo em memória | | Memória | Registrador | =mov rax, [var]= | Carrega conteúdo da memória | | Imediato | Memória | =mov [var], 10= | Atribuição direta a endereço | O NASM não permite movimentos diretos de memória para memória. *Exemplo de uso:* #+begin_src asm section .data msg db 42 ; variável na memória section .text global _start _start: mov rax, [msg] ; carrega o valor de msg em rax mov rbx, rax ; copia rax para rbx mov [msg], 99 ; sobrescreve msg com novo valor #+end_src ** Instrução =lea= Carrega o endereço efetivo de um operando de memória (não o valor na memória). #+begin_quote O endereço efetivo é o resultado da avaliação de uma expressão que resulta no endereço que será usado para acessar a memória. #+end_quote *Forma geral:* #+begin_example lea destino, [base + índice*escala + deslocamento] #+end_example *Combinações válidas:* | Destino | Origem (memória) | Exemplo | |-------------+-------------------+----------------------| | Registrador | Memória simbólica | =lea rax, [rbx+4]= | | Registrador | Endereços | =lea rsi, [rip+msg]= | *Exemplo de uso:* #+begin_src asm section .rodata msg: db "Olá, mundo!", 10 ; string com quebra de linha section .text global _start _start: lea rsi, [rel msg] ; carrega o endereço de msg em rsi mov rax, 1 ; syscall: write mov rdi, 1 ; descritor: stdout mov rdx, 13 ; tamanho da mensagem syscall ; ... #+end_src ** Instruções aritméticas básicas (=inc=, =dec=, =add= e =sub=) Estas instruções realizam operações aritméticas simples diretamente em registradores ou na memória. *** Instrução =inc= Incrementa (soma 1) o valor de um operando. *Forma geral:* #+begin_example inc destino #+end_example *Operandos válidos:* registrador ou endereço de memória. *Exemplo:* #+begin_src asm mov rcx, 5 inc rcx ; rcx passa a valer 6 #+end_src *** Instrução =dec= Decrementa (subtrai 1) o valor de um operando. *Forma geral:* #+begin_example dec destino #+end_example *Operandos válidos:* registrador ou endereço de memória. *Exemplo:* #+begin_src asm mov rcx, 5 dec rcx ; rcx passa a valer 4 #+end_src *** Instrução =add= Soma um operando ao destino e armazena o resultado no destino. *Forma geral:* #+begin_example add destino, origem #+end_example *Combinações válidas:* | Destino | Origem | Exemplo | |-------------+-----------------+-------------------| | Registrador | Registrador | add rax, rbx | | Registrador | Valor imediato | add rax, 10 | | Registrador | Memória | add rax, [var] | | Memória | Registrador | add [var], rax | | Memória | Valor imediato | add [var], 1 | *Exemplo:* #+begin_src asm mov rax, 3 add rax, 7 ; rax passa a valer 10 #+end_src *** Instrução =sub= Subtrai um operando do destino e armazena o resultado no destino. *Forma geral:* #+begin_example sub destino, origem #+end_example *Combinações válidas:* | Destino | Origem | Exemplo | |-------------+-----------------+-------------------| | Registrador | Registrador | sub rax, rbx | | Registrador | Valor imediato | sub rax, 10 | | Registrador | Memória | sub rax, [var] | | Memória | Registrador | sub [var], rax | | Memória | Valor imediato | sub [var], 1 | *Exemplo:* #+begin_src asm mov rax, 10 sub rax, 3 ; rax passa a valer 7 #+end_src ** Instruções de multiplicação e divisão (=mul=, =imul=, =div= e =idiv=) Estas instruções realizam operações aritméticas de multiplicação e divisão. *** Instrução =mul= (multiplicação sem sinal) Realiza multiplicação sem sinal entre o valor no registrador acumulador e um operando, armazenando o resultado em registradores específicos. *Funcionamento:* | Tamanho dos operandos | Multiplicação | Resultado | |-----------------------+------------------+-----------| | 8 bits | =AL= por operando | =AX= | | 16 bits | =AX= por operando | =DX:AX= | | 32 bits | =EAX= por operando | =EDX:EAX= | | 64 bits | =RAX= por operando | =RDX:RAX= | *Forma geral:* #+begin_example mul operando #+end_example *Operando:* registrador ou memória, sem sinal. *Exemplo:* #+begin_src asm mov rax, 5 mul qword [valor] ; rax * [valor], resultado em RDX:RAX #+end_src *** Instrução =imul= (multiplicação com sinal) Mais flexível que =mul=, pode usar 1, 2 ou 3 operandos. *Forma geral unária (1 operando):* #+begin_example imul operando #+end_example | Tamanho dos operandos | Multiplicação | Resultado | |-----------------------+------------------+-----------| | 8 bits | =AL= por operando | =AX= | | 16 bits | =AX= por operando | =DX:AX= | | 32 bits | =EAX= por operando | =EDX:EAX= | | 64 bits | =RAX= por operando | =RDX:RAX= | *Forma geral binária (2 operandos):* #+begin_example imul destino, origem (resultado em destino) #+end_example - Multiplica =destino= por =origem= e armazena o resultado em =destino=. - Se o resultado exceder a capacidade de =destino=, apenas os bytes mais baixos serão armazenados (não há extensão de registradores). - Afeta as flags =CF= (/carry/) e =OF= (/overflow/) no caso de estouro da capacidade de =destino=. *Forma geral ternária (3 operandos):* #+begin_example imul destino, origem, imediato (resultado em destino) #+end_example - Multiplica =origem= pelo valor de =imediato= e armazena o resultado em =origem=. - Se o resultado exceder a capacidade de =destino=, apenas os bytes mais baixos serão armazenados (não há extensão de registradores). - Afeta as flags =CF= e =OF= no caso de estouro da capacidade de =destino=. *Operandos válidos para =imul=:* | Forma | Operando 1 (destino) | Operando 2 (origem) | Operando 3 (imediato) | |-------------+--------------------------------+---------------------------------+-----------------------| | 1 operando | =RAX=, =EAX=, =AX= ou =AL= (implícito) | Qualquer registrador ou memória | (Não há) | | 2 operandos | Registrador | Registrador ou memória | (Não há) | | 3 operandos | Registrador | Registrador ou memória | Valor imediato | *Exemplo unário:* #+begin_src asm mov rax, 10 mov rbx, -4 imul rbx ; rdx:rax = rax * rbx (com sinal) #+end_src *Exemplo binário:* #+begin_src asm mov rax, 5 mov rbx, -3 imul rax, rbx ; rax = rax * rbx (com sinal) #+end_src *Exemplo ternário:* #+begin_src asm mov rbx, 7 imul rax, rbx, -2 ; rax = rbx * -2 (com sinal) #+end_src *** Instrução =div= (divisão sem sinal) Realiza divisão sem sinal entre o valor no registrador acumulador estendido e um operando. *Funcionamento:* | Tamanho dos operandos | Divisão | Quociente | Resto | |-----------------------+----------------------+-----------+-------| | 8 bits | =AX= por operando | =AL= | =AH= | | 16 bits | =DX:AX= por operando | =AX= | =DX= | | 32 bits | =EDX:EAX= por operando | =EAX= | =EDX= | | 64 bits | =RDX:RAX= por operando | =RAX= | =RDX= | *Forma geral:* #+begin_example div operando #+end_example *Operando:* registrador ou memória sem sinal. *Exemplo:* #+begin_src asm mov rax, 20 xor rdx, rdx ; limpa (zera) RDX antes da divisão div qword [divisor] ; divide RDX:RAX por [divisor] #+end_src *** Instrução =idiv= (divisão com sinal) Realiza divisão com sinal entre o valor no registrador acumulador estendido e um operando, considerando números negativos. *Funcionamento:* | Tamanho dos operandos | Divisão | Quociente | Resto | |-----------------------+----------------------+-----------+-------| | 8 bits | =AX= por operando | =AL= | =AH= | | 16 bits | =DX:AX= por operando | =AX= | =DX= | | 32 bits | =EDX:EAX= por operando | =EAX= | =EDX= | | 64 bits | =RDX:RAX= por operando | =RAX= | =RDX= | *Forma geral:* #+begin_example idiv operando #+end_example *Operando:* registrador ou memória com sinal. *Exemplo:* #+begin_src asm mov rax, -50 cqto ; extende o sinal de RAX para RDX:RAX (antes de idiv) idiv qword [divisor] ; divide RDX:RAX por [divisor] #+end_src #+begin_quote *Nota:* Antes de usar =idiv=, é necessário preparar o registrador de extensão (RDX) com o valor correto usando =cqto= (/Convert Quadword to Octaword/), que estende o sinal de RAX para RDX:RAX. Para operandos menores, são utilizados =cwd= (16 bits) ou =cdq= (32 bits). #+end_quote ** Instrução =jmp= (salto incondicional) Realiza um salto incondicional para o endereço ou rótulo especificado. A execução do programa continua a partir da posição de destino, sem avaliar qualquer condição. *Forma geral:* #+begin_example jmp destino #+end_example *Combinações válidas:* | Destino | Tipo de operando | Exemplo | |-------------+------------------------+-----------| | Rótulo | Endereço relativo | =jmp fim= | | Registrador | Endereço absoluto | =jmp rax= | | Memória | Ponteiro para endereço | =jmp [rax]= | | Imediato | Offset relativo | =jmp $+5= | *Exemplo de uso:* #+begin_src asm section .text global _start _start: mov rax, 1 ; syscall: write mov rdi, 1 ; descritor: stdout mov rsi, msg ; ponteiro para a string mov rdx, 16 ; tamanho da string syscall jmp fim ; salta incondicionalmente para o final meio: ; código que nunca será executado mov rax, 60 mov rdi, 1 syscall fim: mov rax, 60 ; syscall: exit xor rdi, rdi ; Estado de término 0 syscall section .rodata msg: db "Salve, simpatia!", 10 #+end_src ** Instruções de salto condicional As instruções de salto condicional (ou desvio condicional) alteram o fluxo de execução com base no estado das flags da CPU, definidas por instruções de comparação como =cmp= ou =test=. *Forma geral:* #+begin_example jXX destino #+end_example Onde =jXX= representa uma das variantes condicionais e =destino= é um rótulo no código. *Flags envolvidas:* - =ZF= (/Zero Flag/): Ativa (=1=) se o resultado de uma operação for zero. - =SF= (/Sign Flag/): Reflete o bit de sinal do resultado (=0= para positivo, =1= para negativo). - =OF= (/Overflow Flag/): Ativa (=1=) se ocorreu /overflow/ em operação aritmética com sinal. - =CF= (/Carry Flag/): Ativa se houve transporte (/carry/) ou empréstimo (/borrow/) em operações aritméticas com números sem sinal. *Tabela de saltos comuns (em inteiros com sinal):* | Instrução | Sinônimo | Condição | Descrição | |-----------+----------+--------------------+-------------------------------| | =je= | =jz= | ~ZF = 1~ | Salta se igual (zero) | | =jne= | =jnz= | ~ZF = 0~ | Salta se diferente (não zero) | | =jg= | =jnle= | ~ZF = 0~ e ~SF = OF~ | Salta se maior | | =jge= | =jnl= | ~SF = OF~ | Salta se maior ou igual | | =jl= | =jnge= | ~SF != OF~ | Salta se menor | | =jle= | =jng= | ~ZF = 1~ ou ~SF != OF~ | Salta se menor ou igual | *Outras variantes comuns (sem sinal):* | Instrução | Condição | Descrição | |-----------+------------------+--------------------------| | =ja= | ~CF = 0~ e ~ZF = 0~ | Salta se acima | | =jae= | ~CF = 0~ | Salta se acima ou igual | | =jb= | ~CF = 1~ | Salta se abaixo | | =jbe= | ~CF = 1~ ou ~ZF = 1~ | Salta se abaixo ou igual | *Exemplo de uso:* #+begin_src asm section .text global _start _start: mov rax, 5 mov rbx, 3 cmp rax, rbx ; compara rax com rbx jg maior ; salta se rax > rbx jl menor ; salta se rax < rbx je igual ; salta se rax == rbx maior: ; código para o caso de rax > rbx jmp fim menor: ; código para o caso de rax < rbx jmp fim igual: ; código para o caso de rax == rbx fim: mov rax, 60 ; syscall: exit xor rdi, rdi ; Resula em 0 syscall #+end_src ** Instrução =cmp= Compara dois operandos realizando uma subtração entre eles, mas sem armazenar o resultado. O objetivo da instrução é atualizar as flags da CPU, que podem ser usadas posteriormente em instruções de salto condicional. *Forma geral:* #+begin_example cmp operando1, operando2 #+end_example Internamente, equivale a: #+begin_example sub operando1 operando2 #+end_example Mas sem alterar o conteúdo de =operando1=. *Combinações válidas:* | Operando 1 | Operando 2 | Exemplo | |-------------+-------------+------------------| | Registrador | Registrador | =cmp rax, rbx= | | Registrador | Imediato | =cmp rsi, 10= | | Registrador | Memória | =cmp rcx, [array]= | | Memória | Registrador | =cmp [rdi], rax= | | Memória | Imediato | =cmp [rsi], 1= | *Exemplo de uso:* #+begin_src asm section .data valor: dq 42 section .text global _start _start: mov rax, [valor] ; carrega o valor da memória cmp rax, 42 ; compara com 42 je igual ; salta se for igual mov rdi, 1 ; Termina com erro: não é igual jmp fim igual: mov rdi, 0 ; Termina com sucesso: é igual fim: mov rax, 60 ; syscall: exit syscall #+end_src ** Instrução =test= Realiza uma operação lógica =AND= entre dois operandos e atualiza as flags de acordo com o resultado. A instrução é usada principalmente para testar, de forma não destrutiva, se bits estão ligados ou se valores são zero. #+begin_quote A instrução =cmp= testa diferença (via subtração), enquanto =test= testa presença de bits em comum (=AND= bit a bit) entre dois valores. #+end_quote *Forma geral:* #+begin_example test operando1, operando2 #+end_example O resultado da operação =operando1 AND operando2= não é armazenado, apenas as flags da CPU (zero, sinal, paridade, etc.) são atualizadas. *Combinações válidas:* | Operando 1 | Operando 2 | Exemplo | |-------------+--------------+-----------------------| | Registrador | Registrador | =test rax, rax= | | Registrador | Imediato | =test rbx, 1= | | Memória | Registrador | =test [rdi], rax= | | Memória | Imediato | =test [rsi], 0xFF= | *Exemplo de uso:* #+begin_src asm section .text global _start _start: mov rax, 0 ; valor a testar test rax, rax ; testa se é zero jz deu_zero ; salta se resultado for zero ; não deu zero mov rdi, 1 jmp sair deu_zero: ; era zero mov rdi, 0 sair: ; Termina com o valor recebido em rdi... mov rax, 60 ; syscall: exit syscall #+end_src ** Instruções =call= e =ret= As instruções =call= e =ret= são utilizadas para implementar chamadas e retornos de sub-rotinas em Assembly, o que possibilita a estruturação do código em blocos reutilizáveis. A instrução =call= salva o endereço da próxima instrução (retorno) na pilha e salta para o endereço especificado. A instrução =ret=, por sua vez, recupera o endereço de retorno na pilha e desvia a execução de volta para o ponto após a chamada. *Forma geral:* #+begin_example call destino ... destino: ... ret #+end_example *Combinações válidas:* | Instrução | Operando | Exemplo | |-----------+---------------------+--------------| | =call= | Rótulo | =call uma_sub= | | =call= | Registrador | =call rax= | | =call= | Ponteiro de memória | =call [rax]= | | =ret= | (sem operando) | =ret= | *Observações:* - As instruções =call= e =ret= afetam o registrador de instrução (=RIP=), controlando o fluxo de execução. - A instrução =call= também é utilizada para chamar funções importadas com =extern=. - Em x86_64, os argumentos de função geralmente são passados por registradores, conforme a convenção de chamada de funções do sistema. *Exemplo de uso:* #+begin_src asm section .text global _start _start: call salve ; chama a sub-rotina mov rax, 60 ; syscall: exit xor rdi, rdi ; estado de término 0 syscall salve: mov rax, 1 ; syscall: write mov rdi, 1 ; stdout mov rsi, msg mov rdx, msglen syscall ret section .rodata msg: db "Salve, simpatia!", 10 msglen equ $ - msg #+end_src ** Instrução =syscall= A instrução =syscall= realiza uma chamada ao kernel do sistema operacional em 64 bits (em 32 bits, seria a instrução =int 80=), transferindo para ele o controle temporário da execução do programa. É o principal meio de acesso a serviços como leitura e escrita em arquivos, alocação de memória, término do programa, etc. #+begin_quote Ao contrário das funções da biblioteca padrão (como =printf= ou =exit=), que são abstrações em C, a =syscall= faz chamadas diretas ao sistema. #+end_quote *Forma geral:* #+begin_example mov rax, numero_da_syscall mov rdi, argumento1 mov rsi, argumento2 mov rdx, argumento3 mov r10, argumento4 mov r8, argumento5 mov r9, argumento6 syscall #+end_example *Registradores de argumento:* | Posição | Registrador | Descrição | |---------+-------------+--------------------| | 1º | =rdi= | Primeiro argumento | | 2º | =rsi= | Segundo argumento | | 3º | =rdx= | Terceiro argumento | | 4º | =r10= | Quarto argumento | | 5º | =r8= | Quinto argumento | | 6º | =r9= | Sexto argumento | *Resultado:* - O valor de retorno da /syscall/ é carregado em =rax=. - Erros são indicados com valores negativos em =rax=. *Exemplo de uso (escrevendo na saída padrão):* #+begin_src asm section .rodata msg: db "Salve, simpatia!", 10 len: equ $ - msg section .text global _start _start: mov rax, 1 ; syscall: write mov rdi, 1 ; descritor de saída: stdout mov rsi, msg ; ponteiro para os dados mov rdx, len ; tamanho da mensagem syscall mov rax, 60 ; syscall: exit xor rdi, rdi ; código de saída: 0 syscall #+end_src #+begin_quote *Nota:* A lista de números das chamadas varia conforme o sistema operacional. #+end_quote ** Padrões frequentemente utilizados Aqui estão alguns padrões de código muito utilizados em Assembly, seja por performance, clareza para o processador, ou por simples abreviação da escrita do programa. Por exemplo, em vez de usar: #+begin_src asm mov reg, 0 #+end_src É comum escrever: #+begin_src asm xor reg, reg ; operação XOR bit a bit #+end_src Motivação: - É mais rápido em alguns processadores. - Utiliza menos bytes de código. - Não depende de carregar um imediato =0=. Nessa mesma linha, existem outros padrões bastante encontrados, como: | Padrão | Descrição | |---------+--------------------------------------------------------------| | =inc reg= | Incrementa o registrador em 1 em vez de usar =add=. | | =dec reg= | Decrementa o registrador em 1 em vez de usar =sub=. | | =neg reg= | Inverte o sinal do registrador em vez de multiplicar por =-1=. | * Montagem e execução de programas em Assembly NASM ** Requisitos Para garantir que você tenha todas as ferramentas necessárias para acompanhar nossos exemplos e demonstrações, verifique se os programas abaixo estão instalados no seu sistema: - Um editor de código da sua preferência (Vim, Emacs, Geany, etc) - =git= - =nasm= - =gcc= - =as= - =ld= - =gdb= - =readelf= - =objdump= - =nm= - =ldd= - =hexedit= - =xxd= (geralmente instalado com o editor Vim) No Debian, a maioria deles é instalada com os comandos: #+begin_example sudo apt update sudo apt install build-essential git nasm gdb binutils hexedit #+end_example ** Exemplo: "Salve, simpatia!" Tomando como exemplo o programa [[exemplos/00/salve-intel.asm][salve-intel.asm]], escrito com a sintaxe Intel implementada pelo NASM: #+begin_src asm ; salve-intel.asm ; Montar com: nasm -f elf64 salve-intel.asm ; Linkar com: ld salve-intel.o -o salve-intel section .data msg db "Salve, simpatia!", 10 ; 10 = '\n' len equ $ - msg section .text global _start _start: ; write(1, msg, len) mov rax, 1 ; syscall número 1: write mov rdi, 1 ; stdout mov rsi, msg ; endereço da mensagem mov rdx, len ; tamanho da mensagem syscall ; exit(0) mov rax, 60 ; syscall número 60: exit xor rdi, rdi ; status 0 syscall #+end_src O processo de montagem envolve a execução do montador =nasm= informando o formato de saída esperado (no caso, formato ELF de 64 bits): #+begin_example :~$ nasm -f elf64 salve-intel.asm #+end_example O =nasm= gera o binário montado em um arquivo com mesmo nome do código-fonte, mas com a extensão =.o=, de /objeto/: #+begin_example ls salve-intel.asm salve-intel.o #+end_example Esse arquivo objeto até poderia ser vinculado a outros binários para compor um programa (/ligação estática/), mas ele ainda não pode ser executado como está, pois ainda não contém todas as informações que o sistema operacional requer para carregá-lo na memória e executá-lo. Para isso, o arquivo objeto precisa ser processado por um editor de ligações (/link-editor/ ou simplesmente /ligador/): #+begin_example :~$ ld salve-intel.o -o salve-intel #+end_example Como resultado, nós teremos um arquivo binário executável (inclusive com as devidas permissões) de nome =salve-intel=: #+begin_example :~$ ls salve-intel salve-intel.asm salve-intel.o #+end_example #+begin_quote Se não tivéssemos definido um nome de saída (opção =-o=), o executável seria chamado, por padrão, de =a.out=. #+end_quote Para testar, basta executar: #+begin_example :~$ ./salve-intel Salve, simpatia! #+end_example * Links e referências úteis - [[https://www.nasm.us/xdoc/2.16.02/html/][Manual do NASM (2.16.02)]] - [[https://namazso.github.io/x86/][Guia de referência das instruções Intel 64]] - [[https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/][Tabela de chamadas de sistema Linux x86_64]]